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. - Want to try out the Course Grading Control Panel? Login with course graders
- Restore a Deleted Enrollment
Hierarchy
- Course contains 1+
Lessons.- Each Lesson contains 1+
LessonItems.- Each LessonItem may contain zero or more
Quizzes:- Multiple-Choice Question (MCQ)
- Open-Ended Question (OEQ)
- Each LessonItem may contain zero or more
- Each Lesson contains 1+
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 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.
Lesson Zero
#21169 Document it
Reusing Lessons
Currently, lessons cannot be directly reused across courses. However, we can copy a lesson from one course to another.
#21169 Document it better
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 CourseEnrollment 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 select one Career Program at a time and track their progress toward completion.
Structure
A Career Program consists of:
- Foundational Courses: Core courses required for the Career Program (marked with
is_foundational = true) - Regular Courses: Additional courses that complement the foundational knowledge (marked with
is_foundational = false) - Master Classes: Supplementary 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 regular Course IDs
- List of Master Class IDs
php
$snapshot = $careerProgram->makeSnapshot(); // Returns CareerProgramSnapshot value object
$snapshot->foundationalCourseIds(); // Returns an array of ids
$snapshot->regularCourseIds(); // Returns an array of ids
$snapshot->masterclassIds(); // 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 + regular) must be completed
- All Master Classes 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, regular Courses, and 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 ProgramCareerProgramCourse: Pivot model for Course assignmentsCareerProgramMasterclass: Pivot model for Master Class assignmentsCareerProgramMember: 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.
Testing
Course module contains a lot of Models and multi-level hierarchy:
Course
- Lesson (1:m)
- LessonItem (1:m)
- Quiz (1:m)
- MCQ (1:m)
- OEQ (1:m)
- Quiz (1:m)
- LessonItem (1:m)
- Lesson (1:m)
Course
- CourseSchedule (1:m)
- LessonSchedule (1:m)
- CourseSchedule (1:m)
CourseSchedule
- CourseEnrollment (1:m)
- QuizAnswer (1:m)
- CourseEnrollment (1:m)
While you don't always need to create the whole structure in your tests, there are some methods to help you to do at least part of it: There are few methods that will help
php
$course = \Tests\Factories\Course\CourseFactory::new()->withLessonsAndLessonItemsAndQuizzes();This one creates a Course with a Lessons, LessonItems, and Quizzes.
php
$schedule = \Tests\Factories\Course\CourseScheduleFactory::new()->withLessonSchedules()->for($course)->createOne();This one will create a CourseSchedule with LessonSchedules for the given Course (the Course should have Lesson(s)).
So, the full setup looks like:
php
use Tests\Factories\Course\CourseFactory;
use Tests\Factories\Course\CourseEnrollmentFactory;
use Tests\Factories\Course\CourseScheduleFactory;
use Tests\Factories\Member\MemberFactory;
$course = CourseFactory::new()->withLessonsAndLessonItemsAndQuizzes();
$schedule = CourseScheduleFactory::new()->withLessonSchedules()->for($course, 'course')->createOne();
return CourseEnrollmentFactory::new()
->for(MemberFactory::new()->withMembership())
->forCourseSchedule($schedule)
->withEmptyMetadata()
->createOne();But again, it's pretty heavy, please minimize the number of Models you create for your tests.