Skip to content

Competency data delete protection and archive-only retirement #798

Closed as duplicate

Description

@jesperhodge

Status: placeholder

This issue is not ready to be worked. It exists to hold the delete-protection work that came out of
#613 while #655 decides how to implement it. The acceptance criteria below are provisional and will be
rewritten once #655 lands.

Blocked by: #655, "[Arch] Implementation approach for competency data delete/edit guardrails."

Parent: #613.

Why this is separate

ADR-0002 Decision 7 and ADR-0003 Decision 4 already decide what should happen when someone tries to
delete or edit competency data after a learner has mastery status against it. #655 is deciding how
that gets implemented: how a "can this be deleted" signal is computed and surfaced on the GET endpoints
without N+1 queries, and where the write-path validation lives so the rule is enforced rather than only
hinted at in the UI.

Those answers change what the model layer has to look like, so the delete-protection criteria were
pulled out of #613's model tickets rather than guessed at. #641 and #642 build the tables with no
delete behavior of their own.

What #641 and #642 leave behind

Django requires an on_delete value on every ForeignKey, so #641 and #642 cannot simply omit the
question. Both declare every new foreign key as on_delete=PROTECT, provisionally, so that the
undecided state fails closed: nothing can be hard-deleted until this issue relaxes each foreign key to
its real value. Neither ticket ships an authoring API, so nothing performs deletes in the meantime.

That means the first job here, whatever #655 decides, is to replace those provisional values.

Provisional acceptance criteria

Moved from #613. Expect these to change once #655 is resolved.

  • The provisional on_delete=PROTECT values that Competency criteria models (authoring/definition layer) #641 and Mastery status lookup + learner progress models #642 declared are replaced with the real per-foreign-key values, and no foreign key still carries a TODO referencing this issue.
  • Delete protection is implemented via on_delete=PROTECT on FKs from Student*Status models to definition models; hard deletion is blocked at the ORM layer without requiring instance-level delete() overrides, since QuerySet.delete() bypasses those.
  • Every foreign key from one definition table to another uses on_delete=CASCADE: criterion to group, group to parent group, group to tag, and criterion to objecttag. The one exception is the criterion's foreign key to CompetencyRuleProfile, which uses PROTECT, because a rule profile is never hard-deleted at all. These values are what make ADR-0002 Decision 7's delete protection work. That protection is conditional, not a blanket ban: deleting a tag has to succeed when no learner has a status under it, and fail when one does. The PROTECT that produces the failure lives on the foreign keys from the three Student*Status tables, and two of those point at rows one and two levels below the tag. Django reaches them only by walking down the tree from the tag, and it only walks down a foreign key marked CASCADE. Change any link in that chain to SET_NULL and deleting the tag succeeds while learner statuses still exist.
  • The transitive cases are tested, not only the direct ones: deleting an oel_tagging_tag with a learner status row anywhere beneath it raises ProtectedError, and deleting one with no status rows beneath it succeeds and cascades.
  • CompetencyRuleProfile is never hard-deleted, even before any learner status exists. Retirement is archive-only through its archived column, per ADR-0002 Decision 7's exception, because a profile is shared across many criteria rather than tied to one.

The archived column itself is not part of this issue. #641 creates it as one of CompetencyRuleProfile's columns, per ADR-0002 Decision 3; what this issue adds is the rule that retirement goes through it.

Depends on

#655 for the approach. #641 and #642 for the tables and the foreign keys to change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions