Skip to content

Competency criteria models (authoring/definition layer) #641

Description

@jesperhodge

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-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.

Acceptance criteria (from #613)

  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions