Skip to content

[BE] Update Get endpoint to return Taxonomy Type. #618

Description

@mgwozdz-unicon

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

  1. The frontend Taxonomies page reads taxonomy data from GET /api/content_tagging/v1/taxonomies/{id}/ and GET /api/content_tagging/v1/taxonomies/, served by TaxonomyOrgSerializer (a subclass of openedx-tagging's TaxonomySerializer). Currently, nothing identifies whether a taxonomy is a Competency Taxonomy.
  2. openedx-tagging's own TaxonomySerializer and its raw /api/tagging/v1/taxonomies/... endpoints have no knowledge of Competency Taxonomies and, per this ticket, never will.

Requested change

  1. Add a read-only taxonomy_type SerializerMethodField to TaxonomyOrgSerializer, following the same additive pattern already used for orgs/all_orgs (Meta.fields = TaxonomySerializer.Meta.fields + [...]). Returned on both the single-taxonomy and list GET endpoints.
  2. Value is "competency" if the Taxonomy instance has a related CompetencyTaxonomy row, "tags" otherwise. Never null.
  3. TaxonomyOrgView.get_queryset() fetches the related CompetencyTaxonomy row alongside the Taxonomy queryset (via select_related) so the list endpoint costs no extra query per row.

Depends on:

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 gain taxonomy_type.
  • No new field, method, or enum in openedx_tagging or 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

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

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

Activity

  1. bradenmacdonald commented on Jun 30, 2026

    @bradenmacdonald
    Contributor

    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?

  2. jesperhodge commented on Jul 1, 2026

    @jesperhodge
    Contributor

    Claude-supported feedback:

    • taxonomy_type return 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."

  3. jesperhodge commented on Jul 1, 2026

    @jesperhodge
    Contributor

    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.

  4. jesperhodge commented on Jul 1, 2026

    @jesperhodge
    Contributor

    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."

  5. mgwozdz-unicon commented on Jul 2, 2026

    @mgwozdz-unicon
    ContributorAuthor

    @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_type is 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 null vs "tags" naming (@jesperhodge): agreed. The field now returns "tags" for standard taxonomies and "competency" for Competency Taxonomies, never null. This also makes it symmetric with the Create endpoint's taxonomy_type field 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 TaxonomyOrgSerializer passes taxonomy_type through) 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') 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.

    On where the shared constant lives: TaxonomyType(models.TextChoices) in src/openedx_tagging/models/base.py, next to the Taxonomy model. It's just the two string values, not a model reference, so it doesn't create a layering problem — openedx_tagging already owns the write-side ChoiceField choices 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.

  6. moved this from Todo to Ready for Community Review in Competency Criteria and Student Progresson Jul 2, 2026
  7. moved this from Ready for Community Review to Final Axim Review in Competency Criteria and Student Progresson Jul 8, 2026
  8. bradenmacdonald commented on Jul 8, 2026

    @bradenmacdonald
    Contributor

    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 plain Taxonomy objects, and calling get_type() will always just return "tags", never allowing for overrides.

    What would work:

    1. Making taxonomy_type an optional string field on the base model
    2. Adding a tiny bit of Competency awareness to the base tagging app (technically breaking the layering)
    3. 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.

  9. mgwozdz-unicon commented on Jul 16, 2026

    @mgwozdz-unicon
    ContributorAuthor

    @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 base Taxonomy fields 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 like CompetencyTaxonomy, 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_type field directly on Taxonomy, set explicitly by whichever code creates a CompetencyTaxonomy row, in the same transaction. openedx_tagging never inspects or names CompetencyTaxonomy anywhere; 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: #662

    Would appreciate a look when you have a chance.

  10. 21 remaining items

  11. moved this from In Progress to Community Code Review in Competency Criteria and Student Progresson Sep 16, 2026
  12. moved this from Community Code Review to Ready for QA in Competency Criteria and Student Progresson Sep 17, 2026
  13. dvarenikqaconsultant commented on Sep 18, 2026

    @dvarenikqaconsultant

    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_type

    GET /api/content_tagging/v1/taxonomies/{id}/ was checked against taxonomies created and imported with each of the three possible taxonomy_type inputs. The response's taxonomy_type field matched the input in every case.

    Taxonomy created/imported with Expected taxonomy_type in response Result
    "taxonomy_type": "competency" "competency" Pass
    "taxonomy_type": "tags" "tags" Pass
    taxonomy_type field 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 via POST /api/content_tagging/v1/taxonomies/import/.

    2. List endpoint returns taxonomy_type for every taxonomy

    GET /api/content_tagging/v1/taxonomies returns an array of taxonomies under the results key. Every taxonomy in the list has a non-empty taxonomy_type field, 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_type is independent of read_only

    Taxonomies created with "read_only": true were checked against the same three taxonomy_type inputs (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_type matched the taxonomy's actual creation input in every combination.

    4. Export endpoint does not leak taxonomy_type

    GET /api/content_tagging/v1/taxonomies/{id}/export/?output_format=json and the equivalent output_format=csv request were checked for taxonomies of all three taxonomy_type cases (competency, tags, omitted). The export endpoint returns tag data only; taxonomy_type is 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_type for every taxonomy creation/import path, the value is independent of the read_only flag, and the export endpoint correctly excludes it. No further action needed on this issue from a QA standpoint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions