Repository navigation
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. |
tbain
left a comment
There was a problem hiding this comment.
Prior Unicon Review was performed here; alezconsultant#3
Light code review of this rebase + Codex review revealed no new issues and no substantive changes with regard to the previous review vs. the current active review against the updated target.
84b92cd to
653cd7b
Compare
653cd7b to
5ea5745
Compare
ormsbee
left a comment
There was a problem hiding this comment.
Thanks for this. Claude ran the CBE suite on 5ea5745 for me (344 tests, all passing) along with the probes I mention inline.
My main concern is the permission gate, with a few other minor concerns and questions inline. Once those are addressed, please feel free to squash and update the commit message, and I'll merge.
Thank you.
Claude drafted the first pass of this review and ran the CBE suite (344 tests) and the probes on 5ea5745 for me. The anonymous-request probe is written out inline so you can reproduce it.
| CompetencyCriterionCreateView: a read has no target object to tag, so there's nothing for | ||
| that predicate to check here. has_object_permission only, no has_permission override: the | ||
| base class's always-True default is correct since the real check is object-level, run | ||
| manually by the view via check_object_permissions(). |
There was a problem hiding this comment.
This reasoning skips authentication. BasePermission.has_permission() returning True means an anonymous request reaches get(), resolves the tag, and then can_view_taxonomy says anyone can view an enabled taxonomy, so check_object_permissions() passes for AnonymousUser as well. Claude ran this on the branch with the tag/course_run fixtures, one leaf, and one criterion:
APIClient().get(reverse("cbe:criteria-tree", kwargs={"tag_id": tag.id}))ANON tree GET (enabled taxonomy): 200
{'groups': [{'id': 1, 'parent_id': None, 'name': 'Writing Poetry (root)', ...},
{'id': 2, 'parent_id': 1, 'course_key': 'course-v1:Org1+Python100+Fall2026', ...},
{'id': 3, 'parent_id': 2, ...}],
'criteria': [{'id': 1, 'group_id': 3, 'object_id': 'block-v1:Org1+Python100+Fall2026+type@sequential+block@s1', ...}]}
ANON rule_profiles GET: 401 Authentication credentials were not provided.
ANON criterion-create POST: 401 Authentication credentials were not provided.
So this is the one endpoint in the applet that hands course keys and subsection usage keys to a caller with no session at all. TaxonomyTagsView, the precedent #681 cites, sits behind a DjangoObjectPermissions subclass, and DjangoModelPermissions.has_permission() refuses an unauthenticated user before anything else runs. CompetencyRuleProfilePermissions right above gets that for free, and the two write views use IsAuthenticated explicitly.
Please either add IsAuthenticated to the view's permission_classes or give this class a has_permission() that checks request.user.is_authenticated, update this docstring to match, and add an anonymous test next to test_no_view_access_to_the_taxonomy_is_403.
There was a problem hiding this comment.
Fixed: IsAuthenticated is on the view, the docstring now says so, and there is an anonymous test.
|
|
||
| def has_object_permission(self, request, view, obj) -> bool: | ||
| """Return True if the requesting user may view the taxonomy that ``obj`` (a Tag) belongs to.""" | ||
| return rules.has_perm("oel_tagging.view_tag", request.user, obj) |
There was a problem hiding this comment.
The other half of the problem: oel_tagging.view_tag isn't the rule that runs in Studio. openedx-platform replaces it with rules.always_allow (content_tagging/rules.py#L384), so once this is mounted under cms/urls.py this line is True for every user and every taxonomy, disabled ones included. test_no_view_access_to_the_taxonomy_is_403 passes only because this repo's own can_view_tag is what test_settings registers.
The rule platform does override with something org-aware is oel_tagging.view_taxonomy (#L377), which is why can_view_competency_rule_profile in cbe/rules.py asks for view_taxonomy rather than calling the predicate (its docstring spells this out). Please check oel_tagging.view_taxonomy against obj.taxonomy here instead; resolve_competency_tag() already guarantees it's set.
This seems like a longer term issue of having permissions overrides being defined in openedx-platform. I get that we basically have to for certain things today, but we can hopefully pull more of the real permissions logic into openedx-core over time.
There was a problem hiding this comment.
Switched to oel_tagging.view_taxonomy on obj.taxonomy. Agreed on the longer-term point about moving the real permission logic into openedx-core.
| CompetencyCriteriaGroup.objects.filter(tag_id=tag_id, archived=False).select_related("course") | ||
| ) | ||
| criteria = list( | ||
| CompetencyCriterion.objects.filter(group__in=groups, archived=False).select_related("object_tag") |
There was a problem hiding this comment.
Question: should a read prune everything under an archived group, rather than filtering row by row? bulk_update_competency_criteria() treats an archived ancestor as closing the whole subtree ("An archived ancestor counts too"), and #675 archives a subtree as a unit, but this query only looks at each row's own flag. Claude archived just the course-level group on the branch and left its leaf and criterion alone, and the response was:
group 1 parent_id None
group 3 parent_id 2 (2 is archived, and not in the response)
criterion 1 group_id 3
So the client gets a group whose parent_id points at nothing, plus a criterion under it. I think the response should always be something the client can rebuild without special cases, which means walking down from the roots after the first query and dropping whatever isn't reachable (it's all in memory already, so no extra queries). If you're not doing this because we're leaning on #675's guarantee that subtrees are archived whole, that's okay with me too. I'm just reflexively paranoid about allowing illegal states to exist, even if it's meant to be a temporary state during implementation.
There was a problem hiding this comment.
Done in memory after the first query, as you suggested; details in the top-level reply.
| criteria: list[CompetencyCriterion] | ||
|
|
||
|
|
||
| def get_competency_criteria_tree(tag_id: int) -> CompetencyCriteriaTree: |
There was a problem hiding this comment.
[question] Should this take the resolved Tag instead of an id? The view already has it from resolve_competency_tag(), and as written an unknown or non-competency tag_id from a Python-API caller returns CompetencyCriteriaTree(groups=[], criteria=[]), which is exactly what "nothing authored yet" looks like (Claude checked with get_competency_criteria_tree(999999)). associate_competency_criterion() resolves its own tag_id so an in-process caller gets Tag.DoesNotExist; this one is the odd one out. Taking the Tag gets the same guarantee without a second lookup. Not blocking.
| behavior may change this function's shape without a deprecation cycle. | ||
| """ | ||
| groups = list( | ||
| CompetencyCriteriaGroup.objects.filter(tag_id=tag_id, archived=False).select_related("course") |
There was a problem hiding this comment.
Nit: Please give both querysets an order_by() so the response is stable across databases. get_competency_rule_profiles() makes the same point in its docstring, and #682 will need it for pagination anyway. ("parent_id", "ordering", "id") for groups would also hand the client its siblings in evaluation order.
There was a problem hiding this comment.
Added for both querysets, with groups in (parent_id, ordering, id) order and NULLs first.
|
|
||
| class Meta: | ||
| model = CompetencyCriteriaGroup | ||
| fields = ["id", "parent_id", "tag_id", "course_key", "name", "ordering", "logic_operator", "archived"] |
There was a problem hiding this comment.
My understanding is that archived is always false in this response, since the API function filters on it, and the criterion serializer below leaves it out entirely. If that's correct, please drop it so a client doesn't learn to read a field that can never change.
There was a problem hiding this comment.
Removed.
5ea5745 to
039bd9e
Compare
|
@ormsbee
Also from a final pass: the two read serializers and the view now carry the UNSTABLE note, the view docstring describes the pruning, and a test asserts the other HTTP methods return 405. One thing worth knowing for Studio: under the platform's |
ormsbee
left a comment
There was a problem hiding this comment.
Thank you for the update. I have two tiny docstring formatting suggestions, and a request to undo a previous request of mine (the data.py move).
Please also bring the PR description in line with the commit message: the scope note still describes the inherited #665 history, and the "Other information" section says this PR adds the archived column, which landed in #857. Then I'll merge.
Thank you.
Claude drafted this review and re-ran the CBE suite (357 tests), makemigrations --check, and the quality checks (pycodestyle, isort, pylint, mypy, import-linter) on 039bd9e for me, along with the anonymous-request and archived-group probes from last round, plus a check that a parent cycle with no root is dropped rather than looped on.
| from dataclasses import dataclass | ||
| from decimal import Decimal | ||
|
|
||
| from .models import CompetencyCriteriaGroup, CompetencyCriterion |
There was a problem hiding this comment.
Sorry, this is completely my fault. Please put this and CompetencyCriteriaTree back in the api.py module. I hadn't put it together that the dataclasses have models in them, and we've tried to keep data.py free of model dependencies. OEP-49 was written before we started passing models back through api.py on a regular basis, and maybe it needs some amending, but that's not something I should bog this PR down with.
| group_id. An empty tree (the tag exists | ||
| but nothing has been authored under it yet) is a valid 200 with two empty arrays; a 404 is | ||
| reserved for tag resolution failure only. |
There was a problem hiding this comment.
| group_id. An empty tree (the tag exists | |
| but nothing has been authored under it yet) is a valid 200 with two empty arrays; a 404 is | |
| reserved for tag resolution failure only. | |
| group_id. An empty tree (the tag exists but nothing has been authored under it yet) is a | |
| valid 200 with two empty arrays; a 404 is reserved for tag resolution failure only. |
There was a problem hiding this comment.
Applied.
| ``oel_tagging.view_taxonomy`` on the tag's taxonomy; a profile on a disabled taxonomy stays admin-only. Course- and | ||
| organization-scoped profiles are refused to everyone until #645 decides who may see them. |
There was a problem hiding this comment.
| ``oel_tagging.view_taxonomy`` on the tag's taxonomy; a profile on a disabled taxonomy stays admin-only. Course- and | |
| organization-scoped profiles are refused to everyone until #645 decides who may see them. | |
| ``oel_tagging.view_taxonomy`` on the tag's taxonomy; a profile on a disabled taxonomy stays | |
| admin-only. Course- and organization-scoped profiles are refused to everyone until #645 | |
| decides who may see them. |
There was a problem hiding this comment.
Applied.
Adds GET /api/cbe/v1/competencies/<tag_id>/criteria-groups/, which returns a competency's criteria tree as two flat lists, groups and criteria, for the client to rebuild. - The caller must be authenticated and must pass oel_tagging.view_taxonomy on the tag's taxonomy, the rule openedx-platform overrides with its org-aware check. An unknown or non-competency tag is a 404. - Only groups whose whole ancestor chain is non-archived are returned, with the non-archived criteria under them, so every parent_id and group_id in the response names a returned row. Pruning happens in memory after one groups query; the criteria query covers only the kept groups. - Groups are ordered by parent (roots first), ordering, then id, and criteria by group, then id, so the response is stable across databases. - get_competency_criteria_tree(tag) takes the resolved Tag and returns CompetencyCriteriaTree, both in api.py and both UNSTABLE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
039bd9e to
968bdfe
Compare
Description
Adds a read endpoint that returns a competency's
CompetencyCriteriaGrouptree (root, course-level group, leaf) and theCompetencyCriterionrows under it, as two flat lists for the client to rebuild:Studio's Manage & Apply Competencies flow needs this endpoint to render a competency's existing groups, criteria, and tagged subsections from persisted backend state, so the next write (adding or removing an association) acts on what the database holds rather than on state tracked in the client.
oel_tagging.view_taxonomyon the tag's taxonomy. openedx-platform overrides that rule with its org-aware check. A caller who fails it gets a403. An unknown tag, or a tag that is not on aCompetencyTaxonomy, is a404.parent_idandgroup_idin the response names a returned row. Pruning happens in memory after one groups query; the criteria query then covers only the kept groups.ordering, thenid. Criteria are ordered by group, thenid. The order is stable across databases.200with two empty arrays.Public API additions, all marked UNSTABLE:
get_competency_criteria_tree(tag), which takes the resolvedTag, and theCompetencyCriteriaTreedataclass it returns, both inopenedx_learning.applets.cbe.api. The REST layer addsCompetencyCriteriaGroupSerializerandCompetencyCriterionReadSerializerinv1/serializers.pyandCompetencyReadPermissioninv1/permissions.py.Which roles this affects: Course Authors and Platform Administrators, through Studio's competency-management UI (read-only in this PR); Developers, as a new API to build against (#672 and #683 consume it next).
Supporting information
CompetencyTaxonomy), [BE] Create CompetencyCriteria #665 (theCompetencyCriterionandCompetencyCriteriaGroupmodels and the create-criterion endpoint), and 716 archived field cc group models #857 (thearchivedcolumns), all already onmain.docs/openedx_learning/decisions/0002-competency-criteria-model.rst, Decision 5, for the indexes this read path relies on.Testing instructions
python manage.py migrate openedx_learning.POST /api/cbe/v1/competencies/<tag_id>/criteria/oropenedx_learning.api.associate_competency_criterion(...).GET /api/cbe/v1/competencies/<tag_id>/criteria-groups/as a user who can view the taxonomy. Expect200with:groups: the root (parent_idandcourse_keybothnull), the course-level group, and the leaf, each with the correctparent_id.criteria: the criterion, withgroup_idmatching the leaf, andid,object_tag_id,object_id(the tagged subsection's usage key),rule_profile_id,rule_type_override, andrule_payload_override.archived=Trueon the course-level group and repeat step 3. Expect the course-level group, the leaf under it, and the criterion to be absent; the root stays.GETthe same URL for atag_idthat does not exist, and for one that exists but is not on aCompetencyTaxonomy. Expect404in both cases.GETas an anonymous user (expect401) and as a signed-in user without view access to the taxonomy (expect403).pytest tests/openedx_learning/applets/cbe/ -o addopts="".Other information
deletableconvenience flag, which is deferred until an ADR settles the mechanism.🤖 Generated with Claude Code