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
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.
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().
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:
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— extendsTaxonomyvia Django multi-table inheritance to mark taxonomies as CBE-enabledCompetencyCriteriaGroup— AND/OR expression tree internal nodesCompetencyRuleProfile— reusable evaluation rule defaults scoped by taxonomy, course, or organizationCompetencyCriteria— leaf nodes in the criteria tree, linking tag/object associations to optional rule overridesLookup Data
CompetencyMasteryStatuses— shared status values: Demonstrated, AttemptedNotDemonstrated, PartiallyAttemptedLearner Progress Models (append-only)
StudentCompetencyCriteriaStatusStudentCompetencyCriteriaGroupStatusStudentCompetencyStatusPer ADR-0003,
django-simple-historyis 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)
CompetencyTaxonomyuses Django MTI (not ataxonomy_typecolumn);taxonomy_ptr_idis both the PK and FK tooel_tagging_taxonomy.idCompetencyCriteriaGrouphas all required columns:id,parent_id(nullable self-FK),oel_tagging_tag_id,course_id(nullable),name,ordering,logic_operator(AND/OR/null)logic_operatoris 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)UniqueConstrainton(parent_id, ordering)is present onCompetencyCriteriaGroup; duplicate sibling ordering values that would break deterministic short-circuit evaluation are prevented at the DB layerCompetencyRuleProfilehas all required columns;rule_payloadis a validated JSON field with shape enforced perrule_typeCompetencyCriteriahas all required columns including nullablerule_type_overrideandrule_payload_overridewith the same validation contractrule_payloadandrule_payload_overrideshape validation is enforced at thefull_clean()layer; a test must callfull_clean()with an invalid payload and assertValidationErroris raisedCompetencyMasteryStatusesis 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()overridesVersioning (ADR-0003)
django-simple-history(HistoricalRecords()) is applied toCompetencyCriteriaGroup,CompetencyCriteria, andCompetencyRuleProfiledjango-simple-historyis not applied tooel_tagging_tag,oel_tagging_taxonomy, orCompetencyTaxonomycreatedtimestamp, noupdated); no history package appliedGeneral
Technical Notes
Files to Create
(Adjust path to match where the new app lands, e.g.,
src/openedx_learning/applets/cbe/)apps.py,__init__.pymodels.py— all new modelsmigrations/0001_initial.py— schema migrationmigrations/0002_seed_mastery_statuses.py— data migration to seedCompetencyMasteryStatusesadmin.py— basic admin registrationsFiles to Modify
INSTALLED_APPS— add the new apprequirements/*.txt— adddjango-simple-historyif not already presentImplementation Notes
CompetencyTaxonomy(Taxonomy)uses Django's built-in multi-table inheritance.taxonomy_ptr_idis implicit — do not declare it manually.CompetencyCriteriaGroup.parent_idisForeignKey('self', null=True, blank=True)for root nodes.logic_operatornull semantics:nullis permitted only when the group has at most one child, where AND/OR logic is undefined. A group with two or more children must havelogic_operatorset. Enforce this inclean()/full_clean().UniqueConstraint(fields=['parent_id', 'ordering'], name='unique_sibling_ordering')toCompetencyCriteriaGroup.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.rule_payloadandrule_payload_overrideuseJSONField. Payload shape validation byrule_typebelongs inclean()(called viafull_clean()). Rawsave()does not invokeclean()— never rely onsave()alone. Serializers and forms callfull_clean()automatically; raw ORM tests must call it explicitly.CompetencyMasteryStatusesrows must be created in a dedicated data migration, not in fixtures or application code.Student*Statustables have onlycreated, noupdated. Current status = latest row for a given(user_id, target_entity_id)pair ordered bycreated DESC, id DESC.on_delete=PROTECTon the FKs fromStudent*Statusmodels that point to definition models (CompetencyCriteriaGroup,CompetencyCriteria,CompetencyRuleProfile). Django raisesProtectedErrorat the ORM layer before any SQL is issued. Do not rely on overridingdelete()—QuerySet.delete()bypasses instance-level overrides. Consider whether this should also apply tooel_tagging_objecttagrows per Decision 7.django-simple-historyregistration: Addhistory = HistoricalRecords()toCompetencyCriteriaGroup,CompetencyCriteria, andCompetencyRuleProfileonly. The correspondingHistorical*tables are auto-generated in migrations.StudentCompetencyStatusstatus restriction: The FK toCompetencyMasteryStatusesis unrestricted at the DB layer. Enforce the allowed values (Demonstrated, PartiallyAttempted) inclean(). Raw ORM saves bypassclean()— tests must callfull_clean()explicitly to verify this constraint, consistent with therule_payloadvalidation 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-corerepository. Create a new Django app atopenedx-core/src/openedx_learning/applets/cbe. Models to implement:CompetencyTaxonomy(MTI extendingTaxonomy),CompetencyCriteriaGroup(self-referential tree with AND/ORlogic_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 threeStudentCompetency*Statusappend-only tables. Applydjango-simple-historyto the three authoring definition models only. Include all 10 indexes from Decision 5 of ADR-0002. Add a data migration to seedCompetencyMasteryStatuses. Implement delete protection viaon_delete=PROTECTon FKs fromStudent*Statusmodels to definition models. Enforcerule_payloadshape validation inclean()/full_clean(), not insave().