@happyvertical/smrt-education 0.57.47 → 0.57.49
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.md +9 -0
- package/README.md +23 -2
- package/agents/course-authoring-service.md +44 -1
- package/agents/course-bundle-import.md +148 -0
- package/agents/group-sharing.md +20 -0
- package/agents/learner-service.md +35 -0
- package/agents/question-extraction.md +4 -1
- package/agents/tutor-context.md +62 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/chunks/{workbook-photo-service-B7Tujv__.js → workbook-photo-service-WwDDC5GF.js} +692 -18
- package/dist/chunks/workbook-photo-service-WwDDC5GF.js.map +1 -0
- package/dist/course-authoring-contracts.d.ts +26 -3
- package/dist/course-authoring-contracts.d.ts.map +1 -1
- package/dist/course-authoring-models.d.ts +44 -1
- package/dist/course-authoring-models.d.ts.map +1 -1
- package/dist/course-authoring-service.d.ts +39 -0
- package/dist/course-authoring-service.d.ts.map +1 -1
- package/dist/course-bundle-contracts.d.ts +93 -0
- package/dist/course-bundle-contracts.d.ts.map +1 -0
- package/dist/course-bundle-models.d.ts +66 -0
- package/dist/course-bundle-models.d.ts.map +1 -0
- package/dist/course-bundle-service.d.ts +126 -0
- package/dist/course-bundle-service.d.ts.map +1 -0
- package/dist/course-bundle-zip.d.ts +48 -0
- package/dist/course-bundle-zip.d.ts.map +1 -0
- package/dist/course-bundle.d.ts +135 -0
- package/dist/course-bundle.d.ts.map +1 -0
- package/dist/course-visibility.d.ts +22 -0
- package/dist/course-visibility.d.ts.map +1 -0
- package/dist/group-sharing-reader.d.ts.map +1 -1
- package/dist/index.d.ts +17 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2563 -510
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/learner-history.d.ts +50 -1
- package/dist/learner-history.d.ts.map +1 -1
- package/dist/learner-plan-service.d.ts.map +1 -1
- package/dist/learner-service.d.ts +91 -2
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/learners.d.ts +2 -0
- package/dist/learners.d.ts.map +1 -1
- package/dist/manifest.json +11774 -7626
- package/dist/question-response-service.d.ts +21 -3
- package/dist/question-response-service.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3507 -2008
- package/dist/tutor-context-service.d.ts +34 -0
- package/dist/tutor-context-service.d.ts.map +1 -0
- package/dist/tutor-contracts.d.ts +123 -0
- package/dist/tutor-contracts.d.ts.map +1 -0
- package/dist/tutor-models.d.ts +77 -0
- package/dist/tutor-models.d.ts.map +1 -0
- package/dist/tutor-session-service.d.ts +115 -0
- package/dist/tutor-session-service.d.ts.map +1 -0
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-B7Tujv__.js.map +0 -1
package/dist/chunks/{workbook-photo-service-B7Tujv__.js → workbook-photo-service-WwDDC5GF.js}
RENAMED
|
@@ -113,7 +113,28 @@ ExternalCourse = __decorateClass$11([smrt({
|
|
|
113
113
|
tableName: "courses",
|
|
114
114
|
...closed$9
|
|
115
115
|
})], ExternalCourse);
|
|
116
|
-
var
|
|
116
|
+
var COURSE_VISIBILITIES = [
|
|
117
|
+
"family",
|
|
118
|
+
"groups",
|
|
119
|
+
"public"
|
|
120
|
+
];
|
|
121
|
+
var AuthoredCourse = class extends Course {
|
|
122
|
+
/**
|
|
123
|
+
* Authored-course visibility; '' means never set (reads as 'family'). Only the
|
|
124
|
+
* course-authoring service writes it, with an append-only change row.
|
|
125
|
+
*/
|
|
126
|
+
visibility = "";
|
|
127
|
+
/** Education-owned image asset shown as the course cover; '' means none. */
|
|
128
|
+
coverAssetId = "";
|
|
129
|
+
constructor(options = {}) {
|
|
130
|
+
super(options);
|
|
131
|
+
applyModelOptions(this, options);
|
|
132
|
+
}
|
|
133
|
+
async save(options) {
|
|
134
|
+
if (this.licenseKind === "personal-use-copy" && (this.visibility === "groups" || this.visibility === "public")) throw new Error("personal-use-copy courses can only be family-visible");
|
|
135
|
+
return await super.save(options);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
117
138
|
__publicField$11(AuthoredCourse, "__smrtPackage__", "@happyvertical/smrt-education");
|
|
118
139
|
AuthoredCourse = __decorateClass$11([smrt({
|
|
119
140
|
tableName: "courses",
|
|
@@ -374,6 +395,8 @@ var Enrollment = class extends SmrtObject {
|
|
|
374
395
|
startedAt = null;
|
|
375
396
|
completedAt = null;
|
|
376
397
|
targetMinutesPerWeek = 0;
|
|
398
|
+
/** Planned lessons per week (1-14); `0` means no pace is set. */
|
|
399
|
+
paceLessonsPerWeek = 0;
|
|
377
400
|
sortOrder = 0;
|
|
378
401
|
constructor(options = {}) {
|
|
379
402
|
super(options);
|
|
@@ -398,6 +421,7 @@ var Enrollment = class extends SmrtObject {
|
|
|
398
421
|
tenant_id: this.tenantId
|
|
399
422
|
})) throw new Error("Enrollment learner must match its tenant curriculum");
|
|
400
423
|
if (!Number.isSafeInteger(this.targetMinutesPerWeek) || this.targetMinutesPerWeek < 0) throw new Error("Target minutes must be a nonnegative integer");
|
|
424
|
+
if (!Number.isSafeInteger(this.paceLessonsPerWeek) || this.paceLessonsPerWeek < 0 || this.paceLessonsPerWeek > 14) throw new Error("Pace must be zero or 1 to 14 lessons per week");
|
|
401
425
|
return super.save(options);
|
|
402
426
|
}
|
|
403
427
|
};
|
|
@@ -1169,7 +1193,9 @@ var COURSE_AUTHORING_ACTIONS = [
|
|
|
1169
1193
|
"education.course.review",
|
|
1170
1194
|
"education.course.release",
|
|
1171
1195
|
"education.course.assign",
|
|
1172
|
-
"education.course.archive"
|
|
1196
|
+
"education.course.archive",
|
|
1197
|
+
"education.course.set-visibility",
|
|
1198
|
+
"education.course.set-cover"
|
|
1173
1199
|
];
|
|
1174
1200
|
var COURSE_PROVENANCES = [
|
|
1175
1201
|
"supplied",
|
|
@@ -1265,6 +1291,20 @@ var CourseMaterialCopyError = class extends CourseAuthoringError {
|
|
|
1265
1291
|
super("material-copy-failed", message);
|
|
1266
1292
|
}
|
|
1267
1293
|
};
|
|
1294
|
+
var CourseVisibilityRefusedError = class extends CourseAuthoringError {
|
|
1295
|
+
constructor(visibility) {
|
|
1296
|
+
super("visibility-refused", "A personal-use course can only be family-visible.");
|
|
1297
|
+
this.visibility = visibility;
|
|
1298
|
+
}
|
|
1299
|
+
visibility;
|
|
1300
|
+
};
|
|
1301
|
+
var CourseCoverInvalidError = class extends CourseAuthoringError {
|
|
1302
|
+
constructor(reason) {
|
|
1303
|
+
super("cover-invalid", "The cover must be a readable education-owned image.");
|
|
1304
|
+
this.reason = reason;
|
|
1305
|
+
}
|
|
1306
|
+
reason;
|
|
1307
|
+
};
|
|
1268
1308
|
//#endregion
|
|
1269
1309
|
//#region src/course-authoring-licence.ts
|
|
1270
1310
|
var AUTHORED_RELEASE_SOURCE_KEY = "authored-release";
|
|
@@ -1690,6 +1730,96 @@ CourseArchiveDecision = __decorateClass$7([TenantScoped({ mode: "required" }), s
|
|
|
1690
1730
|
var CourseArchiveDecisionCollection = class extends SmrtCollection {
|
|
1691
1731
|
static _itemClass = CourseArchiveDecision;
|
|
1692
1732
|
};
|
|
1733
|
+
var CourseVisibilityChange = class extends SmrtObject {
|
|
1734
|
+
tenantId = "";
|
|
1735
|
+
courseId = "";
|
|
1736
|
+
sequence = 1;
|
|
1737
|
+
previousVisibility = "family";
|
|
1738
|
+
visibility = "family";
|
|
1739
|
+
reason = "request";
|
|
1740
|
+
actorUserId = "";
|
|
1741
|
+
delegatingGuardianUserId = "";
|
|
1742
|
+
delegationId = "";
|
|
1743
|
+
requestKey = "";
|
|
1744
|
+
recordedAt = /* @__PURE__ */ new Date();
|
|
1745
|
+
constructor(options = {}) {
|
|
1746
|
+
super(options);
|
|
1747
|
+
adopt$3(this, options);
|
|
1748
|
+
}
|
|
1749
|
+
async delete() {
|
|
1750
|
+
noDelete$4();
|
|
1751
|
+
}
|
|
1752
|
+
async save(options) {
|
|
1753
|
+
await requireInsert$3(this);
|
|
1754
|
+
await requireOwned$2(this, "courses", this.courseId);
|
|
1755
|
+
requireText$3(this.actorUserId, "Visibility change requires an actor");
|
|
1756
|
+
requireText$3(this.requestKey, "Visibility change requires a request key", 300);
|
|
1757
|
+
const visibilities = COURSE_VISIBILITIES;
|
|
1758
|
+
if (!visibilities.includes(this.previousVisibility) || !visibilities.includes(this.visibility) || !["request", "licence"].includes(this.reason) || !Number.isSafeInteger(this.sequence) || this.sequence < 1) throw new Error("Invalid visibility change");
|
|
1759
|
+
validDate$3(this.recordedAt);
|
|
1760
|
+
this.requireInsertOnSave();
|
|
1761
|
+
return super.save(options);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
__publicField$7(CourseVisibilityChange, "__smrtPackage__", "@happyvertical/smrt-education");
|
|
1765
|
+
__decorateClass$7([tenantId()], CourseVisibilityChange.prototype, "tenantId", 2);
|
|
1766
|
+
__decorateClass$7([foreignKey("Course", { required: true })], CourseVisibilityChange.prototype, "courseId", 2);
|
|
1767
|
+
CourseVisibilityChange = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
|
|
1768
|
+
tableName: "course_visibility_changes",
|
|
1769
|
+
conflictColumns: [
|
|
1770
|
+
"tenant_id",
|
|
1771
|
+
"course_id",
|
|
1772
|
+
"request_key"
|
|
1773
|
+
],
|
|
1774
|
+
...closed$6
|
|
1775
|
+
})], CourseVisibilityChange);
|
|
1776
|
+
var CourseVisibilityChangeCollection = class extends SmrtCollection {
|
|
1777
|
+
static _itemClass = CourseVisibilityChange;
|
|
1778
|
+
};
|
|
1779
|
+
var CourseCoverChange = class extends SmrtObject {
|
|
1780
|
+
tenantId = "";
|
|
1781
|
+
courseId = "";
|
|
1782
|
+
sequence = 1;
|
|
1783
|
+
previousAssetId = "";
|
|
1784
|
+
assetId = "";
|
|
1785
|
+
actorUserId = "";
|
|
1786
|
+
delegatingGuardianUserId = "";
|
|
1787
|
+
delegationId = "";
|
|
1788
|
+
requestKey = "";
|
|
1789
|
+
recordedAt = /* @__PURE__ */ new Date();
|
|
1790
|
+
constructor(options = {}) {
|
|
1791
|
+
super(options);
|
|
1792
|
+
adopt$3(this, options);
|
|
1793
|
+
}
|
|
1794
|
+
async delete() {
|
|
1795
|
+
noDelete$4();
|
|
1796
|
+
}
|
|
1797
|
+
async save(options) {
|
|
1798
|
+
await requireInsert$3(this);
|
|
1799
|
+
await requireOwned$2(this, "courses", this.courseId);
|
|
1800
|
+
requireText$3(this.actorUserId, "Cover change requires an actor");
|
|
1801
|
+
requireText$3(this.requestKey, "Cover change requires a request key", 300);
|
|
1802
|
+
if (this.previousAssetId === this.assetId || !Number.isSafeInteger(this.sequence) || this.sequence < 1) throw new Error("Invalid cover change");
|
|
1803
|
+
validDate$3(this.recordedAt);
|
|
1804
|
+
this.requireInsertOnSave();
|
|
1805
|
+
return super.save(options);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
__publicField$7(CourseCoverChange, "__smrtPackage__", "@happyvertical/smrt-education");
|
|
1809
|
+
__decorateClass$7([tenantId()], CourseCoverChange.prototype, "tenantId", 2);
|
|
1810
|
+
__decorateClass$7([foreignKey("Course", { required: true })], CourseCoverChange.prototype, "courseId", 2);
|
|
1811
|
+
CourseCoverChange = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
|
|
1812
|
+
tableName: "course_cover_changes",
|
|
1813
|
+
conflictColumns: [
|
|
1814
|
+
"tenant_id",
|
|
1815
|
+
"course_id",
|
|
1816
|
+
"request_key"
|
|
1817
|
+
],
|
|
1818
|
+
...closed$6
|
|
1819
|
+
})], CourseCoverChange);
|
|
1820
|
+
var CourseCoverChangeCollection = class extends SmrtCollection {
|
|
1821
|
+
static _itemClass = CourseCoverChange;
|
|
1822
|
+
};
|
|
1693
1823
|
var CourseDiscardTombstone = class extends SmrtObject {
|
|
1694
1824
|
tenantId = "";
|
|
1695
1825
|
courseId = "";
|
|
@@ -2093,6 +2223,26 @@ function orderEffectiveLessons(selection, orderOf) {
|
|
|
2093
2223
|
});
|
|
2094
2224
|
}
|
|
2095
2225
|
//#endregion
|
|
2226
|
+
//#region src/course-visibility.ts
|
|
2227
|
+
function effectiveCourseVisibility(stored) {
|
|
2228
|
+
return COURSE_VISIBILITIES.includes(stored) ? stored : "family";
|
|
2229
|
+
}
|
|
2230
|
+
async function authoredVisibilityPermitsGroups(db, tenantId, courseId) {
|
|
2231
|
+
if (!await db.tableExists("course_releases")) return true;
|
|
2232
|
+
const release = await db.get("course_releases", {
|
|
2233
|
+
tenant_id: tenantId,
|
|
2234
|
+
release_course_id: courseId
|
|
2235
|
+
});
|
|
2236
|
+
if (!release) return true;
|
|
2237
|
+
const identity = await db.get("courses", {
|
|
2238
|
+
id: release.course_id,
|
|
2239
|
+
tenant_id: tenantId
|
|
2240
|
+
});
|
|
2241
|
+
if (!identity) return false;
|
|
2242
|
+
const stored = identity.visibility;
|
|
2243
|
+
return stored === void 0 || stored === null || stored === "" || stored === "groups" || stored === "public";
|
|
2244
|
+
}
|
|
2245
|
+
//#endregion
|
|
2096
2246
|
//#region src/group-sharing-hash.ts
|
|
2097
2247
|
function groupSharingDigest(value) {
|
|
2098
2248
|
const crypto = typeof process !== "undefined" && process.getBuiltinModule?.("node:crypto");
|
|
@@ -2243,6 +2393,12 @@ var bool = (value, what) => {
|
|
|
2243
2393
|
};
|
|
2244
2394
|
var iso$1 = (date) => new Date(date).toISOString();
|
|
2245
2395
|
var optional = (value) => value || null;
|
|
2396
|
+
var COVER_MIME_TYPES = [
|
|
2397
|
+
"image/png",
|
|
2398
|
+
"image/jpeg",
|
|
2399
|
+
"image/webp",
|
|
2400
|
+
"image/gif"
|
|
2401
|
+
];
|
|
2246
2402
|
var SKIP_REASON = "not-authorized";
|
|
2247
2403
|
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
|
|
2248
2404
|
var SKIP_CODE_PATTERN = /^[a-z0-9._-]{1,64}$/;
|
|
@@ -2669,7 +2825,7 @@ var CourseAuthoringService = class {
|
|
|
2669
2825
|
objectives: draft.objectives,
|
|
2670
2826
|
sections: input.sections
|
|
2671
2827
|
}));
|
|
2672
|
-
|
|
2828
|
+
const revision = await (await CourseRevisionCollection.create({ db: tx })).create({
|
|
2673
2829
|
_insertOnly: true,
|
|
2674
2830
|
tenantId: actor.tenantId,
|
|
2675
2831
|
courseId: input.courseId,
|
|
@@ -2693,6 +2849,8 @@ var CourseAuthoringService = class {
|
|
|
2693
2849
|
contentDigest,
|
|
2694
2850
|
recordedAt: /* @__PURE__ */ new Date()
|
|
2695
2851
|
});
|
|
2852
|
+
if (input.licenseKind === "personal-use-copy") await this.narrowForLicence(tx, actor, input.courseId, revision);
|
|
2853
|
+
return revision;
|
|
2696
2854
|
}
|
|
2697
2855
|
/**
|
|
2698
2856
|
* Appends a revision and moves the draft head by expected-head compare and
|
|
@@ -3052,6 +3210,8 @@ var CourseAuthoringService = class {
|
|
|
3052
3210
|
state,
|
|
3053
3211
|
licenseKind,
|
|
3054
3212
|
groupShareable: isGroupShareableLicense(licenseKind),
|
|
3213
|
+
visibility: effectiveCourseVisibility(course.visibility),
|
|
3214
|
+
coverAssetId: optional(course.cover_asset_id),
|
|
3055
3215
|
headRevisionId: head ? String(head.head_revision_id) : null,
|
|
3056
3216
|
releases
|
|
3057
3217
|
};
|
|
@@ -3064,6 +3224,7 @@ var CourseAuthoringService = class {
|
|
|
3064
3224
|
async listCourses(request) {
|
|
3065
3225
|
pageLimit(request.limit);
|
|
3066
3226
|
decodeCursor(request.cursor);
|
|
3227
|
+
if (request.visibility !== void 0 && !COURSE_VISIBILITIES.includes(request.visibility)) throw new CourseAuthoringValidationError("Invalid visibility");
|
|
3067
3228
|
const includeDiscarded = request.includeDiscarded === void 0 ? false : bool(request.includeDiscarded, "includeDiscarded");
|
|
3068
3229
|
return this.run(async (tx) => {
|
|
3069
3230
|
await this.authorize(tx, request, "education.course.read-draft", "list-courses");
|
|
@@ -3071,7 +3232,7 @@ var CourseAuthoringService = class {
|
|
|
3071
3232
|
const page = paginate((await tx.list("courses", {
|
|
3072
3233
|
tenant_id: request.tenantId,
|
|
3073
3234
|
source_key: AUTHORED_IDENTITY_SOURCE_KEY
|
|
3074
|
-
})).filter((row) => includeDiscarded || !discarded.has(String(row.id))), (row) => [millis(row.created_at), String(row.id)], request);
|
|
3235
|
+
})).filter((row) => includeDiscarded || !discarded.has(String(row.id))).filter((row) => request.visibility === void 0 || effectiveCourseVisibility(row.visibility) === request.visibility), (row) => [millis(row.created_at), String(row.id)], request);
|
|
3075
3236
|
const pageIds = page.rows.map((row) => String(row.id));
|
|
3076
3237
|
const byCourse = (rows, key) => {
|
|
3077
3238
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -3131,6 +3292,8 @@ var CourseAuthoringService = class {
|
|
|
3131
3292
|
sourceKey: "authored-release",
|
|
3132
3293
|
licenseKind
|
|
3133
3294
|
}),
|
|
3295
|
+
visibility: effectiveCourseVisibility(row.visibility),
|
|
3296
|
+
coverAssetId: optional(row.cover_asset_id),
|
|
3134
3297
|
releaseCount: releases.length,
|
|
3135
3298
|
head: head && revision ? {
|
|
3136
3299
|
revisionId: String(head.head_revision_id),
|
|
@@ -3142,6 +3305,7 @@ var CourseAuthoringService = class {
|
|
|
3142
3305
|
revisionId: String(latest.revision_id),
|
|
3143
3306
|
releaseNumber: Number(latest.release_number),
|
|
3144
3307
|
releaseCourseId: String(latest.release_course_id),
|
|
3308
|
+
coverAssetId: optional(lookup.releaseCourses.get(String(latest.release_course_id))?.cover_asset_id),
|
|
3145
3309
|
recordedAt: iso$1(latest.recorded_at)
|
|
3146
3310
|
} : null,
|
|
3147
3311
|
archive: {
|
|
@@ -3319,8 +3483,12 @@ var CourseAuthoringService = class {
|
|
|
3319
3483
|
const lessons = view.sections.flatMap((s) => s.lessons);
|
|
3320
3484
|
const personal = view.licenseKind === "personal-use-copy";
|
|
3321
3485
|
const modalities = new Set(lessons.map((l) => l.modality));
|
|
3486
|
+
const identity = await this.identityRow(tx, actor.tenantId, view.courseId);
|
|
3487
|
+
const cover = String(identity.cover_asset_id ?? "");
|
|
3488
|
+
const coverAssetId = cover && !await this.coverIneligibility(tx, actor.tenantId, view.courseId, cover) ? cover : "";
|
|
3322
3489
|
await (await AuthoredCourseCollection.create({ db: tx })).create({
|
|
3323
3490
|
id: input.releaseCourseId,
|
|
3491
|
+
coverAssetId,
|
|
3324
3492
|
tenantId: actor.tenantId,
|
|
3325
3493
|
title: view.title,
|
|
3326
3494
|
summary: view.summary,
|
|
@@ -3444,6 +3612,10 @@ var CourseAuthoringService = class {
|
|
|
3444
3612
|
revisionId: release.revisionId,
|
|
3445
3613
|
releaseNumber: release.releaseNumber,
|
|
3446
3614
|
releaseCourseId: release.releaseCourseId,
|
|
3615
|
+
coverAssetId: optional((await tx.get("courses", {
|
|
3616
|
+
id: release.releaseCourseId,
|
|
3617
|
+
tenant_id: tenantId
|
|
3618
|
+
}))?.cover_asset_id),
|
|
3447
3619
|
reviewId: optional(release.reviewId),
|
|
3448
3620
|
actorUserId: release.actorUserId,
|
|
3449
3621
|
delegatingGuardianUserId: optional(release.delegatingGuardianUserId),
|
|
@@ -3452,6 +3624,217 @@ var CourseAuthoringService = class {
|
|
|
3452
3624
|
lessons: lessons.sort((a, b) => a.lessonKey.localeCompare(b.lessonKey))
|
|
3453
3625
|
};
|
|
3454
3626
|
}
|
|
3627
|
+
/** Most restrictive of the head revision and every release's materialized course. */
|
|
3628
|
+
async effectiveLicence(tx, tenantId, courseId) {
|
|
3629
|
+
const head = await tx.get("course_draft_heads", {
|
|
3630
|
+
tenant_id: tenantId,
|
|
3631
|
+
course_id: courseId
|
|
3632
|
+
});
|
|
3633
|
+
const kinds = [(head ? await tx.get("course_revisions", {
|
|
3634
|
+
id: head.head_revision_id,
|
|
3635
|
+
tenant_id: tenantId
|
|
3636
|
+
}) : null)?.license_kind ?? "own-work"];
|
|
3637
|
+
for (const release of await tx.list("course_releases", {
|
|
3638
|
+
tenant_id: tenantId,
|
|
3639
|
+
course_id: courseId
|
|
3640
|
+
})) {
|
|
3641
|
+
const course = await tx.get("courses", {
|
|
3642
|
+
id: release.release_course_id,
|
|
3643
|
+
tenant_id: tenantId
|
|
3644
|
+
});
|
|
3645
|
+
kinds.push(course?.license_kind ?? "own-work");
|
|
3646
|
+
}
|
|
3647
|
+
return mostRestrictiveLicense(kinds);
|
|
3648
|
+
}
|
|
3649
|
+
async requireChangeTable(tx, table) {
|
|
3650
|
+
if (!await tx.tableExists(table)) throw new CourseAuthoringError("schema-unavailable", "Course visibility and cover require the latest education schema.");
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Sets who the course is meant for. `groups` is the precondition for sharing a
|
|
3654
|
+
* released course with the family's groups; `public` is a listing label only
|
|
3655
|
+
* and grants no read. A personal-use-copy course (any part, ever) can only be
|
|
3656
|
+
* `family`. Appends a change row (actor, previous, new, time); a request that
|
|
3657
|
+
* leaves the stored value unchanged writes nothing.
|
|
3658
|
+
*/
|
|
3659
|
+
async setVisibility(request) {
|
|
3660
|
+
const requestKey = requireKey$1(request.requestKey);
|
|
3661
|
+
if (!COURSE_VISIBILITIES.includes(request.visibility)) throw new CourseAuthoringValidationError("Invalid visibility");
|
|
3662
|
+
return this.run(async (tx) => {
|
|
3663
|
+
await this.authorize(tx, request, "education.course.set-visibility", "set-visibility", { courseId: request.courseId });
|
|
3664
|
+
await this.writableDraft(tx, request.tenantId, request.courseId);
|
|
3665
|
+
await this.requireChangeTable(tx, "course_visibility_changes");
|
|
3666
|
+
await this.lockHead(tx, request.tenantId, request.courseId);
|
|
3667
|
+
const changes = (await (await CourseVisibilityChangeCollection.create({ db: tx })).list({
|
|
3668
|
+
where: {
|
|
3669
|
+
tenantId: request.tenantId,
|
|
3670
|
+
courseId: request.courseId
|
|
3671
|
+
},
|
|
3672
|
+
limit: 1e5
|
|
3673
|
+
})).sort((a, b) => a.sequence - b.sequence);
|
|
3674
|
+
const replay = changes.find((c) => c.requestKey === requestKey);
|
|
3675
|
+
if (replay) {
|
|
3676
|
+
if (replay.visibility !== request.visibility || replay.reason !== "request" || replay.delegationId !== (request.delegationId ?? "")) throw new CourseRequestKeyConflictError();
|
|
3677
|
+
return this.courseState(tx, request.tenantId, request.courseId);
|
|
3678
|
+
}
|
|
3679
|
+
const identity = await this.identityRow(tx, request.tenantId, request.courseId);
|
|
3680
|
+
const stored = String(identity.visibility ?? "");
|
|
3681
|
+
if (stored === request.visibility) return this.courseState(tx, request.tenantId, request.courseId);
|
|
3682
|
+
if (request.visibility !== "family" && await this.effectiveLicence(tx, request.tenantId, request.courseId) === "personal-use-copy") throw new CourseVisibilityRefusedError(request.visibility);
|
|
3683
|
+
await (await CourseVisibilityChangeCollection.create({ db: tx })).create({
|
|
3684
|
+
_insertOnly: true,
|
|
3685
|
+
tenantId: request.tenantId,
|
|
3686
|
+
courseId: request.courseId,
|
|
3687
|
+
sequence: changes.length + 1,
|
|
3688
|
+
previousVisibility: effectiveCourseVisibility(stored),
|
|
3689
|
+
visibility: request.visibility,
|
|
3690
|
+
reason: "request",
|
|
3691
|
+
actorUserId: request.actorUserId,
|
|
3692
|
+
...delegationRecordFields(request),
|
|
3693
|
+
requestKey,
|
|
3694
|
+
recordedAt: /* @__PURE__ */ new Date()
|
|
3695
|
+
});
|
|
3696
|
+
await this.writeIdentity(tx, request.tenantId, request.courseId, { visibility: request.visibility });
|
|
3697
|
+
return this.courseState(tx, request.tenantId, request.courseId);
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
/** Narrows to family (with a change row) when a revision made the course personal-use. */
|
|
3701
|
+
async narrowForLicence(tx, actor, courseId, revision) {
|
|
3702
|
+
const identity = await this.identityRow(tx, actor.tenantId, courseId);
|
|
3703
|
+
const stored = String(identity.visibility ?? "");
|
|
3704
|
+
if (stored !== "groups" && stored !== "public") return;
|
|
3705
|
+
await this.requireChangeTable(tx, "course_visibility_changes");
|
|
3706
|
+
const changes = await tx.list("course_visibility_changes", {
|
|
3707
|
+
tenant_id: actor.tenantId,
|
|
3708
|
+
course_id: courseId
|
|
3709
|
+
});
|
|
3710
|
+
await (await CourseVisibilityChangeCollection.create({ db: tx })).create({
|
|
3711
|
+
_insertOnly: true,
|
|
3712
|
+
tenantId: actor.tenantId,
|
|
3713
|
+
courseId,
|
|
3714
|
+
sequence: changes.length + 1,
|
|
3715
|
+
previousVisibility: stored,
|
|
3716
|
+
visibility: "family",
|
|
3717
|
+
reason: "licence",
|
|
3718
|
+
actorUserId: actor.actorUserId,
|
|
3719
|
+
...delegationRecordFields(actor),
|
|
3720
|
+
requestKey: `licence:${String(revision.id)}`,
|
|
3721
|
+
recordedAt: /* @__PURE__ */ new Date()
|
|
3722
|
+
});
|
|
3723
|
+
await this.writeIdentity(tx, actor.tenantId, courseId, { visibility: "family" });
|
|
3724
|
+
}
|
|
3725
|
+
async writeIdentity(tx, tenantId, courseId, values) {
|
|
3726
|
+
const course = await (await AuthoredCourseCollection.create({ db: tx })).get({
|
|
3727
|
+
id: courseId,
|
|
3728
|
+
tenantId
|
|
3729
|
+
}, { cache: false });
|
|
3730
|
+
if (!course) throw new CourseNotFoundError();
|
|
3731
|
+
if (values.visibility !== void 0) course.visibility = values.visibility;
|
|
3732
|
+
if (values.coverAssetId !== void 0) course.coverAssetId = values.coverAssetId;
|
|
3733
|
+
await course.save();
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Why an asset cannot be this course's cover, or null. Education-owned means a
|
|
3737
|
+
* material this course copied into the tenant (`materialCopy`); it must be an
|
|
3738
|
+
* image and never an answer key anywhere in the tenant.
|
|
3739
|
+
*/
|
|
3740
|
+
async coverIneligibility(tx, tenantId, courseId, assetId) {
|
|
3741
|
+
const asset = await tx.get("assets", {
|
|
3742
|
+
id: assetId,
|
|
3743
|
+
tenant_id: tenantId
|
|
3744
|
+
});
|
|
3745
|
+
if (!asset) return "missing";
|
|
3746
|
+
if (!COVER_MIME_TYPES.includes(String(asset.mime_type ?? "").toLowerCase())) return "not-image";
|
|
3747
|
+
let known = false;
|
|
3748
|
+
for (const row of await tx.list("lesson_revisions", {
|
|
3749
|
+
tenant_id: tenantId,
|
|
3750
|
+
course_id: courseId
|
|
3751
|
+
})) for (const m of JSON.parse(String(row.materials_json))) if (m.assetId === assetId) {
|
|
3752
|
+
known = true;
|
|
3753
|
+
if (m.relationship === "answer-key") return "answer-key";
|
|
3754
|
+
}
|
|
3755
|
+
if (!known) return "not-course-material";
|
|
3756
|
+
if (await tx.get("lesson_assets", {
|
|
3757
|
+
tenant_id: tenantId,
|
|
3758
|
+
asset_id: assetId,
|
|
3759
|
+
relationship: "answer-key"
|
|
3760
|
+
})) return "answer-key";
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
/** Fails closed: the asset must be readable by this very actor through the asset guard. */
|
|
3764
|
+
async coverReadable(tx, actor, assetId) {
|
|
3765
|
+
try {
|
|
3766
|
+
const raw = await tx.get("assets", {
|
|
3767
|
+
id: assetId,
|
|
3768
|
+
tenant_id: actor.tenantId
|
|
3769
|
+
});
|
|
3770
|
+
if (!raw) return false;
|
|
3771
|
+
const read = async () => {
|
|
3772
|
+
const asset = await (await AssetCollection.create({ db: tx })).get(assetId, { cache: false });
|
|
3773
|
+
return Boolean(asset) && asset?.tenantId === actor.tenantId;
|
|
3774
|
+
};
|
|
3775
|
+
if (raw.access_mode !== "private") return await read();
|
|
3776
|
+
const user = await tx.get("users", { id: actor.actorUserId });
|
|
3777
|
+
const owner = raw.owner_profile_id ? String(raw.owner_profile_id) : "";
|
|
3778
|
+
if (!owner || !user || String(user.profile_id ?? "") !== owner) return false;
|
|
3779
|
+
return await withPrivateAssetAccess({
|
|
3780
|
+
tenantId: actor.tenantId,
|
|
3781
|
+
actorUserId: actor.actorUserId,
|
|
3782
|
+
ownerProfileId: owner,
|
|
3783
|
+
assetIds: [assetId]
|
|
3784
|
+
}, read);
|
|
3785
|
+
} catch {
|
|
3786
|
+
return false;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Sets (or, with `assetId: null`, clears) the course cover: an education-owned
|
|
3791
|
+
* image the actor can read, never an answer key. The next release snapshots it
|
|
3792
|
+
* onto the release row. Appends a change row; an unchanged cover writes nothing.
|
|
3793
|
+
*/
|
|
3794
|
+
async setCover(request) {
|
|
3795
|
+
const requestKey = requireKey$1(request.requestKey);
|
|
3796
|
+
if (request.assetId !== null && (typeof request.assetId !== "string" || !request.assetId || request.assetId.length > 200)) throw new CourseAuthoringValidationError("Invalid cover asset");
|
|
3797
|
+
const assetId = request.assetId ?? "";
|
|
3798
|
+
return this.run(async (tx) => {
|
|
3799
|
+
await this.authorize(tx, request, "education.course.set-cover", "set-cover", { courseId: request.courseId });
|
|
3800
|
+
await this.writableDraft(tx, request.tenantId, request.courseId);
|
|
3801
|
+
await this.requireChangeTable(tx, "course_cover_changes");
|
|
3802
|
+
await this.lockHead(tx, request.tenantId, request.courseId);
|
|
3803
|
+
const changes = (await (await CourseCoverChangeCollection.create({ db: tx })).list({
|
|
3804
|
+
where: {
|
|
3805
|
+
tenantId: request.tenantId,
|
|
3806
|
+
courseId: request.courseId
|
|
3807
|
+
},
|
|
3808
|
+
limit: 1e5
|
|
3809
|
+
})).sort((a, b) => a.sequence - b.sequence);
|
|
3810
|
+
const replay = changes.find((c) => c.requestKey === requestKey);
|
|
3811
|
+
if (replay) {
|
|
3812
|
+
if (replay.assetId !== assetId || replay.delegationId !== (request.delegationId ?? "")) throw new CourseRequestKeyConflictError();
|
|
3813
|
+
return this.courseState(tx, request.tenantId, request.courseId);
|
|
3814
|
+
}
|
|
3815
|
+
const identity = await this.identityRow(tx, request.tenantId, request.courseId);
|
|
3816
|
+
const current = String(identity.cover_asset_id ?? "");
|
|
3817
|
+
if (current === assetId) return this.courseState(tx, request.tenantId, request.courseId);
|
|
3818
|
+
if (assetId) {
|
|
3819
|
+
const reason = await this.coverIneligibility(tx, request.tenantId, request.courseId, assetId) ?? (await this.coverReadable(tx, request, assetId) ? null : "unreadable");
|
|
3820
|
+
if (reason) throw new CourseCoverInvalidError(reason);
|
|
3821
|
+
}
|
|
3822
|
+
await (await CourseCoverChangeCollection.create({ db: tx })).create({
|
|
3823
|
+
_insertOnly: true,
|
|
3824
|
+
tenantId: request.tenantId,
|
|
3825
|
+
courseId: request.courseId,
|
|
3826
|
+
sequence: changes.length + 1,
|
|
3827
|
+
previousAssetId: current,
|
|
3828
|
+
assetId,
|
|
3829
|
+
actorUserId: request.actorUserId,
|
|
3830
|
+
...delegationRecordFields(request),
|
|
3831
|
+
requestKey,
|
|
3832
|
+
recordedAt: /* @__PURE__ */ new Date()
|
|
3833
|
+
});
|
|
3834
|
+
await this.writeIdentity(tx, request.tenantId, request.courseId, { coverAssetId: assetId });
|
|
3835
|
+
return this.courseState(tx, request.tenantId, request.courseId);
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3455
3838
|
/** Reversible; never touches enrollments, progress, submissions, grades or notes. */
|
|
3456
3839
|
async archive(request) {
|
|
3457
3840
|
return this.setArchived(request, true);
|
|
@@ -29299,7 +29682,11 @@ var LessonProgressTransition = class extends SmrtObject {
|
|
|
29299
29682
|
learner_id: this.learnerId,
|
|
29300
29683
|
lesson_id: this.lessonId
|
|
29301
29684
|
})) throw new Error("Progress history requires its tenant progress");
|
|
29302
|
-
if (!PROGRESS_STATUSES.includes(this.fromStatus) || !PROGRESS_STATUSES.includes(this.toStatus) || ![
|
|
29685
|
+
if (!PROGRESS_STATUSES.includes(this.fromStatus) || !PROGRESS_STATUSES.includes(this.toStatus) || ![
|
|
29686
|
+
"put-back",
|
|
29687
|
+
"start",
|
|
29688
|
+
"complete"
|
|
29689
|
+
].includes(this.kind)) throw new Error("Invalid progress transition");
|
|
29303
29690
|
requireText$1(this.actorUserId, "Progress history requires an actor");
|
|
29304
29691
|
validDate$1(this.recordedAt);
|
|
29305
29692
|
this.requireInsertOnSave();
|
|
@@ -29369,6 +29756,111 @@ EnrollmentStatusChange = __decorateClass$3([TenantScoped({ mode: "required" }),
|
|
|
29369
29756
|
var EnrollmentStatusChangeCollection = class extends SmrtCollection {
|
|
29370
29757
|
static _itemClass = EnrollmentStatusChange;
|
|
29371
29758
|
};
|
|
29759
|
+
var LessonCompletionRecord = class extends SmrtObject {
|
|
29760
|
+
tenantId = "";
|
|
29761
|
+
progressId = "";
|
|
29762
|
+
enrollmentId = "";
|
|
29763
|
+
learnerId = "";
|
|
29764
|
+
lessonId = "";
|
|
29765
|
+
/** Progress status before completion. */
|
|
29766
|
+
fromStatus = "not_started";
|
|
29767
|
+
actorUserId = "";
|
|
29768
|
+
/** Host setting value that let this actor complete the lesson. */
|
|
29769
|
+
policyBasis = "";
|
|
29770
|
+
submissionId = null;
|
|
29771
|
+
recordedAt = /* @__PURE__ */ new Date();
|
|
29772
|
+
constructor(options = {}) {
|
|
29773
|
+
super(options);
|
|
29774
|
+
adopt$1(this, options);
|
|
29775
|
+
}
|
|
29776
|
+
async delete() {
|
|
29777
|
+
noDelete$1();
|
|
29778
|
+
}
|
|
29779
|
+
async save(options) {
|
|
29780
|
+
await requireInsert$1(this);
|
|
29781
|
+
if (!await this.db.get("lesson_progress", {
|
|
29782
|
+
id: this.progressId,
|
|
29783
|
+
tenant_id: this.tenantId,
|
|
29784
|
+
enrollment_id: this.enrollmentId,
|
|
29785
|
+
learner_id: this.learnerId,
|
|
29786
|
+
lesson_id: this.lessonId
|
|
29787
|
+
})) throw new Error("Completion history requires its tenant progress");
|
|
29788
|
+
if (this.submissionId !== null && !await this.db.get("submissions", {
|
|
29789
|
+
id: this.submissionId,
|
|
29790
|
+
tenant_id: this.tenantId,
|
|
29791
|
+
learner_id: this.learnerId,
|
|
29792
|
+
lesson_id: this.lessonId,
|
|
29793
|
+
progress_id: this.progressId
|
|
29794
|
+
})) throw new Error("Completion evidence must belong to the same progress");
|
|
29795
|
+
if (!PROGRESS_STATUSES.includes(this.fromStatus)) throw new Error("Invalid completion record");
|
|
29796
|
+
requireText$1(this.actorUserId, "Completion history requires an actor");
|
|
29797
|
+
requireText$1(this.policyBasis, "Completion history requires a policy basis");
|
|
29798
|
+
validDate$1(this.recordedAt);
|
|
29799
|
+
this.requireInsertOnSave();
|
|
29800
|
+
return super.save(options);
|
|
29801
|
+
}
|
|
29802
|
+
};
|
|
29803
|
+
__publicField$3(LessonCompletionRecord, "__smrtPackage__", "@happyvertical/smrt-education");
|
|
29804
|
+
__decorateClass$3([tenantId()], LessonCompletionRecord.prototype, "tenantId", 2);
|
|
29805
|
+
__decorateClass$3([foreignKey("LessonProgress", { required: true })], LessonCompletionRecord.prototype, "progressId", 2);
|
|
29806
|
+
__decorateClass$3([foreignKey("Enrollment", { required: true })], LessonCompletionRecord.prototype, "enrollmentId", 2);
|
|
29807
|
+
__decorateClass$3([foreignKey("Learner", { required: true })], LessonCompletionRecord.prototype, "learnerId", 2);
|
|
29808
|
+
__decorateClass$3([foreignKey("Lesson", { required: true })], LessonCompletionRecord.prototype, "lessonId", 2);
|
|
29809
|
+
__decorateClass$3([field({
|
|
29810
|
+
type: "text",
|
|
29811
|
+
nullable: true
|
|
29812
|
+
})], LessonCompletionRecord.prototype, "submissionId", 2);
|
|
29813
|
+
LessonCompletionRecord = __decorateClass$3([TenantScoped({ mode: "required" }), smrt({
|
|
29814
|
+
tableName: "lesson_completion_records",
|
|
29815
|
+
conflictColumns: ["tenant_id", "progress_id"],
|
|
29816
|
+
...closed$2
|
|
29817
|
+
})], LessonCompletionRecord);
|
|
29818
|
+
var LessonCompletionRecordCollection = class extends SmrtCollection {
|
|
29819
|
+
static _itemClass = LessonCompletionRecord;
|
|
29820
|
+
};
|
|
29821
|
+
var EnrollmentPaceChange = class extends SmrtObject {
|
|
29822
|
+
tenantId = "";
|
|
29823
|
+
enrollmentId = "";
|
|
29824
|
+
learnerId = "";
|
|
29825
|
+
previousLessonsPerWeek = 0;
|
|
29826
|
+
newLessonsPerWeek = 0;
|
|
29827
|
+
actorUserId = "";
|
|
29828
|
+
policyBasis = "";
|
|
29829
|
+
recordedAt = /* @__PURE__ */ new Date();
|
|
29830
|
+
constructor(options = {}) {
|
|
29831
|
+
super(options);
|
|
29832
|
+
adopt$1(this, options);
|
|
29833
|
+
}
|
|
29834
|
+
async delete() {
|
|
29835
|
+
noDelete$1();
|
|
29836
|
+
}
|
|
29837
|
+
async save(options) {
|
|
29838
|
+
await requireInsert$1(this);
|
|
29839
|
+
if (!await this.db.get("enrollments", {
|
|
29840
|
+
id: this.enrollmentId,
|
|
29841
|
+
tenant_id: this.tenantId,
|
|
29842
|
+
learner_id: this.learnerId
|
|
29843
|
+
})) throw new Error("Pace history requires its tenant enrollment");
|
|
29844
|
+
for (const value of [this.previousLessonsPerWeek, this.newLessonsPerWeek]) if (!Number.isSafeInteger(value) || value < 0 || value > 14) throw new Error("Invalid enrollment pace change");
|
|
29845
|
+
if (this.previousLessonsPerWeek === this.newLessonsPerWeek) throw new Error("Invalid enrollment pace change");
|
|
29846
|
+
requireText$1(this.actorUserId, "Pace history requires an actor");
|
|
29847
|
+
requireText$1(this.policyBasis, "Pace history requires a policy basis");
|
|
29848
|
+
validDate$1(this.recordedAt);
|
|
29849
|
+
this.requireInsertOnSave();
|
|
29850
|
+
return super.save(options);
|
|
29851
|
+
}
|
|
29852
|
+
};
|
|
29853
|
+
__publicField$3(EnrollmentPaceChange, "__smrtPackage__", "@happyvertical/smrt-education");
|
|
29854
|
+
__decorateClass$3([tenantId()], EnrollmentPaceChange.prototype, "tenantId", 2);
|
|
29855
|
+
__decorateClass$3([foreignKey("Enrollment", { required: true })], EnrollmentPaceChange.prototype, "enrollmentId", 2);
|
|
29856
|
+
__decorateClass$3([foreignKey("Learner", { required: true })], EnrollmentPaceChange.prototype, "learnerId", 2);
|
|
29857
|
+
EnrollmentPaceChange = __decorateClass$3([TenantScoped({ mode: "required" }), smrt({
|
|
29858
|
+
tableName: "enrollment_pace_changes",
|
|
29859
|
+
...closed$2
|
|
29860
|
+
})], EnrollmentPaceChange);
|
|
29861
|
+
var EnrollmentPaceChangeCollection = class extends SmrtCollection {
|
|
29862
|
+
static _itemClass = EnrollmentPaceChange;
|
|
29863
|
+
};
|
|
29372
29864
|
//#endregion
|
|
29373
29865
|
//#region src/workbook-completion.ts
|
|
29374
29866
|
var __defProp$2 = Object.defineProperty;
|
|
@@ -29578,6 +30070,32 @@ var EnrollmentStatusTransitionError = class extends LearnerValidationError {
|
|
|
29578
30070
|
from;
|
|
29579
30071
|
to;
|
|
29580
30072
|
};
|
|
30073
|
+
var EnrollmentNotActiveError = class extends LearnerValidationError {
|
|
30074
|
+
constructor(status) {
|
|
30075
|
+
super(`An enrollment that is ${status} cannot have lessons completed`);
|
|
30076
|
+
this.status = status;
|
|
30077
|
+
this.name = "EnrollmentNotActiveError";
|
|
30078
|
+
}
|
|
30079
|
+
status;
|
|
30080
|
+
};
|
|
30081
|
+
var LessonAlreadyCompleteError = class extends LearnerValidationError {
|
|
30082
|
+
constructor() {
|
|
30083
|
+
super("The lesson is already complete");
|
|
30084
|
+
this.name = "LessonAlreadyCompleteError";
|
|
30085
|
+
}
|
|
30086
|
+
};
|
|
30087
|
+
var WorkbookOwnedLessonError = class extends LearnerValidationError {
|
|
30088
|
+
constructor() {
|
|
30089
|
+
super("A workbook lesson is completed through the workbook workflow");
|
|
30090
|
+
this.name = "WorkbookOwnedLessonError";
|
|
30091
|
+
}
|
|
30092
|
+
};
|
|
30093
|
+
var LessonSkippedError = class extends LearnerValidationError {
|
|
30094
|
+
constructor() {
|
|
30095
|
+
super("A skipped lesson cannot be completed");
|
|
30096
|
+
this.name = "LessonSkippedError";
|
|
30097
|
+
}
|
|
30098
|
+
};
|
|
29581
30099
|
var LearnerHistoryUnavailableError = class extends Error {
|
|
29582
30100
|
constructor(table) {
|
|
29583
30101
|
super(`Education history table ${table} is not migrated`);
|
|
@@ -29586,6 +30104,15 @@ var LearnerHistoryUnavailableError = class extends Error {
|
|
|
29586
30104
|
}
|
|
29587
30105
|
table;
|
|
29588
30106
|
};
|
|
30107
|
+
var MIN_LESSONS_PER_WEEK = 1;
|
|
30108
|
+
var MAX_LESSONS_PER_WEEK = 14;
|
|
30109
|
+
var MAX_NEXT_LESSONS_COUNT = 14;
|
|
30110
|
+
var EnrollmentPaceError = class extends LearnerValidationError {
|
|
30111
|
+
constructor() {
|
|
30112
|
+
super(`Pace must be null or a whole number of lessons per week from 1 to 14`);
|
|
30113
|
+
this.name = "EnrollmentPaceError";
|
|
30114
|
+
}
|
|
30115
|
+
};
|
|
29589
30116
|
var POLICY_BASIS_CONTROL = /[\u0000-\u001f\u007f]/;
|
|
29590
30117
|
function requirePolicyBasis(value) {
|
|
29591
30118
|
if (typeof value !== "string" || !value.trim() || value.length > 200 || POLICY_BASIS_CONTROL.test(value)) throw new LearnerValidationError("A valid policy basis is required");
|
|
@@ -29605,7 +30132,8 @@ function enrollmentView(row) {
|
|
|
29605
30132
|
learnerId: row.learnerId,
|
|
29606
30133
|
curriculumId: row.curriculumId,
|
|
29607
30134
|
courseId: row.courseId,
|
|
29608
|
-
status: row.status
|
|
30135
|
+
status: row.status,
|
|
30136
|
+
pace: row.paceLessonsPerWeek ? { lessonsPerWeek: row.paceLessonsPerWeek } : null
|
|
29609
30137
|
};
|
|
29610
30138
|
}
|
|
29611
30139
|
function progressView(row) {
|
|
@@ -30218,10 +30746,13 @@ var LearnerService = class LearnerService {
|
|
|
30218
30746
|
})).map(enrollmentView);
|
|
30219
30747
|
}
|
|
30220
30748
|
/**
|
|
30221
|
-
* Projects the first unfinished lesson in each authorized enrollment
|
|
30749
|
+
* Projects the first unfinished lesson in each authorized enrollment, or up
|
|
30750
|
+
* to `count` (an integer from 1 to 14, default 1) in lesson order. No
|
|
30222
30751
|
* catalog assets, answer material, model handles, or source locators escape.
|
|
30223
30752
|
*/
|
|
30224
30753
|
async nextLessons(request) {
|
|
30754
|
+
const count = request.count === void 0 ? 1 : request.count;
|
|
30755
|
+
if (!Number.isInteger(count) || count < 1 || count > 14) throw new LearnerValidationError(`Count must be a whole number from 1 to 14`);
|
|
30225
30756
|
const enrollments = request.enrollmentId ? [await this.requireEnrollment(request, "education.learner.read")] : await (await EnrollmentCollection.create({ db: this.db })).list({
|
|
30226
30757
|
where: {
|
|
30227
30758
|
tenantId: request.tenantId,
|
|
@@ -30239,16 +30770,13 @@ var LearnerService = class LearnerService {
|
|
|
30239
30770
|
learnerId: request.learnerId
|
|
30240
30771
|
} });
|
|
30241
30772
|
const byLesson = new Map(await Promise.all(progresses.map(async (row) => [row.lessonId, await this.projectProgress(request, row)])));
|
|
30242
|
-
const
|
|
30243
|
-
const
|
|
30244
|
-
return !
|
|
30245
|
-
});
|
|
30246
|
-
|
|
30247
|
-
const { lesson, course } = next;
|
|
30248
|
-
const progress = byLesson.get(String(lesson.id));
|
|
30249
|
-
result.push({
|
|
30773
|
+
const unfinished = scope.entries.filter(({ lesson: candidate }) => {
|
|
30774
|
+
const progress = byLesson.get(String(candidate.id));
|
|
30775
|
+
return !progress || !["complete", "skipped"].includes(progress.status);
|
|
30776
|
+
}).slice(0, count);
|
|
30777
|
+
for (const { lesson, course } of unfinished) result.push({
|
|
30250
30778
|
enrollment: enrollmentView(enrollment),
|
|
30251
|
-
progress:
|
|
30779
|
+
progress: byLesson.get(String(lesson.id)) ?? null,
|
|
30252
30780
|
lesson: {
|
|
30253
30781
|
id: String(lesson.id),
|
|
30254
30782
|
title: lesson.title,
|
|
@@ -30425,6 +30953,152 @@ var LearnerService = class LearnerService {
|
|
|
30425
30953
|
};
|
|
30426
30954
|
});
|
|
30427
30955
|
}
|
|
30956
|
+
/**
|
|
30957
|
+
* The learner completes their own lesson of any kind under
|
|
30958
|
+
* `education.progress.complete-own`; the host decides per setting, for the
|
|
30959
|
+
* learner and for a guardian it also allows. Moves progress to `complete`,
|
|
30960
|
+
* sets `completedAt` and appends a `LessonCompletionRecord` with the actor,
|
|
30961
|
+
* `policyBasis` and optional `evidence.submissionId` (which must be a
|
|
30962
|
+
* submission of this tenant, learner and lesson). Counters and score are
|
|
30963
|
+
* untouched and no score is invented. Guardian `attestLesson` is unchanged.
|
|
30964
|
+
*
|
|
30965
|
+
* Repeat rule: when the lesson already completed through this path
|
|
30966
|
+
* (a completion record exists) the call returns `{ changed: false }`. A
|
|
30967
|
+
* lesson complete any other way (guardian attestation or grade, workbook
|
|
30968
|
+
* completion) throws `LessonAlreadyCompleteError`; `skipped` throws
|
|
30969
|
+
* `LessonSkippedError`; an enrollment that is not `active` throws
|
|
30970
|
+
* `EnrollmentNotActiveError`.
|
|
30971
|
+
*/
|
|
30972
|
+
async completeOwnLesson(request) {
|
|
30973
|
+
const policyBasis = requirePolicyBasis(request.policyBasis);
|
|
30974
|
+
const evidence = request.evidence;
|
|
30975
|
+
if (evidence !== void 0 && (typeof evidence !== "object" || evidence === null || typeof evidence.submissionId !== "string" || !evidence.submissionId.trim())) throw new LearnerValidationError("Evidence needs a submission id");
|
|
30976
|
+
return this.mutate(async (service) => {
|
|
30977
|
+
const enrollment = await service.requireEnrollment(request, "education.progress.complete-own");
|
|
30978
|
+
await service.lock("enrollments", request.enrollmentId, request.tenantId);
|
|
30979
|
+
if (enrollment.status !== "active") throw new EnrollmentNotActiveError(enrollment.status);
|
|
30980
|
+
if (!await service.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
|
|
30981
|
+
let row = await (await LessonProgressCollection.create({ db: service.db })).get({
|
|
30982
|
+
enrollmentId: String(enrollment.id),
|
|
30983
|
+
lessonId: request.lessonId,
|
|
30984
|
+
tenantId: request.tenantId
|
|
30985
|
+
}, { cache: false });
|
|
30986
|
+
if (row && row.learnerId !== enrollment.learnerId) throw new LearnerAuthorizationError();
|
|
30987
|
+
const hasTables = await service.db.tableExists("lesson_completion_records") === true;
|
|
30988
|
+
const projected = row ? await service.projectProgress(request, row) : null;
|
|
30989
|
+
if (row && (row.status === "complete" || projected?.status === "complete")) {
|
|
30990
|
+
if ((hasTables ? await service.db.get("lesson_completion_records", {
|
|
30991
|
+
tenant_id: request.tenantId,
|
|
30992
|
+
progress_id: String(row.id)
|
|
30993
|
+
}) : null) && row.status === "complete") return {
|
|
30994
|
+
changed: false,
|
|
30995
|
+
progress: projected
|
|
30996
|
+
};
|
|
30997
|
+
throw new LessonAlreadyCompleteError();
|
|
30998
|
+
}
|
|
30999
|
+
if (row?.status === "skipped") throw new LessonSkippedError();
|
|
31000
|
+
if (await service.findWorkbookActivity(request, {
|
|
31001
|
+
enrollmentId: String(enrollment.id),
|
|
31002
|
+
lessonId: request.lessonId
|
|
31003
|
+
})) throw new WorkbookOwnedLessonError();
|
|
31004
|
+
if (!hasTables) throw new LearnerHistoryUnavailableError("lesson_completion_records");
|
|
31005
|
+
if (evidence) {
|
|
31006
|
+
const submission = row ? await service.db.get("submissions", {
|
|
31007
|
+
id: evidence.submissionId,
|
|
31008
|
+
tenant_id: request.tenantId,
|
|
31009
|
+
learner_id: enrollment.learnerId,
|
|
31010
|
+
lesson_id: request.lessonId,
|
|
31011
|
+
progress_id: String(row.id)
|
|
31012
|
+
}) : null;
|
|
31013
|
+
if (!submission || submission.status === "rejected") throw new LearnerAuthorizationError();
|
|
31014
|
+
}
|
|
31015
|
+
const from = row?.status ?? "not_started";
|
|
31016
|
+
const now = /* @__PURE__ */ new Date();
|
|
31017
|
+
if (!row) row = await (await LessonProgressCollection.create({ db: service.db })).create({
|
|
31018
|
+
tenantId: request.tenantId,
|
|
31019
|
+
enrollmentId: String(enrollment.id),
|
|
31020
|
+
learnerId: enrollment.learnerId,
|
|
31021
|
+
lessonId: request.lessonId,
|
|
31022
|
+
status: "complete",
|
|
31023
|
+
completedAt: now
|
|
31024
|
+
});
|
|
31025
|
+
else {
|
|
31026
|
+
row.status = "complete";
|
|
31027
|
+
row.completedAt = now;
|
|
31028
|
+
await row.save();
|
|
31029
|
+
}
|
|
31030
|
+
await (await LessonCompletionRecordCollection.create({ db: service.db })).create({
|
|
31031
|
+
_insertOnly: true,
|
|
31032
|
+
tenantId: request.tenantId,
|
|
31033
|
+
progressId: String(row.id),
|
|
31034
|
+
enrollmentId: row.enrollmentId,
|
|
31035
|
+
learnerId: row.learnerId,
|
|
31036
|
+
lessonId: row.lessonId,
|
|
31037
|
+
fromStatus: from,
|
|
31038
|
+
actorUserId: request.actorUserId,
|
|
31039
|
+
policyBasis,
|
|
31040
|
+
submissionId: evidence?.submissionId ?? null,
|
|
31041
|
+
recordedAt: now
|
|
31042
|
+
});
|
|
31043
|
+
if (await service.db.tableExists("lesson_progress_transitions")) await service.recordProgressTransition(request, row, from, "complete");
|
|
31044
|
+
return {
|
|
31045
|
+
changed: true,
|
|
31046
|
+
progress: await service.projectProgress(request, row)
|
|
31047
|
+
};
|
|
31048
|
+
});
|
|
31049
|
+
}
|
|
31050
|
+
/**
|
|
31051
|
+
* Sets or clears (`pace: null`) an enrollment's planned lessons per week
|
|
31052
|
+
* (a whole number from 1 to 14) under `education.enrollment.set-pace`; the
|
|
31053
|
+
* host decides whether a learner may act on their own enrollment. Appends an
|
|
31054
|
+
* immutable `EnrollmentPaceChange` with the actor, `policyBasis` and the
|
|
31055
|
+
* previous and new value (`0` means no pace). An unchanged pace is an
|
|
31056
|
+
* idempotent `{ changed: false }` that records nothing; an invalid pace
|
|
31057
|
+
* throws `EnrollmentPaceError`. Status, progress and history are untouched.
|
|
31058
|
+
*/
|
|
31059
|
+
async setEnrollmentPace(request) {
|
|
31060
|
+
const policyBasis = requirePolicyBasis(request.policyBasis);
|
|
31061
|
+
const raw = request.pace;
|
|
31062
|
+
let lessonsPerWeek = 0;
|
|
31063
|
+
if (raw !== null) {
|
|
31064
|
+
const value = typeof raw === "object" && raw !== void 0 ? raw.lessonsPerWeek : void 0;
|
|
31065
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 14) throw new EnrollmentPaceError();
|
|
31066
|
+
lessonsPerWeek = value;
|
|
31067
|
+
}
|
|
31068
|
+
return this.mutate(async (service) => {
|
|
31069
|
+
await service.requireEnrollment(request, "education.enrollment.set-pace");
|
|
31070
|
+
await service.lock("enrollments", request.enrollmentId, request.tenantId);
|
|
31071
|
+
const enrollment = await (await EnrollmentCollection.create({ db: service.db })).get({
|
|
31072
|
+
id: request.enrollmentId,
|
|
31073
|
+
learnerId: request.learnerId,
|
|
31074
|
+
tenantId: request.tenantId
|
|
31075
|
+
}, { cache: false });
|
|
31076
|
+
if (!enrollment) throw new LearnerAuthorizationError();
|
|
31077
|
+
const previous = enrollment.paceLessonsPerWeek;
|
|
31078
|
+
if (previous === lessonsPerWeek) return {
|
|
31079
|
+
changed: false,
|
|
31080
|
+
enrollment: enrollmentView(enrollment)
|
|
31081
|
+
};
|
|
31082
|
+
if (!await service.db.tableExists("enrollment_pace_changes")) throw new LearnerHistoryUnavailableError("enrollment_pace_changes");
|
|
31083
|
+
enrollment.paceLessonsPerWeek = lessonsPerWeek;
|
|
31084
|
+
await enrollment.save();
|
|
31085
|
+
await (await EnrollmentPaceChangeCollection.create({ db: service.db })).create({
|
|
31086
|
+
_insertOnly: true,
|
|
31087
|
+
tenantId: request.tenantId,
|
|
31088
|
+
enrollmentId: String(enrollment.id),
|
|
31089
|
+
learnerId: enrollment.learnerId,
|
|
31090
|
+
previousLessonsPerWeek: previous,
|
|
31091
|
+
newLessonsPerWeek: lessonsPerWeek,
|
|
31092
|
+
actorUserId: request.actorUserId,
|
|
31093
|
+
policyBasis,
|
|
31094
|
+
recordedAt: /* @__PURE__ */ new Date()
|
|
31095
|
+
});
|
|
31096
|
+
return {
|
|
31097
|
+
changed: true,
|
|
31098
|
+
enrollment: enrollmentView(enrollment)
|
|
31099
|
+
};
|
|
31100
|
+
});
|
|
31101
|
+
}
|
|
30428
31102
|
};
|
|
30429
31103
|
//#endregion
|
|
30430
31104
|
//#region src/question-extraction-contracts.ts
|
|
@@ -34513,6 +35187,6 @@ var WorkbookPhotoService = class WorkbookPhotoService extends WorkbookService {
|
|
|
34513
35187
|
}
|
|
34514
35188
|
};
|
|
34515
35189
|
//#endregion
|
|
34516
|
-
export {
|
|
35190
|
+
export { EnrollmentPaceError as $, CourseArchiveDecisionCollection as $n, CatalogService as $r, FamilyIntakeAssetCollection as $t, normalizeQuestionExtraction as A, parseQuestionRubric as Ai, MAX_PRINTABLE_PAGES as An, mostRestrictiveLicense as Ar, WorkbookAttemptCollection as At, MAX_EXTRACTED_OMISSIONS as B, CourseSection as Bi, QUESTION_PROVENANCES as Bn, CourseMaterialCopyError as Br, WorkbookCompletionConflictError as Bt, ExtractedQuestionLink as C, ExternalCompletion as Ci, normalizeQuestionContent as Cn, LessonRevisionCollection as Cr, LessonProgressTransitionCollection as Ct, QUESTION_EXTRACTION_ARGS_SCHEMA as D, Submission as Di, LearningObjectiveKeyConflictError as Dn, AUTHORED_RELEASE_SOURCE_KEY as Dr, WorkbookAssessmentRevision as Dt, QuestionExtractionRecordCollection as E, ScannedSubmission as Ei, GENERATED_PRINTABLE_RELATIONSHIPS as En, AUTHORED_IDENTITY_SOURCE_KEY as Er, WorkbookActivityCollection as Et, EXTRACTED_ORIGINS as F, Learner as Fi, PrintableStorageCleanupError as Fn, CourseAuthoringError as Fr, WorkbookFeedbackReleaseCollection as Ft, MAX_EXTRACTED_TEXT as G, Video as Gi, groupSharingDigest as Gn, CourseStaleHeadError as Gr, summarizeWorkbookEvidence as Gt, MAX_EXTRACTED_QUESTIONS as H, ExternalCourse as Hi, QuestionNotFoundError as Hn, CoursePolicyBasisMismatchError as Hr, validateWorkbookAssessment as Ht, EXTRACTED_PAGE_ROLES as I, LessonProgress as Ii, PrintableStorageNotConfiguredError as In, CourseAuthoringValidationError as Ir, WorkbookWorkFiling as It, QuestionExtractionNotFoundError as J, resolveEnrollmentLessons as Jn, EnrollmentPinConflictError as Jr, GroupSharingPolicyValidationError as Jt, QUESTION_EXTRACTION_SOURCE_KINDS as K, Worksheet as Ki, authoredVisibilityPermitsGroups as Kn, CourseVisibilityRefusedError as Kr, GroupSharingAuthorizationError as Kt, EXTRACTED_QUESTION_STATUSES as L, AuthoredCourse as Li, QUESTION_COMPLETION_MODES as Ln, CourseCoverInvalidError as Lr, WorkbookWorkFilingCollection as Lt, EXTRACTED_LEGIBILITY as M, LessonAsset as Mi, MAX_PRINTABLE_TOTAL_TEXT as Mn, COURSE_PROVENANCES as Mr, WorkbookFeedbackDelivery as Mt, EXTRACTED_MEDIA_KINDS as N, Curriculum as Ni, MAX_RESOLVE_QUESTION_REVISIONS as Nn, CourseArchivedError as Nr, WorkbookFeedbackDeliveryCollection as Nt, extractedQuestionContent as O, SubmissionAsset as Oi, LearningObjectiveNotFoundError as On, isAuthoredReleaseGroupShareable as Or, WorkbookAssessmentRevisionCollection as Ot, EXTRACTED_OMISSION_REASONS as P, Enrollment as Pi, PrintableOwnerError as Pn, CourseAuthoringAuthorizationError as Pr, WorkbookFeedbackRelease as Pt, EnrollmentNotActiveError as Q, CourseArchiveDecision as Qn, CatalogAuthorizationError as Qr, FamilyIntakeAsset as Qt, EXTRACTED_READABILITY as R, COURSE_VISIBILITIES as Ri, QUESTION_FIELDS as Rn, CourseDiscardRefusedError as Rr, WORKBOOK_MATH_VOCABULARY as Rt, questionExtractionProposalDigest as S, Attestation as Si, QuestionRevisionCollection as Sn, LessonRevision as Sr, LessonProgressTransition as St, QuestionExtractionRecord as T, LessonSelfAttestation as Ti, GENERATED_PRINTABLE_AUDIENCES as Tn, ReleasedLessonCollection as Tr, WorkbookActivity as Tt, MAX_EXTRACTED_SECTIONS as U, Lesson as Ui, QuestionReferenceError as Un, CourseRequestKeyConflictError as Ur, workbookAggregateFeedback as Ut, MAX_EXTRACTED_PAGES as V, Exercise as Vi, QUESTION_RESPONSE_TYPES as Vn, CourseNotFoundError as Vr, aggregateWorkbookQuestions as Vt, MAX_EXTRACTED_STUDENT_WORK as W, Reading as Wi, WorksheetVersionMismatchError as Wn, CourseRevisionAlreadyReleasedError as Wr, evaluateGroupSharingPolicy as Wt, QuestionExtractionValidationError as X, delegationRecordFields as Xn, ReviewGateRequiredError as Xr, validateGroupSharingPolicy as Xt, QuestionExtractionPendingError as Y, delegationActorFields as Yn, MAX_PAGE_SIZE as Yr, canonicalGroupSharingPolicy as Yt, ENROLLMENT_STATUS_TRANSITIONS as Z, readDelegation as Zn, ReviewNotCurrentError as Zr, FamilyIntake as Zt, SELF_ATTESTABLE_LESSON_KINDS as _, SubmissionAssetCollection as _i, Question as _n, EnrollmentAssignmentRecordCollection as _r, EnrollmentPaceChangeCollection as _t, WorkbookContextRevisionCollection as a, EnrollmentCollection as ai, buildGuardianWorksheet as an, CourseDraftHeadCollection as ar, LessonAlreadyCompleteError as at, WorkbookWorkflowRequiredError as b, VideoCollection as bi, QuestionResponseCollection as bn, EnrollmentReleasePin as br, LessonCompletionRecord as bt, WorkbookPhotoDecision as c, ExternalCourseCollection as ci, worksheetVersionId as cn, CourseRevision as cr, MAX_LESSONS_PER_WEEK as ct, WorkbookPhotoIntakeCollection as d, LessonAssetCollection as di, GeneratedPrintable as dn, CourseUpgradeChoiceCollection as dr, WorkbookOwnedLessonError as dt, AttestationCollection as ei, FamilyIntakeCollection as en, CourseCoverChange as er, EnrollmentStatusTransitionError as et, validateWorkbookPhotoIdentification as f, LessonCollection as fi, GeneratedPrintableCollection as fn, CourseVisibilityChange as fr, enrollmentView as ft, OwnAttestationLessonKindError as g, ScannedSubmissionCollection as gi, LearningObjectiveRevisionCollection as gn, EnrollmentAssignmentRecord as gr, EnrollmentPaceChange as gt, OwnAttestationConflictError as h, ReadingCollection as hi, LearningObjectiveRevision as hn, CurriculumCreationRecordCollection as hr, WorkbookCompletionDecisionCollection as ht, WorkbookContextRevision as i, CurriculumCollection as ii, QuestionAuthoringService as in, CourseDraftHead as ir, LearnerValidationError as it, questionExtractionSourceIdentity as j, validateRubricResults as ji, MAX_PRINTABLE_TEXT_LENGTH as jn, COURSE_AUTHORING_ACTIONS as jr, WorkbookCollection as jt, matchExtractedQuestions as k, SubmissionIntakeBinding as ki, MAX_PRINTABLE_ITEMS as kn, isGroupShareableLicense as kr, WorkbookAttempt as kt, WorkbookPhotoDecisionCollection as l, GradeCollection as li, renderGuardianWorksheetPdf as ln, CourseRevisionCollection as lr, MAX_NEXT_LESSONS_COUNT as lt, normalizeWorkbookAnswerRegions as m, LessonSelfAttestationCollection as mi, LearningObjectiveCollection as mn, CurriculumCreationRecord as mr, WorkbookCompletionDecision as mt, preservePhotoProposal as n, CourseCollection as ni, FamilyIntakeDecisionCollection as nn, CourseDiscardTombstone as nr, LearnerHistoryUnavailableError as nt, WorkbookPhotoAsset as o, ExerciseCollection as oi, buildLearnerWorksheet as on, CourseRelease as or, LessonPutBackError as ot, normalizeWorkbookObservedLayout as p, LessonProgressCollection as pi, LearningObjective as pn, CourseVisibilityChangeCollection as pr, requirePolicyBasis as pt, QUESTION_EXTRACTION_VERSION as q, effectiveCourseVisibility as qn, DEFAULT_PAGE_SIZE as qr, GroupSharingConflictError as qt, WorkbookService as r, CourseSectionCollection as ri, familyIntakeScopeId as rn, CourseDiscardTombstoneCollection as rr, LearnerService as rt, WorkbookPhotoAssetCollection as s, ExternalCompletionCollection as si, canonical$2 as sn, CourseReleaseCollection as sr, LessonSkippedError as st, WorkbookPhotoService as t, AuthoredCourseCollection as ti, FamilyIntakeDecision as tn, CourseCoverChangeCollection as tr, LearnerAuthorizationError as tt, WorkbookPhotoIntake as u, LearnerCollection as ui, renderLearnerWorksheetPdf as un, CourseUpgradeChoice as ur, MIN_LESSONS_PER_WEEK as ut, SubmissionService as v, SubmissionCollection as vi, QuestionCollection as vn, EnrollmentAutoUpgradeSkip as vr, EnrollmentStatusChange as vt, ExtractedQuestionLinkCollection as w, Grade as wi, CourseAuthoringService as wn, ReleasedLesson as wr, Workbook as wt, QuestionExtractionService as x, WorksheetCollection as xi, QuestionRevision as xn, EnrollmentReleasePinCollection as xr, LessonCompletionRecordCollection as xt, SubmissionStorageCleanupError as y, SubmissionIntakeBindingCollection as yi, QuestionResponse as yn, EnrollmentAutoUpgradeSkipCollection as yr, EnrollmentStatusChangeCollection as yt, MAX_EXTRACTED_ANCHORS as z, Course as zi, QUESTION_PROMPT_SUMMARY_LENGTH as zn, CourseDiscardedError as zr, WorkbookAssessmentValidationError as zt };
|
|
34517
35191
|
|
|
34518
|
-
//# sourceMappingURL=workbook-photo-service-
|
|
35192
|
+
//# sourceMappingURL=workbook-photo-service-WwDDC5GF.js.map
|