Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.11.0] - 2026-02-23
Breaking Changes
SCORMLessonPageintroduced;CoursePage.scorm_packageremoved (#82)- SCORM delivery now lives at the lesson level:
SCORMLessonPageis a Wagtail Page that is a direct child ofCoursePageand holds ascorm_packageFK and anintrorich-text field CoursePage.scorm_packageFK removed; the SCORM package chooser no longer appears in the course editor- A data migration automatically creates a
SCORMLessonPagechild for every existingCoursePagethat had ascorm_packagevalue - SCORM player URL changed:
/lms/player/<course_id>/→/lms/scorm-lesson/<lesson_id>/play/; update any hard-coded URL reversals toreverse("wagtail_lms:scorm_player", args=[scorm_lesson_page.id]) -
CourseEnrollment.get_progress()removed; querySCORMAttemptdirectly per lesson -
LessonPagerenamed toH5PLessonPage;LessonCompletionrenamed toH5PLessonCompletion - DB tables, content types, and all internal references updated via
RenameModelmigrations - Update any downstream references to these model classes and their string labels (e.g.
"wagtail_lms.LessonPage"→"wagtail_lms.H5PLessonPage") WAGTAIL_LMS_CHECK_LESSON_ACCESScallable is now invoked byH5PLessonPage.serve()(interface unchanged)
Added
SCORMLessonPage— new Wagtail Page delivering a single SCORM package; renders a launch button linking to the SCORM player; access gated to enrolled users (Wagtail editors bypass)- System check
wagtail_lms.W002— warns at startup when aCoursePagesubclass definessubpage_typeswithout"wagtail_lms.SCORMLessonPage" - Mixed-mode course completion —
CourseEnrollment.completed_atis now set when all lessons in a course are done, regardless of type:H5PLessonPagerequires anH5PLessonCompletionrecord;SCORMLessonPagerequires aSCORMAttemptwithcompletion_statusof"completed"or"passed" - Completion state visible in default templates — the bundled templates now surface per-lesson and course-level completion state:
course_page.html: SCORM lesson list items show a checkmark when the user has a completed/passed attempt (mirrors existing H5P lesson checkmarks)scorm_lesson_page.html: displays the current attempt'scompletion_statusandsuccess_statusabove the launch button; includes a back-link to the parent courseh5p_lesson_page.html: shows a "Lesson completed" banner when anH5PLessonCompletionrecord exists for the user- Lesson page layout styles moved to
course.css—.lms-lessonand related classes (__header,__nav,__title,__intro,__block,__content,.lms-h5p-activity) are now defined in the shared stylesheet rather than inline inh5p_lesson_page.html;lms-button--secondaryadded to complete the button palette
Fixed
- SCORM player enrollment gate now consistent with H5P lesson access — Wagtail editors (users with
wagtailadmin.access_admin) can access the SCORM player without being enrolled in the course, matching the existing editor bypass inH5PLessonPage.serve() - LMS admin menu items hidden for all users —
ReadOnlyPermissionPolicyblockedadd/change/deleteunconditionally (including for superusers), causing Wagtail's menu-visibility check to hide SCORM Attempts, H5P Attempts, and H5P Lesson Completions from the sidebar. Fixed by overridinguser_has_any_permissionto redirect to theviewpermission check instead. H5PLessonPageandSCORMLessonPagedisplayed with incorrect casing — Wagtail title-cases class names when noverbose_nameis set, producing "H5p lesson page" and "Scorm lesson page" in the page type chooser. Explicitverbose_namevalues added to both models.
Migration notes
- Run
python manage.py migrate wagtail_lms— migrations 0004–0006 apply automatically: - 0004: creates
SCORMLessonPage, migrates existingCoursePage.scorm_packagedata, removes the field - 0005: renames
LessonPage→H5PLessonPageandLessonCompletion→H5PLessonCompletion - 0006: removes any stale
lessonpage/lessoncompletionContentType rows left behind by the rename (see note below) - Run
python manage.py fixtreeafter migrating — if aCoursePagepreviously had ascorm_packageand the data migration in 0004 incremented the parent'snumchildcounter but the child page did not persist (can occur with SQLite under certain transaction conditions), the page tree will have an inconsistentnumchild.fixtreedetects and corrects this automatically; it is a no-op if the tree is already consistent. - Update
subpage_typeson anyCoursePagesubclasses to include"wagtail_lms.H5PLessonPage"and"wagtail_lms.SCORMLessonPage" - Update any template URL tags that referenced the old SCORM player URL (
wagtail_lms:scorm_playernow takes alesson_id) - Remove references to
CoursePage.scorm_packageandCourseEnrollment.get_progress()
Note on stale ContentTypes (migration 0006): Django's
RenameModelmigration updates thedjango_content_typerow for the renamed model in-place. On some database/cache combinations an orphan row for the old model name (lessonpage,lessoncompletion) can survive alongside the new one. When present, this orphan causes anAttributeError: 'NoneType' object has no attribute '_inc_path'crash whenever Wagtail tries to add a child page under aCoursePagewhose existing children include a page with the stale ContentType. Migration 0006 deletes these orphan rows unconditionally; it is safe to run even when the orphans were never created.
[0.10.1] - 2026-02-22
Fixed
- Missing migration state update for
LessonPage.body - Added
src/wagtail_lms/migrations/0003_alter_lessonpage_body.pysomakemigrations wagtail_lms --check --dry-runno longer reports pending model changes on current supported stacks.
Changed
- CI now enforces migration-state consistency for
wagtail_lms - Added a dedicated workflow step running
python example_project/manage.py makemigrations wagtail_lms --check --dry-runbefore tests.
[0.10.0] - 2026-02-21
Added
- Downstream integration extension points (#73, #64, #63, #61)
LessonPage.parent_page_typesset toNonesoCoursePagesubclasses can host lessons without monkey-patchingWAGTAIL_LMS_SCORM_PACKAGE_VIEWSET_CLASS/WAGTAIL_LMS_H5P_ACTIVITY_VIEWSET_CLASSsettings for swapping Wagtail admin upload viewsetsWAGTAIL_LMS_H5P_SNIPPET_VIEWSET_CLASSsetting for swapping the H5P snippet viewset without runtime mutationWAGTAIL_LMS_CHECK_LESSON_ACCESSsetting (dotted-path callable) used byLessonPage.serve()access gateWAGTAIL_LMS_REGISTER_DJANGO_ADMINsetting to opt out of automatic Django admin model registrationWAGTAIL_LMS_SCORM_ADMIN_CLASS/WAGTAIL_LMS_H5P_ADMIN_CLASSsettings for swapping Django admin classes withoutunregister()- New test coverage for all downstream extension points
Changed
- H5P snippet registration now uses explicit hidden snippet viewset
- Removed
@register_snippetdecorator fromH5PActivitymodel - Registered
H5PActivitysnippet in hooks with a snippet viewset configured for chooser support without a duplicate sidebar admin entry -
Added backward-compatible
lms_viewset_groupalias in hooks for downstream projects still migrating off in-place patching -
Snippet model title panels fixed
- Replaced
TitleFieldPanel("title")withFieldPanel("title")onSCORMPackageandH5PActivityto avoidw-syncslug-target JavaScript errors on non-Page models
Documentation
- MkDocs Material site with Read the Docs publishing
- Added
readthedocs.yamlconfiguration for RTD builds using MkDocs - Added
mkdocs.ymlwith Material theme, navigation, and search - Added
mkdocs-materialandmkdocs-include-markdown-pluginto[project.optional-dependencies] docs docs/index.mdrewritten as a proper landing page with feature highlights and quick-install snippetdocs/changelog.mdanddocs/contributing.mdadded as thininclude-markdownwrappers soCHANGELOG.mdandCONTRIBUTING.mdare included verbatim in the site without duplicationdocs/testing.mdupdated: H5P models documented, hardcoded CI matrix replaced with a link to the workflow filedocs/roadmap.mdupdated: current status set to v0.9.0, H5P activity support marked complete, v0.10.0 section added (downstream integration fixes — issues #73, #64, #63, #61, #71), future versions renumbered to 0.11.0 / 0.12.0 / 1.0.0-
RTD documentation badge added to
README.md -
Read the Docs release-versioning rollout for v0.10.0+
- Pinned docs build dependencies, documented one-time RTD automation/default-version setup, added post-release RTD version verification, and fixed docs links so MkDocs strict mode passes
Deprecated
WAGTAIL_LMS_CONTENT_PATHrenamed toWAGTAIL_LMS_SCORM_CONTENT_PATHfor consistency with theWAGTAIL_LMS_SCORM_*prefix convention. The old name still works and its configured value is honoured, but it now emits aDeprecationWarningat startup. It will be removed in a future release — rename the setting in your Django settings to silence the warning.
[0.9.0] - 2026-02-20
Added
- H5P activity support (#57, #60, #65, #66)
H5PActivityWagtail snippet — upload.h5ppackages, auto-extract to any Django storage backend (local, S3, etc.), parseh5p.jsonmetadata; choosable in lesson composition blocks; respectsWAGTAIL_LMS_H5P_UPLOAD_PATHandWAGTAIL_LMS_H5P_CONTENT_PATHsettingsLessonPageWagtail page — long-scroll layout withStreamFieldbody (RichTextBlock+H5PActivityBlock); enforces page hierarchy underCoursePage; enrollment gate inserve()redirects unenrolled users to the parent courseH5PAttempt— per-user, per-activity progress record (completion/success status, raw/min/max/scaled scores); lazily created on first xAPI event; DB-levelunique_togetheron(user, activity)H5PXAPIStatement— append-only xAPI statement log with verb display and full raw payloadPOST /lms/h5p-xapi/<activity_id>/— CSRF-protected xAPI ingestion endpoint; validates thatverbandresultare JSON objects (returns 400 otherwise); mapscompleted,passed,failed, andscoredverbs to attempt fields; setsCourseEnrollment.completed_atoncompletedorpassedGET /lms/h5p-content/<path>— authenticated H5P asset serving viaServeH5PContentView, a subclass ofServeScormContentView; inherits the same extensibility hooks (get_storage_path,get_cache_control,should_redirect,get_redirect_url) and path-traversal protection; content URL built viareverse()so any URL mount point is respectedH5PActivityViewSet(full CRUD) andH5PAttemptViewSet(read-only) added to theLMSViewSetGroupWagtail admin menu; Django admin registrations included- h5p-standalone v3.8.0 (MIT) vendored:
main.bundle.js,frame.bundle.js,styles/h5p.css h5p-lesson.js—IntersectionObserverlazy loading (300 px look-ahead); xAPI listener registered synchronously before player init so the same activity embedded multiple times on a page never double-posts;X-CSRFTokenon every fetch; append?h5pLazy=0to the lesson URL to disable lazy loading and initialise all activities immediately (useful for debugging); append?h5pDebug=1to enable verbose console logging of xAPI routing decisions, resume preload results, and dispatcher lifecycle events- H5P file cleanup on package deletion via
post_deletesignal, mirroring existing SCORM behaviour -
Example project updated with H5P workflow documentation and navigation link
-
H5P resume state persistence
H5PContentUserDatamodel — stores per-user, per-activity H5P runtime state keyed by(attempt, data_type, sub_content_id);unique_togetherconstraint prevents duplicate rows; auto-managedcreated_at/updated_attimestampsGET /lms/h5p-content-user-data/<activity_id>/— returns saved state payload or{"success": true, "data": false}for first-time learners; does not create an attempt record on readPOST /lms/h5p-content-user-data/<activity_id>/— stores or updates state payload; H5P's clear signal (data=0) deletes the row; rejects payloads exceeding 64 KB (HTTP 413) to prevent storage exhaustion- State is pre-fetched before H5P Standalone initialises and passed as
contentUserDataso learners resume where they left off without an extra round-trip after load -
Verified with
H5P.QuestionSet; Django admin registration included -
Per-lesson completion tracking (closes #69)
LessonCompletionmodel — records when a user has completed every H5P activity in aLessonPage;unique_together = ("user", "lesson")prevents duplicate rows; idempotent viaget_or_create- Course enrollment is now marked complete only when every live lesson in the course has a
LessonCompletionrecord, replacing the previous single-activity trigger CoursePage.get_context()now emitscompleted_lesson_ids(asetof lesson PKs) so the template can render per-lesson progress indicators in a single extra querycourse_page.htmllesson items gainlms-lesson-list__item--completedCSS class and a checkmark (✓) when the lesson has a completion recordLessonCompletionViewSet(read-only) added to theLMSViewSetGroupWagtail admin menu; Django admin registration included-
Comprehensive xAPI verb coverage — all verbs that signal a learner has fully interacted with an activity now trigger lesson/course completion:
completed,passed— unchanged behaviouranswered(top-level only) — primary terminal verb for standalone question types (MultiChoice, TrueFalse, Blanks, DragDrop, MarkTheWords, FindHotspot, Summary, ArithmeticQuiz, SpeakTheWords, Flashcards, Crossword, Essay); child-questionansweredstatements from inside containers (QuestionSet, InteractiveVideo, etc.) are filtered out viacontext.contextActivities.parentso they don't prematurely complete a lessonmastered— emitted by H5P.Essay on a perfect score; treated ascompleted + passedfailed— completion and success are now tracked orthogonally; a learner who submits and fails has still finished the activity and must not be blocked from progressingconsumed(http://activitystrea.ms/schema/1.0/consume) — emitted by informational types (H5P.Accordion, H5P.Column) that have no pass/fail state (closes #70)
-
System check
wagtail_lms.W001(#65) -
Raised at startup when a
CoursePagesubclass definessubpage_typeswithout"wagtail_lms.LessonPage", preventing the Wagtail editor from silently omitting lessons; seedocs/api.mdfor the upgrade guide -
Course page lists and links to lesson pages
CoursePage.get_context()now includeslesson_pages(liveLessonPagechildren, ordered by tree position)course_page.htmldisplays a numbered lesson list with direct links when lessons exist; enrolled users see their enrollment date, unenrolled users see an enroll CTA- The "no content" notice now correctly reflects courses that have neither a SCORM package nor any lesson pages
- Sidebar shows the lesson count alongside existing SCORM metadata
Fixed
- H5P completion now requires enrollment before writing lesson/course progress
_mark_h5p_enrollment_complete()now skips completion writes unless the user has aCourseEnrollmentfor the parent course-
Prevents pre-enrollment xAPI submissions from pre-creating
LessonCompletionrecords and bypassing course progression gates after later enrollment -
Migration compatibility restored for Wagtail 6.0/6.1 baselines
wagtailcoremigration dependencies were aligned to the oldest shared node (0091_remove_revision_submitted_for_moderation) across app and example migrations-
H5P
StreamFieldmigration serialization was rewritten to explicit block classes (noblock_lookupkwarg), so historical migrations load on supported Wagtail 6.x versions -
Security: path-normalization bypass in ZIP extraction and content-directory deletion
-
posixpath.normpath("a/..")resolves to".", which previously passed the traversal guard; in the deletion path this would have wiped the entire base content directory on package removal. Guard now also rejects the"."result. Fix applied to ZIP extraction in both the SCORM and H5P extractors and to_delete_extracted_content. -
Example project: broken CSS path since v0.4.0
base.htmlstill referencedlms/css/course.cssafter static assets moved towagtail_lms/css/course.cssin v0.4.0; LMS styles were silently absent in the example project. Corrected to{% static 'wagtail_lms/css/course.css' %}.
Known Limitations (0.9.0)
- Activities that emit only
consumed(e.g.H5P.Accordion) do not persist resume state — they have no meaningful resume position, so resetting on reload is the correct behaviour - Resume state has been verified with
H5P.QuestionSet; other activity types have not yet been systematically tested (#71)
Changed
- Default
WAGTAIL_LMS_AUTO_ENROLLreverted toFalse - v0.8.1 changed the default to
True; upcoming v0.9.0 returns toFalseso explicit enrollment is the default behavior - Set
WAGTAIL_LMS_AUTO_ENROLL = Trueto keep the v0.8.1 auto-enrollment flow
[0.8.1] - 2026-02-19
Added
- Python 3.14 support; CI matrix rebalanced to two entries per Python version (3.11–3.14)
Fixed
CourseEnrollment.completed_atnow set on SCORM course completion (#60)- When
cmi.core.lesson_statusis set to"completed"or"passed", the linkedCourseEnrollment.completed_atis updated atomically in the same transaction - Uses a queryset
.update()withcompleted_at__isnull=Trueto be idempotent — existing timestamps are never overwritten -
Downstream projects relying on
completed_atfor prerequisite checks and dashboard completion tracking will now work correctly without manual admin intervention -
WAGTAIL_LMS_AUTO_ENROLLsetting is now wired up (#62) - Setting was defined in
conf.pybut never read; the SCORM player always auto-enrolled users - Default changed from
False→Trueto preserve existing behaviour - With
WAGTAIL_LMS_AUTO_ENROLL = False, unenrolled users who reach the SCORM player are redirected to the course page with an error message instead of being silently enrolled
[0.8.0] - 2026-02-16
Added
- Wagtail admin interface for SCORM packages, enrollments, and attempts (#52)
- New
ModelViewSetclasses forSCORMPackage(full CRUD),CourseEnrollment(list/edit), andSCORMAttempt(read-only inspect) LMSViewSetGroupprovides a top-level "LMS" menu in Wagtail admin with sub-items for each modelSCORMAttemptuses aReadOnlyPermissionPolicyto disable add/edit/delete — attempts are created automatically by the SCORM player- Panels added to
SCORMPackage,CourseEnrollment, andSCORMAttemptmodels with appropriate read-only fields - Django admin registrations preserved for backward compatibility
- 11 new tests covering viewset registration, admin view access, and read-only enforcement
Removed
SCORMPackageListViewand its template (scorm_package_list.html) — replaced by Wagtail's built-in viewset views/lms/scorm-packages/URL endpoint — SCORM packages are now managed at/admin/scormpackage/- Deprecated
serve_scorm_contentcompatibility wrapper (#56) - Removed
serve_scorm_content()function alias inwagtail_lms.views - Removed one-time deprecation warning globals (
_serve_scorm_content_view,_serve_scorm_content_warned) - Removed deprecated-wrapper warning test (
test_serve_scorm_content_import_warning_emitted_once)
Changed
uv.lockremoved from version control; lock file patterns added to.gitignore- This is a reusable library — pinning transitive dependencies in the repo is inappropriate for end users
- Contributors should run
uv syncto generate a local lock file
[0.7.0] - 2026-02-15
Added
- SCORM file cleanup on package deletion (#51)
- Uploaded ZIP and extracted content directory are now deleted from storage when a
SCORMPackageis removed - Uses
post_deletesignal withtransaction.on_commit()to avoid deleting files on rollback - Works with both
FileSystemStorageand remote backends (S3, etc.) - Empty directories are cleaned up on filesystem-backed storage
Fixed
- Publish workflow: TestPyPI now runs on release events
- Previously
publish-to-testpypiandverify-testpypiwere gated toworkflow_dispatchonly - TestPyPI publish and verification now run before PyPI publish on every release
[0.6.1] - 2026-02-12
Added
- Django 6.0 and Wagtail 7.2/7.3 support with two new CI matrix entries
- Example project: add
setup_pagesmanagement command replacing manual home page setup - Example project: document all
WAGTAIL_LMS_*settings, update lockfile to latest versions
Changed
- Remove extra
default_storage.exists()call fromServeScormContentViewredirect path (#44) - Eliminates ~50-100ms latency per media asset request in S3-backed deployments
- Missing files now produce a redirect to S3 (which returns 403/404) instead of a Django 404
Fixed
- Handle exceptions in
ServeScormContentViewredirect path (#43) get_redirect_url()failures (expired AWS credentials, transient S3 errors) now return 404 instead of unhandled 500- Intentional Django exceptions (
Http404,PermissionDenied,SuspiciousOperation) from subclass overrides propagate correctly
[0.6.0] - 2026-02-11
Added
- Extensible SCORM content serving with cache and media redirect hooks (#41)
- Replaced function-based content serving with
ServeScormContentView(CBV), making downstream subclass overrides straightforward - Added
WAGTAIL_LMS_CACHE_CONTROLsetting with exact MIME, wildcard (e.g.image/*), anddefaultmatching - Added
WAGTAIL_LMS_REDIRECT_MEDIAsetting to redirectaudio/*andvideo/*assets to storage-backed URLs (useful for S3) - Preserved upstream path traversal protection and iframe headers
Changed
serve_scorm_contentnow appliesCache-Controlheaders by default viaWAGTAIL_LMS_CACHE_CONTROL- Migration: set
WAGTAIL_LMS_CACHE_CONTROL = {}to restore previous no-header behavior
Fixed
serve_scorm_contentnow returns404(not500) for directory-like and dot/empty normalized pathsWAGTAIL_LMS_CACHE_CONTROLnow supports explicit per-MIMENonevalues to disable the header without falling back to wildcard/default- README cache-control example now includes both
application/javascriptandtext/javascriptMIME variants
[0.5.0] - 2026-02-10
Added
- Django storage backend support for SCORM content (#38)
extract_package()now usesdefault_storage.save()instead ofzipfile.extractall(), enabling S3 and other remote storage backendsserve_scorm_content()now usesdefault_storage.exists()anddefault_storage.open()instead ofos.pathoperations- Works transparently with both
FileSystemStorage(local) andS3Boto3Storage(or any Django storage backend) - Fixes SCORM content loss on platforms with ephemeral filesystems (Railway, Heroku, Render)
- Path traversal security in ZIP extraction
- Rejects ZIP members containing
..segments or absolute paths during extraction (logged as warnings) serve_scorm_content()now explicitly rejects..segments and leading/in request paths- 8 new tests for storage backend compatibility, path traversal protection, and
parse_manifest()flexibility
Changed
parse_manifest()parameter renamed frommanifest_pathtomanifest_sourceto reflect it now accepts both file paths and file-like objectsextract_package()captures manifest content in-memory during extraction to avoid an extra storage round-tripextract_package()andserve_scorm_content()now useconf.WAGTAIL_LMS_CONTENT_PATHinstead of hardcoded"scorm_content/"- Replaced
osimport withposixpathin views (forward slashes for S3 key compatibility)
[0.4.1] - 2025-12-27
Changed
- User foreign keys now use
settings.AUTH_USER_MODELinstead ofauth.Userin model definitions, aligning code with existing migrations and Django best practices for reusable apps.
[0.4.0] - 2025-12-04
Breaking
- Static assets moved from
static/lms/*tostatic/wagtail_lms/*; any custom templates that manually includecourse.cssmust update the path ({% static 'wagtail_lms/css/course.css' %}).
Fixed
- Corrected admin SCORM CSS/JS namespace to match the app, resolving 404s in production.
- Admin SCORM assets now use Django's
static()helper, soSTATIC_URLprefixes and hashed filenames fromManifestStaticFilesStorageare honored.
0.3.1 - 2025-11-09
Fixed
- GitHub Actions publish workflow
- Fixed workflow stopping after build step when triggered via workflow_dispatch
- Added conditional
if: github.event_name == 'release'to publish-to-pypi job - Separated TestPyPI testing workflow from production PyPI release workflow
- TestPyPI publish now only runs on manual workflow_dispatch trigger
- PyPI publish now correctly runs on GitHub release events
- Removed verify-testpypi dependency from publish-to-pypi job to prevent blocking
0.3.0 - 2025-11-09
Added
- Multi-version testing infrastructure
- Comprehensive CI testing across Python 3.11-3.13
- Django 4.2 (LTS), 5.0, 5.1, 5.2 (LTS) support verified
- Wagtail 6.0-7.1 compatibility confirmed
- Strategic test matrix with 6 key version combinations
- Enhanced GitHub Actions workflow with version-specific installations
- Updated package classifiers and dependency specifications
Breaking Changes
- SCORM content serving now requires authentication
- The
serve_scorm_contentview now has@login_requireddecorator - Migration: SCORM content files can no longer be accessed without login
-
Rationale: Security fix - prevents unauthorized access to course content
-
URL name correction for SCORM content endpoint
- Changed URL name from
scorm_contenttoserve_scorm_contentinwagtail_lms/urls.py - Impact: Code using
reverse('wagtail_lms:scorm_content', ...)will break - Migration: Update to
reverse('wagtail_lms:serve_scorm_content', ...) - Note: This fixes a bug where the URL name didn't match the view function name
Fixed
- Test suite reliability
- Fixed 9 failing tests and 2 transaction errors
- All 65 tests now passing consistently
-
Wrapped IntegrityError tests in atomic transactions to prevent cleanup errors
-
SCORM 2004 version detection
- Fixed ElementTree compatibility (removed lxml-specific
nsmapusage) - Implemented explicit version string matching to prevent false positives
- Now correctly identifies SCORM 2004 packages via schemaversion element
-
Supports: "2004 3rd Edition", "2004 4th Edition", "CAM 1.3", "2004"
-
Security improvements
- Fixed open redirect vulnerability in enrollment view
- Now uses
settings.ALLOWED_HOSTSfor redirect validation -
Added proper null checks to prevent AttributeError in manifest parsing
-
Course enrollment redirect handling
- Fixed crash when
course.urlis None - Added safe fallback to HTTP_REFERER with proper validation
- Falls back to home page if no safe redirect available
Changed
- Test assertions updated for Django 5.2
- Fixed FileResponse test to use
streaming_contentinstead ofcontent - Updated test expectations to match actual template output
[0.2.0] - 2025-11-05
Status
Breaking Changes Release - Removes Bootstrap classes, adds framework-agnostic styling.
Breaking Changes
IMPORTANT: This release changes template class names. If you installed v0.1.0, you'll need to update your templates.
- Template class names changed from Bootstrap-style to LMS-prefixed classes
- Old:
.container,.row,.col-md-8,.btn-primary,.alert-info - New:
.lms-course,.lms-course__layout,.lms-button--primary,.lms-notice--info - Migration: Update any custom CSS targeting old classes, or override templates with your own
- See Template Customization Guide for examples
Added
- Framework-Agnostic Styling
- New
lms/css/course.csswith minimal, functional default styles - BEM-style naming convention (
.lms-component__element--modifier) - Works out of the box without external CSS frameworks
- Fully responsive with mobile-first grid layout
-
Easy to override or replace with your own styles
-
Comprehensive Template Customization Documentation
- New
docs/template_customization.mdguide - Examples for Bootstrap 5, Tailwind CSS, and Bulma
- Three customization approaches: default styles, CSS overrides, or template replacement
- Guidance for API-first/headless projects
-
Dynamic template location finder command
-
Accessibility Improvements
- Added
:focusstate to SCORM player back button for keyboard navigation - Visible focus indicator with outline for better accessibility
-
Semantic HTML structure with proper ARIA roles
-
Developer Experience
- In-template comments explaining CSS requirements
- Clear documentation on including stylesheets in base templates
- Example project now demonstrates default LMS styling
-
Updated release process documentation to use
uv publishinstead oftwine(closes #2) -
Testing Infrastructure
- Added
tests/templates/base.htmlfor template rendering in tests - Updated test settings to include test templates directory
- Fixed 4 previously failing tests related to missing base template
Changed
- Templates now use semantic HTML with BEM-style CSS classes
- Example project updated to include LMS CSS in base template
- Removed unused Bootstrap-style classes from example project
- Updated README with template customization section
Fixed
- v0.1.0 templates had Bootstrap classes but no CSS (broken out of the box)
- Now includes working CSS by default
- Clear documentation on how to include it
- FOUC (Flash of Unstyled Content) issue resolved
- Documented proper CSS placement in
<head>section - Removed inline CSS loading from templates
Notes for v0.1.0 Users
If you installed v0.1.0 and customized the templates:
- If you added Bootstrap yourself: Continue using it by overriding templates (see docs)
- If you have custom CSS: Update selectors to target new
.lms-*classes - Fresh install recommended: v0.1.0 templates were incomplete, v0.2.0 provides working defaults
Planned for 0.3.0
- Pure Wagtail admin interface (remove Django admin dependency)
[0.1.0] - 2025-10-26
Status
Alpha Release - Initial release with core SCORM functionality and comprehensive testing.
Added
- Core SCORM Features
- SCORM 1.2 and 2004 package support
- Automatic ZIP extraction and manifest parsing
- Version detection from manifest metadata
- Launch URL extraction and validation
- Course Management
- Course page model integrated with Wagtail's page system
- Student enrollment system with progress tracking
- SCORM attempt tracking with CMI data model
- Support for suspend/resume functionality
- SCORM API Implementation
- Complete SCORM API 2004 Runtime
- Methods: Initialize, Terminate, GetValue, SetValue, Commit, GetLastError, GetErrorString, GetDiagnostic
- Proper error code handling
- CMI data model storage with key-value pairs
- Security & Content Delivery
- Secure SCORM content serving with path traversal protection
- CSRF-exempt API with login_required authentication
- Iframe-friendly headers (X-Frame-Options: SAMEORIGIN)
- User-isolated progress tracking
- Database Concurrency Handling
- Retry decorator with exponential backoff for SQLite database locks
- Atomic transactions for SCORM data operations
- Configurable timeout settings (20s default for example project)
- Testing & Quality
- Comprehensive test suite with 86% code coverage
- 65+ tests covering models, views, API, and integration workflows
- SCORM package extraction and manifest parsing tests
- Full SCORM API endpoint testing
- Integration tests for complete course enrollment workflows
- Concurrent operation and error handling tests
- Security and authentication tests
- Test fixtures for SCORM 1.2 and 2004 packages
- Example Project
- Fully functional Wagtail project for development and testing
- Complete setup guide in
example_project/README.md - Pre-configured settings optimized for SCORM operations
- Home page and course page templates
- Admin Integration
- Wagtail admin menu item for SCORM Packages
- Django admin interface for SCORM package management
- Custom admin panels for course configuration
- Documentation
- Comprehensive README with installation and usage instructions
- Test running instructions with PYTHONPATH configuration
- Database configuration recommendations
Fixed
- Wagtail preview mode error when viewing unsaved course pages
- Added
self.pkcheck before querying enrollments - Graceful handling of pages without primary keys
- SQLite database lock errors during concurrent SCORM API calls
- Implemented retry logic (up to 5 attempts with exponential backoff)
- Transaction atomicity for data consistency
Limitations
- Only tested on Python 3.13.0, Django 5.2.3, Wagtail 7.0.1
- Other versions may work but are untested
- Django admin required for SCORM package upload (not pure Wagtail interface)
- SQLite recommended for development only (PostgreSQL for production)
- No CI/CD pipeline yet
Known Issues & Future Plans
- xAPI/TinCan API support not yet implemented (planned for future release)
- No certificate generation (planned for future release)
- No batch enrollment features
- No course completion certificates
- Multi-version testing planned for 0.2.0