Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
record_graded_object_statuses()now locks the tag association (ObjectTag) behind every criterion whose learner status it records, so that association is archived rather than deleted later and the learner's recorded mastery keeps a row to point at. This is the first caller oflock_object_tags_for_deletion()from openedx#777, so it is also the first time adeletion_locked=Truerow exists outside the tests.What changed
One function,
record_graded_object_statuses()insrc/openedx_learning/applets/cbe/api.py, and one new test module.CompetencyCriterionattached to the graded objects and writes a leaf status for each one whose rule applies. It now also collectscriterion.object_tag_idfor each of those criteria, and after the loop makes a singlelock_object_tags_for_deletion()call with the collected ids. A criterion whose rule cannot be applied (the existingValidationErrorskip path) writes no status, so it is not collected and its association stays unlocked.ObjectTag.DoesNotExistunder:raises.if locked_object_tag_ids:guard means a call that evaluated no criterion, which is the common case for ungraded or untagged content, issues no lock query at all.openedx_tagging.apiis imported. The setter joins the existingfrom openedx_tagging.api import ...line. Nothing is called for the tag or the taxonomy; [BE] Add deletion-lock functions to openedx_tagging openedx/openedx-core#777's reader functions derive their protection from the locked association.object_tag__archived=Falsenext to the existingarchived=Falseon the criterion. This is one line outside the ticket's stated scope, added because this PR is what makes the state reachable: once the lock exists, removing a tag from content throughtag_object()ordelete_object_tags()archives the association ([BE] Branch tag association deletes between archiving and hard delete openedx/openedx-core#779) instead of deleting it, and without this condition later learners kept earning statuses through the archived row. The condition rides on the join the query already makes, so it adds no query. The learner who earned a status before the archive still resolves to the archivedObjectTagthrough the status row's foreign keys, which this query does not touch. [BE] Exclude archived competency criteria and criteria groups from evaluation when a grade is recorded openedx/openedx-core#816's Context section states that [BE] Evaluate learner’s criterion status for grade openedx/openedx-core#699 already excluded archived associations; the merged [BE] Evaluate learner’s criterion status for grade openedx/openedx-core#699 query filters only the criterion's ownarchivedflag, so this PR is where the association condition lands.Choices that differ from the ticket
lock_object_tag_for_deletion()lock_object_tags_for_deletion()ProtectedErrorthroughCompetencyCriteriaGroup.tag. The end-to-end test instead deletes the locked association through openedx#779'sdelete_object_tags(), asserts the tag and taxonomy report as protected through openedx#777's readers, and asserts the learner's status still resolves to the archived association.Who is affected
tag_object(),delete_object_tags(),copy_tags()).record_graded_object_statuses()call that evaluates at least one criterion now also issues one read againstoel_tagging_objecttag, plus oneUPDATEwhen any of those associations was not yet locked. A later learner on already-locked associations costs the read alone. There is no unlock; a locked row stays locked. A criterion whose association is archived is no longer evaluated.Supporting information
tbain/779_archive_locked_object_tag_deletes.Testing instructions
Automated:
The new tests (10 tests, 12 cases):
They cover: a status write locks the association; a second learner on a locked association succeeds and it stays locked; content with no criteria never reaches the lock call; a criterion whose rule cannot be applied leaves its association unlocked; the lock is exactly one
UPDATEand a constant threeoel_tagging_objecttagqueries for one criterion and for three; a rolled-back write is locked inside the transaction and unlocked after; a lock that raises rolls the status write back; an existing status row still gets its association locked; deleting the locked association archives it whileget_tags_locked_for_deletion()andis_taxonomy_locked_for_deletion()report the tag and taxonomy protected before and after, neither model carries adeletion_lockedfield, and a freshly loaded status for the learner dereferences to the archived row; and an association archived after one learner earned a status through it records nothing for a later learner while the first learner's status still dereferences to the archived row.The full suite:
pytest --no-cov tests(1437 tests: 1436 pass, 1 is skipped).pylint,mypy,pycodestyle,isort --check-only, andlint-imports.Manual, in a Django shell. This creates rows in your dev database.
Deadline
None.
Other information
select_for_update()ordering, not by a test here..importlinterhas no contract that forbidsopenedx_learningfrom importingopenedx_tagging.models(the module already does, forTagandTaxonomy), so "import only fromopenedx_tagging.api" is honored by reading, andlint-importspasses trivially.record_graded_object_statuses(), thenlock_object_tags_for_deletion()insrc/openedx_tagging/api.pyfor the query pattern the count test asserts, then the test module top to bottom.backend-code-reviewskill found the archived-association gap. I reviewed the change and ran the tests and checks locally.🤖 Generated with Claude Code