Proposal for course revisioning strategy

Created on 14 June 2025, 3 months ago

Problem/Motivation

The LMS module already includes data integrity measures to protect student progress from edited course content, which helps make sure that changes to lessons or activities don't alter the context for students already taking a course.

But the current approach can prevent course creators from continually improving courses after students have started them, which may be a requirement for online courses that have continuous enrollment. Any time a course creator needs to make a significant update or correction, they may have to duplicate the course and republish it β€” which has its own complications for current students, and for previous students who want to revisit their answers.

The goal of this proposal is to allow new versions of courses, lessons, and activities to be created and published at any time, without affecting students in progress, who will seamlessly continue with the versions they started with.
 

Proposed Resolution

We can implement a fully-dynamic course version system by using entity revisioning. The main concept is to link student progress entities (CourseStatus, LessonStatus, and Answer) to specific revisions of their content entities (Course, Lesson, and Activity).

Newly enrolling students will get the latest published revisions of a course and its components. They will then be locked in to those revisions for their course, evaluation, and revisits.

This will create a clear and reviewable history of content changes, and keep each student's learning path consistent with the materials they started with.
 

Benefits:

  • Continuous Improvement: Course creators can update, refine, and correct course materials at any time, knowing that students currently in progress will not be affected.
     
  • Improved Data Integrity: Builds on the existing data protection by making sure each student always interacts with a specific version of the learning materials.
     
  • Better Content Management: Provides a full revision history for all learning content, allowing for easy review, comparison, and rollback if necessary. Also allows course creators to collaborate in Workspaces and publish changes when ready.
     
  • Admin Efficiency: Minimizes the need to duplicate entire courses, or other workaround strategies, when updates are needed.
     
  • Fairness and Consistency: Students are evaluated based on the version of the course, lessons, and activities that they started with β€“ including scoring criteria like max points or required percentages β€“ even if content is updated later. And they can always revisit the exact same activities they originally started with.
     
  • Architectural Excellence: By using Drupal's native revisioning, this feature has the potential to position Drupal LMS as one of the best-architected learning management systems available, offering a higher level of flexibility, editability, and content control than any other LMS platform.
     

Phased Implementation Plan

If this proposal is agreed upon, it will be a significant update, but it can be broken down into phases. Each phase will need to maintain full module functionality, include automated testing, and provide an upgrade path for existing sites.

Phase 1: Enable Revisioning & Basic Referencing

  • All of the Course (Group), Lesson, and Activity entity types need to be set up for revisioning (new_revision: true, appropriate revision metadata keys).
     
  • Add new *_vid (integer) fields to CourseStatus, LessonStatus, and Answer entities to store the target revision IDs.
     
  • Create update hooks (hook_update_N) to:
    • Apply schema changes for the new *_vid fields.
    • For existing entities, ensure they have an initial revision.
    • For existing CourseStatus, LessonStatus, and Answer records, populate the new *_vid fields to point to the (then) current/latest revision of their referenced content. This establishes a baseline for existing in-progress courses.
       

Phase 2: Save Revisions & Reference vid on Creation

  • Modify entity forms (Course, Lesson, Activity) to default to "Create new revision" on save, and include instructions on the effects of revisions.
     
  • Update TrainingManager's creation logic (e.g., initializeTraining(), initializeLesson(), createAnswer()) to store the vid of the latest published revision of the parent/referenced entity into the new *_vid fields when new student progress records are created.
     

Phase 3: Load Specific Revisions for Student Progress

  • Refactor TrainingManager, status entities (CourseStatus, LessonStatus), AnswerForm, CourseController, BlockBuilder, and any other relevant services/controllers to load specific entity revisions (using loadRevision($vid)) based on the *_vid fields stored in the student's progress records.
     
  • Make sure that the lessons for a course, and the activities for a lesson, are populated from their specific parent's revisions.
     
  • Implement revision-aware cache tags and contexts for all relevant display components (e.g., course navigation block, activity views, start links).
     
  • Add functional testing: Student A starts Course v1. Admin edits content, creating Course v2. Student A continues their course interacting only with v1 content. Student B enrolls and starts Course v2, interacting only with v2 content.
     

Phase 4: Deletion Protection & UI/UX Refinements

  • Add access checks or entity hooks to prevent the deletion of entity revisions that are actively referenced by student progress entities.
     
  • Enhance DataIntegrityChecker and entity delete forms (ActivityDeleteForm, LessonDeleteForm) to account for revision references.
     
  • Make sure admins can view, compare, and revert revisions for Courses, Lessons, and Activities.
     
  • Refine rules and UI for deleting old, unreferenced revisions versus revisions actively in use.
     

