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 plugin developer building competency authoring on this library, I want to read the rule profiles an instance defines, in order to know what the instance currently requires for mastery before deciding whether a criterion should follow that rule or override it.
Acceptance Criteria
# Reading the rule
Scenario: Read the rule an instance currently requires for mastery
Given an instance whose only rule profile is the instance-wide default
And a caller permitted to administer instance-wide competency configuration
When the caller requests the rule profiles
Then the caller receives a collection containing that one profile
And the profile reports the rule learners are evaluated against
And the profile reports that it applies instance-wide rather than to one taxonomy, course, or organization
And the profile carries a reference by which a criterion can later be pointed at it
Scenario: Report a rule complete enough for a caller to act on without guessing
Given an instance whose instance-wide default requires a grade at or above a given fraction
When the caller requests the rule profiles
Then the profile reports which kind of rule it evaluates, the comparison it applies,
and the threshold it compares against
And the threshold is reported together with the scale it is expressed on,
so a caller cannot read the fraction as a percentage or the percentage as a fraction
# Staying correct once narrower profiles exist
Scenario: Tell the instance-wide default apart from a narrower profile
Given an instance with a rule profile scoped to a single taxonomy alongside the instance-wide default
When the caller requests the rule profiles
Then every returned profile reports the scope it applies to
And the caller can identify the instance-wide default from the scope each profile reports,
without depending on how many profiles were returned or on the order they arrived in
# Verified via a unit test creating the taxonomy-scoped row directly as a fixture: no create
# endpoint exists yet to reach this state through a live call, so this is not Postman-testable.
Scenario: Read a collection larger than one response carries
Given more rule profiles exist than a single response carries at once
When the caller requests the rule profiles
Then the caller receives part of the collection together with a way to request the remainder
And requesting the remainder yields the profiles not already returned, with none repeated and none skipped
# Verified via a unit test creating enough rows directly as fixtures to exceed one page: no
# create endpoint exists yet to reach this state through a live call, so this is not
# Postman-testable.
Scenario: Return the profiles in a stable order
Given an instance with several rule profiles
When the caller requests the rule profiles twice, with no profile changing in between
Then the profiles arrive in the same order both times
# Verified via a unit test creating several rows directly as fixtures: no create endpoint
# exists yet to reach this state through a live call, so this is not Postman-testable.
# What the collection leaves out
Scenario: Leave retired profiles out of the collection
Given an instance with a retired rule profile alongside the instance-wide default
When the caller requests the rule profiles
Then the retired profile is not among those returned
And the instance-wide default is returned
# Verified via a unit test creating a retired row directly as a fixture: no archive endpoint
# exists yet to reach this state through a live call, so this is not Postman-testable.
Scenario: Keep the instance's internal scope bookkeeping out of the response
Given an instance with the instance-wide default rule profile
When the caller requests the rule profiles
Then the response describes each profile's scope in terms a caller outside this system can act on
And it does not expose the internal value the system uses to enforce one profile per scope
# Empty and refused cases
Scenario: Report an instance with no rule profiles as empty rather than as a failure
Given an instance where no rule profile exists
When the caller requests the rule profiles
Then the caller receives an empty collection
And the request is not reported as having failed
# This is the scenario that fails if the endpoint was built as a single-object read.
# Verified via a unit test against a database with no seeded row: #613's migration seeds this
# row on every real deployment, so this state is not reachable on a live instance and is not
# Postman-testable.
Scenario: Refuse a caller who may not author competency criteria
Given a caller who is not permitted to administer instance-wide competency configuration
When the caller requests the rule profiles
Then the request is refused
And no rule profile is returned
Scenario: Refuse a caller the system cannot identify
Given a caller the system cannot identify
When the caller requests the rule profiles
Then the request is refused
And no rule profile is returned
Description
A criterion can already be told to follow the default rule that applies to it, because #759 resolves that profile itself. What nothing exposes is the rule the profile holds: no caller can read the comparison and the threshold that mastery currently requires, so nothing can report it, record it, or judge whether the shared default suits a given criterion.
The instance-wide default is the only rule profile this phase creates, and it is seeded rather than authored. Profiles scoped to a taxonomy, a course, or an organization exist in the data model, but no authoring flow creates one yet.
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 the endpoint is. A single collection route, rule_profiles/, returning every non-archived CompetencyRuleProfile the requester may read, in the standard paginated envelope with the profiles under results. There is no scope filter and no separate route for the system default. Today the instance holds exactly one profile, the seeded system default, so the collection has one element. The endpoint is still a collection in every respect, and a client that treats it as a singleton will be wrong the day a second scope is enabled.
How a client tells the profiles apart. Each row carries a derived scope_type field, so a client identifies a profile by value rather than by its position in the list. The derivation recognizes all four scope kinds from the outset, so it never has to be edited when a scope is enabled. The lookup that finds the system default queries the three scope columns for null; it never matches on the scope_code string, because ADR 0002 Decision 3 states that column embeds internal identifier references, exists solely to enforce uniqueness, and is not intended to be exported or exposed outside this system.
Why the response is paginated from the first commit. ADR 0002 Decision 2's closing boundaries state that pagination is supported for authoring and list APIs. Beyond that, pagination is the one thing here that cannot be added later without breaking clients: an unpaginated list is a bare JSON array, and wrapping it in an envelope changes the top-level JSON type. Because this repository is a published library, the page sizes are pinned on the view rather than inherited from the consuming project, so the envelope and the page size are part of this contract instead of varying by deployment.
What the response carries, and what it deliberately omits. Each row carries an identifier, a scope type, the rule type, the rule payload, and whether the profile is retired. The identifier is the model's own primary key, because that is what the criterion-update endpoint accepts (#759), and because the tagging app's REST API already exposes the primary key for taxonomies, which have no external identifier either. The retired flag is included even though today's active-only filter means it is always false, because CompetencyRuleProfile is exempt from hard deletion entirely per ADR 0002 Decision 7: retirement is archive-only, retired rows are permanent and stay queryable, and a client must be able to tell one apart without a later contract change. Three things are omitted on purpose. scope_code is omitted for the reason above. The three raw scope columns are omitted because the course and organization references are internal foreign keys that mean nothing to an authoring client, and emitting them as always-null today would commit the contract to that representation before any caller needs it. An in-use indicator is omitted because the guardrail it serves fires when a profile is edited, and no write endpoint is in scope here.
Why the collection is active-only, and why that is decided now. Because retired profiles are never deleted, they accumulate permanently, so an unfiltered collection that included them would degrade without bound. Deciding this now also avoids a later behavior change: if the collection meant "everything" today and narrowed to "active" later, clients would notice. A parameter to include retired rows is a plausible future addition and is out of scope, as are scope filters; both are additive, because a client that sends no parameters keeps today's behavior.
Who may read it, and how that gate composes with future scopes. Reading is gated on a new permission whose predicate reuses the tagging app's existing taxonomy-administrator check, which resolves to platform staff. The system default is instance-wide competency configuration administered by the same people who administer taxonomies, and defining a second notion of administrator inside CBE would let the two drift. Starting staff-only is the safe direction, because loosening a read gate later gives clients more data and is not a breaking change, whereas tightening one is. The predicate accepts an optional profile instance from the start, so when scoped profiles arrive with a possibly broader audience, that work adds a branch to one predicate and filters rows in the queryset, rather than refusing a whole request in order to hide rows.
What this ticket extends, and what it depends on. The CBE REST package, the app-root URL configuration, the applet's public API module, and the mount that makes CBE reachable from Studio all already exist, established by #664. This ticket adds one API function, one serializer, one viewset, one router registration, a pinned paginator, and the first CBE-local permission, plus tests. It adds no model, no migration, and no openedx-platform work of its own. It depends on #613 for the CompetencyRuleProfile model and for the seeded system-default row, and it does not seed that row itself. If no row has been seeded, the endpoint reports an empty collection rather than an error, because a collection with no members is an empty collection and not a missing resource.
Implementation specifics
Applet API function.get_competency_rule_profiles() -> QuerySet[CompetencyRuleProfile] in src/openedx_learning/applets/cbe/api.py, added to that module's existing __all__, returning CompetencyRuleProfile.objects.filter(archived=False).order_by("id"). Returning a lazy queryset from a public API function follows openedx_tagging.api.get_taxonomies, which TaxonomyView.get_queryset consumes the same way, and it is what makes pagination work.
The competency_ name prefix is required. The umbrella src/openedx_learning/api.py flattens every applet's API into one namespace shared with learning_pathways.
Umbrella re-export needs no edit, provided the umbrella wildcard-imports the applet API modules and the applet declares __all__. src/openedx_content/api.py is the working example, and its own comment records why the wildcard is safe.
Deterministic ordering is mandatory, not cosmetic. An unordered queryset produces inconsistent pages and triggers Django's UnorderedObjectListWarning. Order by id ascending, and do not order the system default first: a stable position would invite clients to identify profiles positionally, which is the assumption this endpoint exists to prevent.
Serializer.CompetencyRuleProfileSerializer(serializers.ModelSerializer) in src/openedx_learning/applets/cbe/rest_api/v1/serializers.py, with fields = ["id", "scope_type", "rule_type", "rule_payload", "archived"] and every field read-only.
scope_type derivation. A SerializerMethodField returning "system_default" when all three scope columns are null, and otherwise "taxonomy", "course", or "organization" for whichever column is non-null. When a second scope kind is actually enabled, move that derivation onto the model so non-REST callers, in particular the profile-assignment computation in ADR 0002 Decision 4, share one definition; a single caller does not justify the move yet.
Return rule_payload verbatim as stored, without normalizing it. ADR 0002 Decision 3 fixes the threshold as a fraction between 0.0 and 1.0 inclusive, "matching the platform's existing fractional grade representation, not a 0-100 scale", while its own worked example pairs a value of 0.75 with a scale of "percent". That ambiguity is why the scale must be reported alongside the threshold, and why this endpoint must not become a second definition of the payload contract. Cover the pairing in a test asserting the exact stored shape. If the seeded row turns out to omit scale, that is a defect in the seed and belongs to [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613; raise it rather than defaulting the key here.
scope_type values are snake_case lowercase, rule_type values are CamelCase ("Grade"), because ADR 0002 defines them that way. Do not harmonize the two; the rule type's casing belongs to the model.
Viewset.CompetencyRuleProfileView(mixins.ListModelMixin, GenericViewSet) in src/openedx_learning/applets/cbe/rest_api/v1/views.py, with serializer_class, permission_classes, pagination_class, and a get_queryset returning get_competency_rule_profiles(). Use a viewset rather than a ListAPIView so that create and a by-identifier detail route can later be added as extra mixins with no change to the URL module.
Set authentication_classes on the viewset class directly. Do not import view_auth_classes from openedx_tagging/rest_api/utils.py, which is another app's REST internals rather than its public API.
URL registration. Add router.register("rule_profiles", views.CompetencyRuleProfileView, basename="rule_profile") to the router already defined in src/openedx_learning/applets/cbe/rest_api/v1/urls.py. Do not create a second router or a second URL module. The path segment is underscored, matching object_tags and object_tag_counts in src/openedx_tagging/rest_api/v1/urls.py.
The client-visible path is inherited, not decided here.[BE] Build endpoint for creating a Competency Criteria Group #664 mounts CBE at api/cbe/rest_api/, so this endpoint resolves to api/cbe/rest_api/v1/rule_profiles/. This ticket does not revisit that prefix.
Pagination class.CompetencyRuleProfilePagination in a new src/openedx_learning/applets/cbe/rest_api/paginators.py, subclassing DefaultPagination from edx_rest_framework_extensions.paginators, with explicit page_size and max_page_size rather than inheriting the consuming project's DEFAULT_PAGINATION_CLASS. The tagging app pins TaxonomyPagination and TagsPagination the same way. Keep the module at rest_api/ rather than rest_api/v1/, matching the tagging app, because pagination is version-independent.
Permission predicate.can_view_competency_rule_profile(user, profile=None) -> bool in a new src/openedx_learning/applets/cbe/rules.py, returning is_taxonomy_admin(user) imported from openedx_tagging.rules, registered with rules.add_perm. The profile argument is optional and currently unused.
Permission scaffolding is new to CBE.[BE] Build endpoint for creating a Competency Criteria Group #664 gates its endpoint by reusing the tagging app's TaxonomyObjectPermissions and can_change_taxonomy directly and explicitly introduces no CBE permission, so this ticket adds the applet's rules.py and its rest_api/v1/permissions.py. If either module exists by the time this is picked up, extend it rather than creating a parallel one.
Rules autodiscovery requires an app-root module. The rules autodiscovery app config imports <app_package>.rules, so src/openedx_learning/rules.py must exist and import the applet's rules module for the permission to register, mirroring how the umbrella API module pulls in its applets.
DRF permission class.CompetencyRuleProfilePermissions(DjangoObjectPermissions) in src/openedx_learning/applets/cbe/rest_api/v1/permissions.py, with a perms_map granting the read methods the view permission and an empty list for OPTIONS, matching TaxonomyObjectPermissions. The base class resolves the model through view.get_queryset(), which is lazy, so no query is issued during the permission check. An unidentified caller gets 401 and an identified but unpermitted caller gets 403 from DRF, with no CBE-specific handling.
Mark the API unstable.README.rst states that APIs not marked "UNSTABLE" are considered stable and that breaking them goes through the community deprecation process. Mark both get_competency_rule_profiles and the viewset UNSTABLE in their docstrings while the rule-profile family is incomplete, so the deferred create, update, and archive endpoints can adjust the shape without a deprecation cycle. There is no existing use of the marker anywhere in src/, so this establishes the pattern rather than copying one.
Layering is a dependency, not a task here.openedx_learning is registered in .importlinter's root_packages and placed in the layering contract by [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613, and lint-imports must pass. Worth knowing: a top-level package absent from root_packages is silently exempt from every layering contract, so CBE imports would go unchecked with no failing build to reveal it.
Tests go in tests/openedx_learning/applets/cbe/, mirroring tests/openedx_content/applets/, split into test_api.py, test_views.py, and test_rules.py as tests/openedx_tagging/ is split. This repository keeps every test under the top-level tests/ tree and has none under src/. Migrations run in the test database, so the seeded system-default row is already present; do not write tests that assume an empty table.
Specific cases to cover:
A permitted request returns one result whose scope type is the instance-wide one and whose payload matches the seeded row exactly, including its scale key.
The response contains no scope_code key and no scope-column keys.
The lookup still succeeds when a row's scope_code is unrelated to the query, which proves the null-column query rather than a string match is in use.
A taxonomy-scoped row created directly in a fixture appears in the same collection alongside the default, with the taxonomy scope type and in identifier order. This is the actual proof that a second scope is additive.
A retired row created in a fixture is excluded.
An empty table reports an empty collection rather than a missing resource.
An unpermitted identified caller is refused, and an unidentified caller is refused.
The response is the paginated envelope rather than a bare array.
Scope filters and scope reference fields. Until a scope reference field exists, a non-default row tells a client the kind of scope but not which object is scoped, so a client must not try to act on one.
A by-identifier detail route.
An exception handler module, which the deferred write endpoints will need for their conflict response.
add filtering, ordering, and null-scope lookup tests
tests/openedx_learning/applets/cbe/test_views.py
add response-shape, envelope, empty-collection, and refusal tests
Context ADR 0002, docs/openedx_learning/decisions/0002-competency-criteria-model.rst: Decision 3 for the profile's columns, the seeded all-null-scope system default, the check constraint, and scope_code being internal-only; Decision 2's closing boundaries for pagination on list APIs; Decision 7 for archive-only retirement.
ADR 0003, docs/openedx_learning/decisions/0003-competency-criteria-versioning.rst, Decision 4: the pinned definition of a profile being in use.
ADR 0001, docs/openedx_learning/decisions/0001-competency-criteria-location.rst: why CBE code sits at src/openedx_learning/applets/cbe/, and how the umbrella app aggregates its applets.
docs/openedx_content/decisions/0010-merge-authoring-apps-into-openedx-content.rst for the applet convention and umbrella aggregation, with src/openedx_content/api.py as the working example.
docs/openedx_content/decisions/0003-identifier-conventions.rst for id, uuid, and key.
Prior art for a read-path list endpoint: src/openedx_tagging/rest_api/v1/views.py, serializers.py, permissions.py, and urls.py, plus src/openedx_tagging/rest_api/paginators.py; and src/openedx_tagging/rules.py for the taxonomy-administrator predicate and the permission-registration pattern.
User Story
As a plugin developer building competency authoring on this library, I want to read the rule profiles an instance defines, in order to know what the instance currently requires for mastery before deciding whether a criterion should follow that rule or override it.
Acceptance Criteria
Description
A criterion can already be told to follow the default rule that applies to it, because #759 resolves that profile itself. What nothing exposes is the rule the profile holds: no caller can read the comparison and the threshold that mastery currently requires, so nothing can report it, record it, or judge whether the shared default suits a given criterion.
The instance-wide default is the only rule profile this phase creates, and it is seeded rather than authored. Profiles scoped to a taxonomy, a course, or an organization exist in the data model, but no authoring flow creates one yet.
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 the endpoint is. A single collection route,
rule_profiles/, returning every non-archivedCompetencyRuleProfilethe requester may read, in the standard paginated envelope with the profiles underresults. There is no scope filter and no separate route for the system default. Today the instance holds exactly one profile, the seeded system default, so the collection has one element. The endpoint is still a collection in every respect, and a client that treats it as a singleton will be wrong the day a second scope is enabled.How a client tells the profiles apart. Each row carries a derived
scope_typefield, so a client identifies a profile by value rather than by its position in the list. The derivation recognizes all four scope kinds from the outset, so it never has to be edited when a scope is enabled. The lookup that finds the system default queries the three scope columns for null; it never matches on thescope_codestring, because ADR 0002 Decision 3 states that column embeds internal identifier references, exists solely to enforce uniqueness, and is not intended to be exported or exposed outside this system.Why the response is paginated from the first commit. ADR 0002 Decision 2's closing boundaries state that pagination is supported for authoring and list APIs. Beyond that, pagination is the one thing here that cannot be added later without breaking clients: an unpaginated list is a bare JSON array, and wrapping it in an envelope changes the top-level JSON type. Because this repository is a published library, the page sizes are pinned on the view rather than inherited from the consuming project, so the envelope and the page size are part of this contract instead of varying by deployment.
What the response carries, and what it deliberately omits. Each row carries an identifier, a scope type, the rule type, the rule payload, and whether the profile is retired. The identifier is the model's own primary key, because that is what the criterion-update endpoint accepts (#759), and because the tagging app's REST API already exposes the primary key for taxonomies, which have no external identifier either. The retired flag is included even though today's active-only filter means it is always false, because
CompetencyRuleProfileis exempt from hard deletion entirely per ADR 0002 Decision 7: retirement is archive-only, retired rows are permanent and stay queryable, and a client must be able to tell one apart without a later contract change. Three things are omitted on purpose.scope_codeis omitted for the reason above. The three raw scope columns are omitted because the course and organization references are internal foreign keys that mean nothing to an authoring client, and emitting them as always-null today would commit the contract to that representation before any caller needs it. An in-use indicator is omitted because the guardrail it serves fires when a profile is edited, and no write endpoint is in scope here.Why the collection is active-only, and why that is decided now. Because retired profiles are never deleted, they accumulate permanently, so an unfiltered collection that included them would degrade without bound. Deciding this now also avoids a later behavior change: if the collection meant "everything" today and narrowed to "active" later, clients would notice. A parameter to include retired rows is a plausible future addition and is out of scope, as are scope filters; both are additive, because a client that sends no parameters keeps today's behavior.
Who may read it, and how that gate composes with future scopes. Reading is gated on a new permission whose predicate reuses the tagging app's existing taxonomy-administrator check, which resolves to platform staff. The system default is instance-wide competency configuration administered by the same people who administer taxonomies, and defining a second notion of administrator inside CBE would let the two drift. Starting staff-only is the safe direction, because loosening a read gate later gives clients more data and is not a breaking change, whereas tightening one is. The predicate accepts an optional profile instance from the start, so when scoped profiles arrive with a possibly broader audience, that work adds a branch to one predicate and filters rows in the queryset, rather than refusing a whole request in order to hide rows.
What this ticket extends, and what it depends on. The CBE REST package, the app-root URL configuration, the applet's public API module, and the mount that makes CBE reachable from Studio all already exist, established by #664. This ticket adds one API function, one serializer, one viewset, one router registration, a pinned paginator, and the first CBE-local permission, plus tests. It adds no model, no migration, and no
openedx-platformwork of its own. It depends on #613 for theCompetencyRuleProfilemodel and for the seeded system-default row, and it does not seed that row itself. If no row has been seeded, the endpoint reports an empty collection rather than an error, because a collection with no members is an empty collection and not a missing resource.Implementation specifics
get_competency_rule_profiles() -> QuerySet[CompetencyRuleProfile]insrc/openedx_learning/applets/cbe/api.py, added to that module's existing__all__, returningCompetencyRuleProfile.objects.filter(archived=False).order_by("id"). Returning a lazy queryset from a public API function followsopenedx_tagging.api.get_taxonomies, whichTaxonomyView.get_querysetconsumes the same way, and it is what makes pagination work.competency_name prefix is required. The umbrellasrc/openedx_learning/api.pyflattens every applet's API into one namespace shared withlearning_pathways.__all__.src/openedx_content/api.pyis the working example, and its own comment records why the wildcard is safe.UnorderedObjectListWarning. Order byidascending, and do not order the system default first: a stable position would invite clients to identify profiles positionally, which is the assumption this endpoint exists to prevent.CompetencyRuleProfileSerializer(serializers.ModelSerializer)insrc/openedx_learning/applets/cbe/rest_api/v1/serializers.py, withfields = ["id", "scope_type", "rule_type", "rule_payload", "archived"]and every field read-only.scope_typederivation. ASerializerMethodFieldreturning"system_default"when all three scope columns are null, and otherwise"taxonomy","course", or"organization"for whichever column is non-null. When a second scope kind is actually enabled, move that derivation onto the model so non-REST callers, in particular the profile-assignment computation in ADR 0002 Decision 4, share one definition; a single caller does not justify the move yet.rule_payloadverbatim as stored, without normalizing it. ADR 0002 Decision 3 fixes the threshold as a fraction between 0.0 and 1.0 inclusive, "matching the platform's existing fractional grade representation, not a 0-100 scale", while its own worked example pairs a value of0.75with ascaleof"percent". That ambiguity is why the scale must be reported alongside the threshold, and why this endpoint must not become a second definition of the payload contract. Cover the pairing in a test asserting the exact stored shape. If the seeded row turns out to omitscale, that is a defect in the seed and belongs to [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613; raise it rather than defaulting the key here.scope_typevalues are snake_case lowercase,rule_typevalues are CamelCase ("Grade"), because ADR 0002 defines them that way. Do not harmonize the two; the rule type's casing belongs to the model.CompetencyRuleProfileView(mixins.ListModelMixin, GenericViewSet)insrc/openedx_learning/applets/cbe/rest_api/v1/views.py, withserializer_class,permission_classes,pagination_class, and aget_querysetreturningget_competency_rule_profiles(). Use a viewset rather than aListAPIViewso that create and a by-identifier detail route can later be added as extra mixins with no change to the URL module.authentication_classeson the viewset class directly. Do not importview_auth_classesfromopenedx_tagging/rest_api/utils.py, which is another app's REST internals rather than its public API.router.register("rule_profiles", views.CompetencyRuleProfileView, basename="rule_profile")to the router already defined insrc/openedx_learning/applets/cbe/rest_api/v1/urls.py. Do not create a second router or a second URL module. The path segment is underscored, matchingobject_tagsandobject_tag_countsinsrc/openedx_tagging/rest_api/v1/urls.py.api/cbe/rest_api/, so this endpoint resolves toapi/cbe/rest_api/v1/rule_profiles/. This ticket does not revisit that prefix.CompetencyRuleProfilePaginationin a newsrc/openedx_learning/applets/cbe/rest_api/paginators.py, subclassingDefaultPaginationfromedx_rest_framework_extensions.paginators, with explicitpage_sizeandmax_page_sizerather than inheriting the consuming project'sDEFAULT_PAGINATION_CLASS. The tagging app pinsTaxonomyPaginationandTagsPaginationthe same way. Keep the module atrest_api/rather thanrest_api/v1/, matching the tagging app, because pagination is version-independent.can_view_competency_rule_profile(user, profile=None) -> boolin a newsrc/openedx_learning/applets/cbe/rules.py, returningis_taxonomy_admin(user)imported fromopenedx_tagging.rules, registered withrules.add_perm. Theprofileargument is optional and currently unused.TaxonomyObjectPermissionsandcan_change_taxonomydirectly and explicitly introduces no CBE permission, so this ticket adds the applet'srules.pyand itsrest_api/v1/permissions.py. If either module exists by the time this is picked up, extend it rather than creating a parallel one.<app_package>.rules, sosrc/openedx_learning/rules.pymust exist and import the applet's rules module for the permission to register, mirroring how the umbrella API module pulls in its applets.CompetencyRuleProfilePermissions(DjangoObjectPermissions)insrc/openedx_learning/applets/cbe/rest_api/v1/permissions.py, with aperms_mapgranting the read methods the view permission and an empty list forOPTIONS, matchingTaxonomyObjectPermissions. The base class resolves the model throughview.get_queryset(), which is lazy, so no query is issued during the permission check. An unidentified caller gets 401 and an identified but unpermitted caller gets 403 from DRF, with no CBE-specific handling.README.rststates that APIs not marked "UNSTABLE" are considered stable and that breaking them goes through the community deprecation process. Mark bothget_competency_rule_profilesand the viewset UNSTABLE in their docstrings while the rule-profile family is incomplete, so the deferred create, update, and archive endpoints can adjust the shape without a deprecation cycle. There is no existing use of the marker anywhere insrc/, so this establishes the pattern rather than copying one.openedx_learningis registered in.importlinter'sroot_packagesand placed in the layering contract by [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613, andlint-importsmust pass. Worth knowing: a top-level package absent fromroot_packagesis silently exempt from every layering contract, so CBE imports would go unchecked with no failing build to reveal it.make pii_checkis unaffected; annotations belong with the models in [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613.tests/openedx_learning/applets/cbe/, mirroringtests/openedx_content/applets/, split intotest_api.py,test_views.py, andtest_rules.pyastests/openedx_tagging/is split. This repository keeps every test under the top-leveltests/tree and has none undersrc/. Migrations run in the test database, so the seeded system-default row is already present; do not write tests that assume an empty table.scope_codekey and no scope-column keys.scope_codeis unrelated to the query, which proves the null-column query rather than a string match is in use.Files to create and modify New files
CompetencyRuleProfilePaginationCompetencyRuleProfilePermissionscan_view_competency_rule_profileand its permission registrationModified files
get_competency_rule_profiles()and extend__all__CompetencyRuleProfileSerializerCompetencyRuleProfileViewrule_profileson the existing routerdocs/openedx_learning/decisions/0002-competency-criteria-model.rst: Decision 3 for the profile's columns, the seeded all-null-scope system default, the check constraint, andscope_codebeing internal-only; Decision 2's closing boundaries for pagination on list APIs; Decision 7 for archive-only retirement.docs/openedx_learning/decisions/0003-competency-criteria-versioning.rst, Decision 4: the pinned definition of a profile being in use.docs/openedx_learning/decisions/0001-competency-criteria-location.rst: why CBE code sits atsrc/openedx_learning/applets/cbe/, and how the umbrella app aggregates its applets.docs/openedx_content/decisions/0010-merge-authoring-apps-into-openedx-content.rstfor the applet convention and umbrella aggregation, withsrc/openedx_content/api.pyas the working example.docs/openedx_content/decisions/0003-identifier-conventions.rstforid,uuid, andkey.src/openedx_tagging/rest_api/v1/views.py,serializers.py,permissions.py, andurls.py, plussrc/openedx_tagging/rest_api/paginators.py; andsrc/openedx_tagging/rules.pyfor the taxonomy-administrator predicate and the permission-registration pattern.CompetencyRuleProfilemodel. [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613 seeds the system-default row via a data migration, validates the rule payload at the model layer, adds the profile's retired flag, and registersopenedx_learningin the installed apps and the layering contract. This ticket depends on all of it.api/cbe/rest_api/.