Skip to content

feat: add GET competency criteria-groups endpoint - #3

Closed
ufedaseyeuconsultant wants to merge 47 commits into
alezconsultant:alezconsultant/665-criterion-endpointfrom
ufedaseyeuconsultant:feat--681-get-competency-criteria-and-groups
Closed

ufedaseyeuconsultant wants to merge 47 commits into
alezconsultant:alezconsultant/665-criterion-endpointfrom
ufedaseyeuconsultant:feat--681-get-competency-criteria-and-groups

Conversation

@ufedaseyeuconsultant

Copy link
Copy Markdown

Description

Adds a read endpoint that returns a competency's full CompetencyCriteriaGroup tree (root,
course-level, leaf) and CompetencyCriterion leaves under it, as two flat lists:

GET /api/cbe/v1/competencies/<int:tag_id>/criteria-groups/

Studio's Manage & Apply Competencies authoring flow needs this to render a competency's
existing groups, criteria, and already-tagged subsections from persisted backend state,
instead of tracking or guessing at that state client-side, so the next write action (add or
remove an association) acts against ground truth.

An empty tree (the tag exists, nothing authored yet) is a valid 200 with two empty arrays,
not a 404. A 404 means the tag_id doesn't exist or isn't on a CompetencyTaxonomy; a
403 means the caller can't view that taxonomy.

Which roles this affects: Course Author and Platform Administrator, via Studio's
competency-management UI (read-only in this PR); Developer, as a new API surface to build
against (openedx#672, openedx#683 consume it next).

Scope note for reviewers: the CompetencyCriteriaGroup/CompetencyRuleProfile/
CompetencyCriterion models, migrations, and the create-criterion endpoint (openedx#665) that this
PR builds on haven't merged into main on their own yet, so this branch carries that full
foundation along with the new read endpoint. The commits from a6f3b04 through 5293666
are that inherited work, brought in from the branch where openedx#665 was developed; d3e8a7e is
this PR's own addition. That inherited history includes its own authors' fixes for two real
issues (a duplicate, redundant migration chain that briefly built the same schema twice, and
a handful of tests pinned to stale behavior) found and corrected independently on that branch
before it was merged in here; both were re-verified as part of stabilizing this branch, and
the full suite is green.

Public API additions: get_competency_criteria_tree(tag_id) in
openedx_learning.applets.cbe.api, CompetencyCriteriaGroupSerializer and
CompetencyCriterionReadSerializer in the CBE REST API's v1/serializers.py, and
CompetencyReadPermission in v1/permissions.py. No breaking changes; this whole surface is
new.

Supporting information

Testing instructions

  1. Apply migrations: python manage.py migrate openedx_learning.
  2. Create a competency taxonomy and tag, and attach at least one criterion under a course run
    (via POST /api/cbe/v1/competencies/<tag_id>/criteria/ from [BE] Create CompetencyCriteria openedx/openedx-core#665, or directly through the
    Python API: openedx_learning.api.associate_competency_criterion(...)).
  3. GET /api/cbe/v1/competencies/<tag_id>/criteria-groups/ as a user who can view the
    taxonomy. Expect 200 with:
    • groups: the root (parent_id and course_key both null), the course-level group,
      and the leaf, each with the correct parent_id.
    • criteria: the one criterion, with group_id matching the leaf, id, object_tag_id,
      object_id (the tagged subsection's usage key), rule_profile_id, rule_type_override,
      rule_payload_override.
  4. Archive that group or criterion directly (archived=True) and repeat step 3: confirm both
    are excluded from the response.
  5. GET the same URL for a tag_id that doesn't exist, and for one that exists but isn't on
    a CompetencyTaxonomy: expect 404 in both cases.
  6. GET as a user without view access to the taxonomy: expect 403.
  7. Automated coverage: pytest tests/openedx_learning/applets/cbe/.

Other information

irfanuddinahmad and others added 11 commits September 3, 2026 16:53
Modernize openedx-core to uv + pyproject.toml (PEP 621/735) +
python-semantic-release.

Part of openedx/public-engineering#506 (tracked in
openedx/public-engineering#516).

- Replace setup.py/setup.cfg with pyproject.toml (PEP 621 static
  metadata), version derived from git tags via setuptools-scm.
- Switch from pip-compile to uv with PEP 735 dependency groups;
  commit uv.lock.
- Update tox.ini to use tox-uv with uv-venv-lock-runner.
- Update CI to use astral-sh/setup-uv; SHA-pin all actions.
- Add python-semantic-release + release.yml, replacing the old
  token-based pypi-publish.yml with OIDC trusted publishing.
- src/ layout not touched -- this repo already used it, so no change
  was needed here.
- Delete setup.py, setup.cfg, .coveragerc, requirements/ (all
  .in/.txt files, constraints.txt, private.readme),
  .github/workflows/pypi-publish.yml.

Addresses salman2013's PR review feedback:
- Keep the CI job name as tests, matching the pre-migration name
  (deferring the per-toxenv rename to avoid breaking
  required-status-check matching).
- __version__ falls back to "unknown" on PackageNotFoundError instead
  of leaving it unset, with the except branch marked
  pragma: no cover since the package is always installed in CI.
- Remove the legacy, unused .readthedocs.yml (superseded by
  .readthedocs.yaml).
- Drop an unintended fetch-depth: 0 on the checkout step -- a
  leftover side effect of removing the separate setup-python step,
  not a deliberate choice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ernize-python-tooling

# Conflicts:
#	requirements/base.txt
#	requirements/ci.txt
#	requirements/dev.txt
#	requirements/doc.txt
#	requirements/quality.txt
#	requirements/test.txt
…penedx#803)

Add taxonomy_type as a write-only ChoiceField on TaxonomySerializer ("tags" default,
"competency" via a new TaxonomyType enum). TaxonomyView.perform_create() and
create_import() discard it after validation: openedx_tagging must never import
openedx_learning, so it can't act on it. create_import()'s taxonomy-creation step is
extracted into an overridable _create_taxonomy_for_import() hook for subclasses.

Add create_competency_taxonomy() to the CBE applet: creates the Taxonomy and linked
CompetencyTaxonomy row in one transaction, via save_base(raw=True) since save() would
re-save Taxonomy with unpopulated field values.

Add CompetencyTaxonomyView(TaxonomyView), overriding perform_create() and
_create_taxonomy_for_import() to dispatch to create_competency_taxonomy() when
taxonomy_type="competency". Lives here since this is the layer that can see both
openedx_tagging and openedx_learning.

See openedx#628

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mad/modernize-python-tooling

# Conflicts:
#	requirements/base.txt
#	requirements/ci.txt
#	requirements/dev.txt
#	requirements/doc.txt
#	requirements/pip-tools.txt
#	requirements/quality.txt
#	requirements/test.txt
#	src/openedx_core/__init__.py
…e job

Matches openedx/sample-plugin's current release.yml; the custom
OPENEDX_SEMANTIC_RELEASE_GITHUB_TOKEN secret isn't needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…openedx#818)

AI note: Claude slightly assisted with this, though most of it was written by me.

@tbain tbain left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. Bug: Codex points out that the serialization is changing the names of values unexpectedly; it states:
    The response field names do not match the contract consumed by tickets openedx#672/openedx#681. serializers.py:104 emits tag_id, while the contract requires oel_tagging_tag_id; serializers.py:125-126 emits group_id, object_tag_id, and rule_profile_id instead of competency_criteria_group_id, oel_tagging_objecttag_id, and competency_rule_profile_id. After camel-casing, the frontend receives groupId rather than the expected competencyCriteriaGroupId, so it cannot associate criteria with groups or resolve their rule metadata. The new tests assert these incorrect names at test_views.py:676-679, masking the contract break.

Is the resolution for these naming changes resolved elsewhere and this is expected, or is this a genuine bug and we need to adjust these names to match expected names for these attributes?

  1. Improvement: Comments in src/openedx_learning/applets/cbe/rest_api/v1/serializers.py seem, perhaps, overly wordy and I'm not sure they convey anything of great value - maybe we can cut these down or outright delete them? Unless this is the standard for these sorts of files?

  2. Improvement: Comment in src/openedx_learning/applets/cbe/rest_api/v1/views.py line 136 is unnecessary.


Raw AI output:

Details ## Verdict

Request changes.

Findings

  • [P1] Bug — response field names do not match the agreed API contract. serializers.py:104 emits tag_id, while the contract requires oel_tagging_tag_id. serializers.py:125-126 emits group_id, object_tag_id, and rule_profile_id instead of competency_criteria_group_id, oel_tagging_objecttag_id, and competency_rule_profile_id. After camel-casing, the frontend receives groupId rather than competencyCriteriaGroupId, so it cannot associate criteria with groups or resolve their rule metadata. The tests assert these incorrect names at test_views.py:676-679, masking the contract break.

Checks

  • git diff --check: passed
  • Python compilation of changed files: passed
  • Focused pytest run: could not start because openedx_events is missing from the environment
  • PR description includes AI attribution and manual testing steps

Makefile now assumes an already-synced/activated local env; uv run
stays in CI workflow steps only. See openedx/ccx-keys#190 (review
comment r4097501942).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ernize-python-tooling

# Conflicts:
#	requirements/base.txt
#	requirements/ci.txt
#	requirements/dev.txt
#	requirements/doc.txt
#	requirements/quality.txt
#	requirements/test.txt
@ufedaseyeuconsultant

Copy link
Copy Markdown
Author

@tbain 2 and 3 done
About the first one. In several tickets, there is a discrepancy between the field names listed in them and the actual field names that were agreed upon after discussion and have already been implemented in tickets 672, 665, and this one.

Also including detailed investigation of Claude Checked this against the real downstream consumer rather than against the ticket's own pseudocode: fork AShatsila/frontend-app-authoring, branch ashatsila/672-manage-and-apply-competencies (commit 9f76732e), the actual implementation for openedx#672.

src/taxonomy/competency-management/data/types.ts defines CompetencyCriteriaGroupsResponse, explicitly commented as "openedx#681's documented response shape", and it matches the current implementation field-for-field: CompetencyCriteriaGroup reads tagId, parentId, courseKey; CompetencyCriterion reads groupId, ruleProfileId. Not oelTaggingTagId, competencyCriteriaGroupId, or competencyRuleProfileId. object_tag_id isn't even modeled on this GET response; the frontend only reads objectId.

This isn't just a type declaration nobody consumes. The actual usage site confirms it: CompetencyAssociationsContext.tsx:322 reads criterion.groupId when writing focus after a create call, not criterion.competencyCriteriaGroupId.

There is a real documentation gap here, but it's not in this code: ticket openedx#672's own "Implementation specifics" section states that fields read back from openedx#681 are competencyCriteriaGroupId-style, which matches openedx#681's own stale AC text but not what either side actually built. Both openedx#681's AC and openedx#672's description appear to have been written before implementation, against the ticket's original pseudocode, and neither was updated once the two sides converged on the shorter names during actual implementation.

Given that, I'm leaving serializers.py and the tests as they are: renaming to the long form would break the real, already-built openedx#672 integration rather than fix a contract mismatch. I'll follow up separately on getting openedx#681's and openedx#672's written text corrected so this doesn't mislead the next reviewer.

@ufedaseyeuconsultant
ufedaseyeuconsultant force-pushed the feat--681-get-competency-criteria-and-groups branch from d3e8a7e to 46df87f Compare September 29, 2026 17:27
irfanuddinahmad and others added 2 commits September 30, 2026 15:03
… published deps

The last real release (1.4.0) published Django, edx-drf-extensions, and
edx-organizations with no version bounds at all, since pre-migration
setup.py read requirements/base.in (unpinned) rather than base.txt or
constraints.txt. The pyproject.toml dependencies list introduced by the
uv migration added bounds that never existed in published metadata --
most importantly Django<6.0, which would block openedx-platform (or any
other consumer) from resolving Django 6 until openedx-core cuts a new
release.

The edx-drf-extensions>=10.7.0 lower bound also didn't match history: it
came from a stale requirements/base.txt snapshot on this branch at the
time pyproject.toml was authored. The actual last-released pin (v1.4.0
tag) was 10.8.0, and the latest pre-migration pin on main (before
requirements/base.txt was deleted by the migration) was 10.9.0. Neither
matches 10.7.0, and none of these pins were ever part of published
Requires-Dist in the first place.

Restore all three to unbounded, matching PyPI's published metadata for
1.4.0. Django<6.0 is still enforced for this repo's own lockfile/test
resolution via [tool.uv].constraint-dependencies, which only affects
this repo and does not propagate to consumers.

uv.lock regenerated; resolved package set is unchanged (specifiers only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tox.ini's envlist listed a lint-imports entry with no matching
[testenv:lint-imports] section, so it silently fell back to the generic
[testenv] and just re-ran pytest instead of linting imports. This also
meant Makefile's `make quality` (which delegates to `tox -e quality`)
never actually ran import-linter locally.

Delete the bogus envlist entry and instead run `lint-imports` as a step
in [testenv:quality], right after isort. import-linter is already pulled
in via the quality dependency-group (through test -> test-base), so no
new dependency is needed.

.github/workflows/lint-imports.yml is intentionally left in place: it is
currently a required branch-protection status check on main. Removing it
now would leave that required check permanently stuck at "Expected --
Waiting for status" on this PR's head commit, deadlocking the PR. Per
feanil, that workflow should only be removed once quality's lint-imports
step is confirmed working and he lifts the branch-protection requirement
on "Lint Python Imports" (since tox -e quality is already a required
check and will cover the same ground).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ci.yml: added --locked to uv sync/uv run tox steps. Verified
  uv sync --locked --group ci passes cleanly.
- pyproject.toml: the comment next to the unbounded Django/etc.
  dependencies pointed at [tool.uv].constraint-dependencies as the
  place to add a repo-specific bound -- that block is tool-generated
  output (edx_lint write_uv_constraints), not an editing target.
  Corrected to point at [tool.edx_lint].uv_constraints instead, and
  noted Django<6.0 is already covered globally via edx-lint's
  common_constraints.txt (confirmed directly), not something this
  repo added itself.

Per feanil's review on openedx#704: discussion_r4147327735, r4147327755.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@tbain tbain left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved from me, and Codex has no further comments or corrections

feanil and others added 5 commits October 1, 2026 10:27
…odernize-python-tooling

feat: modernize Python tooling to uv + pyproject.toml + semantic-release
lint-imports already runs as part of the quality tox env in ci.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#800)

Add the CBE criteria models: CompetencyRuleProfile, CompetencyCriteriaGroup
and CompetencyCriterion (the criteria tree's AND/OR node and leaf), the
rule payload contract and its validation, and
CompetencyTaxonomy.taxonomy_overrides_org. Delete behavior cascades from
tags, groups and object tags. Register django-simple-history, layer
openedx_catalog beside openedx_content in the import contracts, and record
the scope_code and on_delete decisions in ADR-0002.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Every tag now always carries an external identifier (the Competency ID shown
on the Competency Management page), whether an institution assigns one or the
system generates it from the tag's name. Previously a tag only got an
external_id via file import; one created through the taxonomy editor or the
tagging API had none at all.

Tag.save() generates a candidate from the tag's value when external_id is
empty, resolving collisions with a casefold-based per-taxonomy uniqueness
check and a numeric suffix. Taxonomy.add_tag() rejects a caller-supplied
duplicate (exact or case-different) with ValueError instead of letting it
reach the database as an IntegrityError.

A new migration backfills every existing null or empty-string external_id, in
capped in-memory and SQL batches, without overwriting institution-assigned
ones, then makes the column NOT NULL; rollback only loosens that constraint
and leaves generated data in place. A stale pre-upgrade export whose id
collides with another tag's real external_id still fails safely on
re-import, tripping a co-triggered rename action's existing duplicate-value
check; this behavior and its narrower residual limitation are documented in
ADR 0012.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…emove-lint-imports-workflow

build: remove redundant lint-imports workflow
edx-requirements-bot and others added 3 commits October 5, 2026 11:19
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 9.0.0 to 10.2.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@c771a70...c18668a)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 10.6.2 to 10.7.0.
- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases)
- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.rst)
- [Commits](python-semantic-release/python-semantic-release@9a026e9...b700dbe)

---
updated-dependencies:
- dependency-name: python-semantic-release/python-semantic-release
  dependency-version: 10.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
mgwozdz-unicon and others added 22 commits October 7, 2026 10:10
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>
…/python-semantic-release/python-semantic-release-10.7.0

build(deps): bump python-semantic-release/python-semantic-release from 10.6.2 to 10.7.0
…/astral-sh/setup-uv-10.2.0

build(deps): bump astral-sh/setup-uv from 9.0.0 to 10.2.0
Add the last two of openedx#642's four learner-status models.
StudentCompetencyCriterionStatus holds a learner's mastery status for
one leaf criterion, and StudentCompetencyCriteriaGroupStatus holds it
for one criteria group. Each holds one row per learner and criterion,
or per learner and group, updated in place, and accepts all three
status values. Deleting a user deletes their status rows, and a
criterion, group, object tag, tag or taxonomy with status rows beneath
it cannot be deleted.

Closes openedx#642.

Co-authored-by: Jesper Hodge <jhodge@unicon.net>
A taxonomy re-import row can change an existing tag's external_id in place by
setting a previous_id column alongside its new id: when previous_id matches
an existing tag's external_id and the row's id differs, the import renames
that tag instead of deleting it and creating a new one, preserving its
primary key, its associations, and its import/export identity (see ADR 0010).

Renames within one import are order-independent, including a swap or longer
cycle where two or more tags trade external_id values in the same file: any
tag whose current external_id is another row's rename target is staged
through a temporary placeholder before the row claiming that id executes,
since (taxonomy, external_id) is a per-statement unique constraint. A rename
or parent update resolves its target tag by primary key rather than
re-resolving by external_id at execute time, so a tag already renamed earlier
in the same import is still matched correctly. This staging covers
external_id only; a row that also tries to take the other tag's current
value in the same swap is still rejected, since value carries the same
per-taxonomy uniqueness without the same staging treatment.

The import plan rejects, before any action executes: two or more rows
sharing the same previous_id; two or more rows resolving to the same final
external_id; a parent_id referencing a parent's stale pre-rename external_id;
and a rename targeting a tag a replace-mode delete sweep is removing in the
same import. Conversely, a replace-mode delete sweep now correctly treats a
rename row's id as the tag's desired new external_id, not proof that the tag
currently holding that id should survive, so a rename can reuse an
external_id the same import's delete sweep is freeing up.

Test coverage runs the rename round-trip, replace-mode, and idempotent
re-import scenarios through the CSV parser as well as JSON, matching the
path Studio's import wizard actually uses.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…edx#853)

django-simple-history bakes the SIMPLE_HISTORY_DATE_INDEX setting's value
into the db_index it generates for every HistoricalRecords() model's
history_date field. This library's test settings never set it, so its
committed migrations recorded db_index=True (the django-simple-history
default) for CompetencyCriteriaGroup, CompetencyCriterion, and
CompetencyRuleProfile's historical tables. openedx-platform, the primary
consumer these models are built for, sets SIMPLE_HISTORY_DATE_INDEX = False
globally in openedx/envs/common.py, so installing this library there makes
the live model state disagree with the shipped migrations: Django detects a
missing migration for history_date on every one of these three tables the
moment openedx-platform's own test suite loads them, failing its
MigrationTests.test_migrations_are_in_sync check.

Sets the same value in test_settings.py and dev.py so this library's own
migrations match what its actual consumer produces, and adds the migration
that `makemigrations` generates as a result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…terion (openedx#716)

Adds archived = models.BooleanField(default=False) to both models, in one
migration, per ADR 0002 Decision 7's archive-only retirement rule. No
archived_at companion field: django-simple-history (ADR 0003) already
tracks both models, covering the "when" without a separate timestamp.

Both models' existing `.. no_pii:` class-level docstring annotation
already covers the new field, matching CompetencyRuleProfile.archived's
existing non-PII precedent; no separate registry change needed.

This ticket makes no behavior changes: nothing here sets, reads, or
filters on the field. That is downstream tickets' scope (openedx#674, openedx#675, openedx#681).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…edx#861)

openedx-platform's own import-linter config already treats openedx_catalog
(and openedx_learning) as an isolated app, reachable from outside only
through its api/models_api/data/tests modules, which is how a foreign-key
import into openedx_catalog.models (instead of models_api) got caught on
the platform's CI and not here: this repo's own .importlinter only layers
top-level packages, it says nothing about which modules within a package
may be reached from outside it.

Adds an equivalent "isolated_apps" contract type, scoped to openedx_catalog
only for now; extending it to openedx_content, openedx_learning, or
openedx_tagging would surface pre-existing violations (confirmed by testing
the broader scope and reverting), out of scope for this change.

IsolatedAppsContract itself is ported verbatim from openedx-platform's
openedx.testing.importlinter.isolated_apps_contract (this library can't
depend on openedx-platform), with one change: self.isolated_apps and
self.allowed_modules are copied through list() before iterating, working
around a pylint false positive (E1133) caused by pylint not understanding
that Contract._populate_fields() replaces these class-level Field
descriptors with their parsed list values at the instance level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@c18668a...1c37ad0)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@3e5f45b...9000827)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@043fb46...cf430e0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
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>
* docs: ADR proposals for improved static/media asset handling

