Skip to content

feat: add blocked-courses endpoint for a competency - #7

Closed
alezconsultant wants to merge 2 commits into
alezconsultant/666-competency-hierarchy-dominancefrom
alezconsultant/796-blocked-courses
Closed

alezconsultant wants to merge 2 commits into
alezconsultant/666-competency-hierarchy-dominancefrom
alezconsultant/796-blocked-courses

Conversation

@alezconsultant

@alezconsultant alezconsultant commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Description

Implements openedx#796: a read-only endpoint that lists the courses where a competency
can't get a new CompetencyCriterion, because a taxonomy ancestor or descendant of it already has
one there.

GET /api/cbe/v1/competencies/<int:tag_id>/blocked-courses/[?course_keys=<key>,<key>]

Each row carries the blocked course's key and the conflicting tag's id and value:

{"count": 1, "next": null, "previous": null, "results": [
  {"course_key": "course-v1:Org+Course+Run", "conflicting_tag_id": 12, "conflicting_tag_value": "Critical Thinking"}
]}

This PR is built on top of openedx#875 (pr), which provides get_courses_with_relative_criteria and the
tagging hierarchy helpers.

CompetencyReadPermission is copied verbatim from openedx#878 (openedx#681), so this PR doesn't depend on
openedx#878. The two copies are identical, so whichever PR merges second reuses the class from the
first, and the merge conflict there resolves to the same lines.

Changes

Testing instructions

Automated: pytest tests/openedx_learning/applets/cbe tests/openedx_tagging/test_api.py --no-cov.

Manual, against a Tutor devstack with this branch's openedx-core mounted. Mount the CBE API in
your local cms/urls.py with path('api/cbe/', include('openedx_learning.urls')), run
tutor dev exec cms ./manage.py cms migrate, and restart CMS. Log in as a staff user and get a JWT
Authorization header, then use three real course keys as Course X, Y and Z.

  1. Create a competency taxonomy (POST /api/content_tagging/v1/taxonomies/ with
    "taxonomy_type": "competency") with the tags "Cognitive Skills" > "Critical Thinking" >
    "Inference", plus "Analysis" under "Critical Thinking" as a sibling of "Inference".
  2. Create criteria with POST /api/cbe/v1/competencies/<tag_id>/criteria/: "Critical Thinking" in
    Course X, "Cognitive Skills" in Course Y, "Analysis" in Course Z.
  3. GET /api/cbe/v1/competencies/<inference_id>/blocked-courses/. Expect 200 with Course X naming
    "Critical Thinking" and Course Y naming "Cognitive Skills". Course Z doesn't appear, because
    "Analysis" is a sibling of "Inference".
  4. GET the same endpoint for "Critical Thinking". Expect Course Y naming "Cognitive Skills" and
    Course Z naming "Analysis". Course X doesn't appear, because a tag's own criterion doesn't
    block it.
  5. Repeat step 3 with ?course_keys=<Course X>. Expect only Course X, with count 1.
  6. Repeat step 3 with ?page_size=1. Expect count 2, one row, and a next link that returns
    the other course.
  7. Repeat step 3 with ?course_keys=not-a-key and with ?course_keys=. Expect 400 for both.
  8. GET the endpoint for tag_id=999999. Expect 404.
  9. Disable the taxonomy and GET the endpoint as a non-staff user. Expect 403.

Other information

  • Public API: one new UNSTABLE function on openedx_learning.api. The change is purely additive.
  • BlockedCourseSerializer and the query-params serializer carry
    # pylint: disable=abstract-method, the same as the plain Serializer subclasses in
    openedx_tagging and openedx_content.
  • AI usage: implemented with Claude Code (Claude Opus 5.5), then reviewed and tested by the author.

Related to openedx#796

🤖 Generated with the help of Claude Code

@alezconsultant
alezconsultant force-pushed the alezconsultant/666-competency-hierarchy-dominance branch from 4957939 to d5091d0 Compare October 26, 2026 13:01
@alezconsultant
alezconsultant force-pushed the alezconsultant/796-blocked-courses branch from 727cc84 to 2e373c5 Compare October 27, 2026 10:22
@alezconsultant
alezconsultant force-pushed the alezconsultant/666-competency-hierarchy-dominance branch 2 times, most recently from a8114ea to 7033c48 Compare October 28, 2026 10:58
alezconsultant and others added 2 commits October 28, 2026 07:29
Reject a CompetencyCriterion at creation time if any taxonomy ancestor or descendant of
its competency tag already has a live CompetencyCriterion in the same course. Siblings,
and a repeat criterion for the same tag in the same course, stay allowed. Archived
criteria, and criteria under an archived leaf, course-level, or root group, do not count,
since ADR 0002 hides archived rows from authoring and new associations.

Add to openedx_tagging/api.py: get_ancestor_tags(tag) and get_descendant_tags(tag), both
returning a QuerySet[Tag], mirroring Tag.descendant_count's own lineage-prefix mechanism
and its free-text-taxonomy guard.

Add to applets/cbe/api.py: get_courses_with_relative_criteria(tag_ids, course=None), a
public function mapping each course already blocked by one of the given tags to the
lowest-id conflicting tag, narrowed to one course when course is given. It only
counts criteria on a leaf group, whose parent carries the course, so a criterion
misplaced on another group is skipped rather than failing every create for the tag. Add
_validate_containment(group, course), called from create_competency_criterion() before
the object is tagged.

create_competency_criterion() takes the caller's course_run instead of re-parsing
object_id and fetching the course run a second time, and its rule fields are
keyword-only. It runs inside the transaction and locks that
associate_competency_criterion() holds.

Add CompetencyCourseRun, a CBE-owned row whose primary key is a OneToOneField to the
catalog's CourseRun (migration 0016). associate_competency_criterion() creates it with
get_or_create() on a course's first criterion and locks it with select_for_update()
right after the Tag lock, so two concurrent creates in the same course cannot both pass
the containment check. Locking this row instead of CourseRun keeps the lock from
blocking other apps' inserts that reference the course run, and keeps openedx_learning
from querying catalog tables directly.

Related to openedx#666

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds GET api/cbe/v1/competencies/<tag_id>/blocked-courses/, which lists
the courses where a taxonomy ancestor or descendant of the competency
already has a criterion, so authoring can gray them out before the
create-criterion endpoint rejects them. An optional comma-separated
course_keys parameter narrows the list, and the result is paginated.

get_blocked_courses() reuses get_courses_with_relative_criteria(), so this
read and the write-side check always agree. CompetencyReadPermission is
copied verbatim from the criteria-groups endpoint's PR.

Related to openedx#796

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alezconsultant
alezconsultant force-pushed the alezconsultant/796-blocked-courses branch from 2e373c5 to f36c0a2 Compare October 28, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant