Appearance
Course Module Overview
The Course module handles everything related to our courses and the gamification of design education.
Related Links
- Courses Testing Panel: Find
LessonItems of different types, members enrolled in different courses, etc. - Try Course Grading Control Panel on staging: log in with course graders
- Modifying Published Courses: Guide for safely updating course content
- Restore a Deleted Enrollment
- ⚙️ Testing Courses: Practical patterns and factory examples for writing course tests
Hierarchy
Courses have a nested Hierarchy of sub-elements:
- Course contains 1+ Lessons.
- Each Lesson contains 1+ LessonItems.
- Each LessonItem may contain zero or more Quizzes
- Quiz has one Question of these types:
- Multiple-Choice Question (MCQ)
- Open-Ended Question (OEQ)
- Quiz has one Question of these types:
- Each LessonItem may contain zero or more Quizzes
- Each Lesson contains 1+ LessonItems.
Course
A course can be created by an Instructor or a Course Manager (as defined in the Permission module). A created course will not be available for enrollment until it is published. A course may have one or more instructors, marked with a sequence number similar to article author sequences.
When a course is ready, it is scheduled to launch by creating a CourseSchedule. A course may be scheduled as many times as needed. By default, there is only one active CourseSchedule at a time, and another is automatically scheduled to start right after the current one ends. However, special CourseSchedules can be launched as required.
Course States
| State | Visible | Auto-schedule | Ranking |
|---|---|---|---|
| Draft | 🔴 | 🔴 | 🔴 |
| Published | 🟢 | 🟢 | 🟢 |
| Deprecated | Partially* | 🔴 | 🟢 |
*Visible to enrolled Members only
Course Difficulty Levels
Courses have difficulty levels: beginner, intermediate, and advanced. The course level is defined by the instructor and is used to:
- Group courses (mainly for display) on the
/coursespage. - Automatic course scheduling (see below).
Course Schedules and Auto-Scheduling
Each course can have an indefinite number of CourseSchedules, to which our members can enroll. This means that the course enrollment (CourseEnrollment) is linked to the CourseSchedule, not directly to the Course.
NOTE
The relation between Course and CourseEnrollment exists to enhance performance (denormalization).
There are currently two ways to create a new CourseSchedule:
- Scheduled Job: courses:schedule, which processes all auto-scheduled courses and creates new schedules.
- Course Admin Panel: Manually create schedules via the Course Admin Panel.
How and Why the App Schedules Courses
The application schedules courses to manage enrollment periods and control when course content becomes available. This allows for cohort-based learning experiences and staggered release of content.
CourseSchedule Model
The CourseSchedule model represents a specific offering of a course with defined start and end dates.
- Attributes:
course_id: References the associatedCourse.start_date: When the course schedule begins.end_date: When the course schedule ends.is_auto_scheduled: Indicates if the schedule was created automatically.
LessonSchedule Model
When a course is scheduled, each lesson is also scheduled using the LessonSchedule model, determining the availability of lessons.
- Attributes:
lesson_id: References the associatedLesson.course_schedule_id: Associates the lesson schedule with aCourseSchedule.start_date: When the lesson becomes available to enrolled members.
Auto-scheduling Logic
This is the logic used to create the schedules for each course that is published and has auto-scheduling turned on:
- Retrieve the course schedule that has the highest close date
- If that course schedule close date is in the future, then do not create a new schedule
- If that course schedule close date is less than 3 days ago and the course level is beginner or advanced, do not create a new schedule
- If that course schedule close date is less than 2 days ago and the course level is intermediate, do not create a new schedule
- Starting from tomorrow, look for the first day that does not have any course of the same level closing on that day
- Create a new course schedule starting on the day found on step 5
The resulting effect of applying this logic is:
- There will be one, and only one, course of each level starting on each day
- As soon as the course closes, a new schedule is created
- There will be at most one schedule open for each course
- We wait some days after the course is closed in order to schedule it again
- There will always be at least one course closed for each level
Percent Booked
The course cards display a "XX% booked" label. This number is currently generated using a semi-random number generator.
Lesson and Lesson Scheduling
A Lesson is a container for multiple LessonItems and includes a description or introduction. Each core Lesson belongs to one and only one course and has a lesson number, which determines its sequence within the course.
When a course is scheduled, instances of LessonSchedule are created for each lesson, defining their availability to enrolled members.
Lesson Availability
Lessons can be made available either immediately or weekly:
- Immediate Lessons: Available right after enrollment (e.g., Lesson 0 and Lesson 1).
- Weekly Lessons: Become available each week, according to their sequence number.
Shared Lessons
Overview
By default, Lessons are associated with a single Course. From late 2025, we have a new feature of Shared Lessons that allows re-using the same Lessons in different Courses. It is created to make some Lessons skippable (when a user already completed the Lesson in another Course), that is why you can see in some places another name: "Skippable Lesson" — it's the same feature, just a different name.
So far we use Shared Lessons feature for Lesson Zero (L0) and Final Lesson (LF). However, it potentially can be extended to use for any Lesson (which is particularly good when we revamp the whole course but want to keep few Lessons as is).
What "Shared" Means
Only Lessons can be shared — there are no individually "shared quizzes" or "shared lesson items". When you encounter terms like "shared score", "shared quiz answer", or "shared lesson item", they always refer to content or progress that belongs to a Shared Lesson. Quizzes, LessonItems, and quiz answers become "shared" only because they live inside a Shared Lesson.
Shared Points
Shared Lessons don't just share content — they also share quiz answers and points across Courses.
How it works: When a Member correctly answers a quiz in a Shared Lesson, that answer is stored once as a "shared answer" (not tied to any specific Course Enrollment). Every Course that uses that Shared Lesson can then pick up those points.
When does synchronization happen? Points are synchronized automatically when a Member visits the Shared Lesson within a Course Enrollment. For example, if a Member completed quizzes in L0 of Course A, they will see those points appear in Course B only after they visit L0 in Course B.
Important notes:
- Only correct answers are shared. If a Member answers a quiz incorrectly, that answer stays bound to the specific Course Enrollment and is not shared to other Courses.
- If an enrollment-scoped answer exists for a quiz, it takes precedence over the shared answer.
- Completed Course Enrollments can't receive shared points — the whole process on a completed enrollment is locked. If a Member completes a Course without visiting the Shared Lesson, those shared points are permanently missed. This can affect the Member's final score, certificate distinction, and rank.
- Workaround for admins: To manually trigger synchronization for a Member, impersonate the Member and visit the Shared Lesson (e.g., L0 or LF) in the affected Course. This only works for active (not completed) enrollments.
LessonItem
A LessonItem is where the actual content of a lesson resides. Depending on the instructor, a lesson may contain one or more lesson items. A LessonItem may contain zero or more Quizzes.
Reusing LessonItems
LessonItems are created for specific lessons and can only be added to one lesson. If added to another lesson, a copy is created to allow independent updates.
Future Enhancements:
- Reusing LessonItems by Reference: Allow the same LessonItem to be referenced in multiple lessons.
- Skip Already-Completed LessonItems: Enable members to skip lesson items they have already completed in other courses.
Optional vs. Mandatory
LessonItems can be:
- Mandatory: Contains quizzes; requires completion of all quizzes to mark as complete.
- Optional: Does not contain quizzes; marked as complete after being viewed.
External Content Provider Types
Most LessonItems contain text, video, and images. Content can be provided by the LI itself or an associated external content provider:
article: displays the content of an article attached to the LessonItem.
When the External Content Provider is not specified, Lesson Item's content is used to provide the content (so-call normal Lesson Item type). It is possible to use different shortcodes inside the content of “normal” Lesson Items.
Attached Discussion
Technically, a Discussion can be attached to any type of LessonItem.
Course-specific Shortcodes
There are a few shortcodes exclusively available in courses:
- Course Certificate (
course_certificate): Used in the "Final Lesson" of a course. - Evaluation (
course_evaluation): Displays the course evaluation form for feedback. - Peers Offline (
peers_offline): Provides a discussion page for offline interactions. - Peers Online (
peers_online): Shows related meetups, local groups, etc. - Self-Check Information (
self_check_info): Prompts the member to update their profile as seen by other course-takers. - Next Steps (
next_steps): Suggests next steps by build the career (ex-"growth").
Quizzes
A Quiz is mainly a pointer to a question and can be of two types:
- Multiple-Choice Question (MCQ): Allows three answer choices, with one correct answer.
- Open-Ended Question (OEQ): Requires a text/html answer, which is graded by a Course Grader.
Each quiz can be used in one LessonItem. Reusing quizzes in different LessonItems is not highly required but can be achieved by copying quizzes.
Quiz Answer
A Quiz Answer is an answer submitted by a member.
Quiz Answer Score
- MCQs: Always a non-negative integer
[0..]. - Non-Graded OEQs: Always
null. - Rejected OEQs: Always
null. - Accepted OEQs: A natural number
(0..].
Course Scores
Course scores distinguish between confirmed points and potential points (possible from ungraded OEQs).
- Confirmed Points: Points actually earned from MCQs or graded OEQs.
- Potential Points: Maximum possible points from ungraded OEQs.
Scores are stored on the QuizAnswer model. The sum of all scores is stored in the CourseEnrollment to enhance performance.
Recalculating Course Scores
Inconsistencies can occur due to changes in published courses (adding/removing quizzes or LessonItems). Course scores can be recalculated (currently using a Nova action) to resolve discrepancies.
Course Progress
As members answer quizzes, they progress through courses, lessons, and LessonItems. Progress is defined by the number of quizzes answered relative to the total number of quizzes at each level.
The CourseEnrollmentProgress and CourseEnrollment models keeps track of the number of quizzes answered with submission timestamps as metadata.
Fetching Course Progress
Fetching course progress requires aggregating data from multiple models. To optimize performance:
- Eager Loading: Utilize Eloquent eager loading to minimize the number of queries.
- Caching: Implement caching for computed progress percentages.
- Denormalization: Store computed progress percentages on the
CourseEnrollmentmodel where appropriate.
Course Ranks
Course ranks are calculated against others for a whole course, not just a specific schedule.
- Course Ranks: Display the member's rank on course and LessonItem pages.
- Ranking Table: Shows ranks of the member and others, accessible via the rank icon on course and LessonItem pages.
Course Certificates
A course certificate is a document certifying that a member has successfully completed a course. It includes:
- Member's name
- Course title
- Course start and end dates
- Distinction
- Estimated learning time
Members often share their certificates on social media platforms.
Sample Course Certificates
Sample certificates can be generated for each course to showcase the certificate appearance to prospective enrollees. These samples are localized per visitor's country, using randomly selected male or female names from a maintained list.
Career Programs
Career Programs provide a structured learning path for members to achieve specific career goals through a curated set of Courses and Master Classes. Members can enroll into multiple Career Programs and track their progress toward completion. The is_selected boolean field determines which enrolled program opens automatically when a logged-in member visits the Courses page.
Structure
A Career Program consists of:
- Foundational Courses
- Core Courses
- AI Master Classes
- Core Master Classes
Career Program Snapshots
When a member selects a Career Program, a snapshot is created that captures the exact Courses and Master Classes required at that moment. This ensures members can complete the program even if the structure of the Career Program changes later.
The snapshot stores:
- List of foundational Course IDs
- List of core Course IDs
- List of AI Master Class IDs
- List of core Master Class IDs
php
$snapshot = $careerProgram->makeSnapshot(); // Returns CareerProgramSnapshot value object
$snapshot->foundationalCourseIds(); // Returns an array of ids
$snapshot->coreCourseIds(); // Returns an array of ids
$snapshot->aiMasterclassIds(); // Returns an array of ids
$snapshot->coreMasterclassIds(); // Returns an array of idsMember Journey
Selection: Member selects a Career Program (only one can be selected at a time).
php$member->selectCareerProgram($careerProgram); $member->getSelectedCareerProgram(); // Returns CareerProgram instanceProgress Tracking: System tracks completion of Courses and Master Classes against the snapshot. The
VerifyCareerProgramCompletionStatusaction checks if all requirements are met:- All Courses (foundational + core) must be completed
- All Master Classes (ai + core) must have certificates issued
Completion: When all requirements are met,
completed_attimestamp is set to the current time.
Display & Presentation
Career Program data is presented through the CareerProgramData DTO which includes:
- Career Program name and description (with placeholder replacements)
- URL of the Career Program, for example:
/courses?careerProgram=learn-design-skills-to-become-valuable-in-any-job - Collections of foundational Courses, core Courses, AI Master Classes and core Master Classes
The GetCareerProgramData action retrieves all data needed to display Career Program, either from:
- Existing Career Program snapshot
- Current Career Program relations (for guests or non-selected programs)
php
$careerProgramDataForGuests = (new GetCareerProgramData())->execute($careerProgram);
$careerProgramDataForMember = (new GetCareerProgramData())->execute($careerProgram, $member);Business Rules
- Members can only have one selected Career Program at a time
- Selecting a new program deselects the previous one automatically
- Completion status is preserved even after deselecting a program
- Programs use snapshots to ensure stable requirements for enrolled members
Key Classes
CareerProgram: Main model representing a Career ProgramCareerProgramItem: Polymorphic model that represents Career Program item (course or Master Class)CareerProgramMember: Pivot model tracking member enrollments to Career ProgramsCareerProgramSnapshot: Value object storing Career Program snapshot
Events
The Course module dispatches several events to communicate with other modules:
php
use App\Modules\Course\Events\CourseDelete;Event listeners in other modules can subscribe to these events to perform actions such as awarding badges or sending notifications.
Future Considerations
Event Sourcing
We are considering using Event Sourcing for this module to improve scalability and maintainability.
- Benefits:
- Better tracking of state changes.
- Easier debugging and auditing.
- Improved scalability for write-heavy operations.