* docs: fix forward reference to ADR that doesn't exist yet

* docs: minor typos

* docs: blob_... -> media_file_namespace, dedup at org level, link other ADR

* docs: forward ref to next ADR

* docs: mention the future option for flex packages that combine runs

* docs: edit to clarify/simplify some of the rejected alternatives

* docs: mention idea of using collections to separate runs within LP

* docs: update since catalog -> content layering is pending decision

* docs: CourseContent holds the relationship

* docs: typo

* docs: clarifications around using org code as media file namespace

* docs: guiding principle: no 1:1 assumptions between contexts and packages

* docs: clarify wording in ADR 0012

Co-authored-by: Kyle D McCormick <kyle@axim.org>

* docs: suggestions from review

---------

Co-authored-by: Kyle D McCormick <kyle@axim.org>
…/actions/upload-artifact-7.0.2

build(deps): bump actions/upload-artifact from 7.0.1 to 7.0.2
…/astral-sh/setup-uv-10.3.0

build(deps): bump astral-sh/setup-uv from 10.2.0 to 10.3.0
…/actions/download-artifact-8.0.2

build(deps): bump actions/download-artifact from 8.0.1 to 8.0.2
Add POST /api/cbe/v1/competencies/<int:tag_id>/criteria/, associating a gradeable
subsection with a competency. When group_id is supplied, the CompetencyCriterion is
attached to that existing leaf CompetencyCriteriaGroup. When it is omitted, the root and
course-level groups for this competency and course are reused or created, and a new leaf
is always created under them.

