Skip to content

feat: add student status models for competency criteria and groups - #854

Merged
ormsbee merged 1 commit into
openedx:mainfrom
mgwozdz-unicon:cbe-642b-criteria-status
Oct 9, 2026
Merged

ormsbee merged 1 commit into
openedx:mainfrom
mgwozdz-unicon:cbe-642b-criteria-status

Conversation

@mgwozdz-unicon

Copy link
Copy Markdown
Contributor

Description

This PR adds the last two of #642's four models. StudentCompetencyCriterionStatus records a learner's current mastery status for one leaf criterion, and StudentCompetencyCriteriaGroupStatus does the same for one criteria group. Each holds one row per learner and node, updated in place, and points at the CompetencyMasteryStatus lookup table added in #852. Unlike StudentCompetencyStatus, both accept all three status values, including AttemptedNotDemonstrated.

This PR replaces Jesper's stack jesperhodge#20, jesperhodge#21 and jesperhodge#22. Jesper is out sick, and I'm taking over #642 so it can merge.

Supporting information

Note

Implemented with an AI agent (Claude Code), with a human directing the work and reviewing the code and decisions.

Deviation from the acceptance criteria

#642 and #613 say created uses auto_now_add=True and modified uses auto_now=True. Both models use manual_date_time_field() instead, the same as StudentCompetencyStatus in #852. auto_now does not fire on QuerySet.update(), so it would leave modified stale on the single conditional UPDATE that raises a status. #642's Open Questions already records this divergence.

How to review this PR

The first 11 commits are Jesper's commits from jesperhodge#20 through jesperhodge#22, rebased onto main with no conflicts. Their SHAs changed because #852 was squash-merged, but git range-diff shows every one of them is patch-identical to the version reviewed on the fork, which ended at 5e9c2df. @tbain approved jesperhodge#21 and jesperhodge#22 there.

The last two commits address @ormsbee's review on the fork, so they are the only new code to review:

Commit Review comment it addresses
57d55b6 jesperhodge#22 and jesperhodge#21: removes the ADR-0002 index test module, and the acceptance tests that only restated the model and migration definitions or re-checked the seed rows.
29f9bd3 jesperhodge#21: renames StudentCompetencyCriteriaStatus to the singular StudentCompetencyCriterionStatus, along with its migration and unique constraint. Its table is now openedx_learning_studentcompetencycriterionstatus.

Migration numbering

This PR's migrations are 0011_studentcompetencycriterionstatus and 0012_studentcompetencycriteriagroupstatus, which follow #852's 0010 on main. #847 adds an openedx_learning migration too, so whichever of the two merges second renumbers its migrations after the other.

Upgrade note for a local database

If your development database already applied migrations 0011 and 0012 from Jesper's branch, migrate openedx_learning back to 0010 on that code, or reset the database, before switching to this branch. Migration 0011 has a new name and creates a renamed table.

Testing instructions

Confirm there is no migration drift:

python manage.py makemigrations --check --dry-run openedx_learning   # expect: No changes detected

The snippet below covers both models' main behavior. Run python manage.py migrate on this branch, then paste the snippet into python manage.py shell. The lines commented with an error are expected to raise, so in IPython, which stops a pasted block at its first exception, paste one block at a time.

from django.contrib.auth import get_user_model
from django.utils import timezone
from openedx_learning.models import (
    CompetencyCriteriaGroup, CompetencyCriterion, CompetencyRuleProfile, CompetencyTaxonomy,
    MasteryStatus, StudentCompetencyCriteriaGroupStatus, StudentCompetencyCriterionStatus,
)
from openedx_tagging.models import ObjectTag, Tag

now = timezone.now()
learner = get_user_model().objects.create(username="cbe-learner")
taxonomy = CompetencyTaxonomy.objects.create(name="T", export_id="t")
tag = Tag.objects.create(taxonomy=taxonomy, value="Writing")
root = CompetencyCriteriaGroup.objects.create(tag=tag)
child = CompetencyCriteriaGroup.objects.create(tag=tag, parent=root)
object_tag = ObjectTag.objects.create(object_id="block-v1:x+y+z+problem+p1", taxonomy=taxonomy, tag=tag)
default = CompetencyRuleProfile.objects.get(organization=None, course=None, competency_taxonomy=None)
criterion = CompetencyCriterion.objects.create(group=child, object_tag=object_tag, rule_profile=default)

# A learner holds one status row per criterion and one per group, any of the three values allowed.
leaf = StudentCompetencyCriterionStatus.objects.create(
    user=learner, criterion=criterion, status_id=MasteryStatus.PARTIALLY_ATTEMPTED, created=now, modified=now,
)
StudentCompetencyCriteriaGroupStatus.objects.create(
    user=learner, group=child, status_id=MasteryStatus.ATTEMPTED_NOT_DEMONSTRATED, created=now, modified=now,
)
StudentCompetencyCriterionStatus.objects.create(
    user=learner, criterion=criterion, status_id=MasteryStatus.DEMONSTRATED, created=now, modified=now,
)                                                                          # IntegrityError: second row

# The row is updated in place, so a raise is one conditional UPDATE.
raise_leaf = StudentCompetencyCriterionStatus.objects.filter(pk=leaf.pk, status_id__lt=MasteryStatus.DEMONSTRATED)
raise_leaf.update(status_id=MasteryStatus.DEMONSTRATED, modified=timezone.now())  # 1
raise_leaf.update(status_id=MasteryStatus.DEMONSTRATED, modified=timezone.now())  # 0: already Demonstrated

# Learner status protects the definitions above it, including across the CASCADE chain.
criterion.delete()   # ProtectedError
root.delete()        # ProtectedError
tag.delete()         # ProtectedError
taxonomy.delete()    # ProtectedError

# Deleting the learner removes their status rows, and then the tree deletes.
learner.delete()     # deletes the user, one criterion status and one group status
root.delete()        # deletes both groups and the criterion

Checks run

  • The snippet above, on this branch, on both SQLite and MySQL 8.4: every line gives the result in its comment.
  • Full test suite on MySQL 8.4 (tox -e django52): 973 passed.
  • Full test suite on SQLite: 972 passed, 1 skipped.
  • tox -e quality and tox -e docs: both pass.
  • On MySQL 8.4, migrated forward, back to 0010, and forward again: both migrations apply and unapply cleanly.

🤖 Generated with Claude Code

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 7, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @mgwozdz-unicon!

This repository is currently maintained by @axim-engineering.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@ormsbee ormsbee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just a couple of small requests related to tests that I should have included the last round, but missed. Once those are done, feel free to squash, and I'll approve and merge. Thank you.

Thank you.


Claude drafted the first pass of this review and ran the git range-diff, test suite, mypy, import-linter and makemigrations --check on the branch for me.

Comment on lines +1 to +4
"""
#642 acceptance-criteria tests for StudentCompetencyCriterionStatus that test_criterion_status.py
does not already cover.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fold this module into test_criterion_status.py, and likewise the group one into test_criteria_group_status.py. "The acceptance criteria that the other file doesn't already cover" isn't a boundary the next person adding a test can apply, and it's already drifting: both files define the same criterion fixture, and a few cases here are within a line or two of ones over there (test_group_at_depth_delete_is_protected_by_a_criterion_status_beneath_it vs. test_root_group_delete_is_protected_by_a_criterion_status_two_levels_down, test_oel_tagging_taxonomy_delete_is_protected_by_a_criterion_status_beneath_its_tags vs. test_taxonomy_delete_is_protected_by_a_criterion_status, and test_criterion_status_accepts_every_status_value vs. test_attempted_not_demonstrated_is_accepted). The "deleting succeeds when nothing is beneath it" cases and the lower-over-higher one are worth keeping. I just want one module per model.

assert list(rows.values_list("pk", "status_id")) == [(row.pk, MasteryStatus.DEMONSTRATED)]


def test_criterion_status_carries_created_and_modified() -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: What does this tell us that the rest of the module doesn't, given that every write in it passes created and modified? I'd drop this one, test_group_status_carries_created_and_modified in the group file, and test_criterion_status_migration_declares_the_swappable_user_dependency below. The group file already lost its migration-dependency twin after my last round, and I don't want to encourage tests that assert what a migration file says.

Add the last two of openedx#642's four learner-status models.
StudentCompetencyCriterionStatus holds a learner's mastery status for
one leaf criterion, and StudentCompetencyCriteriaGroupStatus holds it
for one criteria group. Each holds one row per learner and criterion,
or per learner and group, updated in place, and accepts all three
status values. Deleting a user deletes their status rows, and a
criterion, group, object tag, tag or taxonomy with status rows beneath
it cannot be deleted.

Closes openedx#642.

Co-authored-by: Jesper Hodge <jhodge@unicon.net>
@mgwozdz-unicon
mgwozdz-unicon force-pushed the cbe-642b-criteria-status branch from 29f9bd3 to e03180e Compare October 9, 2026 12:25
@mgwozdz-unicon mgwozdz-unicon changed the title feat: add StudentCompetencyCriterionStatus and StudentCompetencyCriteriaGroupStatus models feat: add student status models for competency criteria and groups Oct 9, 2026
@mgwozdz-unicon

Copy link
Copy Markdown
Contributor Author

Thanks @ormsbee - I addressed both comments and squashed.

@mgwozdz-unicon
mgwozdz-unicon requested a review from ormsbee October 9, 2026 12:29
@ormsbee
ormsbee merged commit 5984e3e into openedx:main Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Mastery status lookup + learner progress models

3 participants