Repository navigation
Competency data delete protection and archive-only retirement #798
Description
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=PROTECTvalues 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 aTODOreferencing this issue. - Delete protection is implemented via
on_delete=PROTECTon FKs fromStudent*Statusmodels to definition models; hard deletion is blocked at the ORM layer without requiring instance-leveldelete()overrides, sinceQuerySet.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 toCompetencyRuleProfile, which usesPROTECT, 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. ThePROTECTthat produces the failure lives on the foreign keys from the threeStudent*Statustables, 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 markedCASCADE. Change any link in that chain toSET_NULLand deleting the tag succeeds while learner statuses still exist. - The transitive cases are tested, not only the direct ones: deleting an
oel_tagging_tagwith a learner status row anywhere beneath it raisesProtectedError, and deleting one with no status rows beneath it succeeds and cascades. -
CompetencyRuleProfileis never hard-deleted, even before any learner status exists. Retirement is archive-only through itsarchivedcolumn, 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.
Activity