Skip to content

Commit d4b259d

Browse files
committed
feat: create openedx_learning djangoapp and CompetencyTaxonomy model
This PR adds a way to mark a taxonomy as holding competencies rather than ordinary tags, so that competency-based education (CBE) features can later be built on top of it. * Adds a new Django app, openedx_learning, and its first sub-package, cbe. This is a new home for learning-side features. It follows the same layout as the existing openedx_content app: one Django app, split into subfolders for readability. * Adds one model, CompetencyTaxonomy. Marking a taxonomy as a competency taxonomy means creating one of these. It does not copy the taxonomy or replace it; it points at the existing taxonomy and adds a single extra setting to it. * Adds a small public API so other code can ask 'is this taxonomy a competency taxonomy?' without needing to know how that link is stored. * Wires it all up: app registration, the database migration, a Django admin page, and a rule that keeps the dependency pointing one way (the CBE code may use the tagging code, never the reverse).
1 parent 7b2100d commit d4b259d

27 files changed

Lines changed: 361 additions & 22 deletions

File tree

.importlinter

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
[importlinter]
77
root_packages =
8+
openedx_learning
89
openedx_content
910
openedx_tagging
1011
openedx_django_lib
@@ -17,8 +18,12 @@ root_packages =
1718
name = "top-level source folders are layered correctly"
1819
type = layers
1920
layers =
20-
# Content is currently the highest-level thing in this repo.
21-
# Over time, we may add apps "above" or "below" this.
21+
# Learning-domain features (currently CBE; Learning Pathways to follow).
22+
# May build on content and tagging. Nothing below may import it: in
23+
# particular, openedx_tagging must never know that CBE exists.
24+
openedx_learning
25+
26+
# Content: authoring-side models and APIs.
2227
openedx_content
2328

2429
# Tagging is very simple & fundamental. Should probably not depend on any other Django apps.

docs/openedx_learning/decisions/0004-competency-mastery-concurrency.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ Rejected Alternatives
155155
with another writer already in flight.
156156

157157
- Pros:
158-
- Recovers from the race within the same request, without a separate marker or job.
158+
- Recovers from the race within the same request, without a separate marker or job.
159159
- Cons:
160-
- Decision 1 shares a transaction only between the grade and its leaf, and Decision 3 commits each rollup level
161-
separately before reading the next. That leaves no single transaction boundary for a read-after-write check to
162-
run inside: by the time a re-read would happen, the level below has already committed and could change again
163-
before the write completes.
164-
- It also only checks for a race at the moment each parent is read. If the worker crashes mid-cascade before reaching the next read,
165-
nothing notices the rollup was left unfinished. Decision 5's manual recovery mechanism exists to catch that case.
160+
- Decision 1 shares a transaction only between the grade and its leaf, and Decision 3 commits each rollup level
161+
separately before reading the next. That leaves no single transaction boundary for a read-after-write check to
162+
run inside: by the time a re-read would happen, the level below has already committed and could change again
163+
before the write completes.
164+
- It also only checks for a race at the moment each parent is read. If the worker crashes mid-cascade before reaching the next read,
165+
nothing notices the rollup was left unfinished. Decision 5's manual recovery mechanism exists to catch that case.

docs/openedx_tagging/decisions/0013-competency-taxonomy-detection.rst

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Decision
3030

3131
Report a taxonomy's type entirely within **openedx-platform**, using the existing relation
3232
between ``Taxonomy`` and ``CompetencyTaxonomy`` established in ADR 0002, without adding any
33-
field, method, or enum value to ``openedx_tagging`` or the CBE app:
33+
field, method, or enum value to ``openedx_tagging``:
3434

