Skip to content

Alezconsultant/665 criterion endpoint on 846 - #847

Open
alezconsultant wants to merge 2 commits into
openedx:mainfrom
alezconsultant:alezconsultant/665-criterion-endpoint-on-846
Open

alezconsultant wants to merge 2 commits into
openedx:mainfrom
alezconsultant:alezconsultant/665-criterion-endpoint-on-846

Conversation

@alezconsultant

@alezconsultant alezconsultant commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds the create-criterion endpoint, POST /api/cbe/v1/competencies/<int:tag_id>/criteria/.
A course author uses it to associate a gradeable subsection with a competency. When the
request supplies a group_id, the criterion is attached to that existing leaf
CompetencyCriteriaGroup. When it doesn't, the endpoint reuses or creates the root and
course-level groups for this competency and course, and always creates a new leaf under them.

Changes

  • src/openedx_learning/migrations/0015_criteria_group_unique_constraints.py (new): one
    UniqueConstraint on CompetencyCriteriaGroup over (tag, course, parent), so there is at
    most one course-level group per competency, course and parent. It has no condition,
    because MySQL has no partial indexes. A unique index allows repeated NULLs, and course is
    NULL for roots and leaves, so roots and leaves are not constrained by it. The matching
    constraint is in CompetencyCriteriaGroup.Meta in models/criteria.py.
  • src/openedx_learning/applets/cbe/api.py: adds resolve_competency_tag(),
    resolve_supplied_leaf_group(), create_competency_criterion(), and
    associate_competency_criterion(), which is the single entry point the view calls. All of
    them are marked UNSTABLE. They raise Django ValidationError and model DoesNotExist
    exceptions, never HTTP exceptions. Group creation lives in the private
    _create_leaf_group(), so no caller can create groups without going through the
    duplicate-association check.
  • src/openedx_learning/applets/cbe/rest_api/v1/serializers.py: adds
    CompetencyCriterionSerializer. object_id and logic_operator are write-only, because
    neither is a CompetencyCriterion field. The other JSON names match the model attributes
    (group_id, rule_profile_id, object_tag_id). validate_object_id() rejects an
    unparseable usage key with a 400.
  • src/openedx_learning/applets/cbe/rest_api/v1/views.py: adds
    CompetencyCriterionCreateView. It checks oel_tagging.can_tag_object for the tag's taxonomy
    and the object_id, then calls associate_competency_criterion(), translating
    ValidationError into a 400 and DoesNotExist into a 404.
  • src/openedx_learning/applets/cbe/rest_api/v1/urls.py: adds
    competencies/<int:tag_id>/criteria/ as a plain path(), since the URL carries a path
    parameter mid-route.
  • tests/openedx_learning/applets/cbe/test_api.py and test_views.py: unit and DRF tests (see
    Tests).
  • tests/openedx_tagging/test_api.py: test_get_taxonomy looks up 2**31 - 1 instead of 200
    as its missing taxonomy id. MySQL does not reset auto-increment counters between tests, so
    with this PR's extra tests a taxonomy with id 200 can exist by the time test_get_taxonomy
    runs, and CI on MySQL failed on it.

Behavior

  • Concurrency. Group creation and the duplicate-association check run in one transaction
    that first locks the competency's Tag row with select_for_update(). That lock serializes
    concurrent requests for the same competency, and it is the only thing that keeps one root
    per competency, since MySQL can't express that as a constraint. The (tag, course, parent)
    constraint backstops course-level groups.
  • Rule profile. When the request supplies none of rule_profile_id, rule_type_override
    and rule_payload_override, the criterion gets the live rule profile scoped to the
    competency's taxonomy if one exists, and the seeded system-default profile otherwise.
    Course- and organization-scoped profiles are not resolved here, because this endpoint
    operates at taxonomy scope.
  • Rule-field validation. A profile together with an override, only one of the two
    overrides, or an unknown or archived rule_profile_id is a 400 keyed by the offending field,
    returned before anything is written.
  • Duplicates. A second criterion for the same tag and object_id is rejected when it
    targets the same group, or when no group_id is supplied. A different explicit group is
    allowed, because ADR-0002 lets one ObjectTag be referenced by criteria in more than one
    group. The rules are enforced in application code under the tag lock, not by a constraint
    on CompetencyCriterion.
  • Tagging. tag_object() replaces an object's tag list for a taxonomy, so the object's
    existing tags are read first and the competency tag is unioned in. When the taxonomy refuses
    the tag (allow_multiple=False, or the object is at its tag limit), the taxonomy's message
    is returned as a 400 on object_id.
  • Group names. Root and course-level groups are named from the tag value and the course
    title, truncated to the column length. Neither is shown in the UI. Leaf groups are unnamed.
  • Archived groups. The get_or_create() lookups for the root and course-level groups match
    archived rows too, so they never insert a second row into an occupied slot. Rejecting an
    archived course-level group or leaf, and reactivating an archived root, are handled by [BE] Enforce archived in Competency Criteria creation #717.
    Nothing sets archived on groups or criteria yet.

Tests

There is one test per scenario in #665's acceptance criteria. Beyond those, the tests cover:

  • each rule-field rejection
  • the taxonomy-scoped profile lookup, including archived and other-taxonomy profiles
  • a taxonomy that refuses the tag, for both allow_multiple=False and the object tag limit
  • truncated group names
  • unit tests for _create_leaf_group() and resolve_supplied_leaf_group(), including two that
    commit a root or course-level group out of band, standing in for a concurrent request, and
    check that get_or_create() reuses it
  • a rollback test proving that groups created in the transaction are removed when creating
    the criterion fails

Verification

  • pytest tests/openedx_learning/applets/cbe/ tests/openedx_tagging/ --no-cov: 794 passed (223 in the CBE suite).
  • pylint, mypy, pycodestyle, isort --check-only, pydocstyle: clean on every changed
    file.
  • lint-imports: all contracts kept.
  • makemigrations --check: no changes.

Related to #665

🤖 Generated with Claude Code

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 2, 2026
@openedx-webhooks

openedx-webhooks commented Oct 2, 2026 •

Copy link
Copy Markdown

Thanks for the pull request, @alezconsultant!

This repository is currently maintained by @axim-engineering.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Oct 2, 2026
@alezconsultant
alezconsultant marked this pull request as draft October 2, 2026 16:04
@alezconsultant
alezconsultant force-pushed the alezconsultant/665-criterion-endpoint-on-846 branch from 2c888e6 to 2ca89b7 Compare October 9, 2026 19:20
ufedaseyeuconsultant added a commit to ufedaseyeuconsultant/openedx-core that referenced this pull request Oct 12, 2026
The help_text on CompetencyCriteriaGroup.archived and
CompetencyCriterion.archived named "the criteria-tree read endpoint"
(openedx#681, not yet built) as if it were the only consumer, when openedx#674 and
openedx#675 are what actually set the flag and openedx#847's create_leaf_group()
will need to read it too. Reworded both to match
CompetencyRuleProfile.archived's existing convention (ADR-0002
Decision 2): describe what archiving means for the row itself, not
what one downstream endpoint does with it.

Regenerates the migration (Django bakes help_text into it) with a
descriptive name instead of the auto-generated "_and_more" one.

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

@mgwozdz-unicon mgwozdz-unicon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed and approved here: jesperhodge#14

@ormsbee ormsbee assigned ormsbee and unassigned ormsbee Oct 13, 2026
@ormsbee
ormsbee self-requested a review October 13, 2026 15:04
@ormsbee ormsbee moved this from Needs Triage to In Eng Review in Contributions Oct 13, 2026
@ormsbee
ormsbee marked this pull request as ready for review October 13, 2026 15:05
@alezconsultant
alezconsultant force-pushed the alezconsultant/665-criterion-endpoint-on-846 branch 4 times, most recently from 45c0162 to dd837ca Compare October 13, 2026 16:09

@ormsbee ormsbee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apologies for the delay on this. It took me a while to try to wrap my head around this, and I'm honestly still not sure I totally grok everything that's going on.

Claude ran the CBE suite, the quality checks, and makemigrations --check on this head for me (all clean, and CI is green on MySQL), and then sent a handful of requests the tests don't send, which is where most of what follows comes from.

The shape is right and the tests are thorough for the inputs they cover. What I'd like fixed before merge is mostly about input the tests don't send, plus one design question that this PR makes concrete:

  1. Please validate the rule fields before the row reaches the database: two field combinations 500 on the check constraint, an archived profile is accepted for a new association, and a taxonomy with allow_multiple=False 500s (inline on serializers.py and api.py).
  2. The generated root and course-level group names can be longer than the column, which is a DataError on MySQL in strict mode (inline on api.py).
  3. #665's taxonomy-scoped profile scenario isn't implemented; the criterion always gets the system default (inline on api.py). Please implement it or write down that it's deferred; I'd rather have the lookup.
  4. Please keep Http404 out of api.py (inline).
  5. [Question] How archived container groups are meant to interact with get_or_create() and the new unique constraint (inline on criteria.py). #857 has merged since I started this review, so archived is on both models now, and once this is rebased, this is the code that ignores it. I don't need code for this here, but I want the decision written down before this merges, because the fix if we get it wrong is another scope_code-style column and a data migration on a table that will have grown by then.

There are also some smaller, non-blocking issues in the inline comments.

Logistics: #846 and #857 have both merged, so please rebase just this commit onto main. Claude did that for me and it applies with no conflicts; the one thing that breaks is the migration number, since #857 took 0014. Please rename it to 0015_criteria_group_unique_constraints and point its dependency at 0014_add_archived_to_criteria_group_and_criterion (a renumber, not a merge migration). With just that change, makemigrations --check is clean and the CBE suite passes on the rebased tree (212 tests).

Thank you.


Claude drafted this review and ran the test suite, quality checks, makemigrations --check, and the probe requests quoted inline on 2ca89b7 for me, and then did the trial rebase onto main once #846 and #857 had merged.

logic_operator = serializers.ChoiceField(
choices=LogicOperator.choices, write_only=True, required=False, allow_null=True,
)
rule_profile_id = serializers.IntegerField(required=False, allow_null=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please validate the rule fields before the row reaches the database. Claude sent these requests at 2ca89b7, with the same fixtures and permission stub as test_views.py:

rule_payload_override alone              -> IntegrityError on oel_cbe_criterion_profile_xor_override_check (a 500)
rule_profile_id + both overrides         -> IntegrityError, same constraint (a 500)
rule_profile_id of an archived profile   -> 201
rule_type_override alone                 -> 400, from clean()
unknown rule_profile_id                  -> 400, from full_clean(), keyed "rule_profile"

The xor rule is already in the model's constraint and docstring, so the API should say the same thing as a 400 keyed by the offending field before the database gets to say it as a 500. And an archived profile shouldn't be accepted for a new association: ADR-0002 Decision 3 says archived profiles are hidden from authoring and new associations, and get_competency_rule_profiles() already hides them from the list the UI will pick from, so accepting one here only happens when a client guesses an id. Consider putting the checks in associate_competency_criterion() so that a Python caller gets the same answer as the view.

Comment thread src/openedx_learning/applets/cbe/api.py Outdated
existing_values = [existing_tag.value for existing_tag in get_object_tags(object_id, taxonomy_id=tag.taxonomy_id)]
if tag.value not in existing_values:
existing_values.append(tag.value)
tag_object(object_id, tag.taxonomy, existing_values)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tag_object() raises ValueError when the taxonomy has allow_multiple=False and this is the second competency on the object, and again when the object is already at OBJECT_MAX_TAGS (_get_current_tags() and _check_new_tag_count()). Claude hit the first one and it came back as a 500. Please catch it here and raise a ValidationError on object_id, so the taxonomy's message reaches the caller.

Comment thread src/openedx_learning/applets/cbe/api.py Outdated
root, _ = CompetencyCriteriaGroup.objects.get_or_create(
tag=tag,
parent=None,
defaults={"name": f"{tag.value} (root)"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tag.value is max_length=500 and CourseRun.title is 255, but CompetencyCriteriaGroup.name is 255, so both of these names can overflow. Claude created a 300-character tag and a 255-character course title, and create_leaf_group() stored a 307-character root name and a 558-character course-level name (SQLite doesn't enforce the length). On MySQL with STRICT_TRANS_TABLES, which Tutor sets and Django expects, that's a DataError and a 500; without strict mode, MySQL truncates silently. Since this is a real outlier and #665 says these two groups are never shown in the UI, I'd just truncate name to fit and make a note of it in the docstring.

Related question: is the leaf meant to have a name? #665 says "named from the course's display name" for the leaf, and you've put that on the course-level group instead, which seems more sensible to me. But whoever is building #672 should know which it is.

Comment thread src/openedx_learning/applets/cbe/api.py Outdated
object_tag = ObjectTag.objects.get(object_id=object_id, taxonomy_id=tag.taxonomy_id, tag_id=tag.id)

if rule_profile_id is None and rule_type_override is None and rule_payload_override is None:
rule_profile_id = CompetencyRuleProfile.objects.get(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This always assigns the system default, but #665's acceptance criteria include "A taxonomy-scoped rule profile is assigned ahead of the system default", and ADR-0002 Decision 4's table says the taxonomy profile wins when no course or organization profile exists. Claude created a live taxonomy-scoped profile for the tag's taxonomy and POSTed with no rule fields: the criterion came back with the system default. The ADR's MVP note says every criterion gets the default because no authoring screen supplies a context, but this endpoint does know the taxonomy (it's tag.taxonomy), and taxonomy-scoped profiles can be created today through the admin, which is what test_api.py's own fixtures do. Since the ADR also says the FK is never re-resolved at read time, a criterion created with the wrong profile stays wrong until something edits it.

Please either look up the live taxonomy-scoped profile first and fall back to the default, or state in the commit message that profile resolution beyond the default is deferred and get #665 updated to match. I'd rather have the lookup; it's a few lines. (I wouldn't go as far as course-scoped profiles here: the ADR says a criterion is assigned using the scope the creating screen operates in, and this one is taxonomy-scoped.)

Comment thread src/openedx_learning/applets/cbe/api.py Outdated
Raises Http404 if no such Tag exists, or if it isn't on a CompetencyTaxonomy. Shared by
the view and :func:`associate_competency_criterion` so both agree on what counts as valid.
"""
tag = get_object_or_404(Tag, pk=tag_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please keep HTTP out of api.py. The only other api.py in this repo that imports from django.http is the components one, where the function's whole job is to build the response the reverse proxy serves, 404s included; nothing else here raises Http404 from a lookup. And this module is going to be called from places that aren't views (the containment check in #666, event handlers, management commands). Raise Tag.DoesNotExist and CompetencyCriteriaGroup.DoesNotExist here (or a small CBE exception, if you'd rather callers not depend on model classes), and have the view turn those into Http404. #665 suggested get_object_or_404, but that was about the view not having an exception handler, and the view is where the translation belongs. Same note for resolve_supplied_leaf_group().

"""
import pytest
import rules
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UserType in openedx_tagging.rules is the User | AnonymousUser alias, so aliasing the concrete model to the same name is confusing when the two files are read side by side. #846 dropped this alias from conftest.py and test_rules.py before it merged, so after the rebase this would be the only one left. get_user_model() is fine, or just drop the annotation.


expected = [default_rule_profile, taxonomy_scoped, organization_scoped]
assert list(get_competency_rule_profiles()) == expected
assert list(get_competency_rule_profiles()) == expected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert list(get_competency_rule_profiles()) == expected

Is there a reason this was duplicated?

assert CompetencyCriteriaGroup.objects.filter(tag=tag, course=course_run).count() == 1


def test_create_leaf_group_locks_the_tag_row_before_creating_groups(tag: Tag, course_run: CourseRun) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: This test and test_associate_competency_criterion_locks_the_tag_row_before_the_duplicate_check pin the implementation (that select_for_update() was called, and in what order) rather than any behavior, and they'll have to be rewritten by whoever changes the locking, including if you take my suggestion on _reject_duplicate_criterion(). I'd drop them; the pre-committed-row tests above are the useful ones.

Comment on lines +52 to +57
# A different tag from `childless`'s: the one-root-per-tag constraint allows only one root group
# per tag, and this scenario is about child count, not about tag reuse.
other_tag = Tag.objects.create(taxonomy=tag.taxonomy, value=f"{tag.value} (other)")
parent = CompetencyCriteriaGroup.objects.create(tag=other_tag, logic_operator=logic_operator)
CompetencyCriteriaGroup.objects.create(tag=other_tag, parent=parent)
CompetencyCriteriaGroup.objects.create(tag=other_tag, parent=parent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change isn't needed, and the comment is wrong: there's no one-root-per-tag constraint in this PR (the commit message explains why MySQL can't have one), and NULLs are distinct in a unique index, so two roots for the same tag still save. Claude ran the pre-commit version of this file against this branch and it passes. Please revert the hunk, and fix the sentence in the PR description that says the constraint rejects this.

Suggested change
# A different tag from `childless`'s: the one-root-per-tag constraint allows only one root group
# per tag, and this scenario is about child count, not about tag reuse.
other_tag = Tag.objects.create(taxonomy=tag.taxonomy, value=f"{tag.value} (other)")
parent = CompetencyCriteriaGroup.objects.create(tag=other_tag, logic_operator=logic_operator)
CompetencyCriteriaGroup.objects.create(tag=other_tag, parent=parent)
CompetencyCriteriaGroup.objects.create(tag=other_tag, parent=parent)
parent = CompetencyCriteriaGroup.objects.create(tag=tag, logic_operator=logic_operator)
CompetencyCriteriaGroup.objects.create(tag=tag, parent=parent)
CompetencyCriteriaGroup.objects.create(tag=tag, parent=parent)

Comment thread src/openedx_learning/applets/cbe/api.py Outdated
Must run inside a transaction holding the tag row lock.
"""
assert tag.taxonomy_id is not None # narrowing for mypy; callers resolve competency tags only
# Locking reads, so a duplicate committed by a concurrent request is seen under MySQL REPEATABLE READ.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should never be running MySQL with REPEATABLE_READ as the isolation level. Django has defaulted to READ COMMITTED with MySQL since Django 2.0. There isn't any good reason to override that behavior, given that the framework assumes READ COMMITTED.

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>
@alezconsultant
alezconsultant force-pushed the alezconsultant/665-criterion-endpoint-on-846 branch from dd837ca to dd5f40e Compare October 14, 2026 20:13
@alezconsultant

alezconsultant commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@ormsbee Thanks for the review. Everything is addressed in the new commit, branch was rebased. PR description updated. Several of these decisions were made before the archived logic was designed.
What I've done:

  • Rule fields are now validated: invalid combinations return a 40, archived profiles are rejected
  • A tag_object refusal (allow_multiple=False or the tag limit) returns a 400 on object_id
  • Root and course-level group names are truncated to fit the column limit
  • The taxonomy-scoped rule profile is used ahead of the system default
  • api.py no longer raises Http404
  • The select_for_update calls are dropped
  • validate_object_id is added to the serializer
  • _create_leaf_group is now private, without atomic
  • rule_profile_id is used everywhere
  • UNSTABLE markers are added to the new public functions
  • The permission fixture is a yield fixture that restores the original predicate
  • The UserType alias, the duplicate assert, and the lock-pinning tests are removed
  • The test_criteria_group.py hunk is reverted
  • The branch is rebased onto main, with the migration renamed to 0015_criteria_group_unique_constraints

Here are the answers and proposals to implementation:

1. Archived container groups

How is this supposed to interact with archived? … either container groups are never archived (only leaves and criteria are, and the containers get reused), or they can be and there's a column that makes that work.

Container groups can be archived. #674, #675 and #717 tickets suggest that we should archive every level. #674's "must run all the way to the root group, not just the immediate parent, stopping only once a level still has active content of its own". I think option with scope_code field is better choice, and we can use that field in get_or_create function to filter out archived and create new records.

2. Leaf name

Related question: is the leaf meant to have a name? #665 says "named from the course's display name" for the leaf, and you've put that on the course-level group instead, which seems more sensible to me. But whoever is building #672 should know which it is.

A course can have multiple leaf, so a name based on the course would be duplicated for leafs, and a name based on subsections would mean handling leaves with several subsections and edge cases after rename. I think it's better to update the #665 AC to match the current implementation.

3. Unique constraint on (object_tag, group)

And should CompetencyCriterion have a unique constraint on (object_tag, group)?
The code already check with the tag lock. It would also conflict with #717 ticket, which stops archived criteria from blocking a "re-linking": re-adding the same content to the same leaf by group_id would hit the archived row and fail as a 500, since MySQL has no partial indexes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: In Eng Review

Development

Successfully merging this pull request may close these issues.

4 participants