You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a course author, I want to change how an existing bottom-tier group combines its rule boxes, or the score an existing rule box requires, in order to correct a mastery rule without deleting and rebuilding the whole branch.
Acceptance Criteria
Scenario: Change how a saved bottom-tier group combines its rule boxes
Given a bottom-tier group whose rule boxes already have saved content, set to require any one of them
When I change it to require all of them
Then the change is saved
And it still reads as requiring all of them after I reload
Scenario: Change the score a rule box requires
Given a rule box that already has content associated with it
When I change the score it requires
Then every piece of content in that rule box is judged by the new score
And the new score is still shown after I reload
Scenario: A persisted rule box's score cannot duplicate one already in the same group
Given a bottom-tier group containing two rule boxes with different scores, both already holding content
When I change one rule box's score to match the other's
Then the change is refused and the reason is shown
And both rule boxes keep the scores they had
Scenario: Setting a rule box back to the default value returns it to the default
Given a rule box I previously changed to a score of my own
When I set it back to the same value the system default supplies
Then the displayed score is unchanged, since the page never distinguishes a default from a matching override (see #672)
And inspecting the underlying data directly confirms the override has been cleared and the criterion now follows the system default
Scenario: A change is rejected by the backend
Given I change how a bottom-tier group combines its rule boxes, or the score a rule box requires
When the save is rejected
Then the control returns to the value it held before my change
And an error is shown, following this feature's existing generic error-handling pattern
Scenario: I lack permission to change an existing bottom-tier group's or rule box's settings
Given a bottom-tier group or rule box I am not permitted to edit
When I view it
Then no control to change how it combines its children or what score it requires is offered
And I can still read the combining choice and the score it currently has
Description
#672 renders the bottom-tier group cards and rule boxes and builds the two controls this ticket makes editable: the combining-logic control on a bottom-tier group and the score field on a rule box. Both render as read-only text there, because each takes an optional change handler and #672 passes none. This ticket passes one on a persisted row, wired to the backend.
Technical Details
This section is background and a suggested approach, not the ticket's source of truth. The User Story and Acceptance Criteria define what must be true when the work is done; what follows exists to save the implementer some thinking, not to bind them.
In short
This ticket adds no new rendering.#672's LogicOperatorSelect and ScoreThresholdField already render an editable control when given a change handler and read-only text when not, and #672 passes none. This ticket passes one on a persisted bottom-tier group and a persisted rule box, wired to a mutation, when the requesting user is permitted to make the change. Nothing about the components themselves changes, and there is no second "is it editable" flag to keep in step with the handler.
Two different endpoints back the two controls. The any/all control on a persisted bottom-tier group calls #760, which changes how a Competency Criteria Group combines the criteria and groups beneath it, addressed by the bottom-tier group's own id. The score field on a persisted rule box calls #759, which changes the rule that a set of Competency Criteria are evaluated by in one request, addressed by every criterion id currently inside that rule box. A rule box is a display grouping of criteria that share one rule, so changing "the box's score" really means re-issuing #759's batch update against every criterion in it, so that they continue to share one rule, and therefore remain one rule box, afterward.
A rule box's identity is still its rule, which constrains what a score change can validly produce. If the new score matches another rule box already in the same group, the criteria being updated would, after the call succeeds, share a rule with that other box and merge into it on the next refetch, leaving the author looking at one box where there had been two, with no explanation. This ticket refuses that value in the field before the call is made. #672 owns both halves of the check: the helper that computes a rule's identity key and the helper that lists a group's existing rule boxes. Write the check directly against those two rather than extracting a shared validator; #671 writes the same one-line check for a not-yet-saved rule box, and neither ticket needs the other to exist.
Setting an override back to the applicable default is not a special case this ticket needs to detect. Per ADR 0002's own write-event rules, the backend itself reassigns a criterion from a matching override back to a profile reference whenever the value matches, so this ticket only needs to send whatever value the author entered. The "following the default again" outcome is the backend's doing, and #672's effective-rule display already renders that state correctly whether the value arrived as a profile reference or as a matching override.
Rejection rolls the control back to what it displayed before the change, mirroring #672's own rejection handling for the create call: the control is optimistic-free, meaning it does not adopt the attempted value until the mutation succeeds, so "rolling back" is simply not having changed the displayed value in the first place, and only the error needs surfacing.
Enable editing on a persisted row. In criteria-groups/CriteriaGroupBox.tsx and criteria-groups/RuleBox.tsx, pass an onChange to the LogicOperatorSelect and the ScoreThresholdField on a persisted row, wired to the mutations above, gated by [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672's canEditCourse(courseId), resolved for whichever course the group or criterion being edited belongs to (a bottom-tier group's own course, via its course-level parent; a criterion's, via the subsection its ObjectTag points at). Passing no handler when the predicate is false is what satisfies the permission scenario: the value still renders, as read-only text, with no control offered.
Do not touch the placeholder call sites.[FE] Build the create / remove Competency Criteria Group interactions #671 passes its own onChange to the same two components on a not-yet-saved card, writing to local provider state rather than to an endpoint. Leave those call sites alone; the permission predicate does not apply to them.
Pass an onChange and a getInlineValidationMessage to ScoreThresholdField on a persisted rule box, wired to the rule mutation and gated by the permission predicate.
src/taxonomy/competency-management/messages.ts
Add the duplicate-score message if it is not already present.
Context [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672: renders the bottom-tier group cards and rule boxes, builds LogicOperatorSelect and ScoreThresholdField with the optional onChange and getInlineValidationMessage props this ticket supplies, and owns ruleKeyOf and ruleBoxesForGroup, which this ticket's duplicate check reads. It also owns the error-toast pattern this ticket's rejection handling follows.
ADR docs/openedx_learning/decisions/0002-competency-criteria-model.rst in openedx-core: the write-event rule that reassigns a matching override back to a profile reference, relevant to the "back to default" scenario above.
Frontend prior art: src/taxonomy/tree-table/EditableCell.tsx for the getInlineValidationMessage prop, src/taxonomy/tag-list/hooks.ts for a caller building one of those validators, and src/generic/toast-context for the error toast.
User Story
As a course author, I want to change how an existing bottom-tier group combines its rule boxes, or the score an existing rule box requires, in order to correct a mastery rule without deleting and rebuilding the whole branch.
Acceptance Criteria
Description
#672 renders the bottom-tier group cards and rule boxes and builds the two controls this ticket makes editable: the combining-logic control on a bottom-tier group and the score field on a rule box. Both render as read-only text there, because each takes an optional change handler and #672 passes none. This ticket passes one on a persisted row, wired to the backend.
Technical Details
This section is background and a suggested approach, not the ticket's source of truth. The User Story and Acceptance Criteria define what must be true when the work is done; what follows exists to save the implementer some thinking, not to bind them.
In short
This ticket adds no new rendering. #672's
LogicOperatorSelectandScoreThresholdFieldalready render an editable control when given a change handler and read-only text when not, and #672 passes none. This ticket passes one on a persisted bottom-tier group and a persisted rule box, wired to a mutation, when the requesting user is permitted to make the change. Nothing about the components themselves changes, and there is no second "is it editable" flag to keep in step with the handler.Two different endpoints back the two controls. The any/all control on a persisted bottom-tier group calls #760, which changes how a Competency Criteria Group combines the criteria and groups beneath it, addressed by the bottom-tier group's own id. The score field on a persisted rule box calls #759, which changes the rule that a set of Competency Criteria are evaluated by in one request, addressed by every criterion id currently inside that rule box. A rule box is a display grouping of criteria that share one rule, so changing "the box's score" really means re-issuing #759's batch update against every criterion in it, so that they continue to share one rule, and therefore remain one rule box, afterward.
A rule box's identity is still its rule, which constrains what a score change can validly produce. If the new score matches another rule box already in the same group, the criteria being updated would, after the call succeeds, share a rule with that other box and merge into it on the next refetch, leaving the author looking at one box where there had been two, with no explanation. This ticket refuses that value in the field before the call is made. #672 owns both halves of the check: the helper that computes a rule's identity key and the helper that lists a group's existing rule boxes. Write the check directly against those two rather than extracting a shared validator; #671 writes the same one-line check for a not-yet-saved rule box, and neither ticket needs the other to exist.
Setting an override back to the applicable default is not a special case this ticket needs to detect. Per ADR 0002's own write-event rules, the backend itself reassigns a criterion from a matching override back to a profile reference whenever the value matches, so this ticket only needs to send whatever value the author entered. The "following the default again" outcome is the backend's doing, and #672's effective-rule display already renders that state correctly whether the value arrived as a profile reference or as a matching override.
Rejection rolls the control back to what it displayed before the change, mirroring #672's own rejection handling for the create call: the control is optimistic-free, meaning it does not adopt the attempted value until the mutation succeeds, so "rolling back" is simply not having changed the displayed value in the first place, and only the error needs surfacing.
Implementation specifics
useUpdateCompetencyCriteriaGroupOperator(groupId)anduseUpdateCompetencyCriteriaRule(criterionIds, rulePayload)insrc/taxonomy/competency-management/data/apiHooks.ts, modeled onuseCreateCompetencyCriterionin the same file, which [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672 adds. Both invalidate the [BE] Build GET endpoint to fetch Competency Criteria Groups and Criteria for a competency #681 groups query on success, following the same pattern.updateCompetencyCriteriaGroupOperator(groupId, logicOperator)andupdateCompetencyCriteriaRule(criterionIds, rulePayload)indata/api.ts, against [BE] Build Update endpoint to handle a user removing a Competency Criteria Group. #760's and [BE] Build Update endpoint to handle a user de-selecting a Competency Criteria association. #759's endpoints respectively.criteria-groups/CriteriaGroupBox.tsxandcriteria-groups/RuleBox.tsx, pass anonChangeto theLogicOperatorSelectand theScoreThresholdFieldon a persisted row, wired to the mutations above, gated by [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672'scanEditCourse(courseId), resolved for whichever course the group or criterion being edited belongs to (a bottom-tier group's own course, via its course-level parent; a criterion's, via the subsection itsObjectTagpoints at). Passing no handler when the predicate is false is what satisfies the permission scenario: the value still renders, as read-only text, with no control offered.onChangeto the same two components on a not-yet-saved card, writing to local provider state rather than to an endpoint. Leave those call sites alone; the permission predicate does not apply to them.getInlineValidationMessageto the persisted rule box'sScoreThresholdField, returning a message whenruleBoxesForGroup(groupId)already contains a box whose key equalsruleKeyOfof the candidate rule and is not the box being edited. Both helpers are [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672's. Add the message tomessages.tsif it is not already there; [FE] Build the create / remove Competency Criteria Group interactions #671 writes the same check for a placeholder and may have added it first.ScoreThresholdFieldalready converts between the displayed percentage and the stored fraction, so the value handed toupdateCompetencyCriteriaRuleis already the fraction [BE] Build Update endpoint to handle a user de-selecting a Competency Criteria association. #759 expects.showToastfromuseToastContext(src/generic/toast-context), the same pattern [FE] Manage & Apply Competencies: select gradeable-subsection associations and target a competency's active group #672 uses for a failed create call, so the two error paths look identical to the author.canEditCourse(courseId); do not introduce a second gating mechanism. Resolving which course a bottom-tier group or a criterion belongs to, to pass into it, is this ticket's own job, since [FE] Build the create / remove Competency Criteria Group interactions #671 only ever resolves it from a group already known to be in a rendered course section.Files to create and modify New files
src/taxonomy/competency-management/criteria-groups/CriteriaGroupBox.edit.test.tsxsrc/taxonomy/competency-management/criteria-groups/RuleBox.edit.test.tsxModified files
src/taxonomy/competency-management/data/api.tsupdateCompetencyCriteriaGroupOperatorandupdateCompetencyCriteriaRule.src/taxonomy/competency-management/data/apiHooks.tsuseUpdateCompetencyCriteriaGroupOperatoranduseUpdateCompetencyCriteriaRule, both invalidating the groups query on success.src/taxonomy/competency-management/criteria-groups/CriteriaGroupBox.tsxonChangetoLogicOperatorSelecton a persisted bottom-tier group, wired to the group-operator mutation and gated by the permission predicate.src/taxonomy/competency-management/criteria-groups/RuleBox.tsxonChangeand agetInlineValidationMessagetoScoreThresholdFieldon a persisted rule box, wired to the rule mutation and gated by the permission predicate.src/taxonomy/competency-management/messages.tsLogicOperatorSelectandScoreThresholdFieldwith the optionalonChangeandgetInlineValidationMessageprops this ticket supplies, and ownsruleKeyOfandruleBoxesForGroup, which this ticket's duplicate check reads. It also owns the error-toast pattern this ticket's rejection handling follows.docs/openedx_learning/decisions/0002-competency-criteria-model.rstinopenedx-core: the write-event rule that reassigns a matching override back to a profile reference, relevant to the "back to default" scenario above.src/taxonomy/tree-table/EditableCell.tsxfor thegetInlineValidationMessageprop,src/taxonomy/tag-list/hooks.tsfor a caller building one of those validators, andsrc/generic/toast-contextfor the error toast.