3535
.. image:: images/CompetencyTypeDetection.png
3636
:alt: Studio calls openedx-platform's serializer, which delegates to oel_tagging's pure
@@ -41,22 +41,29 @@ field, method, or enum value to ``openedx_tagging`` or the CBE app:
4141
- openedx-platform's REST layer adds a read-only ``taxonomy_type`` value to its taxonomy
4242
serializer, computed by checking whether a related ``CompetencyTaxonomy`` row exists for
4343
that ``Taxonomy``: ``"competency"`` if so, ``"tags"`` otherwise.
44+
- It performs that check through the CBE app's public API,
45+
``openedx_learning.api.is_competency_taxonomy()``, rather than naming the relation itself.
46+
The relation name is a Django-generated default derived from the model's class name, so
47+
spelling it in openedx-platform would let a rename upstream break Studio with nothing
48+
failing in either repository's tests.
4449
- That same layer's queryset fetches the related ``CompetencyTaxonomy`` row alongside the
45-
``Taxonomy`` list, so the check costs no extra query per row.
46-
- ``openedx_tagging``'s ``Taxonomy`` model, its base ``TaxonomySerializer``, and the CBE app
47-
stay fully unaware of each other for this purpose: no new field, no new enum value, no
48-
import.
50+
``Taxonomy`` list, using the companion ``select_competency_taxonomies()``, so the check
51+
costs no extra query per row.
52+
- ``openedx_tagging``'s ``Taxonomy`` model and its base ``TaxonomySerializer`` gain nothing
53+
for this purpose: no new field, no new enum value, no import. That constraint is on
54+
``openedx_tagging`` alone. The CBE app owns this relation, so exposing it through the CBE
55+
app's own public API is expected rather than avoided.
4956
- No creation-time wiring is needed to keep this accurate: ADR 0002 Decision 1 already
5057
creates the ``CompetencyTaxonomy`` row in the same transaction as its parent ``Taxonomy``
5158
row, so the existence check can never drift out of sync the way a separately-stored
5259
field could.
5360

5461
**Known trade-off.** A future third taxonomy type needs another hardcoded branch in
5562
openedx-platform's shared serializer, the same cost a field-based approach would have
56-
avoided with a one-line enum addition. Accepted because keeping ``openedx_tagging`` and the
57-
CBE app free of any competency-specific reference, even an inert stored value, was judged
58-
more valuable than that extensibility, particularly given the project's move away from
59-
system-defined taxonomies, which makes a third taxonomy flavor unlikely soon.
63+
avoided with a one-line enum addition. Accepted because keeping ``openedx_tagging`` free of
64+
any competency-specific reference, even an inert stored value, was judged more valuable
65+
than that extensibility, particularly given the project's move away from system-defined
66+
taxonomies, which makes a third taxonomy flavor unlikely soon.
6067

6168
Rejected Alternatives
6269
----------------------
@@ -69,9 +76,8 @@ A ``TaxonomyType(models.TextChoices)`` field (``TAGS``/``COMPETENCY``) added dir
6976
transaction as ADR 0002 Decision 1's existing lifecycle rule. Although this requires no
7077
per-request check and was more extensible for a hypothetical third taxonomy flavor, it
7178
still named a CBE-specific concept, a ``COMPETENCY`` enum value, directly in
72-
``openedx_tagging``'s own schema and public API. Keeping ``openedx_tagging`` and the CBE
73-
app fully free of any competency-specific reference, even an inert one, is worth the lost
74-
extensibility.
79+
``openedx_tagging``'s own schema and public API. Keeping ``openedx_tagging`` fully free of
80+
any competency-specific reference, even an inert one, is worth the lost extensibility.
7581

7682
Check for a related ``CompetencyTaxonomy`` row directly inside ``openedx_tagging``
7783
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

projects/dev.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
# Our Apps
4141
"openedx_catalog",
42+
"openedx_learning",
4243
"openedx_tagging",
4344
"openedx_content",
4445
*openedx_content_backcompat_apps_to_install(),

src/openedx_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"""
77

88
# The version for the entire repository
9-
__version__ = "1.2.0"
9+
__version__ = "1.3.0"

src/openedx_learning/README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Learning App
2+
============
3+
4+
The ``openedx_learning`` app holds models and APIs for what learners are meant to achieve
5+
and how they get there. Its sibling ``openedx_content`` holds the material itself.
6+
7+
Like ``openedx_content``, it is one Django app split into applets. Its first applet is
8+
``cbe``, for Competency-Based Education; Learning Pathways are expected to follow.
9+
10+
In the layering that ``.importlinter`` enforces, this app sits above ``openedx_content``
11+
and ``openedx_tagging``. It may build on either of them; neither may import it.

src/openedx_learning/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Learning-domain features for Open edX Core.
3+
"""

src/openedx_learning/admin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
This module aggregates all applet Django Admin modules.
3+
"""
4+
# pylint: disable=wildcard-import
5+
6+
from .applets.cbe.admin import *

src/openedx_learning/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
This is the public API for learning-domain features in Open edX Core.
3+
"""
4+
# This wildcard import is okay because the applet api module declares __all__.
5+
# pylint: disable=wildcard-import
6+
from .applets.cbe.api import *
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
The applets that make up the openedx_learning Django app.
3+
"""

0 commit comments

Comments
 (0)