Repository navigation
[BE] Update Get endpoint to return Taxonomy Type. #618
Description
Activity
- moved this to Todo in Competency Criteria and Student Progress
on Jun 30, 2026 As a Platform Administrator, I need the taxonomy Get endpoint to return the Taxonomy Type so that platform interfaces can identify which taxonomies are Competency Taxonomies and which are standard tag taxonomies.
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?
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."
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.
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."@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.
Reacted by Jesper Hodge- moved this from Todo to Ready for Community Review in Competency Criteria and Student Progress
on Jul 2, 2026 - moved this from Ready for Community Review to Final Axim Review in Competency Criteria and Student Progress
on Jul 8, 2026 On the layering violation (@jesperhodge): confirmed, same class of problem as #614 and resolved the same way. The original proposal (hasattr(obj, 'competencytaxonomy') inside openedx_tagging's serializer) hardcoded the CBE applet's MTI relation name into a standalone library and was dropped. Instead, Taxonomy gets an overridable get_type() method returning "tags" by default — the same base/override shape already used by Taxonomy.system_defined / SystemDefinedTaxonomy in this codebase. A future CompetencyTaxonomy, owned entirely outside openedx_tagging, overrides it to return "competency". openedx_tagging never references CompetencyTaxonomy or the competencytaxonomy relation name anywhere.
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:
- Making
taxonomy_typean optional string field on the base model - Adding a tiny bit of Competency awareness to the base tagging app (technically breaking the layering)
- Avoiding taxonomy_type entirely on the tagging backend(s) but implementing awareness of Competency taxonomies in the Studio frontend.
I think I lean toward 1+3, but open to other suggestions.
- Making
@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
- moved this from Next Sprint to In Progress in Competency Criteria and Student Progress
on Sep 2, 2026 - moved this from In Progress to Community Code Review in Competency Criteria and Student Progress
on Sep 16, 2026 - moved this from Community Code Review to Ready for QA in Competency Criteria and Student Progress
on Sep 17, 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 created/imported with Expected taxonomy_typein responseResult "taxonomy_type": "competency""competency"Pass "taxonomy_type": "tags""tags"Pass taxonomy_typefield omitted"tags"Pass 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.- moved this from Ready for QA to Ready for UAT in Competency Criteria and Student Progress
on Sep 18, 2026 - moved this from Ready for UAT to Done in Competency Criteria and Student Progress
on Sep 18, 2026 - closed this as completedby moving to Done in Competency Criteria and Student Progress
on Sep 18, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
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:
TaxonomyOrgSerializerandTaxonomyOrgView(openedx/core/djangoapps/content_tagging/rest_api/v1/) gain ataxonomy_typefield computed by checking for a relatedCompetencyTaxonomyrow. No field, method, or enum is added toopenedx_taggingor the CBE app, per ADR 0013 (merged via #662).Current behavior
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.TaxonomySerializerand its raw/api/tagging/v1/taxonomies/...endpoints have no knowledge of Competency Taxonomies and, per this ticket, never will.Requested change
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."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:
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.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
/api/tagging/v1/taxonomies/...endpoints. Per ADR 0013, only openedx-platform's/api/content_tagging/v1/taxonomies/...endpoints gaintaxonomy_type.openedx_taggingor the CBE app.Acceptance Criteria
These scenarios are verifiable via Postman, against openedx-platform's endpoints.
Open Questions
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_typeas aSerializerMethodFieldonTaxonomyOrgSerializer, the same shape already used fororgsandall_orgs:(Confirm the
competencytaxonomyaccessor 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 matchingselect_relatedalongside its existingprefetch_related("taxonomyorg_set__org"), so the list endpoint's per-row check costs no extra query:Per ADR 0013, this is the entire change. Nothing in
openedx_tagging'sTaxonomymodel, its baseTaxonomySerializer, or the CBE app should reference this concept.Example Resolution Prompt
In
edx-platform, add a read-onlytaxonomy_typefield toTaxonomyOrgSerializer(openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py), following ADR 0013 (openedx-coredocs/openedx_tagging/decisions/0013-competency-taxonomy-detection.rst): aSerializerMethodFieldreturning"competency"if theTaxonomyinstance has a relatedCompetencyTaxonomyrow,"tags"otherwise, added toMeta.fieldsthe same wayorgs/all_orgsalready are. Confirm the exact reverse-relation accessor name against theCompetencyTaxonomymodel introduced by #640 (do not assumecompetencytaxonomywithout checking, since #640 may not have landed yet — this ticket is blocked until it does). UpdateTaxonomyOrgView.get_queryset()toselect_relatedthat relation alongside its existingprefetch_related("taxonomyorg_set__org")so list responses issue no extra query per row. Do not touchopenedx_tagging'sTaxonomymodel or baseTaxonomySerializerin 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 existingTaxonomyOrgSerializer/TaxonomyOrgViewtest coverage.Context
Taxonomy, an in-openedx_taggingrelation check, and an overridableget_type()method).CompetencyTaxonomymodel this ticket detects; hard blocker.taxonomy_typeon write; this ticket's read-side value ("tags"/"competency") matches those, but the two are otherwise independent under ADR 0013.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
openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.pytaxonomy_typeSerializerMethodFieldtoTaxonomyOrgSerializer, appended toMeta.fieldsopenedx/core/djangoapps/content_tagging/rest_api/v1/views.pyselect_relatedfor theCompetencyTaxonomyrelation toTaxonomyOrgView.get_queryset()openedx/core/djangoapps/content_tagging/rest_api/v1/tests/...(confirm exact path)