Repository navigation
[BE] Update Get endpoint to return Taxonomy Type. #618
Description
Use Case
As a Platform Administrator, I want the taxonomy retrieval API to report each taxonomy's type ("tags" or "competency"), so that the Taxonomies page can support displaying a badge for Competency Taxonomies and correctly gate access to the Competency Management page.
Description
This ticket's implementation is entirely in openedx-platform, not openedx-core: TaxonomyOrgSerializer and TaxonomyOrgView (openedx/core/djangoapps/content_tagging/rest_api/v1/) gain a taxonomy_type field computed by checking for a related CompetencyTaxonomy row. No field, method, or enum is added to openedx_tagging or the CBE app, per ADR 0013 (merged via #662).
Current behavior
- The frontend Taxonomies page reads taxonomy data from
GET /api/content_tagging/v1/taxonomies/{id}/andGET /api/content_tagging/v1/taxonomies/, served byTaxonomyOrgSerializer(a subclass of openedx-tagging'sTaxonomySerializer). Currently, nothing identifies whether a taxonomy is a Competency Taxonomy. - openedx-tagging's own
TaxonomySerializerand its raw/api/tagging/v1/taxonomies/...endpoints have no knowledge of Competency Taxonomies and, per this ticket, never will.
Requested change
- Add a read-only
taxonomy_typeSerializerMethodFieldtoTaxonomyOrgSerializer, following the same additive pattern already used fororgs/all_orgs(Meta.fields = TaxonomySerializer.Meta.fields + [...]). Returned on both the single-taxonomy and list GET endpoints. - Value is
"competency"if theTaxonomyinstance has a relatedCompetencyTaxonomyrow,"tags"otherwise. Nevernull. TaxonomyOrgView.get_queryset()fetches the relatedCompetencyTaxonomyrow alongside theTaxonomyqueryset (viaselect_related) so the list endpoint costs no extra query per row.
Depends on:
- CBE app foundation + CompetencyTaxonomy model #640 (CompetencyTaxonomy model, part of [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613's CBE core data models) — hard blocker. Confirmed by repo search on 2026-07-29: the
CompetencyTaxonomymodel does not exist in code yet, only in ADR form (ADR 0002). This ticket cannot be finished, and its exact relation-name cannot be confirmed, until that model lands. - [BE] Update taxonomy Create & Import endpoints to accept Taxonomy Type #614 / [BE] openedx-core - Accept taxonomy_type on Create & Import endpoints #628 (Create/Import endpoint accepting
taxonomy_type) — soft dependency, needed only for the Postman-verifiable acceptance criteria's realistic end-to-end flow (creating a Competency Taxonomy through the API). ACompetencyTaxonomyrow created directly (fixture, shell, admin) would also satisfy this ticket's AC.
Explicitly out of scope
- No "competency only" / "non-competency only" filter or query parameter on Get/List. The list endpoint keeps returning all taxonomy types together, undifferentiated.
- No change to content-tagging surfaces (Course Outline, Libraries). Tagging content with a Competency Taxonomy without linking Competency Criteria shall stay possible; this ticket adds no restriction there.
- No change to openedx-tagging's own
/api/tagging/v1/taxonomies/...endpoints. Per ADR 0013, only openedx-platform's/api/content_tagging/v1/taxonomies/...endpoints gaintaxonomy_type. - No new field, method, or enum in
openedx_taggingor the CBE app.
Acceptance Criteria
These scenarios are verifiable via Postman, against openedx-platform's endpoints.
Scenario: Get a single Competency Taxonomy
Given a Competency Taxonomy exists (a Taxonomy with a related CompetencyTaxonomy row)
When a client sends GET /api/content_tagging/v1/taxonomies/{id}/ for that taxonomy
Then the response has status code 200
And the response body's "taxonomy_type" field equals "competency"
Scenario: Get a single standard tag taxonomy
Given a standard (non-competency) taxonomy exists
When a client sends GET /api/content_tagging/v1/taxonomies/{id}/ for that taxonomy
Then the response has status code 200
And the response body's "taxonomy_type" field equals "tags"
And the field is never null
Scenario: List taxonomies of mixed type
Given at least one Competency Taxonomy and at least one standard taxonomy exist
When a client sends GET /api/content_tagging/v1/taxonomies/
Then every Competency Taxonomy entry has "taxonomy_type" equal to "competency"
And every other entry has "taxonomy_type" equal to "tags"
And no entry's "taxonomy_type" is null
And entries of both types are returned together, unfiltered
And the list endpoint issues no additional query per row to determine taxonomy_type
Open Questions
- The exact reverse-relation accessor Django generates on
TaxonomyforCompetencyTaxonomy(e.g.taxonomy.competencytaxonomy, the MTI default) is unconfirmed until CBE app foundation + CompetencyTaxonomy model #640 lands with a concrete model and migration. Implementer: confirm against the actual model rather than assuming the default name, in case a customrelated_nameis used. Owner: implementer, once CBE app foundation + CompetencyTaxonomy model #640 merges.
Technical Details
Recommended Approach
Add taxonomy_type as a SerializerMethodField on TaxonomyOrgSerializer, the same shape already used for orgs and all_orgs:
# openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py
class TaxonomyOrgSerializer(TaxonomySerializer):
orgs = serializers.SerializerMethodField()
all_orgs = serializers.SerializerMethodField()
taxonomy_type = serializers.SerializerMethodField()
class Meta:
model = TaxonomySerializer.Meta.model
fields = TaxonomySerializer.Meta.fields + ["orgs", "all_orgs", "taxonomy_type"]
read_only_fields = ["orgs", "all_orgs", "taxonomy_type"]
def get_taxonomy_type(self, obj) -> str:
return "competency" if hasattr(obj, "competencytaxonomy") else "tags"(Confirm the competencytaxonomy accessor name against #640's actual model before implementing; the right existence check depends on the concrete relation type #640 lands with, verified against the real model rather than assumed.)
TaxonomyOrgView.get_queryset() needs the matching select_related alongside its existing prefetch_related("taxonomyorg_set__org"), so the list endpoint's per-row check costs no extra query:
# openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
queryset = queryset.select_related("competencytaxonomy")Per ADR 0013, this is the entire change. Nothing in openedx_tagging's Taxonomy model, its base TaxonomySerializer, or the CBE app should reference this concept.
Example Resolution Prompt
In edx-platform, add a read-only taxonomy_type field to TaxonomyOrgSerializer (openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py), following ADR 0013 (openedx-core docs/openedx_tagging/decisions/0013-competency-taxonomy-detection.rst): a SerializerMethodField returning "competency" if the Taxonomy instance has a related CompetencyTaxonomy row, "tags" otherwise, added to Meta.fields the same way orgs/all_orgs already are. Confirm the exact reverse-relation accessor name against the CompetencyTaxonomy model introduced by #640 (do not assume competencytaxonomy without checking, since #640 may not have landed yet — this ticket is blocked until it does). Update TaxonomyOrgView.get_queryset() to select_related that relation alongside its existing prefetch_related("taxonomyorg_set__org") so list responses issue no extra query per row. Do not touch openedx_tagging's Taxonomy model or base TaxonomySerializer in openedx-core — the whole point of ADR 0013 is that this concept never appears there. Verify against the Acceptance Criteria above and add regression tests to the existing TaxonomyOrgSerializer/TaxonomyOrgView test coverage.
Context
- ADR 0013 (openedx-core, merged via docs: add ADR for competency taxonomy detection #662) — the decision this ticket implements, including its Rejected Alternatives (an enum field on
Taxonomy, an in-openedx_taggingrelation check, and an overridableget_type()method). - CBE app foundation + CompetencyTaxonomy model #640 — implements the
CompetencyTaxonomymodel this ticket detects; hard blocker. - [BE] Implement CBE core data models (CompetencyTaxonomy, criteria, learner status) #613 — parent CBE core data model ticket.
- [BE] Update taxonomy Create & Import endpoints to accept Taxonomy Type #614 / [BE] openedx-core - Accept taxonomy_type on Create & Import endpoints #628 — Create/Import endpoint accepting
taxonomy_typeon write; this ticket's read-side value ("tags"/"competency") matches those, but the two are otherwise independent under ADR 0013. - Remove Taxonomy subclasses functionality + system-level taxonomies #634 — Remove Taxonomy subclasses + system-level taxonomies; its planned removal of
Taxonomy._taxonomy_class/.cast()is part of why ADR 0013 rejected an overridableget_type()method. openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py:70— currentTaxonomyOrgSerializer, showing theorgs/all_orgspattern this ticket mirrors.openedx/core/djangoapps/content_tagging/rest_api/v1/views.py:50-100— currentTaxonomyOrgView.get_queryset(), showing the existingprefetch_related/annotatethis ticket adds to.
Files to create and modify
Modified files
| File | Nature of modification |
|---|---|
openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py |
Add taxonomy_type SerializerMethodField to TaxonomyOrgSerializer, appended to Meta.fields |
openedx/core/djangoapps/content_tagging/rest_api/v1/views.py |
Add select_related for the CompetencyTaxonomy relation to TaxonomyOrgView.get_queryset() |
openedx/core/djangoapps/content_tagging/rest_api/v1/tests/... (confirm exact path) |
Add coverage for a competency taxonomy, and a standard taxonomy, across both detail and list endpoints |
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
bradenmacdonald commentedon Jun 30, 2026
Just to push back on this a little: I can imagine an alternative where most parts of the system don't care about taxonomy type (as is the case today), so that the regular taxonomy API continues to return both competency and "normal" taxonomies, because you can generally use both types for tagging course content (or can you?). And for parts of the system that specifically need Competency taxonomies only, they could call a Competency API to get those, rather than calling the regular taxonomy API and filtering by type.
I guess what I'm asking is: do we have any use case for retrieving only non-competency taxonomies?
jesperhodge commentedon Jul 1, 2026
Claude-supported feedback:
taxonomy_typereturn value is inconsistent and a leaky abstraction: See also here."Type 'competency' | null is a leaky abstraction. The component renders "Tags" for null, which means the component is encoding the business rule that "null means Tags." A cleaner API contract: the GET endpoint returns "tags" for standard taxonomies and "competency" for competency taxonomies. Then the type is 'competency' | 'tags' and the badge component doesn't need to translate null → "Tags". This inconsistency (Create accepts "tags", GET returns null) is a design smell that will confuse future consumers."
jesperhodge commentedon Jul 1, 2026
I'd also like this to be split up into two tickets - one for edx-platform, one for openedx-core. It makes it much easier to get the two PRs across the finish line.
jesperhodge commentedon Jul 1, 2026
Same layering violation risk as #614. openedx_learning (or at least the cbe applet) needs to depend on openedx_tagging, but openedx_tagging should not depend on or know about competencies.
openedx_tagging/rest_api/v1/serializers.py using hasattr(obj, 'competencytaxonomy') means openedx_tagging's serializer is now aware of the MTI child class name. Even without importing the class, this encodes a semantic dependency on openedx_learning's CBE models by string reference. If openedx_tagging is truly standalone, it shouldn't know the name competencytaxonomy. The same architectural question from #614 applies here: the taxonomy REST endpoints may need to be extended from openedx_learning.
Claude:
"Defining "competency" as a named constant shared between the Create serializer (#614) and GET serializer (#618) is called out but neither issue says where the constant lives. It should be in a shared constants module in openedx_tagging — but only if the layering problem is resolved first."
mgwozdz-unicon commentedon Jul 2, 2026
@bradenmacdonald @jesperhodge Addressing everything raised above, updated the ticket description to match:
On the "non-competency only" question (@bradenmacdonald): I don't think there's a use case for it.
taxonomy_typeis informational only. It drives a badge display on the Taxonomies page and gates access to the Competency Management page. Get/List keep returning all taxonomy types together, undifferentiated; no filter or query param is being added. Content-tagging surfaces (Course Outline, Libraries) will continue to not consume this value at all. Tagging content with a Competency Taxonomy without linking Competency Criteria needs to stay possible, so no restriction is added there either.On the
nullvs"tags"naming (@jesperhodge): agreed. The field now returns"tags"for standard taxonomies and"competency"for Competency Taxonomies, nevernull. This also makes it symmetric with the Create endpoint'staxonomy_typefield from #614, which already only accepts those two values.On splitting by repo (@jesperhodge): done. This ticket now covers the openedx-core change only. The edx-platform verification (confirming
TaxonomyOrgSerializerpassestaxonomy_typethrough) is split into its own companion ticket #630 .On the layering violation (@jesperhodge): confirmed, same class of problem as #614 and resolved the same way. The original proposal (
hasattr(obj, 'competencytaxonomy')insideopenedx_tagging's serializer) hardcoded the CBE applet's MTI relation name into a standalone library and was dropped. Instead,Taxonomygets an overridableget_type()method returning"tags"by default — the same base/override shape already used byTaxonomy.system_defined/SystemDefinedTaxonomyin this codebase. A futureCompetencyTaxonomy, owned entirely outsideopenedx_tagging, overrides it to return"competency".openedx_taggingnever referencesCompetencyTaxonomyor thecompetencytaxonomyrelation name anywhere.On where the shared constant lives:
TaxonomyType(models.TextChoices)insrc/openedx_tagging/models/base.py, next to theTaxonomymodel. It's just the two string values, not a model reference, so it doesn't create a layering problem —openedx_taggingalready owns the write-sideChoiceFieldchoices from #614, and this lets both the read side (this ticket) and write side (#614) import one definition instead of drifting.Full updated ticket description reflects all of this.
bradenmacdonald commentedon Jul 8, 2026
I don't think this makes sense? As we discussed in #634 , we will be removing subclasses altogether, so "the same base/override shape already used by Taxonomy.system_defined" will not be available.
If CompetencyTaxonomy is implemented using Django's Multi-Table inheritance, it will work well on the Competency side, but on the Taxonomy side, loading
Taxonomy.objects.all()will just return plainTaxonomyobjects, and callingget_type()will always just return "tags", never allowing for overrides.What would work:
taxonomy_typean optional string field on the base modelI think I lean toward 1+3, but open to other suggestions.
mgwozdz-unicon commentedon Jul 16, 2026
@bradenmacdonald
Dug into this, thanks for flagging it.
You're right that
get_type()wouldn't have worked regardless because.cast()/.copy()only copies a hardcoded list of baseTaxonomyfields in Python and never queries a subclass's own table. That's fine for a proxy model with no separate table, but it would silently misrepresent a true multi-table-inheritance subclass likeCompetencyTaxonomy, independent of whether #634 lands. So the override approach was broken either way.But we also don't want to revive the original
hasattr(instance, "competencytaxonomy")approach either since that hardcodes a downstream applet's relation name into a generic library.I think we should go with your option 1: a
taxonomy_typefield directly onTaxonomy, set explicitly by whichever code creates aCompetencyTaxonomyrow, in the same transaction.openedx_taggingnever inspects or namesCompetencyTaxonomyanywhere; the field's value is opaque to it. Full reasoning, including why this doesn't reopen ADR 0002's rejection of a flat column, is in a new ADR here: #662Would appreciate a look when you have a chance.
21 remaining items
dvarenikqaconsultant commentedon Sep 18, 2026
Environment: Master Sandbox
Test user: dmitryvarenikqa
Test type: API, verified manually via browser DevTools (network tab)
Overall result: Pass. No bugs found.
1. Detail endpoint returns
taxonomy_typeGET /api/content_tagging/v1/taxonomies/{id}/was checked against taxonomies created and imported with each of the three possibletaxonomy_typeinputs. The response'staxonomy_typefield matched the input in every case.taxonomy_typein response"taxonomy_type": "competency""competency""taxonomy_type": "tags""tags"taxonomy_typefield omitted"tags"All three cases were verified twice: once for a taxonomy created via
POST /api/content_tagging/v1/taxonomies/, and once for a taxonomy imported viaPOST /api/content_tagging/v1/taxonomies/import/.2. List endpoint returns
taxonomy_typefor every taxonomyGET /api/content_tagging/v1/taxonomiesreturns an array of taxonomies under theresultskey. Every taxonomy in the list has a non-emptytaxonomy_typefield, and its value correctly reflects how that taxonomy was created or imported ("tags","competency", or"tags"when the field was omitted at creation).3.
taxonomy_typeis independent ofread_onlyTaxonomies created with
"read_only": truewere checked against the same threetaxonomy_typeinputs (competency,tags, omitted) to confirm that marking a taxonomy read-only does not change how its type is reported. Verified on both the detail and list endpoints.taxonomy_typematched the taxonomy's actual creation input in every combination.4. Export endpoint does not leak
taxonomy_typeGET /api/content_tagging/v1/taxonomies/{id}/export/?output_format=jsonand the equivalentoutput_format=csvrequest were checked for taxonomies of all threetaxonomy_typecases (competency,tags, omitted). The export endpoint returns tag data only;taxonomy_typeis not present in the response in any case, in either output format. No regression or unintended leak of taxonomy metadata into the export payload.Summary
All scenarios in scope for issue #618 passed as expected: the detail and list endpoints correctly surface
taxonomy_typefor every taxonomy creation/import path, the value is independent of theread_onlyflag, and the export endpoint correctly excludes it. No further action needed on this issue from a QA standpoint.