Skip to content

[BE] Build Update endpoint to handle a user removing a Competency Criteria Group. #760

Description

@thelmick-unicon

User Story

As a Platform Administrator, I want to change how a Competency Criteria Group combines the criteria and groups beneath it, in order to correct the logic of a mastery rule without removing the whole branch and rebuilding everything under it.

Acceptance Criteria

Every outcome below is observable in the response to the update itself, in the learner status tables, or in the group's change history.

# Changing how a group combines its children

Scenario: Change a group from requiring all of its children to requiring any one
  Given a Competency Criteria Group that requires all of its children
  And the requesting user is permitted to manage the taxonomy that owns the group's competency
  When the user updates the group to require any one of its children
  Then the update succeeds
  And the response reports the group as requiring any one of its children

Scenario: Change a group from requiring any one of its children to requiring all of them
  Given a Competency Criteria Group that requires any one of its children
  When the user updates the group to require all of its children
  Then the update succeeds
  And the response reports the group as requiring all of its children

# Rejecting a request the system cannot store

Scenario: Reject a way of combining children that the system cannot store
  Given a Competency Criteria Group
  When the user submits an update that names no way of combining the group's children, or names one the system does not support
  Then the update is rejected
  And the group still combines its children the same way as before the request

# Leaving alone what this endpoint does not change

Scenario: An update leaves everything else about the group and its branch untouched
  Given a Competency Criteria Group with a name, a competency, a course scope, and a position in its tree
  When the user updates how the group combines its children
  Then the update succeeds
  And the response reports the group's name, competency, course scope, and position in its tree unchanged
  And every group and criterion beneath it is unchanged

Scenario: Reject an update that would change anything other than how the group combines its children
  Given a Competency Criteria Group
  When the user submits an update that would change the group's name, its evaluation order among its siblings, its competency, its course scope, or its parent
  Then the update is rejected
  And the response names the attribute that may not be changed
  And the group is unchanged

Scenario: Accept an update that repeats attributes it may not change, when their values match
  Given a Competency Criteria Group
  When the user submits an update that restates the group's name and competency, matching what is stored, alongside a new way of combining its children
  Then the update succeeds
  And the response reports the new way of combining its children

Scenario: An update that changes nothing leaves no audit entry
  Given a Competency Criteria Group
  When the user submits an update carrying the way of combining children that the group already has
  Then the update succeeds
  And no new entry is added to the group's change history

# Requests that address nothing, or come from someone who may not make them

Scenario: Reject an update to a retired group
  Given a Competency Criteria Group that has been retired
  When the user submits an update to it
  Then the update is refused as conflicting with the group's current state
  And the group remains retired and otherwise unchanged

Scenario: Reject an update addressing a group under the wrong competency
  Given a Competency Criteria Group that belongs to one competency
  When the user submits an update addressing that group as though it belonged to a different competency
  Then the request is treated as addressing something that does not exist
  And no group is changed

Scenario: Reject an update to a group that does not exist
  Given no Competency Criteria Group exists for the identifier in the request
  When the user submits an update for it
  Then the request is treated as addressing something that does not exist
  And nothing is changed

Scenario: Reject an update from a user without permission
  Given a user who is not permitted to manage the taxonomy that owns the group's competency
  When that user submits an update to the group
  Then the request is refused
  And the group is unchanged

Description

A Competency Criteria Group can be created (#664) and retired together with everything beneath it (#675), but not changed once it exists. Correcting a group that combines its children the wrong way therefore means removing the whole branch and rebuilding every group and criterion under it. Where learners already have progress in that branch, #675 retires the branch rather than deleting it, so the rebuild leaves their recorded progress attached to retired rows.

This ticket makes exactly one thing editable in place: how the group combines the criteria and groups beneath it. The rule an individual criterion is evaluated by is edited on that criterion (#759), not here.

Technical Details

This section is background and a suggested approach, not the source of truth. The User Story and Acceptance Criteria define what must be true when the work is done; the notes below exist to save the implementer some thinking.

In short

What an author edits. One field: the AND/OR operator that decides how the group combines its children. That operator is the substance of a criteria group, and changing it changes how mastery is computed for every learner evaluated from that point forward.

Retirement is the only state that stops an edit. A group retired by #675 is not editable at all, and no confirmation can override that.

Where the logic lives, and how the audit trail stays honest. The endpoint is a thin adapter: resolve the group from the URL, check permissions, validate the request's shape, delegate to a function in the applet's api.py, serialize the result. Keeping the refusals and the audit attribution in the api function means an in-process Studio caller behaves identically to an HTTP one. ADR 0003 Decision 1 puts django-simple-history on this model, so an edit that changes nothing must not save at all, or the history fills with entries that record no change.

Implementation specifics

  • Endpoint. PATCH /cbe/rest_api/v1/competencies/<int:competency_tag_id>/criteria-groups/<int:group_id>/. DRF routes methods on one view class per path, so add a patch() method to the class [BE] Build endpoint for removing a Competency Criteria Group #675 registers at that detail path rather than a second class, renaming it to CompetencyCriteriaGroupDetailView if it landed as CompetencyCriteriaGroupDeleteView; if [BE] Build endpoint for removing a Competency Criteria Group #675 has not landed, register the detail route here and let [BE] Build endpoint for removing a Competency Criteria Group #675 add delete() to this class. Set http_method_names so PUT is not offered. Resolve the group with get_object_or_404(..., pk=group_id, oel_tagging_tag_id=competency_tag_id), matching [BE] Build endpoint for removing a Competency Criteria Group #675's convention. The create route (competency-criteria-group-create, CompetencyCriteriaGroupCreateView) is on the collection path and is untouched.
  • Api function. update_competency_criteria_group(group_id: int, *, logic_operator: str, user) -> CompetencyCriteriaGroup in src/openedx_learning/applets/cbe/api.py, added to that module's __all__. The umbrella src/openedx_learning/api.py re-exports by wildcard, so it needs no edit. logic_operator is a required keyword because it is the only editable field, so there is no partial-update case to model.
  • Accepted body. logic_operator, required. For name, ordering, parent_id, course_id, oel_tagging_tag_id, and archived: when one is present and its value matches what is stored, ignore it so a read-modify-write client can send the whole object back; when it differs, reject and name the field, rather than silently ignoring a failed authoring intent. Reject unrecognized keys.
  • Field validation. Accept only the model's AND and OR values, and not null, derived from the model field's choices so the check comes from one place. [BE] Build endpoint for creating a Competency Criteria Group #664 describes logic_operator as both required and defaulting to OR without reconciling the two; this endpoint requires an explicit AND or OR either way, so it does not depend on how that is resolved. Do not validate the group's child count: setting the operator on a group with no children or one child is accepted. Studio's authoring flow does not call this endpoint before a group has its first child, though; the operator choice on a still-childless group is held client-side and flows through the child's create request instead.
  • Check order. Resolve the group and check permissions; validate the request's shape; refuse a retired group; detect a no-op and return success without saving; save. Retirement precedes the no-op check because a retired row is not editable at all and a success response would imply the edit was accepted.
  • Status codes. 400 means the request is malformed or self-contradictory. 409 means the request is well formed but the state of the group blocks it.
Code When
200 The operator changed, or the request was a no-op. Body is the group representation from CompetencyCriteriaGroupSerializer, echoing logic_operator.
400 logic_operator missing, null, or not AND/OR; an unrecognized key; or an attempt to change a field that is not editable.
403 The user lacks can_change_taxonomy on the taxonomy owning the group's competency.
404 No group for that id, or the group does not belong to the URL's competency_tag_id.
405 PUT.
409 The group is retired.

Files to create and modify Modified files

File Nature of modification
src/openedx_learning/applets/cbe/api.py add update_competency_criteria_group() and list it in __all__
src/openedx_learning/applets/cbe/rest_api/v1/serializers.py add the update request-body serializer
src/openedx_learning/applets/cbe/rest_api/v1/views.py add patch() to the group detail view and restrict http_method_names
src/openedx_learning/applets/cbe/rest_api/v1/urls.py register the group detail route if #675 has not already
tests/openedx_learning/applets/cbe/test_api.py api-level tests
tests/openedx_learning/applets/cbe/test_views.py endpoint-level tests

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Ready for Community Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions