Repository navigation
feat: reject new criteria under archived competency groups (#717) - #899
ufedaseyeuconsultant wants to merge 4 commits into
Conversation
|
Thanks for the pull request, @ufedaseyeuconsultant! This repository is currently maintained by 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 approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo 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:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere 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:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
|
Hi @ufedaseyeuconsultant! Just flagging that some branch conflicts have popped up. Would you mind taking a look? Thanks! |
9e5f022 to
cd9c511
Compare
|
Hi @ufedaseyeuconsultant , this is the AI-generated Code Review (raw - I haven't edited / triaged it yet). |
cd9c511 to
ea5785b
Compare
There was a problem hiding this comment.
Hi @ufedaseyeuconsultant , thanks for the PR. Looks good!
I've reviewed the PR. Everything tested well.
The AI code review found some things that look worth addressing. Can you go through review-pr-899.md and address its findings?
Also the AI code review did flag a point that I think is worth a look:
"Ticket #717's user story and scenario 7 promise that an old archived association never blocks a new one, but the ticket's description, its out-of-scope list, and ADR 0002 keep a course with an archived course-level group closed, and the code follows the closed-course side."
The ticket itself contradicts itself here, so this is probably best settled in the ticket discussion, and the answer may affect the PR.
One more thing: the PR description says you left a comment on #717 about this, but I couldn't find one. Could you double-check the description and make sure everything is correct? Sometimes AI tools can slip details like this in.
Another thing in the PR description is the point:
"Supplied path: an active leaf that holds an archived criterion for the same content now accepts a second criterion for it, because the duplicate check ignores archived criteria. The ticket describes re-association only on the derive path. I've asked on #717 whether this is intended. No test pins it yet."
I think there's no question for that in 717 either, could you check that as well?
Thank you!
Creating a competency criterion honors CompetencyCriteriaGroup.archived and CompetencyCriterion.archived, per ADR 0002 Decision 7: - A supplied group_id naming an archived leaf is rejected with a 400 keyed group_id. - On the derive path, an archived course-level group for the object's course is rejected with a 400 keyed object_id. No sibling group is created in its place. - On the derive path, an archived root is reactivated in place, so archiving a competency's last course leaves the competency open to new criteria. A rejection later in the same request rolls the reactivation back. - An archived criterion does not count as a duplicate association, so its preserved ObjectTag does not block re-associating the content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The supplied-path leaf check looked only at the leaf's own archived flag. A leaf written outside the API under an archived course-level group was accepted, while the derive path for the same course was rejected. resolve_supplied_leaf_group() now also rejects when the leaf's course-level group is archived, keyed "group_id". The check mirrors get_courses_with_relative_criteria(), which already treats such a leaf as dead. Comment and docstring corrections: - _create_leaf_group() states what the root reactivation and the two archived checks rely on: the caller's tag lock serializes them against other creates, but not against the archive cascade until openedx#674 and openedx#675 take the same lock. - Both get_or_create() lookups say why they match archived rows, and the root reactivation cites ADR 0002 Decision 2 (openedx#872). - The duplicate check comment says any active criterion is a duplicate. - associate_competency_criterion() says a failure also rolls back a root reactivation and lists the archived-group rejections with their error keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a view test for scenario 6 of openedx#717: a supplied group_id of an active leaf that holds only an archived criterion for the same content returns 201. Add a unit test that a request rejected by the hierarchy-dominance check after the root was reactivated leaves the root archived and creates nothing. Test hygiene: - Drop the unit test for the archived course-level group rejection, which the scenario 4 view test and the two scenario 3 associate_competency_criterion tests already cover. - Replace two assertions that cannot fail (new id differs from old id) and one redundant history-count assertion. - Compare ObjectTag reuse in the scenario 7 view test. - Rename the whole-branch test for what it pins, cite scenario 3, and state in both scenario 3 docstrings that they guard the transaction.atomic() wrapper. - Give the archived course-level group view test a docstring with the error key and the no-leaf outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@jesperhodge Addressed everything from the review file: rebased onto current main (the conflict was the one docstring; it now also says what the reactivation relies on for serialization), the stale duplicate-check comment is fixed, the supplied path now also rejects a leaf under an archived course-level group, the get_or_create rationale is back with the ADR 0002 / #872 citation, and the supplied-path re-association got its view test plus a rollback test through main's dominance check. The two ticket questions you couldn't find are now actually posted on #717 - sorry about that, the description claimed them before they existed. The description is rewritten: stale counts and claims fixed, the planned ADR amendment dropped (it already landed via #872), and the 400-on-create vs 409-on-update difference recorded as deliberate. On the contradiction: Mary already accepted the closed-course consequence for MVP on #674, so I proposed on #717 to narrow the two user-story bullets and scenario 7 accordingly. |
ea5785b to
e1cf491
Compare
|
@ufedaseyeuconsultant Please see my comment response on the ticket since I am requesting that an archived criterion blocks the creation of a new criterion for the same content in the same group instead of allowing it. #717 (comment) |
Per the ticket owner's decision on openedx#717, _reject_duplicate_criterion() no longer filters out archived criteria. An archived criterion for a content keeps blocking a new criterion for the same content in the same group, and, when no group_id is supplied, anywhere under the competency. ADR 0002 makes archiving permanent below the root for the MVP, and keeping at most one criterion row per content per group leaves the data clean for a post-MVP unarchive. A different explicit group stays a deliberate second association. The 400 now names the archived state when every matching criterion is archived: the object_id or group_id key is unchanged, and the message says the content was previously associated and the association was archived. When an active criterion matches, the plain duplicate messages apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@mgwozdz-unicon @jesperhodge Implemented Mary's decision from the ticket: the archived=False filter is gone from the duplicate check, so an archived criterion blocks a new criterion for the same content again - in its group when group_id is supplied, and anywhere under the competency on the derive path. When every blocker is archived, the 400 now says the association was archived instead of the plain duplicate message, so an author who can't see the content doesn't read it as a bug. Tests flipped accordingly: same-group re-add is a 400, the derive-path re-add is a 400, a different active group still gets a 201, and a mixed active-plus-archived case keeps the plain duplicate message. This supersedes the re-association part of my previous comment - the view test that pinned the 201 there now pins the 400. The PR description is updated to match, and the suite is green |
|
approved it and moved it to @mgwozdz-unicon since you added a comment there. |
Description
Closes #717.
POST /api/cbe/v1/competencies/<tag_id>/criteria/creates aCompetencyCriterionfor a piece of course content. It either uses a leaf group the client supplies asgroup_id(the supplied path), or derives the root, course-level, and leaf groups for the competency and the content's course (the derive path). Until now that flow ignored thearchivedflag onCompetencyCriteriaGroupandCompetencyCriterion. ADR 0002 says archived groups and criteria are hidden from authoring and new associations. With this PR, creating a criterion behaves as follows:group_idthat names an archived leaf is rejected with a 400 keyedgroup_id: "group_id references an archived CompetencyCriteriaGroup, which no longer accepts new associations."object_id: "The course-level CompetencyCriteriaGroup for this competency and course is archived and no longer accepts new associations." No replacement course-level group is created, because the unique constraintoel_cbe_criteria_group_one_course_group_per_tag_courseallows only one per competency and course.archived=False) and reused. A root is archived once every course-level group under it is archived, so without this, archiving a competency's last course would block new criteria for every course. The root's archived children stay archived. If a later check in the same request rejects it, the transaction rolls the reactivation back.group_idis supplied, and anywhere under the competency on the derive path. When every blocking criterion is archived, the 400 says the content was previously associated and the association archived, so the author is not left staring at a duplicate error for content they cannot see. A different active group still accepts the content, as a deliberate second association. Decided by the ticket owner in [BE] Enforce archived in Competency Criteria creation #717 (comment): archiving is permanent below the root for MVP, and one criterion row per content per group keeps the data clean for a post-MVP unarchive.How it works
All changes are in
src/openedx_learning/applets/cbe/api.py:_create_leaf_group()(the derive path, which the ticket callsresolve_or_create_leaf_group()) reactivates an archived root right after itsget_or_create(), and rejects an archived course-level group right after itsget_or_create(). The leaf is always newly created there, so it needs no check. The reactivation usessave(), sosimple_historyrecords it.resolve_supplied_leaf_group()adds the archived checks after its existing leaf, tag, and course checks, so a structurally wronggroup_idkeeps its existing error. It rejects an archived leaf and also a leaf whose course-level parent is archived: the parent is already loaded, so the check costs no query, and it defends against rows written outside the API, mirroringget_courses_with_relative_criteria()._reject_duplicate_criterion()counts every criterion on the content'sObjectTag, archived or not, and picks the 400 message by whether an active one is among the blockers.transaction.atomic()block inassociate_competency_criterion(), under the existing row lock on the competencyTag.No signature, migration, serializer, view, or import-layer change.
associate_competency_criterion()andresolve_supplied_leaf_group()are marked UNSTABLE, so the new rejections need no deprecation.Three details differ from the ticket's Technical Details:
ValidationError, asassociate_competency_criterion()'s other rejections do (the module also raisesCompetencyCriterionArchivedErrorandPermissionDeniedelsewhere), not DRF's. The ticket expected a 500 from Django's exception, butCompetencyCriterionCreateViewdoes not use the tagging exception handler the ticket cites. It translates Django'sValidationErrorinto a DRF 400 itself (rest_api/v1/views.py), so the client gets the same 400 with the field key, andapi.pystays free of HTTP-layer types._reject_if_archived()helper, because they use different keys and messages and there are only two callers.object_id, because the request has nogroup_idand the content's usage key is what selects the archived course-level group.Who is affected
associate_competency_criterion(): the same behaviors, raised as DjangoValidationError.Supporting information
archivedfields this PR checks (merged).docs/openedx_learning/decisions/0002-competency-criteria-model.rst, Decisions 2 and 7: archived groups and criteria are hidden from authoring and new associations.claude-opus-5-5) planned the change and reviewed it; Claude Sonnet 5.5 sub-agents wrote the code and tests under that plan. I reviewed the result.Testing instructions
Automated:
pytest tests/openedx_learning/applets/cbe -o addopts=""(594 tests at this branch's head;-o addopts=""skips the coverage flagstox.iniadds).lint-imports,pylint,pycodestyle,isort --check-only,pydocstyleandmypy --show-traceback(mypy also checks the tests).New tests (16 across
test_api.pyandtest_views.py), one per ticket scenario plus regression guards:group_iderror (unit and view). An active leaf next to an archived one in the same course-level group still resolves.object_iderror and nothing created, under an active root (unit and view) and under an archived root, where the root is still archived afterwards because the transaction rolled back.object_iderror.Each new test was checked by temporarily breaking the code line it covers and confirming it fails for the stated reason.
Manual, against a devstack, with a competency tag
<tag_id>and two course runs X and Y. The archived state can only be set from a Django shell today, because the archive endpoints are in #674 and #675.{"object_id": "<usage key in X>"}to/api/cbe/v1/competencies/<tag_id>/criteria/. Expect 201.archived=Truewithsave()on that criterion's leaf. POST again with{"object_id": "<another usage key in X>", "group_id": <that leaf id>}. Expect 400 with thegroup_idarchived message.{"object_id": "<usage key in X>"}. Expect 400 with theobject_idarchived message, and the root still archived.{"object_id": "<usage key in Y>"}. Expect 201, the root reactivated, and X's course-level group still archived.Other information
StudentCompetencyCriteriaGroupStatusrows on a reactivated root: the roll-up onmainreads and writes these rows with noarchivedfilter, so a reactivated root's old roll-up rows carry over and are read like any other root's. ADR 0004 only ever raises statuses automatically, and the competency-level status is keyed by user and tag, so the reactivation itself changes nothing for recorded learners. Whether old rows should be recomputed against rebuilt rules belongs to the roll-up work._create_leaf_group()cites it.Taglock this flow holds, so a concurrent create and archive serialize once those PRs land; until then the reactivation and the archived checks are serialized only against other creates.🤖 Generated with Claude Code