Phase 5: Additional Enhancements

  • Serve Freshest Unaccessed Content: if an edit is made to a lesson or activity that a student hasn't reached yet, they could be served the newest version. May be valuable for long courses.
     
  • Data Archival/Expiration: Implement strategies for managing progress data of students who have been inactive for an extended period.
     

Discussion

Please add your thoughts, concerns, optimizations, fears, and/or excitement!
 

🌱 Plan
Status

Active

Version

1.0

Component

Courses and lessons

Created by

πŸ‡¨πŸ‡¦Canada ob3ron Canada

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @ob3ron
  • πŸ‡ΊπŸ‡ΈUnited States moshe weitzman Boston, MA

    Drupal Commerce has a similar need. When you buy a product, it should be frozen in time, and not subject to future edits. I’m not sure if they solve this by refusing. Worth a look.

    In general this proposal makes sense to me. There are.a couple downsides. Namely teachers Can’t easily fix typos in a course and have that affect students in progress. Same for scoring. Also we could see revision bloat similar to what happens on sites that use paragraphs module.

  • πŸ‡¨πŸ‡¦Canada ob3ron Canada

    @moshe weitzman thanks for the feedback, much appreciated.

    The thinking is that for something like a typo, teachers/admins can submit an edit and not create a new revision, in which case the change will be pushed live to current students.

    Revisions shouldn't proliferate too badly since it will be a conscious choice to make a new one, and we can implement processes to safely delete old unused revisions.

    I'll check out the Commerce approach, thanks for that tip!

  • πŸ‡¬πŸ‡§United Kingdom catch

    The thinking is that for something like a typo, teachers/admins can submit an edit and not create a new revision, in which case the change will be pushed live to current students.

    I think this is a bit too subtle to expect course authors to understand the implications of or get right all the time even if they do - if you're just doing a quick fix, the default behaviour would be to fix the typo and hit save. Once revisions default to on, that will result in a new revision, you have to take extra steps to avoid it.

    However the issue summary doesn't explicitly mention that e.g. lessons would reference specific versions of activities (which is the only way older versions could be loaded), and I'm not sure it's necessary.

    If e.g. a lesson status references a specific revision of a lesson, then the list of activities will differ by version, and it's that which would allow activities to be added or removed from lessons without affecting students in progress.

    Similar if a course status references a specific revision of a course, the list of lessons will differ by version.

    https://www.drupal.org/project/entity_reference_revisions β†’

    There's another issue though that something like adding a lesson at the end of a course where no-one has reached it yet, you might want everyone to take that lesson

  • πŸ‡΅πŸ‡±Poland Graber

    We could make a global setting so the site owner can choose if students take the course / lesson versions from the moment of starting or current, that'd also solve any possible BC issues.

  • πŸ‡¬πŸ‡§United Kingdom catch

    I think we could do phase 1 without the exact details of what phase 2/3 look like - we definitely want answers, lesson status, and course status to reference revisions. Means switching the entity reference field over to https://www.drupal.org/project/entity_reference_revisions β†’

  • πŸ‡¨πŸ‡¦Canada ob3ron Canada

    Can we start off by adding *_vid fields to CourseStatus, LessonStatus, and Answer entities and run an initial update hook, then transition to Entity Reference Revisions when we're ready to migrate the Course.lessons and Lesson.activities fields? Seems like that could make the updates less monolithic.

    Drupal Commerce has a similar need...I’m not sure if they solve this by revisions.

    Turns out Commerce copies the current product data into an order item. Same need but not really a transferable solution.

    I think [the revision checkbox] is a bit too subtle to expect course authors to understand the implications of or get right all the time even if they do

    The revision checkbox could have explicit instructions marked "IMPORTANT". Or we could alter the form to have two submit buttons, one for a minor live update, the other for a new version. Either way I agree it's hard to make it foolproof. Needs good documentation.

    something like adding a lesson at the end of a course where no-one has reached it yet, you might want everyone to take that lesson

    The "Freshest Unaccessed Content" feature would address this. It adds some complexity to the lock-in timing, but seems worthwhile for long courses.

    We could make a global setting so the site owner can choose if students take the course / lesson versions from the moment of starting or current

    If set to "current" then we'd lose consistency with evaluation & revisits, unless we associate revision lists with uid. The revision schema updates add a BC layer, so not sure if this would add more complication than it's worth.

  • πŸ‡΅πŸ‡±Poland Graber

    Just a β€œvid” field will not be enough, for example lesson status references multiple activities, that’d be too messy. I also wouldn’t like to add an additional dependency but we can use some of the Entity Reference Revisions code as a reference - a new field type plugin should be enough in our case.
    Update may be a bit tricky.

  • πŸ‡΅πŸ‡±Poland Graber

    Actually.. maybe it’s not going to be so bad as we’ll just need to add a new column to an existing schema, reference field names will not change after all.

  • πŸ‡΅πŸ‡±Poland Graber

    This will be bigger as we'll have to reference lesson revisions from course status as we currently reference activities from lesson status. The good thing is consistency though. That should be the first step of the work here. The first step alone will only introduce needless data storage though.

  • πŸ‡΅πŸ‡±Poland Graber

    This will also affect caching a lot - nav block will depend on course status lessons and not course lessons. Course nav structure cache will have to be dropped.

  • πŸ‡¬πŸ‡§United Kingdom catch

    For #12 I think we might want to make this configurable so that you only get a per-course-status navigation structure/activity display when the site owner explicitly wants that - I can equally see people being confused why they can't fix a typo in an activity name for existing users.

    Also we already have logic to cache the navigation block differently depending on whether modules are finished, in progress, or not taken yet so it should be possible to preserve some or most of that.

  • Merge request !107#3530210: Support LMS reference revisions β†’ (Merged) created by Graber
  • πŸ‡΅πŸ‡±Poland Graber

    Added a draft MR, all seems to be working but still a lot of testing is needed and nav block cacheability needs to be updated.

    One thing is very annoying: we need an update hook that converts the old field types to the new one, copies target_id data from the old tables and sets new data basing on current entity revisions. We can also make this feature available in the next major but still - it wouldn't be nice if we didn't provide an upgrade path.

    I'm not any good with that, spend a lot of time on it already with highly unsatisfying results so.. If anyone wants to help or provide a good example from which I could just copy-paste some code, we can do it. Been there: https://www.drupal.org/docs/drupal-apis/update-api/updating-entities-and... β†’ , https://www.drupal.org/project/field_type_converter β†’

    If not - let's release that next major ASAP while we still don't have any or very little projects in production phase. Maintaining 2 branches will also be too much pain.

    Thoughts very welcome.

  • πŸ‡¬πŸ‡§United Kingdom catch

    Couple of questions on the MR.

  • πŸ‡΅πŸ‡±Poland Graber

    Thanks, that clarified a bit.

  • πŸ‡¨πŸ‡¦Canada ob3ron Canada

    For the revisioning UI, what if the edit forms have radio buttons, with no default selection, instead of a revision checkbox? So editors have to make a conscious choice of which option they want. They could be labelled with some detail, like:

    β—‹ Update current version. Apply these changes immediately for all current students. Recommended for minor edits, such as typo corrections, that don't affect the overall meaning or scoring.

    β—‹ Create new revision. This version will only be shown to new students; existing students will remain on the previous revision. Recommended for any major changes.

  • πŸ‡΅πŸ‡±Poland Graber

    For the revisioning UI, what if the edit forms have radio buttons, with no default selection, instead of a revision checkbox?

    It's one extra click in the UI vs a conscious choice. I have no idea really, we need to get more opinions on this, preferably something for later as it's something very easy to alter by project anyway.

  • πŸ‡¬πŸ‡§United Kingdom catch

    We would need to only change the form when the revision mode is enabled, so content editors would get a different form depending on configuration, which could be somewhat confusing?

    I have a question (and I think we should add some documentation about it) for what this means for lessons that aren't taken yet.

    Currently course_status entities don't reference lessons, so it seems like changes to which lessons are in a course won't respect the revision setting?

    Assume I have a course A with lessons 1 and 2.

    All of these things could happen:

    Lesson 1 is removed.

    Lesson 2 is removed.

    Lesson 3 is added.

    A new activity is added to lesson 2

    An activity in lesson 2 is edited.

    What I think should happen is that activity changes in not-yet-started lessons should not be affected by the revision setting, this is because there would not yet be a lesson_status entity yet. If there's not yet a lesson_status entity, then we can use the default revision of the lesson up until the user starts to take it, and only from that point would changes take place. As far as I can tell that's how things would work with the current MR, and it's also what I think the correct behaviour is.

    Because the lesson_status activity includes a reference to every activity, I'm assuming that as soon as a lesson is started, any changes to activities, including not-yet-taken ones, would not have an effect in revision-mode because they all change to revision references.

    However, if we don't have a lessons reference on course_status, then it feels like there wouldn't be equivalent behaviour there, and I'm not entirely sure what the behaviour should be - e.g. should revision mode allow you to add lessons to the end of a course? Or not?

  • πŸ‡΅πŸ‡±Poland Graber

    @catch lessons of a started course respect same rules when revisions are enabled, it's just the entity loading logic is different:
    course_status -> lesson status (if exists) -> lesson revision reference

  • Pipeline finished with Success
    about 1 month ago
    Total: 251s
    #562876
  • Pipeline finished with Success
    about 1 month ago
    Total: 266s
    #562902
  • πŸ‡΅πŸ‡±Poland Graber

    Merged but this will be released in a new minor beta so it gets sufficient testing and feedback from the community before being marked as stable.

  • Status changed to Fixed 14 days ago
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024