-
Notifications
You must be signed in to change notification settings - Fork 23
(WIP) feat: implement Pathways #482
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
Draft
Agrendalath
wants to merge
3
commits into
openedx:main
Choose a base branch
from
open-craft:agrendalath/pathways
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| 23. Pathways | ||
| ============ | ||
|
|
||
| Context | ||
| ------- | ||
|
|
||
| Open edX needs a way to group multiple courses (and, in the future, other learning contexts) into structured learning collections that learners can enroll in and progress through. | ||
|
|
||
| The pathways applet provides foundational models for defining pathways, managing enrollment, tracking learner progress, and evaluating completion criteria. It lives in ``openedx_content.applets.pathways`` as part of the Open edX Core. | ||
|
|
||
| Decisions | ||
| --------- | ||
|
|
||
| 1. Model Overview | ||
| ~~~~~~~~~~~~~~~~~ | ||
|
|
||
| The following diagram shows all pathway models and their relationships. | ||
|
|
||
| .. Run `dot -Tsvg pathways-diagram.dot > pathways-diagram.svg` to regenerate the diagram after making changes to the data model in `images/pathways-diagram.dot`. | ||
| .. image:: images/pathways-diagram.svg | ||
| :alt: Pathways Data Model | ||
| :width: 100% | ||
|
|
||
| 2. Pathway Structure | ||
| ~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| A **Pathway** is an ordered collection of steps that a learner progresses through. Each **PathwayStep** represents a single step within the pathway, which references a learning context (e.g., a course) that the learner must complete to fulfill that step. | ||
|
|
||
| **Publishing and Versioning** | ||
|
|
||
| Both ``Pathway`` and ``PathwayStep`` use ``PublishableEntityMixin`` / ``PublishableEntityVersionMixin``, linking to ``PublishableEntity`` and ``PublishableEntityVersion`` via OneToOneField (as established by the publishing applet). This provides: | ||
|
|
||
| - Draft/publish workflows — operators can prepare pathway changes without exposing incomplete versions to learners. | ||
| - Version history — every published change is tracked, enabling rollback and audit. | ||
| - Consistent tooling — the same publish/draft infrastructure used by other content types (Components, Units, etc.). | ||
|
|
||
| ``PublishableEntity`` already provides ``uuid``, ``key``, ``learning_package``, ``created``, and ``created_by``. ``PublishableEntityVersion`` provides ``uuid``, ``title``, ``version_num``, ``created``, and ``created_by``. The pathway models extend these with domain-specific fields. | ||
|
|
||
| **Pathway** (uses ``PublishableEntityMixin``): | ||
|
|
||
| - ``key`` — ``PathwayKeyField`` with format ``path-v1:{org}+{path_id}`` (extends ``LearningContextKeyField``). Note It provides pathway key validation and parsing. | ||
| - ``org`` — FK to ``Organization``. Ties the pathway to a specific organization. | ||
|
|
||
| **PathwayVersion** (uses ``PublishableEntityVersionMixin``): | ||
|
|
||
| - ``pathway`` — FK back to ``Pathway``. | ||
| - ``description`` — detailed description (``title`` is inherited from ``PublishableEntityVersion``). | ||
| - ``sequential`` — whether steps must be completed in order. | ||
| - ``invite_only`` — whether enrollment is restricted to the allowlist. | ||
| - ``default_step_criteria`` — CEL expression applied to steps that don't override it. | ||
| - ``completion_criteria`` — CEL expression for pathway-level completion. | ||
| - ``metadata`` — JSONField for operator-specific extensibility (duration, difficulty, learning outcomes, etc.). | ||
|
|
||
| **PathwayStep** (uses ``PublishableEntityMixin``): | ||
|
|
||
| - ``pathway`` — FK to ``Pathway``. | ||
|
|
||
| **PathwayStepVersion** (uses ``PublishableEntityVersionMixin``): | ||
|
|
||
| - ``step`` — FK back to ``PathwayStep``. | ||
| - ``context_key`` — ``LearningContextKeyField`` referencing the learning context (e.g., ``course-v1:OpenedX+DemoX+DemoCourse``). TODO: replace it with proper foreign keys (e.g., to ``CourseRun``) before finalizing the data model. | ||
| - ``step_type`` — explicit type label (e.g., ``course``, ``pathway``) stored for query efficiency and validation rather than derived from the key at query time. | ||
| - ``order`` — position within the pathway (0-indexed). | ||
| - ``criteria`` — optional CEL expression overriding the pathway's ``default_step_criteria``. | ||
| - ``group`` — optional label for grouping steps in pathway-level expressions (e.g., "core", "elective"). | ||
|
|
||
| Step display name and description are derived from the referenced learning context (course, subsection, etc.) and are not stored on the step itself. | ||
|
|
||
| Pathways can be **sequential** (steps must be completed in order) or **non-sequential** (any order). Pathways can be **invite-only**, restricting enrollment to allowlisted users. | ||
|
|
||
| Open Questions: | ||
| *************** | ||
|
|
||
| #. Do we need a ``PathwayType``? If yes, what use cases would it serve that couldn't be handled within metadata? | ||
| #. Do we need ``step_type``, after all? Could we derive it from the key prefix (e.g., "course-v1" vs "path-v1")? Storing it explicitly allows for easier querying and validation, but introduces redundancy. | ||
|
|
||
| 3. Enrollment | ||
| ~~~~~~~~~~~~~ | ||
|
|
||
| Enrollment is modeled after the existing Open edX course enrollment patterns: | ||
|
|
||
| - **PathwayEnrollment** tracks user-pathway enrollment state with an ``is_active`` flag for soft unenrollment. | ||
| - **PathwayEnrollmentAllowed** provides a pre-registration allowlist for invite-only pathways, enabling invitations before users register accounts. | ||
| - **PathwayEnrollmentAudit** records all enrollment state transitions for compliance and debugging. | ||
| - A **signal receiver** listens for ``User`` ``post_save`` and converts pending ``PathwayEnrollmentAllowed`` records into real enrollments when a new user registers. | ||
|
|
||
| 4. Completion Criteria via CEL | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Completion criteria use the `Common Expression Language <https://cel.dev/>`_ (CEL), a safe, sandboxed expression language with no side effects and guaranteed termination. | ||
|
|
||
| Criteria operate at two levels: | ||
|
|
||
| **Step-level criteria** define when an individual step is considered "passed": | ||
|
|
||
| - Evaluated per-step against two variables: ``grade`` (0.0–1.0) and ``completion`` (0.0–1.0). | ||
| - ``PathwayVersion.default_step_criteria`` sets the default for all steps in the pathway. | ||
| - ``PathwayStepVersion.criteria`` can override the default for a specific step. | ||
| - An empty expression means the step always passes (no requirement). | ||
| - Example: ``grade >= 0.7 && completion >= 0.8`` | ||
|
|
||
| **Pathway-level criteria** define when the overall pathway is considered "complete": | ||
|
|
||
| - Evaluated once against a ``steps`` list, where each entry is a map containing ``grade``, ``completion``, ``passed`` (computed from step-level criteria), ``group``, and ``order``. | ||
| - ``PathwayVersion.completion_criteria`` defines the expression. An empty expression means all steps must pass. | ||
| - ``PathwayStepVersion.group`` provides an optional label for grouping steps in pathway-level expressions. | ||
| - Examples: | ||
|
|
||
| - ``steps.filter(s, s.passed).size() >= 4`` — pass any 4 steps | ||
| - ``steps.filter(s, s.group == "core" && s.passed).size() >= 3 && steps.filter(s, s.group == "elective" && s.passed).size() >= 2`` — pass 3 core and 2 elective steps | ||
| - ``steps.all(s, s.grade >= 0.8)`` — all steps must have grade at least 80%. TODO: This overrides the step-level criteria, so it could lead to a step being considered "not passed" at the step level but still contributing to pathway completion if its grade is high enough. | ||
| - ``steps.filter(s, s.completion >= 1.0).size() == steps.size()`` — all steps must be fully completed | ||
| - ``steps.filter(s, s.order <= 2 && s.passed).size() == 3 && steps.filter(s, s.order > 2 && s.grade >= 0.9).size() >= 1`` — first 3 steps must pass, plus at least 1 later step requires a grade of at least 90% | ||
| - ``steps.map(s, s.grade).reduce(acc, val, acc + val) / steps.size() >= 0.75`` — average grade across all steps must be at least 75% | ||
|
|
||
| 5. Learner Progress Tracking | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| **PathwayStepAttempt** — records a student's progress on a given attempt to fulfill a pathway step. This data is stored for efficiency in evaluating criteria and supporting progress displays without having to query the underlying learning context (e.g., course) for each step on the fly. | ||
|
|
||
| - ``user`` — FK to the user. | ||
| - ``step`` — FK to ``PathwayStep``. | ||
| - ``grade`` — current grade (0.0–1.0). | ||
| - ``completion_level`` — current completion (0.0–1.0). | ||
| - ``created``, ``updated`` — timestamps. | ||
|
|
||
| This stores one row per student per attempt (e.g., per course run), not per grade change. Detailed progress history is handled by eventing/analytics. | ||
|
|
||
| Whether a student has completed a step or the overall pathway is computed on-the-fly by evaluating the CEL criteria against the attempt data — there are no separate status models. | ||
|
|
||
| Open Questions: | ||
| *************** | ||
|
|
||
| #. **Completion tracking performance**: For grades, we can rely on persistent grades. However, calculating completion currently requires building the course tree for each user, which is slow (and updating this entity may happen thousands of times for each learner within a course). Tracking earned/possible completions in ``PathwayStepAttempt`` could be an option, but introduces invalidation complexity: completions would need to be recalculated on course publish and when gated content becomes available (e.g., due to release dates set in the future). | ||
| #. **Multiple course runs**: The initial assumption was that a Pathway can reference only a single course run. However, one of the described use cases is changing a course run while keeping the same pathway. How do we want to support this? | ||
|
|
||
| 6. Retrieving data from openedx-platform | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| TODO. | ||
|
|
||
| Consequences | ||
| ------------ | ||
|
|
||
| - The pathways applet provides a stable foundation for pathway management within Open edX Core, following the applet pattern established in ADR 0020. | ||
| - Using ``PublishableEntityMixin`` / ``PublishableEntityVersionMixin`` gives pathways proper draft/publish workflows and version history, consistent with other content types in the system. | ||
| - CEL-based criteria provide a flexible, safe way to express arbitrarily complex completion requirements without custom code. | ||
| - The two-level criteria design (step-level + pathway-level) supports both simple per-step thresholds and complex aggregate requirements like "pass N of M steps" or grouped criteria. | ||
| - External consumers (``openedx-platform``, plugins) can interact through either the public Python API or the REST API (both TBD). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| digraph pathways { | ||
| node [shape=record fontname="Helvetica" fontsize=10] | ||
| edge [fontname="Helvetica" fontsize=9] | ||
|
|
||
| subgraph cluster_publishing { | ||
| label="Publishing Layer" | ||
| style=dashed | ||
| color=gray | ||
|
|
||
| PE [label="{PublishableEntity|uuid\llearning_package (FK)\lkey\lcreated\lcreated_by (FK)\l}"] | ||
| PEV [label="{PublishableEntityVersion|uuid\lentity (FK)\ltitle\lversion_num\lcreated\lcreated_by (FK)\l}"] | ||
| PE -> PEV [label="1:N" style=dashed color=gray] | ||
| } | ||
|
|
||
| subgraph cluster_pathway { | ||
| label="Pathway Definition" | ||
| style=solid | ||
|
|
||
| Pathway [label="{Pathway|publishable_entity (1:1 PK)\lkey (PathwayKeyField)\lorg (FK Organization)\l}"] | ||
| PathwayVersion [label="{PathwayVersion|publishable_entity_version (1:1 PK)\lpathway (FK)\ldescription\lsequential\linvite_only\ldefault_step_criteria (CEL)\lcompletion_criteria (CEL)\lmetadata (JSON)\l}"] | ||
| PathwayStep [label="{PathwayStep|publishable_entity (1:1 PK)\lpathway (FK)\l}"] | ||
| PathwayStepVersion [label="{PathwayStepVersion|publishable_entity_version (1:1 PK)\lstep (FK)\lstep_type\lcontext_key (LearningContextKeyField)\lorder\lcriteria (CEL)\lgroup\l}"] | ||
|
|
||
| Pathway -> PathwayVersion [label="1:N"] | ||
| Pathway -> PathwayStep [label="1:N"] | ||
| PathwayStep -> PathwayStepVersion [label="1:N"] | ||
| } | ||
|
|
||
| subgraph cluster_enrollment { | ||
| label="Enrollment" | ||
| style=solid | ||
|
|
||
| User1 [label="{User}" style=dashed] | ||
| PathwayEnrollment [label="{PathwayEnrollment|user (FK)\lpathway (FK)\lis_active\lcreated\lmodified\l}"] | ||
| PathwayEnrollmentAllowed [label="{PathwayEnrollmentAllowed|email\lpathway (FK)\lcreated\l}"] | ||
| PathwayEnrollmentAudit [label="{PathwayEnrollmentAudit|enrollment (FK)\laction\ltimestamp\l}"] | ||
|
|
||
| User1 -> PathwayEnrollment [label="1:N"] | ||
| User1 -> PathwayEnrollmentAllowed [label="1:N"] | ||
| PathwayEnrollment -> PathwayEnrollmentAudit [label="1:N"] | ||
| PathwayEnrollmentAllowed -> PathwayEnrollmentAudit [label="1:N"] | ||
| } | ||
|
|
||
| subgraph cluster_progress { | ||
| label="Learner Progress" | ||
| style=solid | ||
|
|
||
| User2 [label="{User}" style=dashed] | ||
| PathwayStepAttempt [label="{PathwayStepAttempt|user (FK)\lstep (FK PathwayStep)\lgrade\lcompletion_level\lcreated\lupdated\l}"] | ||
|
|
||
| User2 -> PathwayStepAttempt [label="1:N"] | ||
| } | ||
|
|
||
| PE -> Pathway [label="1:1" style=dotted] | ||
| PE -> PathwayStep [label="1:1" style=dotted] | ||
| PEV -> PathwayVersion [label="1:1" style=dotted] | ||
| PEV -> PathwayStepVersion [label="1:1" style=dotted] | ||
|
|
||
| Pathway -> PathwayEnrollment [label="1:N"] | ||
| Pathway -> PathwayEnrollmentAllowed [label="1:N"] | ||
|
|
||
| PathwayStep -> PathwayStepAttempt [label="1:N"] | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
I think the way we'd want to group this is more like container parent-child relationships where:
PathwayVersionreferencesPathwaySteps, i.e. there's aPathwayVersionPathwayStepmodel that joinsPathwayVersiontoPathwayStepand has an optional ordering field. PathwaySteps don't reference Pathways directly. I mean, I guess we could even just call them Steps?That would let us let PathwaySteps be semantically meaningful in a standalone way, e.g. "This represents what it means to finish Intro Accounting", which can be used in various ways by different Pathways if we wanted. It also makes it easier for us to ensure that we don't have weird ordering conflicts between different PathwaySteps, since storing it on the PathwayVersion makes it awkward to put a constraint to prevent corrupt ordering states (two PathwaySteps that have the same order num).
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.
When creating the
PathwayVersion, you would list all itsStepsas dependencies when callingcreate_publishable_entity_version().