Skip to content

feat: add CompetencyMasteryStatus and StudentCompetencyStatus models - #852

Merged
ormsbee merged 1 commit into
openedx:mainfrom
mgwozdz-unicon:cbe-642-competency-status
Oct 7, 2026
Merged

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

Conversation

@mgwozdz-unicon

@mgwozdz-unicon mgwozdz-unicon commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR adds two of #642's four models. CompetencyMasteryStatus is the lookup table of the three mastery ranks (AttemptedNotDemonstrated, PartiallyAttempted, Demonstrated). StudentCompetencyStatus records a learner's current mastery status for one competency (a Tag), with one row per learner per tag, updated in place rather than appended.

This PR replaces #830. 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.

How to review this PR

Every commit up to and including f3226fb is unchanged from jesperhodge#17 and jesperhodge#18, with the same SHAs. That code was reviewed there: @tbain approved both, and @ormsbee's review is addressed by the commits after f3226fb, so those are the only new code to review. In the Commits tab, they are:

Commit Review comment it addresses
8aaa578 jesperhodge#18: drop "load-bearing, not a lookup optimization" from the unique-constraint comment.
3f30ab4 jesperhodge#17: the seed migration's reverse is a no-op, because reversing 0008 drops the table anyway. Replaced by ae491fa below.
29dde41 jesperhodge#17: CompetencyMasteryStatus uses a SmallAutoField primary key, so every learner row's foreign key is 2 bytes instead of 8.
1e46c44 jesperhodge#17: the status ids are now 10, 20 and 30 instead of 1, 2 and 3. The database compares them directly to decide whether a write raises a status, so their order is a contract, and the gaps leave room for a new rank without renumbering stored rows.
f18c12f jesperhodge#18: a comment explaining why StudentCompetencyStatus rejects AttemptedNotDemonstrated. A learner can still demonstrate a competency later in another course, so a top-level row never records "not demonstrated".
ae491fa This PR: the seed migration's reverse deletes the seeded rows again, so rewinding to 0008 leaves an empty table, as 0008 created it.

The rest of the review comments (the tag-taxonomy check and the composite indexes) didn't need code changes in this PR, and I've replied to each of them on the fork PRs.

Migration numbering

This PR's migrations are 0008_competency_mastery_status, 0009_seed_competency_mastery_statuses and 0010_studentcompetencystatus, which follow 0007 on main. #847 also adds an openedx_learning migration 0008. Whichever of the two merges second renumbers its migrations after the other, because Django rejects a migration graph with two leaf nodes.

Upgrade note for a local database

If your development database already applied migrations 0008 to 0010 from Jesper's branch, migrate openedx_learning back to 0007 on that code, or reset the database, before switching to this branch. The migration names are unchanged, but the seeded ids and the check constraint are not, so Django would not re-run them.

Testing instructions

Confirm there is no migration drift:

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

By hand, the two behaviors most worth confirming are the "one row per learner and tag" constraint and that a Tag in use cannot be deleted out from under a recorded status:

from django.contrib.auth import get_user_model
from django.utils import timezone
from openedx_learning.models import CompetencyMasteryStatus, MasteryStatus, StudentCompetencyStatus
from openedx_tagging.models import Tag, Taxonomy

User = get_user_model()
user = User.objects.create(username="learner1")
taxonomy = Taxonomy.objects.create(name="T", export_id="t")
tag = Tag.objects.create(taxonomy=taxonomy, value="Writing")
now = timezone.now()

CompetencyMasteryStatus.objects.count()   # 3, seeded by the migration
demonstrated = CompetencyMasteryStatus.objects.get(id=MasteryStatus.DEMONSTRATED)

# created/modified are caller-supplied, not automatic, so pass both on every create().
record = StudentCompetencyStatus.objects.create(user=user, tag=tag, status=demonstrated, created=now, modified=now)
StudentCompetencyStatus.objects.create(user=user, tag=tag, status=demonstrated, created=now, modified=now)
# IntegrityError: unique constraint on (user, tag)

attempted = CompetencyMasteryStatus.objects.get(id=MasteryStatus.ATTEMPTED_NOT_DEMONSTRATED)
StudentCompetencyStatus.objects.create(
    user=user, tag=Tag.objects.create(taxonomy=taxonomy, value="Reading"), status=attempted,
    created=now, modified=now,
)
# IntegrityError: check constraint, AttemptedNotDemonstrated is not allowed at the top level

tag.delete()   # ProtectedError: a status row still points at this tag
user.delete()  # succeeds, and takes the status row with it
StudentCompetencyStatus.objects.filter(pk=record.pk).exists()   # False

CI's MySQL job is the one that matters for the check constraint above. SQLite enforces it too, but MySQL is the backend #642 was written against.

Checks run

  • The script 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): 939 passed. The suite also covers the check constraint on bulk_create() and QuerySet.update(), which skip model validation.
  • Full test suite on SQLite: 938 passed, 1 skipped.
  • tox -e quality, tox -e docs and tox -e package: all pass.
  • On MySQL 8.4, migrated forward, back to 0008 (table empty), back to 0007 (table gone), and forward again: all three migrations apply and unapply cleanly, and the seeded ids are 10, 20 and 30.

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

Sorry, one tiny, tiny thing, and then I'll merge. Thank you.

]

operations = [
migrations.RunPython(forward, migrations.RunPython.noop),

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.

Sorry, I mis-communicated this. When I made the nit about how everything could just be deleted, I meant it could be:

    CompetencyMasteryStatus = apps.get_model("openedx_learning", "CompetencyMasteryStatus")
    CompetencyMasteryStatus.objects.delete()

Since this is the only thing that adds any rows to that model, as the model was just created. Doing a noop is technically incorrect here, since if I rewind to migration 0008, I would expect to have an empty table, and this noop would mean that the seeded values would be there. It is very unlikely that anyone will ever run into this situation, but please restore a deletion of some sort.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Got it, restored the deletion so rewinding to 0008 leaves an empty table (ae491fa).

@mgwozdz-unicon
mgwozdz-unicon requested a review from ormsbee October 7, 2026 13:28
@ormsbee

ormsbee commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

@mgwozdz-unicon: Happy to merge this. Please squash the commits and give it a good summary commit message? Thank you.

Add two of openedx#642's four learner-status models. CompetencyMasteryStatus
is the mastery-rank lookup table, seeded with ids 10, 20 and 30 so the
database can compare ranks directly. StudentCompetencyStatus holds one
row per learner and competency tag, updated in place, and a check
constraint keeps AttemptedNotDemonstrated out of it. Deleting a user
deletes their status rows, and a tag with status rows cannot be
deleted.

Part of openedx#642.

Co-authored-by: Jesper Hodge <jhodge@unicon.net>
@mgwozdz-unicon
mgwozdz-unicon force-pushed the cbe-642-competency-status branch from ae491fa to 7bc5dd4 Compare October 7, 2026 13:39
@mgwozdz-unicon

Copy link
Copy Markdown
Contributor Author

@ormsbee This is ready for merge. Thank you very much!

@ormsbee
ormsbee merged commit 48a04ee into openedx:main Oct 7, 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.

3 participants