Skip to content

Commit 563f8ee

Browse files
author
Rajat
committed
Removed memory based SCORM caching; Disk based SCORM cache env vars added; Updated docs
1 parent 3fb3c3a commit 563f8ee

File tree

20 files changed

+463
-150
lines changed

20 files changed

+463
-150
lines changed
145 KB
Loading
53.2 KB
Loading
699 KB
Loading
159 KB
Loading
159 KB
Loading
158 KB
Loading

apps/docs/src/pages/en/courses/add-content.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Sections are used to group lessons.
1616

1717
## Lessons
1818

19-
A lesson is a container for the actual learning material. CourseLit supports seven types of lessons, which are as follows.
19+
A lesson is a container for the actual learning material. CourseLit supports multiple types of lessons, which are as follows.
2020

2121
1. Text
2222

@@ -48,6 +48,12 @@ A lesson is a container for the actual learning material. CourseLit supports sev
4848

4949
See the [guide to add a quiz](/en/lessons/add-quiz).
5050

51+
8. SCORM
52+
53+
For sharing SCORM packages.
54+
55+
See the [guide to add a SCORM package](/en/lessons/scorm).
56+
5157
## Steps to add a new lesson
5258

5359
1. From the `Products` section in the dashboard, select your product to open its dashboard.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
title: Add a SCORM package to a course
3+
description: Add a SCORM package to a course
4+
layout: ../../../layouts/MainLayout.astro
5+
---
6+
7+
You can add SCORM packages to your courses in CourseLit. This allows you to import interactive e-learning content created with tools like Articulate Storyline, Rise, Adobe Captivate, iSpring, and more.
8+
9+
> The feature is currently in alpha, which means you may encounter bugs. Please report them in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> group if you run into any.
10+
11+
## What is SCORM?
12+
13+
SCORM (Sharable Content Object Reference Model) is an industry standard for e-learning content. It allows content created in one tool to be used in any SCORM-compliant LMS.
14+
15+
CourseLit supports both **SCORM 1.2** and **SCORM 2004** packages.
16+
17+
## Add a SCORM lesson
18+
19+
1. Go to the `Products` page and click on the course you want to add SCORM content to. Click on `Edit content`.
20+
21+
2. Click on `Add lesson` in any section.
22+
23+
3. On the New Lesson screen, you'll see a row of lesson type cards. Click on the `SCORM` card to select it.
24+
25+
4. Enter a title for your lesson and hit `Save`.
26+
27+
![create SCORM lesson](/assets/lessons/scorm/create.png)
28+
29+
> **Note:** SCORM lessons cannot be previewed. The `Preview` switch will have no effect.
30+
31+
5. A SCORM upload area will appear. Click `Choose File` and select your SCORM package (ZIP file). The maximum file size is **300MB**.
32+
33+
![upload SCORM package](/assets/lessons/scorm/upload.png)
34+
35+
6. Wait for the upload to complete. CourseLit will automatically validate the package and extract the course structure.
36+
37+
![uploaded SCORM package](/assets/lessons/scorm/uploaded.png)
38+
39+
## Replacing a SCORM package
40+
41+
To update an existing SCORM lesson with a new version of the package:
42+
43+
1. Open the SCORM lesson for editing
44+
2. Click the `Replace` button
45+
3. Select the new ZIP file
46+
4. Wait for the upload to complete
47+
48+
## Supported SCORM features
49+
50+
| Feature | SCORM 1.2 | SCORM 2004 |
51+
| --------------------- | --------- | ---------- |
52+
| Progress tracking |||
53+
| Completion status |||
54+
| Resume (suspend data) |||
55+
| Session time |||
56+
| Score reporting |||
57+
58+
## How course completion is calculated
59+
60+
CourseLit uses the data reported by the SCORM package to determine completion. When a learner clicks **Complete and Continue**, CourseLit checks the SCORM status stored in the database.
61+
62+
A lesson is considered complete if **ANY** of the following conditions are met:
63+
64+
1. **Explicit Completion:** The package reports a status of `completed` or `passed`.
65+
66+
- For SCORM 1.2: `cmi.core.lesson_status` is `completed` or `passed`.
67+
- For SCORM 2004: `cmi.completion_status` is `completed` or `cmi.success_status` is `passed`.
68+
69+
2. **Participation Fallback:** If the package does not report a completion status, CourseLit checks for evidence of participation. The lesson will be marked as complete if any of the following fields are present:
70+
- `cmi.suspend_data` (User made progress)
71+
- `cmi.core.session_time` (Time spent is recorded)
72+
- `cmi.core.exit` (Clean exit occurred)
73+
74+
> **Note:** If none of these conditions are met, the learner will see an error message asking them to complete the content first.
75+
76+
## Learner experience
77+
78+
When a learner opens a SCORM lesson:
79+
80+
1. An **Enter** button is displayed
81+
82+
![enter SCORM lesson](/assets/lessons/scorm/learner-enter.png)
83+
84+
2. Clicking the button opens the SCORM content in a popup window
85+
86+
![Popup SCORM lesson](/assets/lessons/scorm/learner-popup.png)
87+
88+
3. Progress is automatically saved as the learner interacts with the content
89+
4. When the learner closes the popup and returns, they can click **Complete and Continue** to proceed
90+
91+
> **Note:** Progress is preserved even if the browser is closed unexpectedly. When the learner returns, they will resume from where they left off.
92+
93+
## Technical notes
94+
95+
### For self-hosted setups
96+
97+
#### Enabling SCORM
98+
99+
SCORM requires disk-based caching to be enabled. Set the `CACHE_DIR` environment variable to enable SCORM support:
100+
101+
| Variable | Description | Required |
102+
| -------------------------- | ------------------------------------------------------- | ------------------- |
103+
| `CACHE_DIR` | Directory path for cache (SCORM uses `CACHE_DIR/scorm`) | **Yes** |
104+
| `SCORM_PACKAGE_SIZE_LIMIT` | Maximum upload size for SCORM packages (in bytes) | No (default: 300MB) |
105+
106+
> **Note:** If `CACHE_DIR` is not set, SCORM uploads will be disabled and the SCORM lesson type will appear grayed out in the lesson creator.
107+
108+
#### Docker Compose Example
109+
110+
```yaml
111+
services:
112+
web:
113+
image: your-app
114+
deploy:
115+
replicas: 3
116+
volumes:
117+
- cache-data:/app/cache
118+
environment:
119+
- CACHE_DIR=/app/cache
120+
121+
volumes:
122+
cache-data:
123+
```
124+
125+
#### Serverless environments
126+
127+
For serverless environments (Vercel, AWS Lambda), you can use `/tmp` as the cache directory:
128+
129+
```
130+
CACHE_DIR=/tmp
131+
```
132+
133+
Note that `/tmp` is ephemeral in serverless - extracted files will be re-extracted on cold starts, but this still works correctly.
134+
135+
## Stuck somewhere?
136+
137+
We are always here for you. Come chat with us in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> channel or send a tweet at <a href="https://twitter.com/courselit" target="_blank">@CourseLit</a>.

