Skip to content

[BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613

Description

@mgwozdz-unicon

Use Case

Competency-Based Education (CBE) programs require learners to demonstrate mastery of specific competencies, not just complete coursework. Open edX currently has no way to define or evaluate the rules that determine whether a learner has demonstrated a competency — for example, "a learner must score 75% or higher on Assignment 1 OR Assignment 2 to demonstrate the Writing Poetry competency."

This issue implements the foundational database layer that makes the following possible:

  • Course Authors and Platform Administrators can define competency achievement criteria in Studio, specifying which course content (assignments, exams, etc.) a learner must complete, at what threshold, and with what AND/OR logic
  • The platform can track each learner's progress toward demonstrating each competency as they receive grades and completions
  • A history of criteria changes is preserved for audit and traceability

Without this data model, no competency progress dashboards, automated competency evaluations, or CBE-specific authoring tools can be built.


Description

Implement the Django database models defined in ADR-0002 (Competency Criteria Model) and ADR-0003 (Competency Criteria Versioning). This is a data-layer-only issue: no API endpoints, no UI, and no business logic beyond schema constraints and delete protection.

Authoring/Definition Models

  • CompetencyTaxonomy — extends Taxonomy via Django multi-table inheritance to mark taxonomies as CBE-enabled
  • CompetencyCriteriaGroup — AND/OR expression tree internal nodes
  • CompetencyRuleProfile — reusable evaluation rule defaults scoped by taxonomy, course, or organization
  • CompetencyCriteria — leaf nodes in the criteria tree, linking tag/object associations to optional rule overrides

Lookup Data

  • CompetencyMasteryStatuses — shared status values: Demonstrated, AttemptedNotDemonstrated, PartiallyAttempted

Learner Progress Models (append-only)

  • StudentCompetencyCriteriaStatus
  • StudentCompetencyCriteriaGroupStatus
  • StudentCompetencyStatus

Per ADR-0003, django-simple-history is applied to the three authoring definition models only. Learner status tables are append-only by design and do not use history tracking.


Acceptance Criteria

Note: This issue delivers a data model, not a user-facing feature. There is no manual QA path. Acceptance is determined by a PR reviewer verifying the following against ADR-0002 and ADR-0003.

Schema Correctness (ADR-0002)

  • CompetencyTaxonomy uses Django MTI (not a taxonomy_type column); taxonomy_ptr_id is both the PK and FK to oel_tagging_taxonomy.id
  • CompetencyCriteriaGroup has all required columns: id, parent_id (nullable self-FK), oel_tagging_tag_id, course_id (nullable), name, ordering, logic_operator (AND/OR/null)
  • logic_operator is non-null (AND or OR) on all groups that have more than one child; null is valid only when a group has zero or one child (short-circuit evaluation is undefined for a single operand)
  • A UniqueConstraint on (parent_id, ordering) is present on CompetencyCriteriaGroup; duplicate sibling ordering values that would break deterministic short-circuit evaluation are prevented at the DB layer
  • CompetencyRuleProfile has all required columns; rule_payload is a validated JSON field with shape enforced per rule_type
  • CompetencyCriteria has all required columns including nullable rule_type_override and rule_payload_override with the same validation contract
  • rule_payload and rule_payload_override shape validation is enforced at the full_clean() layer; a test must call full_clean() with an invalid payload and assert ValidationError is raised
  • 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.

Versioning (ADR-0003)

  • django-simple-history (HistoricalRecords()) is applied to CompetencyCriteriaGroup, CompetencyCriteria, and CompetencyRuleProfile
  • django-simple-history is not applied to oel_tagging_tag, oel_tagging_taxonomy, or CompetencyTaxonomy
  • Learner status tables use append-only rows (only a created timestamp, no updated); no history package applied

General

  • Migrations are present and apply cleanly from scratch
  • No columns exist on any model that are not defined in the ADRs
  • All FK relationships match the ADR definitions exactly

Technical Notes

Files to Create

(Adjust path to match where the new app lands, e.g., src/openedx_learning/applets/cbe/)

  • apps.py, __init__.py
  • models.py — all new models
  • migrations/0001_initial.py — schema migration
  • migrations/0002_seed_mastery_statuses.py — data migration to seed CompetencyMasteryStatuses
  • admin.py — basic admin registrations

Files to Modify

  • App registry / INSTALLED_APPS — add the new app
  • requirements/*.txt — add django-simple-history if not already present

Implementation Notes

  • MTI: CompetencyTaxonomy(Taxonomy) uses Django's built-in multi-table inheritance. taxonomy_ptr_id is implicit — do not declare it manually.
  • Self-referential FK: CompetencyCriteriaGroup.parent_id is ForeignKey('self', null=True, blank=True) for root nodes.
  • logic_operator null semantics: null is permitted only when the group has at most one child, where AND/OR logic is undefined. A group with two or more children must have logic_operator set. Enforce this in clean() / full_clean().
  • Ordering uniqueness: Add UniqueConstraint(fields=['parent_id', 'ordering'], name='unique_sibling_ordering') to CompetencyCriteriaGroup.Meta. Root nodes (parent_id=null) are excluded from the uniqueness guarantee by DB null semantics — this is acceptable since root ordering across different trees is independent.
  • JSON payload validation: rule_payload and rule_payload_override use JSONField. Payload shape validation by rule_type belongs in clean() (called via full_clean()). Raw save() does not invoke clean() — never rely on save() alone. Serializers and forms call full_clean() automatically; raw ORM tests must call it explicitly.
  • Seeded data: CompetencyMasteryStatuses rows must be created in a dedicated data migration, not in fixtures or application code.
  • Append-only status tables: Student*Status tables have only created, no updated. Current status = latest row for a given (user_id, target_entity_id) pair ordered by created DESC, id DESC.
  • Delete protection: Set on_delete=PROTECT on the FKs from Student*Status models that point to definition models (CompetencyCriteriaGroup, CompetencyCriteria, CompetencyRuleProfile). Django raises ProtectedError at the ORM layer before any SQL is issued. Do not rely on overriding delete()QuerySet.delete() bypasses instance-level overrides. Consider whether this should also apply to oel_tagging_objecttag rows per Decision 7.
  • django-simple-history registration: Add history = HistoricalRecords() to CompetencyCriteriaGroup, CompetencyCriteria, and CompetencyRuleProfile only. The corresponding Historical* tables are auto-generated in migrations.
  • StudentCompetencyStatus status restriction: The FK to CompetencyMasteryStatuses is unrestricted at the DB layer. Enforce the allowed values (Demonstrated, PartiallyAttempted) in clean(). Raw ORM saves bypass clean() — tests must call
    full_clean() explicitly to verify this constraint, consistent with the rule_payload validation pattern.

Example Resolution Prompt

Implement the Django data models for CBE Competency Achievement Criteria as specified in ADR-0002 and ADR-0003 in the openedx-core repository. Create a new Django app at openedx-core/src/openedx_learning/applets/cbe. Models to implement: CompetencyTaxonomy (MTI extending Taxonomy), CompetencyCriteriaGroup (self-referential tree with AND/OR logic_operator; null only when group has at most one child; unique constraint on (parent_id, ordering)), CompetencyRuleProfile (JSON-payload rule defaults scoped by taxonomy/course/org), CompetencyCriteria (leaf criterion with optional rule overrides and the same JSON validation contract), CompetencyMasteryStatuses (seeded lookup table), and the three StudentCompetency*Status append-only tables. Apply django-simple-history to the three authoring definition models only. Include all 10 indexes from Decision 5 of ADR-0002. Add a data migration to seed CompetencyMasteryStatuses. Implement delete protection via on_delete=PROTECT on FKs from Student*Status models to definition models. Enforce rule_payload shape validation in clean() / full_clean(), not in save().

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
    Ready for Community Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions