@happyvertical/smrt-education 0.60.3 → 0.60.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/class-crunchlabs.md +9 -0
- package/agents/course-bundle-import.md +29 -4
- package/agents/lesson-materials.md +25 -2
- package/dist/chunks/{workbook-photo-service-DGIkYT6Q.js → workbook-photo-service-DEb2LSC0.js} +151 -29
- package/dist/chunks/workbook-photo-service-DEb2LSC0.js.map +1 -0
- package/dist/course-bundle-manifest-v2.d.ts +13 -1
- package/dist/course-bundle-manifest-v2.d.ts.map +1 -1
- package/dist/course-bundle-service.d.ts +7 -0
- package/dist/course-bundle-service.d.ts.map +1 -1
- package/dist/course-bundle.d.ts +7 -1
- package/dist/course-bundle.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +153 -23
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/lesson-assets.d.ts +35 -0
- package/dist/lesson-assets.d.ts.map +1 -1
- package/dist/manifest.json +129 -9
- package/dist/smrt-knowledge.json +52 -9
- package/dist/sources/class-crunchlabs.d.ts +5 -0
- package/dist/sources/class-crunchlabs.d.ts.map +1 -1
- package/dist/submission-service.d.ts +37 -0
- package/dist/submission-service.d.ts.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-DGIkYT6Q.js.map +0 -1
|
@@ -125,6 +125,15 @@ skipped) or `memoryClassCrunchLabsSource(files)`. It never reads video files.
|
|
|
125
125
|
number), so `attachCourse` merges the parts in lesson order whatever order they
|
|
126
126
|
arrive in.
|
|
127
127
|
|
|
128
|
+
- Lesson steps (#4656): the slides are step `launch` (order 0, "Launch"); each
|
|
129
|
+
matched video is a step keyed by its real step code (`6`, `1b`) with the
|
|
130
|
+
video's title as label, ordered numerically (`classCrunchLabsStepOrder`: `1` <
|
|
131
|
+
`1a` < `1b` < `2`; videos sharing a code share the step, offset by 0.001 each);
|
|
132
|
+
the status check assessment (printable PDF and the editable master) is step
|
|
133
|
+
`check` (order 9999, "Status check"). The mission log, handouts, printables, the
|
|
134
|
+
teacher manual and the assessment guidance carry no step: the first three are the
|
|
135
|
+
learner's setup, the others stay guardian material.
|
|
136
|
+
|
|
128
137
|
The `report` lists lessons, members, parts, pages, matched and unmatched videos,
|
|
129
138
|
duplicates, ignored files and warnings. Fixtures in tests are synthetic files
|
|
130
139
|
with the same naming; no real download is ever copied into the repository.
|
|
@@ -221,10 +221,17 @@ the same way: an unknown key anywhere is `manifest_unknown_key` (with its path i
|
|
|
221
221
|
once), `role` (required) one of `teacher-guide`, `slides`, `learner-pages`,
|
|
222
222
|
`assessment`, `assessment-guidance`, `handout`, `printable`, `answer-key`,
|
|
223
223
|
`attachment` (`manifest_role` otherwise; a video is a reference, never a file),
|
|
224
|
-
optional `audience` and `
|
|
224
|
+
optional `audience`, `title` and (lesson members only) `step`. The audience is the role default
|
|
225
225
|
(`LESSON_ASSET_DEFAULT_AUDIENCE`) unless stated; `answer-key` and
|
|
226
226
|
`assessment-guidance` are always guardian (a learner audience is
|
|
227
227
|
`manifest_audience`). Members not listed stay recorded evidence with no role.
|
|
228
|
+
- **Lesson steps** (SMRT #4656): a lesson member or video may declare
|
|
229
|
+
`step: { key, order, label? }` (a closed object; `manifest_invalid` for a key over
|
|
230
|
+
32 characters or outside `A-Za-z0-9._:-`, a non-finite or non-number `order`
|
|
231
|
+
(|order| up to 1,000,000), or an empty label; `manifest_unknown_key` for anything
|
|
232
|
+
else, and a course member cannot carry one). Materials that share a `key` are
|
|
233
|
+
one step; fractions order `1b` after `1`. A video's `step` is still accepted as a
|
|
234
|
+
whole number (its rank); the object form also ranks it by `floor(order)`.
|
|
228
235
|
- **Pages**: `{ member, from, to, label? }` ranges over a member of the same lesson
|
|
229
236
|
(PDF, Word or PowerPoint; the rendered PDF for Office). Each page becomes a
|
|
230
237
|
`LessonPage` keyed `p<n>`, labelled `<label> <n>` (default `Page <n>`), ordered by
|
|
@@ -232,7 +239,7 @@ the same way: an unknown key anywhere is `manifest_unknown_key` (with its path i
|
|
|
232
239
|
500 pages per lesson.
|
|
233
240
|
- **Videos**: `{ reference, title, step, audience? }` where `reference` is a bare id
|
|
234
241
|
or an allowlisted https YouTube URL (`parseYouTubeReference`; anything else is
|
|
235
|
-
`manifest_video`), `step` an integer 0-9999. They become `LessonExternalVideo`
|
|
242
|
+
`manifest_video`), `step` an integer 0-9999 or a lesson step object (see above). They become `LessonExternalVideo`
|
|
236
243
|
rows; nothing is downloaded.
|
|
237
244
|
- **Course members** become `CourseAsset` rows, not tied to any lesson.
|
|
238
245
|
- **Unit and parts** (SMRT #4632): `course.key` (a manifest key) names the unit
|
|
@@ -251,7 +258,24 @@ plus `skipped` for declared members that were unsupported, refused or failed
|
|
|
251
258
|
rendering. A duplicate member resolves to the earlier identical member's bytes
|
|
252
259
|
(`assetPath`) and one material per (place, role, content).
|
|
253
260
|
|
|
254
|
-
`receiveMember` also gets `audience` and `courseLevel
|
|
261
|
+
`receiveMember` also gets `audience` and `courseLevel`, and an optional `title`
|
|
262
|
+
(SMRT #4653): the manifest title of the one material the member holds ("Teacher
|
|
263
|
+
Mission Manual"), so a host can name the assets it stores. It is absent when the
|
|
264
|
+
manifest gave none or the member backs several materials with different titles.
|
|
265
|
+
**Material titles**: `attachCourse` records that title on the `lesson_assets` /
|
|
266
|
+
`course_assets` row (`title`; null on a lesson row without one, `''` on a course
|
|
267
|
+
row), the same for the original and rendered halves of a pair. Lesson and course
|
|
268
|
+
material views show the row title and fall back to the stored asset's name when
|
|
269
|
+
there is none. A re-attach fills a missing title; a different one is a
|
|
270
|
+
`conflict` unless `supersede` restates it. **Lesson steps** (#4656): the declared
|
|
271
|
+
step is recorded on the `lesson_assets` row (`step_key`, `step_order`, `step_label`;
|
|
272
|
+
both halves of an original/rendered pair share it) and on `lesson_external_videos`
|
|
273
|
+
(`step_key`, `step_position`, `step_label`; its existing integer `step_order` stays
|
|
274
|
+
the rank, `floor(order)` for an object step). All are additive nullable columns.
|
|
275
|
+
A re-attach fills a row's missing step without a conflict; a different step is a
|
|
276
|
+
`conflict` unless `supersede` restates it (replays are unchanged). Hosts read it
|
|
277
|
+
with `SubmissionService.listLessonSteps` ([lesson materials](lesson-materials.md)).
|
|
278
|
+
**Slides** keep the original
|
|
255
279
|
deck (hand `bytes` to the host as a private asset: "Open in PowerPoint") and its
|
|
256
280
|
rendered pages (`rendered`: "Present").
|
|
257
281
|
|
|
@@ -318,7 +342,8 @@ tenant rules and lock order (import row, then unit row) as any attach.
|
|
|
318
342
|
rewritten. Sections and lessons the newer build no longer lists stay.
|
|
319
343
|
- **Part count change** stays `unit-mismatch`, supersede or not.
|
|
320
344
|
- **Materials**: the newer build's materials are attached; an existing row
|
|
321
|
-
(same lesson, asset, role) is restated in place (order, title, current again
|
|
345
|
+
(same lesson, asset, role) is restated in place (order, title, current again; a
|
|
346
|
+
renamed material shows its new title though it keeps its earlier asset),
|
|
322
347
|
not a `conflict`; its audience stays the family's. Pages are restated and
|
|
323
348
|
never retired (learner work points at page keys).
|
|
324
349
|
- **Retirement**: file materials and external videos the replaced build wrote
|
|
@@ -35,7 +35,7 @@ must be private.
|
|
|
35
35
|
|
|
36
36
|
`listLessonMaterials({enrollmentId, lessonId})` and `listCourseMaterials({enrollmentId})`
|
|
37
37
|
return `LessonMaterialView`s (`id`, `kind` file or external-video, `scope`, role,
|
|
38
|
-
audience, title, order, `assetId` or provider/videoId/`embedUrl`, and for files
|
|
38
|
+
audience, title (the bundle's material title, else the stored asset's name), order, `assetId` or provider/videoId/`embedUrl`, and for files
|
|
39
39
|
`mimeType` from the stored asset, `rendition`, `pairedMaterialId` (the other half
|
|
40
40
|
of an original/rendered pair, only when this actor sees both) and
|
|
41
41
|
`thumbnailAssetId`). Hosts read these instead of guessing by file extension. The learner
|
|
@@ -44,7 +44,7 @@ catalog projection without answer keys (every learner-facing catalog read) appli
|
|
|
44
44
|
the same rule. `signLessonMaterial`/`signCourseMaterial` sign only a file in the
|
|
45
45
|
actor's own list (claim scope `lesson:<id>` / `course:<id>`); a guessed id of
|
|
46
46
|
guardian material is denied; `thumbnail: true` signs the thumbnail of a visible
|
|
47
|
-
material instead. `signLessonPrintable`
|
|
47
|
+
material instead. `signLessonPrintable` signs a lesson file for printing: a `printable`, `handout` or `learner-pages` row (handout and learner pages only when the stored asset is `application/pdf`), learner-audience for a learner and any audience for a guardian. A retired row, an `original` rendition (the editable file) and any other role, answer keys included, are refused (SMRT #4657). Storage URIs never appear.
|
|
48
48
|
|
|
49
49
|
`setMaterialAudience({enrollmentId, scope, kind, materialId, audience})` lets a
|
|
50
50
|
guardian with plan authority change a family-owned row within the role's bounds
|
|
@@ -54,6 +54,29 @@ family-owned, so this is the after-attach path; before attach use
|
|
|
54
54
|
Authored courses change audience through course authoring (`CourseMaterialInput.audience`,
|
|
55
55
|
carried to the released join); shared or other families' rows are denied.
|
|
56
56
|
|
|
57
|
+
## Lesson steps (SMRT #4656)
|
|
58
|
+
|
|
59
|
+
`listLessonSteps({enrollmentId, lessonId})` walks a multi-step lesson in order,
|
|
60
|
+
filtered like `listLessonMaterials` (same actor and enrollment checks, learner
|
|
61
|
+
audience only for a learner, retired rows left out, same `LessonMaterialView`s):
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
{ setup: LessonMaterialView[]; // stepless printable / handout / learner-pages files
|
|
65
|
+
steps: Array<{ key: string; label: string; // label falls back to the key
|
|
66
|
+
kind: 'present' | 'watch' | 'do' | 'check';
|
|
67
|
+
order: number; // 1b sorts after 1
|
|
68
|
+
materials: LessonMaterialView[] }> }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Steps come from the `step_key`/`step_order`/`step_label` recorded on lesson files and
|
|
72
|
+
(`step_position`) external videos by a course bundle ([manifest steps](course-bundle-import.md)).
|
|
73
|
+
Materials sharing a key are one step (the original and rendered halves of a deck stay
|
|
74
|
+
together); steps sort by order then key; `kind` is `present` if any material is
|
|
75
|
+
`slides`, else `watch` if any is an external video, else `check` if any is an
|
|
76
|
+
`assessment`, else `do`. A lesson with no declared steps returns `steps: []`.
|
|
77
|
+
Course-level materials (supply lists) are not part of a lesson listing and stay with
|
|
78
|
+
`listCourseMaterials`.
|
|
79
|
+
|
|
57
80
|
## External videos
|
|
58
81
|
|
|
59
82
|
`LessonExternalVideo` references a video at an allowlisted provider (YouTube only):
|
package/dist/chunks/{workbook-photo-service-DGIkYT6Q.js → workbook-photo-service-DEb2LSC0.js}
RENAMED
|
@@ -637,6 +637,14 @@ function materialAudience(relationship, audience) {
|
|
|
637
637
|
return effective;
|
|
638
638
|
}
|
|
639
639
|
var MATERIAL_RENDITIONS = ["original", "rendered"];
|
|
640
|
+
var LESSON_STEP_KEY_MAX = 32;
|
|
641
|
+
var LESSON_STEP_LABEL_MAX = 200;
|
|
642
|
+
var LESSON_STEP_ORDER_MAX = 1e6;
|
|
643
|
+
function checkStep(key, rawOrder, label) {
|
|
644
|
+
if (key === null && rawOrder === null && label === null) return;
|
|
645
|
+
const order = rawOrder === null ? null : Number(rawOrder);
|
|
646
|
+
if (typeof key !== "string" || !key.trim() || key.length > 32 || typeof order !== "number" || !Number.isFinite(order) || Math.abs(order) > 1e6 || label !== null && (typeof label !== "string" || label.length > 200)) throw new Error("Invalid lesson step");
|
|
647
|
+
}
|
|
640
648
|
async function requirePrivateAsset(row, assetId = row.assetId) {
|
|
641
649
|
const asset = await (await AssetCollection.create({ db: row.db })).get(assetId, { cache: false });
|
|
642
650
|
if (!row.tenantId || !asset || asset.tenantId !== row.tenantId || asset.accessMode !== "private" || !asset.ownerProfileId) throw new Error("Private lesson material requires an authorized private asset");
|
|
@@ -678,14 +686,20 @@ var LessonAsset = class extends SmrtObject {
|
|
|
678
686
|
rendition = "";
|
|
679
687
|
renditionOfAssetId = null;
|
|
680
688
|
thumbnailAssetId = null;
|
|
689
|
+
title = null;
|
|
681
690
|
retiredAt = null;
|
|
682
691
|
sourceImportId = null;
|
|
692
|
+
stepKey = null;
|
|
693
|
+
stepOrder = null;
|
|
694
|
+
stepLabel = null;
|
|
683
695
|
constructor(options = {}) {
|
|
684
696
|
super(options);
|
|
685
697
|
for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
|
|
686
698
|
}
|
|
687
699
|
async save(options) {
|
|
688
700
|
materialAudience(this.relationship, this.audience);
|
|
701
|
+
if (this.title !== null && (typeof this.title !== "string" || this.title.length > 200)) throw new Error("Invalid lesson material title");
|
|
702
|
+
checkStep(this.stepKey, this.stepOrder, this.stepLabel);
|
|
689
703
|
const lesson = await this.db.get("lessons", { id: this.lessonId });
|
|
690
704
|
if (!lesson) throw new Error(`education lesson ${this.lessonId} does not exist`);
|
|
691
705
|
if (!await this.db.get("lessons", {
|
|
@@ -711,6 +725,10 @@ __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { requir
|
|
|
711
725
|
__decorateClass$10([field({ required: true })], LessonAsset.prototype, "relationship", 2);
|
|
712
726
|
__decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "renditionOfAssetId", 2);
|
|
713
727
|
__decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "thumbnailAssetId", 2);
|
|
728
|
+
__decorateClass$10([field({
|
|
729
|
+
type: "text",
|
|
730
|
+
nullable: true
|
|
731
|
+
})], LessonAsset.prototype, "title", 2);
|
|
714
732
|
__decorateClass$10([field({
|
|
715
733
|
type: "datetime",
|
|
716
734
|
nullable: true
|
|
@@ -719,6 +737,18 @@ __decorateClass$10([field({
|
|
|
719
737
|
type: "text",
|
|
720
738
|
nullable: true
|
|
721
739
|
})], LessonAsset.prototype, "sourceImportId", 2);
|
|
740
|
+
__decorateClass$10([field({
|
|
741
|
+
type: "text",
|
|
742
|
+
nullable: true
|
|
743
|
+
})], LessonAsset.prototype, "stepKey", 2);
|
|
744
|
+
__decorateClass$10([field({
|
|
745
|
+
type: "decimal",
|
|
746
|
+
nullable: true
|
|
747
|
+
})], LessonAsset.prototype, "stepOrder", 2);
|
|
748
|
+
__decorateClass$10([field({
|
|
749
|
+
type: "text",
|
|
750
|
+
nullable: true
|
|
751
|
+
})], LessonAsset.prototype, "stepLabel", 2);
|
|
722
752
|
LessonAsset = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
|
|
723
753
|
tableName: "lesson_assets",
|
|
724
754
|
conflictColumns: [
|
|
@@ -834,6 +864,9 @@ var LessonExternalVideo = class extends SmrtObject {
|
|
|
834
864
|
audience = "";
|
|
835
865
|
retiredAt = null;
|
|
836
866
|
sourceImportId = null;
|
|
867
|
+
stepKey = null;
|
|
868
|
+
stepPosition = null;
|
|
869
|
+
stepLabel = null;
|
|
837
870
|
constructor(options = {}) {
|
|
838
871
|
super(options);
|
|
839
872
|
for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
|
|
@@ -841,6 +874,7 @@ var LessonExternalVideo = class extends SmrtObject {
|
|
|
841
874
|
async save(options) {
|
|
842
875
|
materialAudience("video", this.audience);
|
|
843
876
|
if (!EXTERNAL_VIDEO_PROVIDERS.includes(this.provider) || !YOUTUBE_ID.test(this.videoId) || typeof this.title !== "string" || !this.title.trim() || this.title.length > 200 || !Number.isSafeInteger(this.stepOrder) || this.stepOrder < 0) throw new Error("Invalid external video reference");
|
|
877
|
+
checkStep(this.stepKey, this.stepPosition, this.stepLabel);
|
|
844
878
|
if (!await this.db.get("lessons", {
|
|
845
879
|
id: this.lessonId,
|
|
846
880
|
tenant_id: this.tenantId
|
|
@@ -859,6 +893,18 @@ __decorateClass$10([field({
|
|
|
859
893
|
type: "text",
|
|
860
894
|
nullable: true
|
|
861
895
|
})], LessonExternalVideo.prototype, "sourceImportId", 2);
|
|
896
|
+
__decorateClass$10([field({
|
|
897
|
+
type: "text",
|
|
898
|
+
nullable: true
|
|
899
|
+
})], LessonExternalVideo.prototype, "stepKey", 2);
|
|
900
|
+
__decorateClass$10([field({
|
|
901
|
+
type: "decimal",
|
|
902
|
+
nullable: true
|
|
903
|
+
})], LessonExternalVideo.prototype, "stepPosition", 2);
|
|
904
|
+
__decorateClass$10([field({
|
|
905
|
+
type: "text",
|
|
906
|
+
nullable: true
|
|
907
|
+
})], LessonExternalVideo.prototype, "stepLabel", 2);
|
|
862
908
|
LessonExternalVideo = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
|
|
863
909
|
tableName: "lesson_external_videos",
|
|
864
910
|
conflictColumns: [
|
|
@@ -32461,6 +32507,21 @@ var ExternalVideoReferenceError = class extends LearnerValidationError {
|
|
|
32461
32507
|
this.name = "ExternalVideoReferenceError";
|
|
32462
32508
|
}
|
|
32463
32509
|
};
|
|
32510
|
+
var PRINTABLE_RELATIONSHIPS = [
|
|
32511
|
+
"printable",
|
|
32512
|
+
"handout",
|
|
32513
|
+
"learner-pages"
|
|
32514
|
+
];
|
|
32515
|
+
var stepOfRow = (row, orderField) => row.step_key ? {
|
|
32516
|
+
key: String(row.step_key),
|
|
32517
|
+
order: Number(row[orderField]) || 0,
|
|
32518
|
+
label: row.step_label ? String(row.step_label) : ""
|
|
32519
|
+
} : null;
|
|
32520
|
+
var SETUP_RELATIONSHIPS = /* @__PURE__ */ new Set([
|
|
32521
|
+
"printable",
|
|
32522
|
+
"handout",
|
|
32523
|
+
"learner-pages"
|
|
32524
|
+
]);
|
|
32464
32525
|
function pairRenditions(rows, views) {
|
|
32465
32526
|
const out = views.map((v) => ({ ...v }));
|
|
32466
32527
|
rows.forEach((row, i) => {
|
|
@@ -33309,7 +33370,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
|
|
|
33309
33370
|
scope,
|
|
33310
33371
|
relationship: row.relationship,
|
|
33311
33372
|
audience: materialAudience(String(row.relationship), String(row.audience ?? "")),
|
|
33312
|
-
title:
|
|
33373
|
+
title: String(row.title ?? "") || asset.name,
|
|
33313
33374
|
order: Number(row.sort_order) || 0,
|
|
33314
33375
|
assetId: String(row.asset_id),
|
|
33315
33376
|
provider: null,
|
|
@@ -33327,6 +33388,9 @@ var SubmissionService = class SubmissionService extends LearnerService {
|
|
|
33327
33388
|
* learner-audience material; keys and guidance never reach them.
|
|
33328
33389
|
*/
|
|
33329
33390
|
async listLessonMaterials(request) {
|
|
33391
|
+
return (await this.collectLessonMaterials(request)).map((m) => m.view);
|
|
33392
|
+
}
|
|
33393
|
+
async collectLessonMaterials(request) {
|
|
33330
33394
|
const enrollment = await this.requireEnrollment(request, "education.learner.read");
|
|
33331
33395
|
if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
|
|
33332
33396
|
const lesson = await this.db.get("lessons", { id: request.lessonId });
|
|
@@ -33334,28 +33398,72 @@ var SubmissionService = class SubmissionService extends LearnerService {
|
|
|
33334
33398
|
const tenant = lesson.tenant_id ?? null;
|
|
33335
33399
|
const learnerOnly = await this.learnerAudienceOnly(request);
|
|
33336
33400
|
const files = (await this.db.list("lesson_assets", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
|
|
33337
|
-
const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || String(a.id).localeCompare(String(b.id))) : [];
|
|
33401
|
+
const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || (Number(a.step_position ?? 0) || 0) - (Number(b.step_position ?? 0) || 0) || String(a.id).localeCompare(String(b.id))) : [];
|
|
33338
33402
|
const fileViews = [];
|
|
33339
33403
|
for (const row of files) fileViews.push(await this.fileView(row, "lesson"));
|
|
33340
33404
|
const result = pairRenditions(files, fileViews);
|
|
33341
|
-
|
|
33342
|
-
|
|
33343
|
-
|
|
33344
|
-
|
|
33345
|
-
|
|
33346
|
-
|
|
33347
|
-
|
|
33348
|
-
|
|
33349
|
-
|
|
33350
|
-
|
|
33351
|
-
|
|
33352
|
-
|
|
33353
|
-
|
|
33354
|
-
|
|
33355
|
-
|
|
33356
|
-
|
|
33357
|
-
|
|
33358
|
-
|
|
33405
|
+
const steps = files.map((row) => stepOfRow(row, "step_order"));
|
|
33406
|
+
for (const row of videos) {
|
|
33407
|
+
steps.push(stepOfRow(row, "step_position"));
|
|
33408
|
+
result.push({
|
|
33409
|
+
id: String(row.id),
|
|
33410
|
+
kind: "external-video",
|
|
33411
|
+
scope: "lesson",
|
|
33412
|
+
relationship: "video",
|
|
33413
|
+
audience: materialAudience("video", String(row.audience ?? "")),
|
|
33414
|
+
title: String(row.title),
|
|
33415
|
+
order: Number(row.step_order) || 0,
|
|
33416
|
+
assetId: null,
|
|
33417
|
+
provider: row.provider,
|
|
33418
|
+
videoId: String(row.video_id),
|
|
33419
|
+
embedUrl: externalVideoEmbedUrl(row.provider, String(row.video_id)),
|
|
33420
|
+
mimeType: null,
|
|
33421
|
+
rendition: null,
|
|
33422
|
+
pairedMaterialId: null,
|
|
33423
|
+
thumbnailAssetId: null
|
|
33424
|
+
});
|
|
33425
|
+
}
|
|
33426
|
+
return result.map((view, i) => ({
|
|
33427
|
+
view,
|
|
33428
|
+
step: steps[i] ?? null
|
|
33429
|
+
}));
|
|
33430
|
+
}
|
|
33431
|
+
/**
|
|
33432
|
+
* A lesson walked in order (SMRT #4656), audience-filtered exactly like
|
|
33433
|
+
* `listLessonMaterials`: stepless handouts, learner pages and printables as
|
|
33434
|
+
* `setup`, then the declared steps. A step groups every material sharing its
|
|
33435
|
+
* key (the original and rendered halves of a deck stay together). A lesson
|
|
33436
|
+
* with no declared steps returns `steps: []`. Course-level supply lists are
|
|
33437
|
+
* not part of a lesson's listing and stay with `listCourseMaterials`.
|
|
33438
|
+
*/
|
|
33439
|
+
async listLessonSteps(request) {
|
|
33440
|
+
const collected = await this.collectLessonMaterials(request);
|
|
33441
|
+
const setup = collected.filter((m) => !m.step && m.view.kind === "file" && SETUP_RELATIONSHIPS.has(m.view.relationship)).map((m) => m.view);
|
|
33442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
33443
|
+
for (const { view, step } of collected) {
|
|
33444
|
+
if (!step) continue;
|
|
33445
|
+
const group = groups.get(step.key) ?? {
|
|
33446
|
+
key: step.key,
|
|
33447
|
+
order: step.order,
|
|
33448
|
+
label: "",
|
|
33449
|
+
materials: []
|
|
33450
|
+
};
|
|
33451
|
+
group.order = Math.min(group.order, step.order);
|
|
33452
|
+
group.label = group.label || step.label;
|
|
33453
|
+
group.materials.push(view);
|
|
33454
|
+
groups.set(step.key, group);
|
|
33455
|
+
}
|
|
33456
|
+
const kindOf = (materials) => materials.some((m) => m.relationship === "slides") ? "present" : materials.some((m) => m.kind === "external-video") ? "watch" : materials.some((m) => m.relationship === "assessment") ? "check" : "do";
|
|
33457
|
+
return {
|
|
33458
|
+
setup,
|
|
33459
|
+
steps: [...groups.values()].sort((a, b) => a.order - b.order || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)).map((g) => ({
|
|
33460
|
+
key: g.key,
|
|
33461
|
+
label: g.label || g.key,
|
|
33462
|
+
kind: kindOf(g.materials),
|
|
33463
|
+
order: g.order,
|
|
33464
|
+
materials: g.materials
|
|
33465
|
+
}))
|
|
33466
|
+
};
|
|
33359
33467
|
}
|
|
33360
33468
|
/** Course-level materials (supply lists, posters) under the same audience rule. */
|
|
33361
33469
|
async listCourseMaterials(request) {
|
|
@@ -33525,16 +33633,30 @@ var SubmissionService = class SubmissionService extends LearnerService {
|
|
|
33525
33633
|
return view;
|
|
33526
33634
|
});
|
|
33527
33635
|
}
|
|
33636
|
+
/**
|
|
33637
|
+
* Sign a lesson file for printing: a `printable`, `handout` or `learner-pages`
|
|
33638
|
+
* PDF the actor may see (learner-audience, or any for a guardian). Never an
|
|
33639
|
+
* editable `original` rendition and never an answer key.
|
|
33640
|
+
*/
|
|
33528
33641
|
async signLessonPrintable(request) {
|
|
33529
33642
|
const enrollment = await this.requireEnrollment(request, "education.learner.read");
|
|
33530
33643
|
if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
|
|
33531
|
-
|
|
33532
|
-
|
|
33533
|
-
|
|
33534
|
-
|
|
33535
|
-
|
|
33536
|
-
|
|
33537
|
-
|
|
33644
|
+
if (!this.options.signAsset) throw new LearnerAuthorizationError();
|
|
33645
|
+
const links = await LessonAssetCollection.create({ db: this.db });
|
|
33646
|
+
let link = null;
|
|
33647
|
+
for (const relationship of PRINTABLE_RELATIONSHIPS) {
|
|
33648
|
+
link = await links.get({
|
|
33649
|
+
tenantId: request.tenantId,
|
|
33650
|
+
lessonId: request.lessonId,
|
|
33651
|
+
assetId: request.assetId,
|
|
33652
|
+
relationship
|
|
33653
|
+
}, { cache: false });
|
|
33654
|
+
if (link?.retiredAt) link = null;
|
|
33655
|
+
if (link) break;
|
|
33656
|
+
}
|
|
33657
|
+
if (!link) throw new LearnerAuthorizationError();
|
|
33658
|
+
if (link.rendition === "original") throw new LearnerAuthorizationError();
|
|
33659
|
+
if (link.relationship !== "printable" && (await this.materialAsset(request.assetId)).mimeType !== "application/pdf") throw new LearnerAuthorizationError();
|
|
33538
33660
|
if (await this.learnerAudienceOnly(request) && materialAudience(link.relationship, link.audience) !== "learner") throw new LearnerAuthorizationError();
|
|
33539
33661
|
const expiresAt = new Date(Date.now() + 300 * 1e3);
|
|
33540
33662
|
return {
|
|
@@ -36968,6 +37090,6 @@ var WorkbookPhotoService = class WorkbookPhotoService extends LessonWorkService
|
|
|
36968
37090
|
}
|
|
36969
37091
|
};
|
|
36970
37092
|
//#endregion
|
|
36971
|
-
export { LessonAlreadyCompleteError as $,
|
|
37093
|
+
export { LessonAlreadyCompleteError as $, externalVideoEmbedUrl as $i, QUESTION_COMPLETION_MODES as $n, ReleasedLesson as $r, WorkFeedbackRelease as $t, QUESTION_EXTRACTION_VERSION as A, Attestation as Ai, QuestionRevision as An, CourseCoverChangeCollection as Ar, WorkbookPhotoDecision as At, SELF_ATTESTABLE_LESSON_KINDS as B, CourseAsset as Bi, normalizeQuestionContent as Bn, CourseUpgradeChoiceCollection as Br, WorkCompletionDecision as Bt, MAX_EXTRACTED_OMISSIONS as C, ReadingCollection as Ci, LearningObjectiveCollection as Cn, MAX_REFUSAL_EXPLANATION_LENGTH as Cr, canonicalGroupSharingPolicy as Ct, MAX_EXTRACTED_STUDENT_WORK as D, SubmissionIntakeBindingCollection as Di, QuestionCollection as Dn, CourseArchiveDecision as Dr, BookContextRevisionCollection as Dt, MAX_EXTRACTED_SECTIONS as E, SubmissionCollection as Ei, Question as En, readDelegation as Er, BookContextRevision as Et, normalizeWorkObservedLayout as F, Submission as Fi, resolveEnrollmentLessons as Fn, CourseRelease as Fr, FamilyIntakeAsset as Ft, EnrollmentNotActiveError as G, LESSON_ASSET_RELATIONSHIPS as Gi, MAX_PRINTABLE_ITEMS as Gn, EnrollmentAssignmentRecord as Gr, LessonWorkFiling as Gt, SubmissionStorageCleanupError as H, EXTERNAL_VIDEO_PROVIDERS as Hi, GENERATED_PRINTABLE_RELATIONSHIPS as Hn, CourseVisibilityChangeCollection as Hr, readWorkCompletion as Ht, normalizeWorkAnswerRegions as I, SubmissionAsset as Ii, applyProposalFields as In, CourseReleaseCollection as Ir, FamilyIntakeAssetCollection as It, InterestThemeError as J, LESSON_STEP_ORDER_MAX as Ji, MAX_PRINTABLE_TOTAL_TEXT as Jn, EnrollmentAutoUpgradeSkipCollection as Jr, WorkAssessmentRevisionCollection as Jt, EnrollmentPaceError as K, LESSON_STEP_KEY_MAX as Ki, MAX_PRINTABLE_PAGES as Kn, EnrollmentAssignmentRecordCollection as Kr, LessonWorkFilingCollection as Kt, LessonWorkWorkflowRequiredError as L, SubmissionIntakeBinding as Li, draftFromRevision as Ln, CourseRevision as Lr, FamilyIntakeCollection as Lt, QuestionExtractionPendingError as M, Grade as Mi, CourseAuthoringService as Mn, CourseDiscardTombstoneCollection as Mr, WorkbookPhotoIntake as Mt, QuestionExtractionValidationError as N, LessonSelfAttestation as Ni, authoredVisibilityPermitsGroups as Nn, CourseDraftHead as Nr, WorkbookPhotoIntakeCollection as Nt, MAX_EXTRACTED_TEXT as O, VideoCollection as Oi, QuestionResponse as On, CourseArchiveDecisionCollection as Or, WorkbookPhotoAsset as Ot, LessonWorkService as P, ScannedSubmission as Pi, effectiveCourseVisibility as Pn, CourseDraftHeadCollection as Pr, FamilyIntake as Pt, LearnerValidationError as Q, MATERIAL_RENDITIONS as Qi, PrintableStorageNotConfiguredError as Qn, LessonRevisionCollection as Qr, WorkFeedbackDeliveryCollection as Qt, OwnAttestationConflictError as R, parseQuestionRubric as Ri, planProposal as Rn, CourseRevisionCollection as Rr, FamilyIntakeDecision as Rt, MAX_EXTRACTED_ANCHORS as S, LessonSelfAttestationCollection as Si, LearningObjective as Sn, MAX_REFUSAL_CATEGORY_LENGTH as Sr, GroupSharingPolicyValidationError as St, MAX_EXTRACTED_QUESTIONS as T, SubmissionAssetCollection as Ti, LearningObjectiveRevisionCollection as Tn, delegationRecordFields as Tr, familyIntakeScopeId as Tt, submissionPageKey as U, GUARDIAN_ONLY_RELATIONSHIPS as Ui, LearningObjectiveKeyConflictError as Un, CurriculumCreationRecord as Ur, LessonActivity as Ut, SubmissionService as V, CourseAssetCollection as Vi, GENERATED_PRINTABLE_AUDIENCES as Vn, CourseVisibilityChange as Vr, WorkCompletionDecisionCollection as Vt, ENROLLMENT_STATUS_TRANSITIONS as W, LESSON_ASSET_DEFAULT_AUDIENCE as Wi, LearningObjectiveNotFoundError as Wn, CurriculumCreationRecordCollection as Wr, LessonActivityCollection as Wt, LearnerHistoryUnavailableError as X, LessonExternalVideo as Xi, PrintableOwnerError as Xn, EnrollmentReleasePinCollection as Xr, WorkAttemptCollection as Xt, LearnerAuthorizationError as Y, LessonAsset as Yi, MAX_RESOLVE_QUESTION_REVISIONS as Yn, EnrollmentReleasePin as Yr, WorkAttempt as Yt, LearnerService as Z, LessonExternalVideoCollection as Zi, PrintableStorageCleanupError as Zn, LessonRevision as Zr, WorkFeedbackDelivery as Zt, EXTRACTED_OMISSION_REASONS as _, Reading as _a, GradeCollection as _i, renderGuardianWorksheetPdf as _n, CourseProposalFieldError as _r, LessonProgressTransitionCollection as _t, questionExtractionProposalDigest as a, LessonProgress as aa, mostRestrictiveLicense as ai, aggregateWorkQuestions as an, QuestionReferenceError as ar, MIN_LESSONS_PER_WEEK as at, EXTRACTED_QUESTION_STATUSES as b, LessonCollection as bi, GeneratedPrintable as bn, MAX_GENERATION_GRADE as br, GroupSharingAuthorizationError as bt, QuestionExtractionRecord as c, interestThemeFromColumns as ca, AttestationCollection as ci, LessonPage as cn, COURSE_GENERATION_STATUSES as cr, EnrollmentPaceChange as ct, extractedQuestionContent as d, COURSE_VISIBILITIES as da, CourseSectionCollection as di, validLessonPageKey as dn, CourseGenerationBriefError as dr, EnrollmentStatusChangeCollection as dt, materialAudience as ea, ReleasedLessonCollection as ei, WorkFeedbackReleaseCollection as en, QUESTION_FIELDS as er, LessonPutBackError as et, matchExtractedQuestions as f, Course as fa, CurriculumCollection as fi, QuestionAuthoringService as fn, CourseGenerationInProgressError as fr, LearnerInterestThemeChange as ft, EXTRACTED_MEDIA_KINDS as g, Lesson as ga, ExternalCourseCollection as gi, worksheetVersionId as gn, CourseGeneratorNotConfiguredError as gr, LessonProgressTransition as gt, EXTRACTED_LEGIBILITY as h, ExternalCourse as ha, ExternalCompletionCollection as hi, canonical$2 as hn, CourseGenerationRefusal as hr, LessonCompletionRecordCollection as ht, QuestionExtractionService as i, Learner as ia, isGroupShareableLicense as ii, WorkCompletionConflictError as in, QuestionNotFoundError as ir, MAX_NEXT_LESSONS_COUNT as it, QuestionExtractionNotFoundError as j, ExternalCompletion as ji, QuestionRevisionCollection as jn, CourseDiscardTombstone as jr, WorkbookPhotoDecisionCollection as jt, QUESTION_EXTRACTION_SOURCE_KINDS as k, WorksheetCollection as ki, QuestionResponseCollection as kn, CourseCoverChange as kr, WorkbookPhotoAssetCollection as kt, QuestionExtractionRecordCollection as l, parseInterestTheme as la, AuthoredCourseCollection as li, LessonPageCollection as ln, COURSE_GENERATION_TARGETS as lr, EnrollmentPaceChangeCollection as lt, questionExtractionSourceIdentity as m, Exercise as ma, ExerciseCollection as mi, buildLearnerWorksheet as mn, CourseGenerationOutputError as mr, LessonCompletionRecord as mt, preservePhotoProposal as n, Curriculum as na, AUTHORED_RELEASE_SOURCE_KEY as ni, WORK_MATH_VOCABULARY as nn, QUESTION_PROVENANCES as nr, LessonWorkOwnedError as nt, ExtractedQuestionLink as o, MAX_INTEREST_THEME_KEY_CHARS as oa, CatalogAuthorizationError as oi, validateWorkAssessment as on, WorksheetVersionMismatchError as or, enrollmentView as ot, normalizeQuestionExtraction as p, CourseSection as pa, EnrollmentCollection as pi, buildGuardianWorksheet as pn, CourseGenerationNotFoundError as pr, LearnerInterestThemeChangeCollection as pt, EnrollmentStatusTransitionError as q, LESSON_STEP_LABEL_MAX as qi, MAX_PRINTABLE_TEXT_LENGTH as qn, EnrollmentAutoUpgradeSkip as qr, WorkAssessmentRevision as qt, validateWorkbookPhotoIdentification as r, Enrollment as ra, isAuthoredReleaseGroupShareable as ri, WorkAssessmentValidationError as rn, QUESTION_RESPONSE_TYPES as rr, MAX_LESSONS_PER_WEEK as rt, ExtractedQuestionLinkCollection as s, MAX_INTEREST_THEME_LABEL_CHARS as sa, CatalogService as si, workAggregateFeedback as sn, groupSharingDigest as sr, requirePolicyBasis as st, WorkbookPhotoService as t, parseYouTubeReference as ta, AUTHORED_IDENTITY_SOURCE_KEY as ti, LessonWorkPlacementError as tn, QUESTION_PROMPT_SUMMARY_LENGTH as tr, LessonSkippedError as tt, QUESTION_EXTRACTION_ARGS_SCHEMA as u, AuthoredCourse as ua, CourseCollection as ui, lessonPageView as un, COURSE_GENERATION_TERMINAL as ur, EnrollmentStatusChange as ut, EXTRACTED_ORIGINS as v, Video as va, LearnerCollection as vi, renderLearnerWorksheetPdf as vn, CourseProposalNotFoundError as vr, evaluateGroupSharingPolicy as vt, MAX_EXTRACTED_PAGES as w, ScannedSubmissionCollection as wi, LearningObjectiveRevision as wn, delegationActorFields as wr, validateGroupSharingPolicy as wt, EXTRACTED_READABILITY as x, LessonProgressCollection as xi, GeneratedPrintableCollection as xn, MAX_GENERATION_OBJECTIVE_CHARS as xr, GroupSharingConflictError as xt, EXTRACTED_PAGE_ROLES as y, Worksheet as ya, LessonAssetCollection as yi, es_exports as yn, MAX_GENERATION_BRIEF_FIELD_CHARS as yr, summarizeWorkEvidence as yt, OwnAttestationLessonKindError as z, validateRubricResults as zi, validateCourseGenerationOutput as zn, CourseUpgradeChoice as zr, FamilyIntakeDecisionCollection as zt };
|
|
36972
37094
|
|
|
36973
|
-
//# sourceMappingURL=workbook-photo-service-
|
|
37095
|
+
//# sourceMappingURL=workbook-photo-service-DEb2LSC0.js.map
|