You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent:#613. This ticket implements the "Lookup Data" and "Learner Progress Models" sections of #613: tracking each learner's actual progress toward demonstrating a competency. See #613 for full background and ADR-0002/ADR-0003.
What to build
CompetencyMasteryStatuses — a small lookup table of possible status values. Seed it with exactly three rows: Demonstrated, AttemptedNotDemonstrated, PartiallyAttempted. Seed these via a dedicated data migration, not via fixtures or application code — this keeps the seeding auditable and repeatable.
StudentCompetencyCriteriaStatus, StudentCompetencyCriteriaGroupStatus, StudentCompetencyStatus — one row per status change for a learner. These are append-only: each row only has a created timestamp (no updated field), and nothing is ever edited in place. To find a learner's current status for something, query for the latest row (created descending, then id descending as a tiebreaker) rather than looking for "the" row. Because they're append-only event logs, they do not use django-simple-history — there's no "previous version" to track, every row already is a permanent record.
Migrations (two, in order): this ticket creates four new tables (CompetencyMasteryStatuses plus the three Student*Status models), so it needs (1) a schema migration that creates those tables (e.g. 0003_...), then (2) a separate data migration that seeds the three CompetencyMasteryStatuses rows (e.g. 0004_seed_mastery_statuses.py). The seed must run after the table exists — don't fold the seed into the schema migration and don't seed a table that hasn't been created yet.
Delete protection: the foreign keys from these three status models to the criteria models (from ticket 2) must use on_delete=PROTECT. This means Django will refuse to delete a CompetencyCriteria or CompetencyCriteriaGroup row (raising ProtectedError) if any learner has a status row pointing at it — at the database-query layer, automatically, before any SQL runs. Don't implement this by overriding a model's delete() method — that approach doesn't work here because Django's bulk QuerySet.delete() (used e.g. by cascading deletes or admin bulk actions) skips instance-level delete() overrides entirely. on_delete=PROTECT is enforced at a lower level and can't be bypassed that way.
Status restriction on StudentCompetencyStatus: a learner's overall competency status should never be recorded as AttemptedNotDemonstrated — only Demonstrated or PartiallyAttempted make sense at that level. The foreign key to CompetencyMasteryStatuses itself doesn't restrict which values are allowed (any status ID is a valid FK target), so enforce this rule in the model's clean() method instead, called via full_clean().
These are copied verbatim from #613; only the ones this ticket is responsible for are listed. As the last ticket to merge, this ticket also owns the whole-feature gates (all 10 indexes present; make pii_check 100% coverage).
CompetencyMasteryStatuses is seeded with all three values: Demonstrated, AttemptedNotDemonstrated, PartiallyAttempted
StudentCompetencyStatus enforces a constraint limiting status to Demonstrated and PartiallyAttempted only (not AttemptedNotDemonstrated), validated in clean() / full_clean(); a test must call full_clean() with AttemptedNotDemonstrated and assert ValidationError is raised
All 10 indexes from Decision 5 of ADR-0002 are present
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
All new models are registered in .annotation_safe_list.yml (or inline docstrings). Authoring/definition models use .. no_pii:. The three StudentCompetency*Status models use .. pii: / .. pii_types: id / .. pii_retirement: consumer_api. make pii_check passes with 100% coverage.
Learner status tables use append-only rows (only a created timestamp, no updated); no history package applied
No columns exist on any model that are not defined in the ADRs
All FK relationships match the ADR definitions exactly
Additional acceptance criteria (from splitting this out of #613)
These are not in #613 verbatim; they are needed because #613 was split into three tickets.
Of [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613's "All 10 indexes from Decision 5 of ADR-0002 are present": this ticket adds indexes 6, 7, 8, and 10 (the ones scoped to the status/lookup models). Indexes 1, 2, 4, 5, 9 come from ticket 2 and index 3 is pre-existing; verify all 10 are present here as the final ticket.
Delete protection is verified at the ORM layer: deleting a referenced CompetencyCriteria / CompetencyCriteriaGroup raises ProtectedError, without relying on any instance-level delete() override.
Two migrations, in order: a schema migration creating the four new tables (CompetencyMasteryStatuses plus the three Student*Status models), then a separate data migration that seeds the three CompetencyMasteryStatuses rows. The seed must run after the tables exist.
Known open question, flagged in #613's comments but not resolved there — worth raising with the reviewer before or during this ticket: ADR-0002's Decision 7 says hard deletes should also be blocked for CompetencyTaxonomy, CompetencyRuleProfile, and oel_tagging_objecttag once learner status references them. The on_delete=PROTECT fields described above only protect CompetencyCriteria, CompetencyCriteriaGroup, and the tag/mastery lookups they directly point to — they don't cover those other three. Confirm with the reviewer whether that gap needs a separate mechanism in this ticket or is being deliberately deferred.
Out of scope
The criteria models themselves (ticket 2); the taxonomy model (ticket 1); any REST API or UI work.
Depends on
Ticket 2 (its on_delete=PROTECT foreign keys point into ticket 2's tables) and ticket 1 (needs the app to exist). This is the last of the three to merge.
Description
Parent: #613. This ticket implements the "Lookup Data" and "Learner Progress Models" sections of #613: tracking each learner's actual progress toward demonstrating a competency. See #613 for full background and ADR-0002/ADR-0003.
What to build
CompetencyMasteryStatuses— a small lookup table of possible status values. Seed it with exactly three rows:Demonstrated,AttemptedNotDemonstrated,PartiallyAttempted. Seed these via a dedicated data migration, not via fixtures or application code — this keeps the seeding auditable and repeatable.StudentCompetencyCriteriaStatus,StudentCompetencyCriteriaGroupStatus,StudentCompetencyStatus— one row per status change for a learner. These are append-only: each row only has acreatedtimestamp (noupdatedfield), and nothing is ever edited in place. To find a learner's current status for something, query for the latest row (createddescending, theniddescending as a tiebreaker) rather than looking for "the" row. Because they're append-only event logs, they do not usedjango-simple-history— there's no "previous version" to track, every row already is a permanent record.Migrations (two, in order): this ticket creates four new tables (
CompetencyMasteryStatusesplus the threeStudent*Statusmodels), so it needs (1) a schema migration that creates those tables (e.g.0003_...), then (2) a separate data migration that seeds the threeCompetencyMasteryStatusesrows (e.g.0004_seed_mastery_statuses.py). The seed must run after the table exists — don't fold the seed into the schema migration and don't seed a table that hasn't been created yet.Delete protection: the foreign keys from these three status models to the criteria models (from ticket 2) must use
on_delete=PROTECT. This means Django will refuse to delete aCompetencyCriteriaorCompetencyCriteriaGrouprow (raisingProtectedError) if any learner has a status row pointing at it — at the database-query layer, automatically, before any SQL runs. Don't implement this by overriding a model'sdelete()method — that approach doesn't work here because Django's bulkQuerySet.delete()(used e.g. by cascading deletes or admin bulk actions) skips instance-leveldelete()overrides entirely.on_delete=PROTECTis enforced at a lower level and can't be bypassed that way.Status restriction on
StudentCompetencyStatus: a learner's overall competency status should never be recorded asAttemptedNotDemonstrated— onlyDemonstratedorPartiallyAttemptedmake sense at that level. The foreign key toCompetencyMasteryStatusesitself doesn't restrict which values are allowed (any status ID is a valid FK target), so enforce this rule in the model'sclean()method instead, called viafull_clean().Acceptance criteria (from #613)
These are copied verbatim from #613; only the ones this ticket is responsible for are listed. As the last ticket to merge, this ticket also owns the whole-feature gates (all 10 indexes present;
make pii_check100% coverage).CompetencyMasteryStatusesis seeded with all three values: Demonstrated, AttemptedNotDemonstrated, PartiallyAttemptedStudentCompetencyStatusenforces a constraint limiting status to Demonstrated and PartiallyAttempted only (not AttemptedNotDemonstrated), validated inclean()/full_clean(); a test must callfull_clean()with AttemptedNotDemonstrated and assertValidationErroris raisedon_delete=PROTECTon FKs fromStudent*Statusmodels to definition models; hard deletion is blocked at the ORM layer without requiring instance-leveldelete()overridescreatedtimestamp, noupdated); no history package appliedAdditional acceptance criteria (from splitting this out of #613)
These are not in #613 verbatim; they are needed because #613 was split into three tickets.
CompetencyCriteria/CompetencyCriteriaGroupraisesProtectedError, without relying on any instance-leveldelete()override.CompetencyMasteryStatusesplus the threeStudent*Statusmodels), then a separate data migration that seeds the threeCompetencyMasteryStatusesrows. The seed must run after the tables exist.Known open question, flagged in #613's comments but not resolved there — worth raising with the reviewer before or during this ticket: ADR-0002's Decision 7 says hard deletes should also be blocked for
CompetencyTaxonomy,CompetencyRuleProfile, andoel_tagging_objecttagonce learner status references them. Theon_delete=PROTECTfields described above only protectCompetencyCriteria,CompetencyCriteriaGroup, and the tag/mastery lookups they directly point to — they don't cover those other three. Confirm with the reviewer whether that gap needs a separate mechanism in this ticket or is being deliberately deferred.Out of scope
The criteria models themselves (ticket 2); the taxonomy model (ticket 1); any REST API or UI work.
Depends on
Ticket 2 (its
on_delete=PROTECTforeign keys point into ticket 2's tables) and ticket 1 (needs the app to exist). This is the last of the three to merge.