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 "Authoring/Definition Models" section of #613: the models that let course authors define what a learner needs to do to demonstrate a competency. See #613 for full background and ADR-0002/ADR-0003.
What to implement
Three models, all living together in one file (e.g. models/criteria.py) since they form one connected tree structure:
CompetencyCriteriaGroup — an internal node in an AND/OR logic tree. Each group can have child groups (via a self-referencing parent foreign key, nullable for root nodes) and an ordering value that determines evaluation order among siblings.
CompetencyRuleProfile — a reusable set of default evaluation settings, scoped to a taxonomy, course, or organization. Has a rule_type field and a rule_payload JSON field whose expected shape depends on rule_type.
CompetencyCriteria — the leaf nodes of the tree. Has a FK to its parent CompetencyCriteriaGroup (competency_criteria_group_id), a FK to the tag/object association (oel_tagging_objecttag_id), and a nullable FK to the default CompetencyRuleProfile it uses (competency_rule_profile_id; when null, evaluation falls back through taxonomy/course/org-scoped profiles per ADR-0002 Decision 4). On top of that default it may carry an optional per-criterion rule override (its own rule_type_override / rule_payload_override, following the same validation pattern as CompetencyRuleProfile).
All three get history = HistoricalRecords() from django-simple-history, so every edit to a criteria definition is tracked for audit purposes (per ADR-0003 — this is the "versioning" part of #613).
Validation rules to implement (in clean(), invoked via full_clean() — not in save(), since Django doesn't call clean() automatically on save):
logic_operator (AND/OR) must be set whenever a group has more than one child. It's only allowed to be null if the group has zero or one child, because AND/OR logic doesn't mean anything for a single operand.
rule_payload on CompetencyRuleProfile, and rule_payload_override on CompetencyCriteria, must match the shape expected for their rule_type.
Database constraint: add UniqueConstraint(fields=['parent', 'ordering'], name='unique_sibling_ordering') on CompetencyCriteriaGroup, so two sibling groups under the same parent can never accidentally share an ordering value (which would make evaluation order ambiguous). Root-level groups are naturally exempt from this since they all have parent=NULL, and NULL values don't collide in a unique constraint — that's expected and fine.
CompetencyCriteriaGroup has all required columns: id, nullable self-referencing parent, oel_tagging_tag FK, nullable course_id, name, ordering, logic_operator (AND/OR/null).
logic_operator is non-null on any group with more than one child; null is only valid for groups with zero or one child.
UniqueConstraint on (parent, ordering) exists on CompetencyCriteriaGroup.
CompetencyRuleProfile has all required columns; rule_payload is a JSON field whose shape is validated against rule_type.
CompetencyCriteria has all required columns, including nullable rule_type_override and rule_payload_override, validated the same way as CompetencyRuleProfile.
A test calls full_clean() on a model with an invalid rule_payload/rule_payload_override and asserts it raises ValidationError.
django-simple-history (HistoricalRecords()) is applied to CompetencyCriteriaGroup, CompetencyCriteria, and CompetencyRuleProfile.
Indexes 1, 2, 4, 5, and 9 from Decision 5 of ADR-0002 are present (these are the ones scoped to these three models — check the ADR for the exact index definitions).
Index 3 from Decision 5, oel_tagging_objecttag(object_id), is already satisfied by the existing db_index=True on ObjectTag.object_id (src/openedx_tagging/models/base.py); no work is required here. This is called out so the "all 10 indexes" check against [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613 isn't read as a miss (tickets 2 + 3 add the other 9: 1,2,4,5,9 here and 6,7,8,10 in ticket 3).
All three models are registered as .. no_pii:.
Migration applies cleanly on top of ticket 1's migration; no extra columns beyond the ADR; FK relationships match the ADR exactly.
Out of scope
Student status models and their on_delete=PROTECT foreign keys (ticket 3 adds those pointing at these models — you don't need to do anything on this side to support that); the app skeleton (already done in ticket 1); any REST API or UI work.
Depends on
Ticket 1 (needs the app to exist; CompetencyRuleProfile has a foreign key to CompetencyTaxonomy). This ticket can be developed in parallel with ticket 3, but should merge before it.
Description
Parent: #613. This ticket implements the "Authoring/Definition Models" section of #613: the models that let course authors define what a learner needs to do to demonstrate a competency. See #613 for full background and ADR-0002/ADR-0003.
What to implement
Three models, all living together in one file (e.g.
models/criteria.py) since they form one connected tree structure:CompetencyCriteriaGroup— an internal node in an AND/OR logic tree. Each group can have child groups (via a self-referencingparentforeign key, nullable for root nodes) and anorderingvalue that determines evaluation order among siblings.CompetencyRuleProfile— a reusable set of default evaluation settings, scoped to a taxonomy, course, or organization. Has arule_typefield and arule_payloadJSON field whose expected shape depends onrule_type.CompetencyCriteria— the leaf nodes of the tree. Has a FK to its parentCompetencyCriteriaGroup(competency_criteria_group_id), a FK to the tag/object association (oel_tagging_objecttag_id), and a nullable FK to the defaultCompetencyRuleProfileit uses (competency_rule_profile_id; when null, evaluation falls back through taxonomy/course/org-scoped profiles per ADR-0002 Decision 4). On top of that default it may carry an optional per-criterion rule override (its ownrule_type_override/rule_payload_override, following the same validation pattern asCompetencyRuleProfile).All three get
history = HistoricalRecords()fromdjango-simple-history, so every edit to a criteria definition is tracked for audit purposes (per ADR-0003 — this is the "versioning" part of #613).Validation rules to implement (in
clean(), invoked viafull_clean()— not insave(), since Django doesn't callclean()automatically on save):logic_operator(AND/OR) must be set whenever a group has more than one child. It's only allowed to be null if the group has zero or one child, because AND/OR logic doesn't mean anything for a single operand.rule_payloadonCompetencyRuleProfile, andrule_payload_overrideonCompetencyCriteria, must match the shape expected for theirrule_type.Database constraint: add
UniqueConstraint(fields=['parent', 'ordering'], name='unique_sibling_ordering')onCompetencyCriteriaGroup, so two sibling groups under the same parent can never accidentally share an ordering value (which would make evaluation order ambiguous). Root-level groups are naturally exempt from this since they all haveparent=NULL, and NULL values don't collide in a unique constraint — that's expected and fine.Acceptance criteria (from #613)
CompetencyCriteriaGrouphas all required columns:id, nullable self-referencingparent,oel_tagging_tagFK, nullablecourse_id,name,ordering,logic_operator(AND/OR/null).logic_operatoris non-null on any group with more than one child; null is only valid for groups with zero or one child.UniqueConstrainton(parent, ordering)exists onCompetencyCriteriaGroup.CompetencyRuleProfilehas all required columns;rule_payloadis a JSON field whose shape is validated againstrule_type.CompetencyCriteriahas all required columns, including nullablerule_type_overrideandrule_payload_override, validated the same way asCompetencyRuleProfile.full_clean()on a model with an invalidrule_payload/rule_payload_overrideand asserts it raisesValidationError.django-simple-history(HistoricalRecords()) is applied toCompetencyCriteriaGroup,CompetencyCriteria, andCompetencyRuleProfile.oel_tagging_objecttag(object_id), is already satisfied by the existingdb_index=TrueonObjectTag.object_id(src/openedx_tagging/models/base.py); no work is required here. This is called out so the "all 10 indexes" check against [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613 isn't read as a miss (tickets 2 + 3 add the other 9: 1,2,4,5,9 here and 6,7,8,10 in ticket 3)... no_pii:.Out of scope
Student status models and their
on_delete=PROTECTforeign keys (ticket 3 adds those pointing at these models — you don't need to do anything on this side to support that); the app skeleton (already done in ticket 1); any REST API or UI work.Depends on
Ticket 1 (needs the app to exist;
CompetencyRuleProfilehas a foreign key toCompetencyTaxonomy). This ticket can be developed in parallel with ticket 3, but should merge before it.