Add to applets/cbe/api.py, all marked UNSTABLE: resolve_competency_tag(),
resolve_supplied_leaf_group(), create_competency_criterion(), and
associate_competency_criterion() as the single entry point the view calls. Group creation
is the private _create_leaf_group(), so groups are only ever created behind the
duplicate-association check. The API raises Django ValidationError and model DoesNotExist
exceptions; the view translates them into 400 and 404 responses, and a 404 carries the
exception's message, so a missing tag, a tag outside a competency taxonomy, and a missing
group can be told apart.

Group creation and the duplicate-association check run inside one transaction that first
locks the competency's Tag row with select_for_update(), so concurrent requests for the
same competency are serialized. MySQL has no partial unique indexes, so the one-root-per-tag
rule cannot be backed by a constraint and relies on that lock. Add migration 0015: a
UniqueConstraint on CompetencyCriteriaGroup (tag, course, parent) that backstops
course-level group creation. The root and course-level lookups match archived rows
deliberately, so an archived container is never replaced by a second row in its slot.

When a request supplies no rule fields, the criterion is assigned the live rule profile
scoped to the competency's taxonomy if one exists, and the seeded system-default profile
otherwise. It is never left with all three rule fields null, because CompetencyCriterion's
profile_xor_override_check constraint forbids that. A request that supplies a profile
together with an override, only one of the two overrides, or an archived or unknown profile
is rejected with a 400 before anything is written.

