Version 5.3.0

October 9, 2026

Added

  • Console commands for doing enrolment and certificate work in bulk, and from cron. Each takes --dry-run and --course=<id or slug>.
    • diploma/enrollments/import <file.csv> --course=… enrols learners from a CSV file (email, status, date, completed, expires) — for moving a customer over from another LMS. Rows already enrolled are skipped, lapsed enrolments are reactivated, bad rows are reported without stopping the rest, and --create-users creates a pending user for an unknown email.
    • diploma/enrollments/expire marks active enrolments whose expiry date has passed as Expired.
    • diploma/certificates/reissue rebuilds certificates' course title and learner name from the current data, keeping verification codes and issue dates; --missing issues certificates to completed enrolments that have none.
    • diploma/drip/unlock (Pro) announces drip lessons as they unlock: one activity-log entry and one event per learner and lesson.
  • New events: Enrollments::EVENT_AFTER_EXPIRE, Certificates::EVENT_AFTER_REISSUE_CERTIFICATE and DripContent::EVENT_AFTER_UNLOCK_LESSON (LessonUnlockEvent), with matching activity-log entries (Enrollment expired, Certificate reissued, Lesson unlocked).
  • The enrolment screen shows an enrolment's expiry date.

Fixed

  • An enrolment's expiry date was never enforced. An active enrolment past its expiresAt kept unlocking lessons and sitting quizzes. It now loses access as soon as the date passes, and reactivating an enrolment (a new purchase, membership or admin) clears the old date.

Version 5.2.0

September 29, 2026

Security

  • Learners could enrol themselves in paid courses for free. The front-end diploma/progress/enroll action accepted any course ID from any logged-in user — including courses sold through Commerce or a Headcount membership, drafts, disabled courses, and courses whose enrolment had expired. Courses now have an Enrollment setting (Open, Purchase, Manual); self-enrolment is allowed only for enabled, published, open courses, and never for a course linked to a Commerce product (a diplomaCourses field) or a Headcount plan, whatever its setting says. On upgrade, every course that is already linked that way is set to Purchase.
  • Quiz scores could be forged. A submission could name another learner's attempt and overwrite their score, re-grade an attempt the learner had already finished (which also meant maxAttempts never ran out), answer every question with one question's correct answer ID, and score 100% by answering a single question. Attempts are now graded once, only by their owner, on the quiz and enrolment they were started for; answers must belong to the question they answer; and every question on the quiz counts towards the total.
  • Plugin settings could be changed by non-admins. Saving plugin settings — project config — needed only the Manage settings permission. It now needs an admin, where admin changes are allowed. The lesson field layout needs an admin too, and stays editable where admin changes are off because it is not stored in project config.

Fixed

  • Saving one settings page erased the others. Each settings page posts only its own fields, and Craft writes exactly the posted keys to project config — so saving the Certificates page wiped the Headcount plan mapping and the Commerce settings. The posted values are now merged over the current settings.
  • A learner whose enrolment had expired or been cancelled could not be enrolled again. Enrolments are unique per course and user, but re-enrolling always inserted a new row, which the index refused — so a Headcount member who re-subscribed stayed locked out. A lapsed enrolment is now reactivated by a purchase, a membership or an admin (but not by the learner enrolling themselves).
  • Expired and cancelled enrolments could still complete lessons and sit quizzes, and so still earn certificates. Both now require an active or completed enrolment.
  • Element index columns use Craft 5's attributeHtml(); the quiz index escapes the course title.

Added

  • Integration checks (tests/integration/checks.php, http.php) covering enrolment, grading and settings, including the attacks above; ECS configuration.

Version 5.1.4

September 17, 2026

Fixed

  • Side-by-side fields on the course, lesson and quiz edit screens had no vertical spacing between rows — the second column in particular ran a field's label straight into the control above it (Difficulty Level into Enrollment Limit, Estimated Duration into Prerequisite Lesson, Time Limit into Questions Per Attempt). Craft zeroes the margins of a .field that is the only child of its wrapper, so the .flex / .flex-grow pairs those screens used collapsed the 24px rhythm to nothing. The rows are now a .diploma-field-row grid that owns both the column gap and the vertical spacing. Within a row the two fields keep their labels on one line and align their inputs even when only one of them carries instructions, and the row drops to a single column below 768px.

Version 5.1.3

September 10, 2026

Fixed

  • Quiz questions couldn't be edited or deleted from the control panel. QuestionsController already supported saving over an existing question and deleting one, but the quiz edit screen never exposed either — question items in the list weren't clickable and carried no edit/delete controls. Each question now has Edit and Delete buttons; Edit reopens the question modal pre-filled with its existing type, text, points, explanation, and answers, and Delete removes it after confirmation.

Version 5.1.2

August 26, 2026

Fixed

  • The enrolments table and the dashboard's recent-enrolments list showed raw numeric IDs in the User and Course columns — 31491 instead of a name, which tells an administrator nothing. EnrollmentRecord gained getEnrolledUser() and getEnrolledCourse(), and both screens now show the user's name and the course title, falling back to #id when the record has since been deleted.
  • The courses index returned HTTP 500 whenever the status column was shown. Craft 5 expects statuses() to return craft\enums\Color cases; the string colours this plugin returned made Cp::componentStatusLabelHtml() fail with "Attempt to read property value on string", leaving the index blank with no error shown. 'light' is not a Color case and is now Color::Gray.

Version 5.1.1

August 19, 2026

Fixed

  • Plugin settings saved from Settings → Plugins → Diploma were silently discarded. Craft namespaces that screen's output under settings, so the fields' settings[…] names posted as settings[settings][…] and never reached the settings model — the page reported "Plugin settings saved" while every change was thrown away. The shared fields template now uses bare input names, and the Diploma → Settings screen (which posts directly to the plugin's own save action) applies the settings namespace itself.

Version 5.1.0

July 15, 2026

Added

  • Lesson content field layout. Lessons now have a proper Craft field layout, designed under Diploma → Settings → Lesson Content Fields. Add rich text/CKEditor, Assets (embedded PDFs, video, images), Matrix, link fields — anything Craft supports — so a single lesson can hold mixed rich content. The fields render on every lesson's edit screen and are available on the lesson element in your templates.
  • "Select all that apply" question type. A new Multiple Response question type extends multiple choice to allow more than one correct answer. It's graded all-or-nothing: full marks only when every correct option is selected and no incorrect ones. Mark multiple answers correct in the quiz builder as usual; on the front end, render the options as checkboxes named responses[<questionId>][answerIds][].
  • Plugin events. Diploma now fires its own events so integrations can react to learner activity: Enrollments::EVENT_AFTER_ENROLL, Enrollments::EVENT_AFTER_COMPLETE_COURSE, ProgressTracker::EVENT_AFTER_COMPLETE_LESSON, QuizGrader::EVENT_AFTER_GRADE_ATTEMPT (check $event->attempt->passed for a pass), and Certificates::EVENT_AFTER_ISSUE_CERTIFICATE. Event classes live in the justinholtweb\diploma\events namespace.
  • Activity / audit log. An append-only audit trail of enrollments, lesson completions, quiz pass/fail (with score), course completions, and certificate issuance — recording who, when, and from which IP address. View and filter it under Diploma → Activity Log, export it as CSV for compliance and record-keeping, or read a user's history in templates with craft.diploma.getUserActivity(user). Entries keep a denormalised snapshot and use nullable references, so the audit trail survives deletion of the related course, user, or enrollment. Available in both editions.
  • New permission: View activity log.

Changed

  • Bumped schemaVersion to 1.1.0. Run php craft migrate/all after upgrading to create the diploma_activity_log table.

Version 5.0.4

June 11, 2026

Added

  • Optional front-end URLs for courses. Enable Course URLs under Diploma > Settings (or set enableCourseUrls, courseUriFormat, and courseTemplate in config/diploma.php) to give published courses their own routable URLs and a course.url. A slug field appears on the course edit screen when enabled. Courses saved before enabling it need to be re-saved from the control panel to generate their slug and URI.

Fixed

  • Front-end template tags (craft.diploma.*) and the diploma* Twig filters could silently fail to register. The CraftVariable::EVENT_INIT listener and the Twig extension were being registered inside the deferred Craft::$app->onInit() callback, which can run after the craft template variable has already initialized. They're now registered directly in Plugin::init() so the variable and filters are always available in front-end templates.

Version 5.0.3

May 18, 2026

Added

  • Full multisite support. Each course, lesson, and quiz lives only on the site it was created on, just like Craft entries with site-scoped sections. Element queries and index pages are now correctly scoped to the active site.
  • Standard Craft breadcrumb-style site switcher on every Diploma CP page (dashboard, course/lesson/quiz edit screens, enrollments, certificates, settings). Course and quiz element indexes show the same switcher when multisite is enabled. Switching sites preserves the current path and query params.
  • Migration m260518_140000_pin_elements_to_primary_site cleans up any cross-site elements_sites rows from earlier installs so existing elements remain pinned to their original site.

Fixed

  • "Add Question" button now appears on the quiz edit screen — the control-panel JS bundle was registering CSS only, so quiz-builder.js never loaded.
  • Question modal is now scrollable when content exceeds the viewport; the Save/Cancel footer stays pinned while the body scrolls.
  • Saving a question now works on multisite installs. The save endpoint URL was built by concatenating Craft.actionUrl + '/diploma/questions/save', which produced a broken URL when Craft.actionUrl already contained a ?site=… query string. Switched to Craft.getActionUrl() so the path is appended before the query string.
  • Frontend progress.js and quiz.js no longer hardcode /diploma/api/… paths, which broke on sites configured under a non-root base URL. The asset bundle now publishes a window.DiplomaUrls object built from UrlHelper::actionUrl() so the scripts always hit the correct per-site action endpoint.

Changed

  • Redesigned the question modal to use Craft's native form styling (proper field labels, select dropdown, fullwidth textareas, bordered answer rows) and Garnish's modal shell.
  • Course, Lesson, and Quiz now declare isLocalized() as true and override getSupportedSites() to return only the element's own site, so each element is pinned to the site it was created on. New elements pick up the active site from Cp::requestedSite() so creating one with ?site=X writes it to that site.
  • Bumped schemaVersion to 1.0.1. Run php craft migrate/all after upgrading.

Version 5.0.1

April 10, 2026

Fixed

  • Dashboard cards and other CP styles weren't loading because the plugin layout never registered the CP asset bundle.

Changed

  • Marked nullable parameters explicitly (?string) on element defineSources() and defineActions() for forward compatibility with PHP 8.4's implicit-nullable deprecation.
  • Switched lesson and course queries from the string form of orderBy() to the array form (['sortOrder' => SORT_ASC]), and quoted column references in select() so cross-driver behavior is consistent.

Version 5.0.0

March 24, 2026

Added

  • Initial release.
  • Courses, lessons, and quizzes as Craft elements.
  • Multiple question types: multiple choice, true/false, short answer, matching.
  • Enrollment and progress tracking.
  • Certificate generation with public verification.
  • Pro edition: drip content, PDF certificates, Commerce/Headcount integration.