apps/web/.env

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@
2424
# SUPER_ADMIN_EMAIL=your@email.com
2525

2626
# Sequence settings
27-
# SEQUENCE_DELAY_BETWEEN_MAILS = 86400000 # 1 day in milliseconds
27+
# SEQUENCE_DELAY_BETWEEN_MAILS = 86400000 # 1 day in milliseconds
28+
29+
# Cache directory
30+
# CACHE_DIR=/tmp

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,32 @@ import {
3838
MANAGE_COURSES_PAGE_HEADING,
3939
TOAST_TITLE_ERROR,
4040
TOAST_TITLE_SUCCESS,
41+
ALPHA_LABEL,
4142
} from "@ui-config/strings";
4243
import DashboardContent from "@components/admin/dashboard-content";
4344
import useProduct from "@/hooks/use-product";
44-
import { AddressContext } from "@components/contexts";
45+
import { AddressContext, ServerConfigContext } from "@components/contexts";
4546
import {
4647
Constants,
4748
Lesson,
4849
LessonType,
4950
TextEditorContent,
5051
UIConstants,
5152
} from "@courselit/common-models";
52-
import { useToast } from "@courselit/components-library";
53+
import { useToast, Chip } from "@courselit/components-library";
5354
import { FetchBuilder } from "@courselit/utils";
5455
import { LessonContentRenderer } from "./lesson-content-renderer";
5556
import { isTextEditorNonEmpty, truncate } from "@ui-lib/utils";
5657
import { Separator } from "@components/ui/separator";
5758
import { emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
5859
import { LessonSkeleton } from "./skeleton";
5960
import { ScormLessonUpload } from "./scorm-lesson-upload";
61+
import {
62+
Tooltip,
63+
TooltipContent,
64+
TooltipProvider,
65+
TooltipTrigger,
66+
} from "@/components/ui/tooltip";
6067

6168
const { permissions } = UIConstants;
6269

@@ -85,6 +92,7 @@ export default function LessonPage() {
8592
const { toast } = useToast();
8693
const [errors, setErrors] = useState<LessonError>({});
8794
const address = useContext(AddressContext);
95+
const config = useContext(ServerConfigContext);
8896
const { product, loaded: productLoaded } = useProduct(productId);
8997
const breadcrumbs = [
9098
{ label: MANAGE_COURSES_PAGE_HEADING, href: "/dashboard/products" },
@@ -458,30 +466,80 @@ export default function LessonPage() {
458466
disabled={isEditing}
459467
>
460468
{lessonTypes.map(
461-
({ value, label, icon: Icon }) => (
462-
<Label
463-
key={value}
464-
htmlFor={value}
465-
className={`flex flex-col items-center justify-center rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-primary ${
466-
lesson.type === value
467-
? "border-primary"
468-
: ""
469-
} ${isEditing && value !== loadedLessonRef.current?.type?.toLowerCase() ? "opacity-50 cursor-not-allowed" : ""}`}
470-
>
471-
<RadioGroupItem
472-
value={value}
473-
id={value}
474-
className="sr-only"
475-
disabled={
476-
isEditing &&
477-
value !==
478-
loadedLessonRef.current?.type?.toLowerCase()
469+
({ value, label, icon: Icon }) => {
470+
const isScormDisabled =
471+
value ===
472+
Constants.LessonType
473+
.SCORM &&
474+
!config.cacheEnabled;
475+
const isTypeDisabled =
476+
isEditing &&
477+
value !==
478+
loadedLessonRef.current?.type?.toLowerCase();
479+
const isDisabled =
480+
isScormDisabled ||
481+
isTypeDisabled;
482+
483+
const cardContent = (
484+
<Label
485+
key={value}
486+
htmlFor={
487+
isDisabled
488+
? undefined
489+
: value
479490
}
480-
/>
481-
<Icon className="mb-2 h-6 w-6" />
482-
{label}
483-
</Label>
484-
),
491+
className={`flex flex-col items-center justify-center rounded-md border-2 border-muted bg-popover p-4 ${!isDisabled ? "hover:bg-accent hover:text-accent-foreground cursor-pointer" : "opacity-50 cursor-not-allowed"} [&:has([data-state=checked])]:border-primary ${
492+
lesson.type === value
493+
? "border-primary"
494+
: ""
495+
}`}
496+
>
497+
<RadioGroupItem
498+
value={value}
499+
id={value}
500+
className="sr-only"
501+
disabled={isDisabled}
502+
/>
503+
<Icon className="mb-2 h-6 w-6" />
504+
<span className="flex items-center gap-2">
505+
{label}
506+
{value ===
507+
Constants.LessonType
508+
.SCORM && (
509+
<Chip>
510+
{ALPHA_LABEL}
511+
</Chip>
512+
)}
513+
</span>
514+
</Label>
515+
);
516+
517+
if (isScormDisabled) {
518+
return (
519+
<TooltipProvider
520+
key={value}
521+
>
522+
<Tooltip>
523+
<TooltipTrigger
524+
asChild
525+
>
526+
{cardContent}
527+
</TooltipTrigger>
528+
<TooltipContent>
529+
<p>
530+
Set
531+
CACHE_DIR
532+
env var to
533+
enable SCORM
534+
</p>
535+
</TooltipContent>
536+
</Tooltip>
537+
</TooltipProvider>
538+
);
539+
}
540+
541+
return cardContent;
542+
},
485543
)}
486544
</RadioGroup>
487545
</div>

0 commit comments

Comments
 (0)