Related to openedx#665

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a read endpoint returning a competency's full CompetencyCriteriaGroup
tree (root, course-level, leaf) and CompetencyCriterion leaves under it, as
two flat lists, so Studio's Manage & Apply Competencies flow can render
existing associations from persisted state instead of tracking them
client-side.

GET /api/cbe/v1/competencies/<int:tag_id>/criteria-groups/ resolves the
competency tag (reusing openedx#665's resolve_competency_tag(), 404 if missing or
not on a CompetencyTaxonomy), checks read access via the new
CompetencyReadPermission (oel_tagging.view_tag, permissive for any enabled
taxonomy), and returns every non-archived group and criterion via the new
get_competency_criteria_tree(). An empty tree is a valid 200 with two empty
arrays, not a 404. Filtering on archived relies on the archived field openedx#716
already added to both models.

Three deviations from the ticket's literal spec, found while verifying
against the actual adopted models rather than the ticket's speculative
pseudocode: the real FK attribute names are tag/group/object_tag/rule_profile
(not the oel_tagging_tag_id-style names the ticket assumed); course_key
uses a plain CharField with a dotted source instead of SlugRelatedField,
since CourseRun.course_key is an opaque-keys field that SlugRelatedField
can't render to JSON; and the criterion read shape is a new
CompetencyCriterionReadSerializer rather than an added field on the
existing create-request serializer, whose own object_id field is
write_only and would have broken under a dotted source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…x#681)

Tightens the two new read-serializer docstrings (CompetencyCriteriaGroupSerializer,
CompetencyCriterionReadSerializer) per PR review, keeping the non-obvious "why"
(opaque-keys course_key serialization, dotted-source object_id write/read split)
without the surrounding restatement. Drops CompetencyCriteriaTreeView.get()'s
docstring, which only restated its three-line body.

Field names on both serializers (tag_id, group_id, object_tag_id, rule_profile_id)
are left unchanged: verified against the real openedx#672 frontend consumer
(AShatsila/frontend-app-authoring@ashatsila/672-manage-and-apply-competencies),
whose types.ts and actual read sites use exactly these short names, not the
oel_tagging_tag_id-style names openedx#681's own AC text assumed. That AC text and
both sides converged on the shorter contract; renaming here would break the
real integration rather than fix a mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The other criteria-authoring entry points carry the UNSTABLE note,
because group update and archive are still to come. Without it this
read would be a stable public API that can only change through DEPR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#681)

The tree view's docstring said each criterion carries
competency_criteria_group_id, but the serializer returns group_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

10 participants