13. Competency taxonomy detection in openedx-platform#
Status#
Accepted
Context#
The taxonomy Get endpoints need to report whether a taxonomy is a Competency Taxonomy, so
that Studio can badge Competency Taxonomies and gate access to the Competency Management
page, symmetrically with the Create/Import endpoint’s taxonomy_type field.
CompetencyTaxonomy is a Django multi-table-inheritance subclass of
Taxonomy, owned by the CBE applet and defined in
ADR 0002.
openedx_tagging is a generic tagging library with no knowledge of any specific taxonomy
flavor built on top of it, CBE or otherwise, and must not gain any: a specific downstream
applet’s model or relation name has no business appearing in this library’s source, since
that reverses the intended dependency direction (CBE depends on openedx_tagging, not
the other way around) and would break for any Open edX install running openedx_tagging
without CBE installed.
Decision#
Report a taxonomy’s type entirely within openedx-platform, using the existing relation
between Taxonomy and CompetencyTaxonomy established in ADR 0002, without adding any
field, method, or enum value to openedx_tagging:
openedx-platform’s REST layer adds a read-only
taxonomy_typevalue to its taxonomy serializer, computed by checking whether a relatedCompetencyTaxonomyrow exists for thatTaxonomy:"competency"if so,"tags"otherwise.It performs that check through the CBE app’s public API,
openedx_learning.api.is_competency_taxonomy(), rather than naming the relation itself. The relation name is a Django-generated default derived from the model’s class name, so spelling it in openedx-platform would let a rename upstream break Studio with nothing failing in either repository’s tests.That same layer’s queryset fetches the related
CompetencyTaxonomyrow alongside theTaxonomylist, using the companionselect_competency_taxonomies(), so the check costs no extra query per row.openedx_tagging’sTaxonomymodel and its baseTaxonomySerializergain nothing for this purpose: no new field, no new enum value, no import. That constraint is onopenedx_taggingalone. The CBE app owns this relation, so exposing it through the CBE app’s own public API is expected rather than avoided.No creation-time wiring is needed to keep this accurate: ADR 0002 Decision 1 already creates the
CompetencyTaxonomyrow in the same transaction as its parentTaxonomyrow, so the existence check can never drift out of sync the way a separately-stored field could.
Known trade-off. A future third taxonomy type needs another hardcoded branch in
openedx-platform’s shared serializer, the same cost a field-based approach would have
avoided with a one-line enum addition. Accepted because keeping openedx_tagging free of
any competency-specific reference, even an inert stored value, was judged more valuable
than that extensibility, particularly given the project’s move away from system-defined
taxonomies, which makes a third taxonomy flavor unlikely soon.
Rejected Alternatives#
taxonomy_type enum field on the base Taxonomy model#
A TaxonomyType(models.TextChoices) field (TAGS/COMPETENCY) added directly to
Taxonomy, set by whichever code creates a CompetencyTaxonomy row, in the same
transaction as ADR 0002 Decision 1’s existing lifecycle rule. Although this requires no
per-request check and was more extensible for a hypothetical third taxonomy flavor, it
still named a CBE-specific concept, a COMPETENCY enum value, directly in
openedx_tagging’s own schema and public API. Keeping openedx_tagging fully free of
any competency-specific reference, even an inert one, is worth the lost extensibility.
Two-call frontend create flow#
Studio calls the plain create-taxonomy endpoint, then makes a second call to a separate, CBE-owned endpoint to convert it to a competency taxonomy, with no new REST surface needed. Rejected because the frontend must orchestrate both calls itself and handle the case where the first succeeds but the second fails, leaving a plain taxonomy behind with no competency conversion.
New combined competency REST API#
A single new CBE-owned endpoint that creates both the Taxonomy and
CompetencyTaxonomy rows atomically. Rejected because it is a new endpoint to design,
build, and maintain, and the frontend still needs both this call and the plain
create-taxonomy call, choosing between them since nothing upstream tells it in advance
whether a Tags or Competency taxonomy is being created; that branching just relocates the
type-awareness into the frontend rather than removing it.
An overridable Taxonomy.get_type() method#
A base Taxonomy.get_type() method returning "tags", overridden by
CompetencyTaxonomy to return "competency", mirroring the existing
Taxonomy.system_defined / SystemDefinedTaxonomy base/override shape. Rejected for
two independent reasons:
That base/override shape is implemented via
Taxonomy._taxonomy_classandTaxonomy.cast()/Taxonomy.copy(), which are planned to be removed as a pattern.Querying taxonomies the normal way (
Taxonomy.objects.all()) returns plainTaxonomyinstances, so a subclass method override is never reached without an explicit cast step first. The existing.cast()/.copy()implementation would not correctly perform that cast for a true multi-table-inheritance subclass in any case: it copies a hardcoded list of baseTaxonomyfield values in Python and never queries the subclass’s own table, so it would silently produce aCompetencyTaxonomyinstance with unset or wrong subclass-specific fields.