-
Notifications
You must be signed in to change notification settings - Fork 26
docs: add ADR for institution-defined tag code field #637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| .. _openedx-tagging-adr-0010: | ||
|
|
||
| 10. Institution-defined tag code field | ||
| ======================================== | ||
|
|
||
| Status | ||
| ------ | ||
|
|
||
| Proposed | ||
|
|
||
| Context | ||
| ------- | ||
|
|
||
| `Issue #625 <https://github.com/openedx/openedx-core/issues/625>`_ asks the project to | ||
| determine an ID field strategy for Competency-Based Education (CBE) competency data. | ||
| Competency Management UX designs show a "Competency ID" on each competency tag: a | ||
| short, human-readable identifier an institution can reference in its own systems | ||
| (spreadsheets, internal catalogs, etc). Clicking it drives the workflow that | ||
| creates a ``CompetencyCriteria`` association between the competency and content | ||
| criteria rules. | ||
|
|
||
| This identifier is explicitly **not** a CASE or CTDL identifier. Those are external | ||
| standards bodies' IDs that may need their own storage in the future; conflating them | ||
| with an institution's internal code would make each harder to reason about | ||
| independently. This ADR is scoped to the internal, institution-defined code only. | ||
|
Comment on lines
+22
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if I agree with that. If we can have a robust system that maps namespaced identifiers to tags, I could imagine us using a system where internal namespaces are peers to standardized ones. That being said, if and when that time comes, we can always migrate the existing data that we have to some new, more general schema. So I do support the idea that we're focusing strictly on internal codes for now, with the simpler model that implies.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When support is added for the CASE and CTDL identifiers, I can definitely envision wanting something akin to a tag_identifiers table where the columns of that table are effectively just tag_id, other_id_type, and other_id to support other_id_types of CASE and CTDL. The institution id (code) and/or external_id could potentially be stored in this table too if desired. I wasn't trying to preclude that kind of an idea with this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although, it might make sense to continue keeping the third party identifiers separate from the institution/openedx identifiers. I'm not sure I'd want to migrate those (external_id and code) into this tags_identifiers table since they'd already be functioning fine where they are. Really the only reason to add a new table at all would be out of a fear that the number of additional identifiers might get unweildy, and really even just the 2 additional (CASE and CTDL) may not constitute "unweildy". |
||
|
|
||
| ``Tag`` already has three identifiers, none of which fit: | ||
|
|
||
| - ``id``: internal primary key, never user-visible. | ||
| - ``uuid``: stable external reference, not human-readable or institution-assigned. | ||
| - ``external_id``: intended to link a Tag to a record in an externally-defined | ||
| taxonomy. It is de facto immutable: no API path mutates it after creation, tag | ||
| re-sync code documents it as "must be an immutable ID", and import/export uses it | ||
| purely as the lookup key to find and update existing tags on re-import. Repurposing | ||
| it as an admin-editable Competency ID would conflict with that existing, load-bearing | ||
| contract. | ||
|
Comment on lines
+34
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you expand on why this is currently immutable, and why it must remain so? My understanding was that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to jog my memory on this... I think it was because we needed an identifier that wasn't internal to Open edX (since sharing identifiers internal to the platform was deemed a security concern) that could be stable on import/export. Because otherwise, if you edit the external_id and re-import, then the platform has no way of knowing what you were editing. Whereas institutions need a mutable identifier that they can keep as human readable and in alignment with the current tag name that may be edited as well.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is it accurate to say that if we either exported the UUID, or provided a "previous_id" column in the import CSV format so that we could remap |
||
|
|
||
| **Placement constraint.** :ref:`openedx-tagging-adr-0002` keeps ``openedx_tagging`` | ||
| standalone, and :ref:`openedx-learning-adr-0001` already rejected putting CBE tables | ||
| inside it for that reason. A CBE-named field on ``Tag`` would repeat that mistake, | ||
| but a single scalar identifier — the same shape ``external_id`` already fills — does | ||
| not. Every taxonomy listing, search, and export already reads ``Tag``; putting a | ||
| per-tag identifier in a separate app would force a cross-app join into that common | ||
| path for what is otherwise a plain string column. | ||
|
|
||
| Decision | ||
| -------- | ||
|
|
||
| Add a new field, ``Tag.code``, to ``openedx_tagging`` as a **generic**, | ||
| non-CBE-specific identifier slot, available to any taxonomy: | ||
|
|
||
| - A plain, optional text field, matching ``external_id``'s shape. | ||
| - **Uniqueness:** enforced per-taxonomy, not globally unique (different | ||
| institutions/taxonomies may reasonably reuse a code) and not advisory-only (the CBE | ||
| workflow uses this value as a click target to create a ``CompetencyCriteria`` | ||
| association; ambiguity within a taxonomy would be a routing bug, not a | ||
| data-quality nit). | ||
| - **Assignment:** user-entered when given in initial import; every non-Competency taxonomy leaves | ||
| ``code`` fully optional with no fallback. For a Competency Taxonomy, every tag | ||
| needs one: creating a new tag without a code — via import, or via the existing | ||
| Taxonomy Editing UI, which has no field for it yet — generates a placeholder (e.g. | ||
| derived once from the tag's current name and position) instead of blocking the | ||
| tag's creation or an otherwise-valid import. If a user forgets to include the code column in their initial import, they can overwrite the placeholder | ||
| with their real code through a follow-up re-import, the same way any other field | ||
| is corrected. Re-importing an *existing* tag whose code is blank in the file leaves | ||
| its current value untouched rather than generating a new one — a blank cell isn't a | ||
| request to clear it since that is not allowed for Competency Taxonomies. | ||
| - **Completeness for Competency Taxonomies:** every tag in a Competency Taxonomy must | ||
| have a ``code``, guaranteed either by an explicit value or the placeholder fallback | ||
| above; every other taxonomy leaves it fully optional (see below for how this is | ||
| enforced without ``openedx_tagging`` knowing what a Competency Taxonomy is). | ||
| - **Read exposure:** ``code`` is exposed read-only outside of import, so it can still | ||
| be displayed on the Competency Management page and assignment workflow even though | ||
| it isn't editable there yet (planned for a later phase). | ||
| - **Migration:** additive only — a new nullable field, no backfill needed. No PII | ||
| annotation is required: ``code`` is institution-authored administrative metadata, | ||
| not personal data. | ||
|
|
||
| This keeps ``openedx_tagging`` free of CBE-specific concepts: the word "Competency" | ||
| never needs to appear in this app. The CBE applet's API and UI are responsible for | ||
| labeling ``code`` as "Competency ID" and driving the click-through workflow. | ||
|
|
||
| Completeness rule for Competency Taxonomies | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| ``openedx_tagging`` must not need to know what a "Competency Taxonomy" is to enforce | ||
| completeness, per the placement constraint above. The base ``Taxonomy`` class gets a | ||
| property that defaults to "not required"; ``CompetencyTaxonomy`` | ||
| (:ref:`openedx-learning-adr-0002`), a real subclass of ``Taxonomy`` defined outside | ||
| ``openedx_tagging``, overrides it to require a code. The rule is enforced at the | ||
| point a tag is saved, so every path that creates one — import, the existing Taxonomy | ||
| Editing UI, or the generic API — is covered by one check rather than one per caller, | ||
| and none of them can accidentally create a code-less tag. | ||
|
|
||
| Import/export | ||
| ~~~~~~~~~~~~~~ | ||
|
|
||
| ``code`` becomes an optional column in the import file format for every taxonomy. A | ||
| new tag with no ``code`` gets the placeholder fallback described above, whether it's | ||
| a Competency Taxonomy tag created via import or via the UI. Re-importing an existing | ||
| tag never clears or regenerates its ``code`` just because the file's column is blank | ||
| for that row; it only changes when the file supplies an explicit new value. | ||
|
|
||
| How a Competency Taxonomy itself gets created is out of this ADR's scope — that is | ||
| already covered by separate, in-progress work. This ADR's changes don't depend on | ||
| that work landing first, and don't introduce any new API endpoints of their own. | ||
|
|
||
| Rejected Alternatives | ||
| ---------------------- | ||
|
|
||
| Repurpose ``external_id`` | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Reuse the existing ``external_id`` field instead of adding a new one. Rejected: | ||
| ``external_id`` is de facto immutable and is the load-bearing lookup key for | ||
| import/export matching and system-defined-taxonomy re-sync. Giving it a second, | ||
| editable meaning would break that existing contract. | ||
|
|
||
| CBE-specific field directly on ``Tag`` | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Add a field explicitly named and documented as the "Competency ID" on ``Tag``, as a | ||
| deliberate exception to :ref:`openedx-tagging-adr-0002`'s boundary. Rejected: this is | ||
| the same schema location as the chosen decision, but naming it as a CBE concept bakes | ||
| domain semantics into a library meant to stay standalone, for no benefit over the | ||
| generic framing. | ||
|
|
||
| Competency-specific field in ``openedx_learning`` | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Add the identifier to a model in the new ``openedx_learning`` app instead, referenced | ||
| from ``Tag``. Rejected: this value is display data needed by every competency tag | ||
| listing, search, and export. A cross-app join for a plain string column adds cost with | ||
| no corresponding benefit, and no table in that app has the right cardinality (one row | ||
| per tag, not per taxonomy or per criterion). | ||
|
|
||
| ``CompetencyTaxonomy`` as the home | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Attach the identifier to the ``CompetencyTaxonomy`` row itself. Rejected on | ||
| cardinality: ``CompetencyTaxonomy`` is one row per taxonomy; a Competency ID is one | ||
| value per tag. | ||
|
|
||
| Keep every code continuously derived from the tag's name and hierarchy | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Treat the code as a live, computed value (e.g. ``PK-1.2``) that stays in sync with | ||
| the tag's current name and position, rather than a value the institution owns once | ||
| it exists. Rejected: once a tag has any code — real or a placeholder from the | ||
| fallback above — only a re-import changes it. Continuously recomputing it on every | ||
| rename or move would contradict that and mean the institution never really owns the | ||
| value once assigned. | ||
|
|
||
| Global uniqueness | ||
| ~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Enforce uniqueness across all taxonomies. Rejected: overreaches into assuming a | ||
| single global namespace for institution-chosen codes, which a shared library | ||
| shouldn't impose. | ||
|
|
||
| Advisory-only uniqueness | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| No database constraint, just a UI warning on collision. Rejected: the field is a | ||
| click target for creating a ``CompetencyCriteria`` association; a duplicate within a | ||
| taxonomy is a functional ambiguity, not just a data-quality nit. | ||
|
|
||
| Enforce completeness only during import, not at the model level | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Rejected: tags can already be created outside the import path today, through the | ||
| existing Taxonomy Editing UI or the generic tag-creation API, neither of which has a | ||
| way to supply a code directly. Enforcing the rule only during import would let those | ||
| paths silently create a code-less tag instead of falling back to a placeholder. | ||
|
|
||
| Changelog | ||
| --------- | ||
|
|
||
| 2026-07-03: | ||
|
|
||
| * Proposed. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it always one identifier, or do institutions sometimes have multiple identifiers for the same tag (e.g. from different systems)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be only one identifier. Today Competency Frameworks/Taxonomies are sourced through hand crafted spreadsheets rather than any sort of source system probably 99% of the time. I'm pretty sure none of the other LMSs support multiple identifiers for competencies. If there were other identifiers, they would most likely be the CASE GUID or CTDL identifier, which should be explicitly stored as such.