Repository navigation
Alezconsultant/665 criterion endpoint on 846 - #1
alezconsultant wants to merge 2 commits into
Conversation
tbain
left a comment
There was a problem hiding this comment.
I didn't see anything in my human review, but Codex points out a few questions/bugs/concerns
-
api.py:196is defaulting to a system-wide default profile rather than one that has tighter scope which is what the AC asks for. However, I recall from another review that the other scopes have not been implemented? If we need to adjust this to match the ticket AND the other profiles do exist, please fix, and if not can you just confirm that that hasn't been implemented yet, or other technical reason why this isn't done? -
serializers.py:81returnsobject_tag_id, but the current ticket’s response contract requiresoel_tagging_objecttag_id. Clients following the ticket contract will not find the required field. I seem to recall we had this on another ticket and this is fine. I'm tempted to ignore this one, I think your implementation is correct, but can you confirm? -
Codex is concerned, and it makes sense, that the duplicate check at
api.py:253happens outside the atomic lock on lineapi.py:273. This seems like it might be a valid concern (albeit very niche); we can do a duplicate check, have it find no duplicates, and then between the time of the check and the actual atomic transaction another process could insert what would have been a duplicate and cause a conflict. Do we need to move the duplicate check into the atomic transaction lock?
Raw Codex output:
Details
Fresh review completed from the live PR and issue state; all earlier findings were discarded.Findings:
-
[P1] Taxonomy-scoped rule profiles are ignored
[api.py:196](https://github.com/javoconsultant/openedx-core/pull/1/files)always selects the system-default profile when rule fields are omitted. The current issue requires using the taxonomy-scoped profile when one exists, falling back to the system default only otherwise. -
[P1] Response uses the wrong object-tag field name
[serializers.py:81](https://github.com/javoconsultant/openedx-core/pull/1/files)returnsobject_tag_id, but the current ticket’s response contract requiresoel_tagging_objecttag_id. Clients following the ticket contract will not find the required field. -
[P2] Duplicate criteria can be created concurrently
[api.py:253](https://github.com/javoconsultant/openedx-core/pull/1/files)performs duplicate detection outside the transaction, and[api.py:273](https://github.com/javoconsultant/openedx-core/pull/1/files)does not re-check it under lock. Concurrent requests can both pass validation and create duplicate criteria for the same object/tag/group combination. No uniqueness constraint protects this association.
Validation:
- Live issue fetched at
2026-09-30T15:51:10Z. - Live PR head:
a1f37191d116ce1bbab6c3aaf1a0b757262a12a6. git diff --check: passed.- Python compilation: passed.
- Targeted pytest collection was blocked by the environment missing
openedx_events; no test results are claimed.
|
tbain
left a comment
There was a problem hiding this comment.
Approved - I found no new issues and neither did Codex. Thanks for addressing that issue!
2c888e6 to
2ca89b7
Compare
ad50631 to
545aa7a
Compare
Adds GET api/cbe/v1/rule_profiles/, a paginated, read-only collection
of every non-archived CompetencyRuleProfile, and the REST scaffolding
that later CBE endpoints will share: the applets/cbe/rest_api package,
openedx_learning/urls.py, and openedx_learning/rules.py. Each row
reports id, scope_type (derived from the scope columns), rule_type,
rule_payload and archived; scope_code and the scope foreign keys stay
internal. The endpoint is UNSTABLE until the rest of the rule profile
API lands.
Reading is gated on a new openedx_learning.view_competencyruleprofile
permission, registered as rules.is_authenticated &
can_view_competency_rule_profile. Any signed-in user may list profiles
and read the system default. A taxonomy-scoped profile is as visible
as its taxonomy, checked through has_perm("oel_tagging.view_taxonomy")
so that openedx-platform's org-aware override applies. Course- and
organization-scoped profiles are refused to everyone for now (openedx#645).
The list does not yet filter rows per profile; that has to change when
the first scoped profile is created.
A consuming project includes openedx_learning.urls under a prefix of
its choice (openedx-platform uses api/) and needs
rules.apps.AutodiscoverRulesConfig in INSTALLED_APPS and
rules.permissions.ObjectPermissionBackend in AUTHENTICATION_BACKENDS,
or every caller is refused. See src/openedx_learning/README.rst.
Closes openedx#773.
Co-authored-by: javoconsultant <jmeliev_consultant@flex.unicon.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2ca89b7 to
e149b53
Compare
Add POST /api/cbe/v1/competencies/<int:tag_id>/criteria/, associating a gradeable subsection with a competency: attaches a CompetencyCriterion to an existing leaf CompetencyCriteriaGroup when group_id is supplied, or derives/creates whatever part of the root/course-level/leaf hierarchy doesn't yet exist for this competency and course when it isn't. Add to applets/cbe/api.py: resolve_competency_tag(), resolve_or_create_leaf_group(), resolve_supplied_leaf_group(), create_competency_criterion(), and associate_competency_criterion() as the single public entry point. Group creation is made race-safe by locking the competency's Tag row with select_for_update() inside the transaction, so two concurrent requests cannot each create a root or course-level group for the same tag. MySQL has no partial unique indexes, so the root group cannot be backed by a constraint. Add migration 0014: a UniqueConstraint on CompetencyCriteriaGroup (tag, course, parent) that backstops course-level group creation. Add CompetencyCriterionSerializer and CompetencyCriterionCreateView to the existing CBE REST module (serializers.py/views.py/urls.py). Reuses oel_tagging.can_tag_object for authorization rather than a new DjangoObjectPermissions class: every such class already in this codebase only ever contributes the authentication gate, since its underlying rules predicate returns True whenever called with no object (the case DRF's has_permission() always hits), so copying that shape here would mean registering a new, otherwise-unused permission for no behavioral gain. Deviations from the ticket's literal text, developer-approved: - competency_rule_profile_id resolves to the seeded system-default CompetencyRuleProfile's id when all three rule fields are omitted, rather than persisting null: CompetencyCriterion's own profile_xor_override_check constraint never allows all three fields null, and ADR 0002 Decision 4 resolves a criterion's profile to a concrete row at creation time, never leaves it unresolved. - The duplicate-association check (reject a second criterion for the same tag/object pair) stays an application-level check with no backing DB constraint. It runs under the same Tag row lock as group creation, with locking reads, so a duplicate committed by a concurrent request is seen under MySQL REPEATABLE READ. ADR 0002's own worked example requires the same ObjectTag to be referenced by more than one CompetencyCriterion across different groups under the same competency, so a schema-level UniqueConstraint would foreclose that documented capability for every current and future caller, not just this endpoint. tag_object() replaces the full tag list for a taxonomy/object_id pair rather than appending, so create_competency_criterion() reads the object's existing tags first and unions in the new one, rather than risking silently dropping a sibling competency's tag applied to the same object under the same taxonomy. Tests cover every acceptance-criteria scenario in openedx#665, plus the concurrent-request race case for group creation, the duplicate-association case, and the atomic-rollback case for a downstream criterion-creation failure. Related to openedx#665 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
e149b53 to
f17ecbf
Compare
Description
Stacked on openedx#846 ; review only the last commit
Adds the create-criterion endpoint from openedx#665:
POST /api/cbe/v1/competencies/<int:tag_id>/criteria/, letting a course author associate agradeable subsection with a competency, either by attaching it into an existing leaf
CompetencyCriteriaGroupor by deriving/creating whatever part of the root/course-level/leafgroup hierarchy doesn't yet exist for this competency and course.
Changes
src/openedx_learning/migrations/0004_criteria_group_unique_constraints.py(new): twopartial
UniqueConstraints onCompetencyCriteriaGroup— one root pertag(
condition=Q(parent__isnull=True)), one course-level group per(tag, course, parent)(
condition=Q(course__isnull=False)). Matching constraints added toCompetencyCriteriaGroup.Metainmodels/criteria.py.src/openedx_learning/applets/cbe/api.py: addsresolve_competency_tag(),resolve_or_create_leaf_group(),resolve_supplied_leaf_group(),create_competency_criterion(), andassociate_competency_criterion()(the single publicentry point this ticket's view calls).
src/openedx_learning/applets/cbe/rest_api/v1/serializers.py: addsCompetencyCriterionSerializeralongside the existingCompetencyRuleProfileSerializer.object_id/group_id/logic_operatorare plain write-only fields (none of them areCompetencyCriterionmodel fields);competency_rule_profile_id,competency_criteria_group_id, andoel_tagging_objecttag_idare the ticket's contractedJSON names, mapped via explicit
source=to the model's actual attributes(
rule_profile_id,group_id,object_tag_id).src/openedx_learning/applets/cbe/rest_api/v1/views.py: addsCompetencyCriterionCreateView(generics.CreateAPIView)alongside the existingCompetencyRuleProfileView.src/openedx_learning/applets/cbe/rest_api/v1/urls.py: addscompetencies/<int:tag_id>/criteria/, a plainpath()(not a second routerregistration — the URL carries a path parameter mid-route, so it isn't a flat
router-friendly resource like
rule_profiles).tests/openedx_learning/applets/cbe/test_api.py,test_views.py,test_criteria_models.py: unit and DRF integration tests (see Tests below); onepre-existing test in
test_criteria_models.pyupdated to use two tags instead of one,since it previously created two root groups for the same tag, which the new constraint
now correctly rejects.
Tests
One test per scenario in openedx#665's Acceptance Criteria (all 20): the full-hierarchy derivation
and reuse scenarios,
logic_operatorprovided/defaulted, thegroup_id/logic_operatormutual-exclusion rejection, every supplied-group validation rejection (non-leaf, wrong
competency, wrong course), all three duplicate-association rejections, the all-null-rule-
fields deviation above, the same-taxonomy tag-preservation scenario, every not-found/malformed
scenario, and the permission rejection. Plus unit tests for
resolve_or_create_leaf_group()and
resolve_supplied_leaf_group()directly, including two tests that pre-commit acompeting row out-of-band to prove the constraint-backed
get_or_create()fallback (standingin for a real concurrent request), two tests proving the constraints themselves exist via a
direct
IntegrityError, and one test proving the atomic block rolls back newly-created groupswhen the final
CompetencyCriterion.objects.create()call fails.Depends on
openedx-core#9 (openedx#773), which this branch is based on and which already
adds the CBE REST scaffolding (
openedx_learning/urls.py, therest_api/package,serializers.py/views.py/urls.py) this PR extends rather than creates. That PR is itselfbased on openedx#641/openedx#613's
CompetencyCriteriaGroup/CompetencyCriterion/CompetencyRuleProfilemodels.
Companion PR
A one-line
openedx-platformchange (cms/urls.py:path('api/cbe/', include('openedx_learning.urls')), mirroring the existingcontent_taggingmount) is aseparate PR against that repo, not included here.
Verification
pytest tests/openedx_learning/applets/cbe/ --no-cov: 147 passed.pylint,mypy,pycodestyle,isort --check-only,pydocstyle: all clean on everychanged file.
lint-imports: 2 contracts kept, 0 broken.make pii_checkcheck: 2 pre-existing, unrelated failures only(
openedx_content.Draft/PublishableEntityVersion, confirmed viagit stashto predatethis branch). Both models this PR touches already carry
.. no_pii:; the migration adds nonew fields.
openedx-platformpermission chain(a real
CourseStaffRoleholder granted access, an unpermitted user refused), isolated froman unrelated, pre-existing import failure elsewhere in that repo's
content_taggingapp —see the companion PR for details.
Related to openedx#665
Created with the help of Claude