@playcademy/sandbox 0.3.17-beta.33 → 0.3.17-beta.35
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/dist/cli.js +242 -182
- package/dist/server.js +242 -182
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1330,7 +1330,7 @@ var package_default;
|
|
|
1330
1330
|
var init_package = __esm(() => {
|
|
1331
1331
|
package_default = {
|
|
1332
1332
|
name: "@playcademy/sandbox",
|
|
1333
|
-
version: "0.3.17-beta.
|
|
1333
|
+
version: "0.3.17-beta.35",
|
|
1334
1334
|
description: "Local development server for Playcademy game development",
|
|
1335
1335
|
type: "module",
|
|
1336
1336
|
exports: {
|
|
@@ -30433,6 +30433,31 @@ function resolveAdminEventTime(data) {
|
|
|
30433
30433
|
}
|
|
30434
30434
|
return toAttributionEventTime(data.date);
|
|
30435
30435
|
}
|
|
30436
|
+
function validateMasteryAdjustment(delta, currentMastered, masterableUnits) {
|
|
30437
|
+
if (delta < 0 && currentMastered + delta < 0) {
|
|
30438
|
+
throw new ValidationError(`Adjustment would go below 0. Current: ${currentMastered}, adjustment: ${delta}`);
|
|
30439
|
+
}
|
|
30440
|
+
if (delta < 0 && typeof masterableUnits === "number" && masterableUnits > 0 && currentMastered > masterableUnits && currentMastered + delta > masterableUnits) {
|
|
30441
|
+
const minDelta = masterableUnits - currentMastered;
|
|
30442
|
+
throw new ValidationError(`Adjustment must reduce mastery to at most ${masterableUnits}. Current: ${currentMastered}/${masterableUnits}, minimum adjustment: ${minDelta}`);
|
|
30443
|
+
}
|
|
30444
|
+
if (delta > 0 && typeof masterableUnits === "number" && masterableUnits > 0) {
|
|
30445
|
+
if (currentMastered >= masterableUnits) {
|
|
30446
|
+
throw new ValidationError(`Mastery is already at maximum (${currentMastered}/${masterableUnits}). Only negative adjustments are allowed.`);
|
|
30447
|
+
}
|
|
30448
|
+
if (currentMastered + delta > masterableUnits) {
|
|
30449
|
+
const remaining = masterableUnits - currentMastered;
|
|
30450
|
+
throw new ValidationError(`Adjustment would exceed maximum. Current: ${currentMastered}/${masterableUnits}, max adjustment: +${remaining}`);
|
|
30451
|
+
}
|
|
30452
|
+
}
|
|
30453
|
+
}
|
|
30454
|
+
function compareEnrollmentsByRecency(a, b) {
|
|
30455
|
+
const dateCompare = (b.beginDate ?? "").localeCompare(a.beginDate ?? "");
|
|
30456
|
+
if (dateCompare !== 0) {
|
|
30457
|
+
return dateCompare;
|
|
30458
|
+
}
|
|
30459
|
+
return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
|
|
30460
|
+
}
|
|
30436
30461
|
var init_timeback_admin_util = __esm(() => {
|
|
30437
30462
|
init_errors();
|
|
30438
30463
|
});
|
|
@@ -30785,17 +30810,6 @@ class TimebackAdminService {
|
|
|
30785
30810
|
}
|
|
30786
30811
|
return this.deps.timeback;
|
|
30787
30812
|
}
|
|
30788
|
-
async recordCourseCompletionHistory(client, data) {
|
|
30789
|
-
await client.recordAdminCourseCompletionChange(data).catch((error) => {
|
|
30790
|
-
logger16.error("Failed to record admin course completion history event", {
|
|
30791
|
-
gameId: data.gameId,
|
|
30792
|
-
courseId: data.courseId,
|
|
30793
|
-
studentId: data.studentId,
|
|
30794
|
-
action: data.action,
|
|
30795
|
-
error: error instanceof Error ? error.message : String(error)
|
|
30796
|
-
});
|
|
30797
|
-
});
|
|
30798
|
-
}
|
|
30799
30813
|
async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
|
|
30800
30814
|
const client = this.requireClient();
|
|
30801
30815
|
if (accessLevel === "dashboard") {
|
|
@@ -30894,7 +30908,7 @@ class TimebackAdminService {
|
|
|
30894
30908
|
if (typeof masterableUnits !== "number" || masterableUnits <= 0) {
|
|
30895
30909
|
return;
|
|
30896
30910
|
}
|
|
30897
|
-
return Math.
|
|
30911
|
+
return Math.round(masteredUnits / masterableUnits * 100);
|
|
30898
30912
|
}
|
|
30899
30913
|
async getMasterableUnits(courseId) {
|
|
30900
30914
|
const client = this.requireClient();
|
|
@@ -30947,6 +30961,7 @@ class TimebackAdminService {
|
|
|
30947
30961
|
}
|
|
30948
30962
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
30949
30963
|
const enrollments = new Map;
|
|
30964
|
+
const allEnrollments = new Map;
|
|
30950
30965
|
const entries = await Promise.all(courseIds.map(async (courseId) => {
|
|
30951
30966
|
const roster = await client.oneroster.enrollments.listByCourse(courseId, {
|
|
30952
30967
|
includeInactive: options?.includeInactive,
|
|
@@ -30958,28 +30973,30 @@ class TimebackAdminService {
|
|
|
30958
30973
|
if (aActive !== bActive) {
|
|
30959
30974
|
return aActive ? -1 : 1;
|
|
30960
30975
|
}
|
|
30961
|
-
return (
|
|
30976
|
+
return compareEnrollmentsByRecency(a.enrollment, b.enrollment);
|
|
30962
30977
|
});
|
|
30963
|
-
return { courseId,
|
|
30978
|
+
return { courseId, matches };
|
|
30964
30979
|
}));
|
|
30965
|
-
for (const { courseId,
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
|
|
30969
|
-
|
|
30970
|
-
|
|
30971
|
-
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
30975
|
-
|
|
30976
|
-
|
|
30977
|
-
|
|
30978
|
-
|
|
30979
|
-
|
|
30980
|
+
for (const { courseId, matches } of entries) {
|
|
30981
|
+
const records = matches.map((match) => ({
|
|
30982
|
+
id: match.enrollment.sourcedId,
|
|
30983
|
+
status: match.enrollment.status ?? "active",
|
|
30984
|
+
role: match.enrollment.role ?? "student",
|
|
30985
|
+
beginDate: match.enrollment.beginDate ?? null,
|
|
30986
|
+
endDate: match.enrollment.endDate ?? null,
|
|
30987
|
+
course: {
|
|
30988
|
+
id: courseId,
|
|
30989
|
+
title: match.class?.title ?? "",
|
|
30990
|
+
subjects: null,
|
|
30991
|
+
grades: null
|
|
30992
|
+
}
|
|
30993
|
+
}));
|
|
30994
|
+
if (records.length > 0) {
|
|
30995
|
+
enrollments.set(courseId, records[0]);
|
|
30980
30996
|
}
|
|
30997
|
+
allEnrollments.set(courseId, records);
|
|
30981
30998
|
}
|
|
30982
|
-
return { enrollments };
|
|
30999
|
+
return { enrollments, allEnrollments };
|
|
30983
31000
|
}
|
|
30984
31001
|
async assertStudentEnrolledInCourse(client, studentId, courseId) {
|
|
30985
31002
|
const enrollments = await client.edubridge.enrollments.listByUser(studentId);
|
|
@@ -31066,7 +31083,7 @@ class TimebackAdminService {
|
|
|
31066
31083
|
const enrollmentId = rosterEntry.enrollment.sourcedId || null;
|
|
31067
31084
|
const summary = enrollmentId ? analyticsByEnrollmentId.get(enrollmentId) : undefined;
|
|
31068
31085
|
const analyticsUnavailable = Boolean(enrollmentId) && summary?.analyticsAvailable !== true;
|
|
31069
|
-
const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() :
|
|
31086
|
+
const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() || "No name specified" : "No name specified";
|
|
31070
31087
|
const inactive = rosterEntry.enrollment.status === "tobedeleted";
|
|
31071
31088
|
return {
|
|
31072
31089
|
studentId: rosterEntry.enrollment.user.sourcedId,
|
|
@@ -31111,14 +31128,15 @@ class TimebackAdminService {
|
|
|
31111
31128
|
throw new NotFoundError("Timeback integration", gameId);
|
|
31112
31129
|
}
|
|
31113
31130
|
const courseIds = new Set(integrations.map((integration) => integration.courseId));
|
|
31114
|
-
const { enrollments: enrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
|
|
31131
|
+
const { enrollments: enrollmentsByCourseId, allEnrollments: allEnrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
|
|
31115
31132
|
includeInactive: true
|
|
31116
31133
|
});
|
|
31117
31134
|
if (enrollmentsByCourseId.size === 0) {
|
|
31118
31135
|
throw new NotFoundError("Student enrollment", courseId ? `${studentId}:${courseId}` : `${studentId}:${gameId}`);
|
|
31119
31136
|
}
|
|
31137
|
+
const allEnrollmentIds = [...allEnrollmentsByCourseId.values()].flat().map((enrollment) => enrollment.id);
|
|
31120
31138
|
const studentProfile = await client.oneroster.users.get(studentId);
|
|
31121
|
-
const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries(
|
|
31139
|
+
const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries(allEnrollmentIds);
|
|
31122
31140
|
const [masterableUnitsByCourse, completionStatusByCourse] = await Promise.all([
|
|
31123
31141
|
this.getMasterableUnitsByCourse(integrations.map((integration) => integration.courseId)),
|
|
31124
31142
|
this.getCompletionStatusByCourse(client, integrations.map((integration) => integration.courseId), studentId)
|
|
@@ -31129,6 +31147,24 @@ class TimebackAdminService {
|
|
|
31129
31147
|
const masterableUnits = masterableUnitsByCourse.get(integration.courseId);
|
|
31130
31148
|
const analyticsUnavailable = Boolean(enrollment?.id) && summary?.analyticsAvailable !== true;
|
|
31131
31149
|
const inactive = enrollment?.status === "tobedeleted";
|
|
31150
|
+
const courseEnrollments = allEnrollmentsByCourseId.get(integration.courseId) ?? [];
|
|
31151
|
+
const enrollmentSummaries = courseEnrollments.length > 1 ? courseEnrollments.map((record) => {
|
|
31152
|
+
const recordSummary = analyticsByEnrollmentId.get(record.id);
|
|
31153
|
+
const recordAnalyticsUnavailable = recordSummary?.analyticsAvailable !== true;
|
|
31154
|
+
return {
|
|
31155
|
+
enrollmentId: record.id,
|
|
31156
|
+
status: record.status === "tobedeleted" ? "tobedeleted" : "active",
|
|
31157
|
+
beginDate: record.beginDate,
|
|
31158
|
+
endDate: record.endDate,
|
|
31159
|
+
analyticsUnavailable: recordAnalyticsUnavailable,
|
|
31160
|
+
totalXp: recordSummary?.totalXp ?? 0,
|
|
31161
|
+
todayXp: recordSummary?.todayXp ?? 0,
|
|
31162
|
+
activeTimeSeconds: recordSummary?.activeTimeSeconds ?? 0,
|
|
31163
|
+
masteredUnits: recordSummary?.masteredUnits ?? 0,
|
|
31164
|
+
pctCompleteApp: TimebackAdminService.computeCompletionPct(recordSummary?.masteredUnits ?? 0, masterableUnits),
|
|
31165
|
+
history: recordSummary?.history ?? []
|
|
31166
|
+
};
|
|
31167
|
+
}) : undefined;
|
|
31132
31168
|
return {
|
|
31133
31169
|
courseId: integration.courseId,
|
|
31134
31170
|
title: enrollment?.course.title || `${integration.subject} Grade ${integration.grade}`,
|
|
@@ -31144,9 +31180,11 @@ class TimebackAdminService {
|
|
|
31144
31180
|
pctCompleteApp: TimebackAdminService.computeCompletionPct(summary?.masteredUnits ?? 0, masterableUnits),
|
|
31145
31181
|
completionStatus: completionStatusByCourse.get(integration.courseId) ?? "none",
|
|
31146
31182
|
history: summary?.history ?? [],
|
|
31147
|
-
...inactive ? { inactive } : {}
|
|
31183
|
+
...inactive ? { inactive } : {},
|
|
31184
|
+
...enrollmentSummaries ? { enrollments: enrollmentSummaries } : {}
|
|
31148
31185
|
};
|
|
31149
31186
|
});
|
|
31187
|
+
courses.sort((a, b) => a.grade - b.grade);
|
|
31150
31188
|
return {
|
|
31151
31189
|
student: {
|
|
31152
31190
|
studentId,
|
|
@@ -31246,111 +31284,87 @@ class TimebackAdminService {
|
|
|
31246
31284
|
return { status: "ok" };
|
|
31247
31285
|
}
|
|
31248
31286
|
async adjustMasteredUnits(data, user) {
|
|
31249
|
-
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user
|
|
31287
|
+
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user);
|
|
31288
|
+
let currentMastered = 0;
|
|
31289
|
+
const masterableUnits = await this.getMasterableUnits(data.courseId);
|
|
31290
|
+
if (data.units !== 0) {
|
|
31291
|
+
const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
|
|
31292
|
+
try {
|
|
31293
|
+
const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
|
|
31294
|
+
currentMastered = this.summarizeAnalyticsFacts(analytics.facts).masteredUnits;
|
|
31295
|
+
} catch {
|
|
31296
|
+
throw new ValidationError("Unable to validate mastery bounds — analytics unavailable. Please retry.");
|
|
31297
|
+
}
|
|
31298
|
+
validateMasteryAdjustment(data.units, currentMastered, masterableUnits);
|
|
31299
|
+
}
|
|
31300
|
+
const pctCompleteApp = typeof masterableUnits === "number" && masterableUnits > 0 ? Math.min(100, Math.max(0, Math.round((currentMastered + data.units) / masterableUnits * 100))) : undefined;
|
|
31250
31301
|
await client.recordAdminMasteryAdjustment({
|
|
31251
31302
|
gameId: data.gameId,
|
|
31252
31303
|
courseId: data.courseId,
|
|
31253
31304
|
studentId: data.studentId,
|
|
31254
31305
|
masteredUnits: data.units,
|
|
31306
|
+
pctCompleteApp,
|
|
31255
31307
|
eventTime: resolveAdminEventTime(data),
|
|
31256
31308
|
reason: data.reason,
|
|
31257
31309
|
actor,
|
|
31258
31310
|
appName,
|
|
31259
31311
|
sensorUrl
|
|
31260
31312
|
});
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
});
|
|
31276
|
-
if (data.action === "complete") {
|
|
31277
|
-
const masterableUnits = await this.getMasterableUnits(data.courseId);
|
|
31278
|
-
if (masterableUnits && masterableUnits > 0) {
|
|
31279
|
-
const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
|
|
31280
|
-
let currentMastered = 0;
|
|
31281
|
-
try {
|
|
31282
|
-
const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
|
|
31283
|
-
const summary = this.summarizeAnalyticsFacts(analytics.facts);
|
|
31284
|
-
currentMastered = summary.masteredUnits;
|
|
31285
|
-
} catch {
|
|
31286
|
-
logger16.warn("Failed to load analytics for mastery gap calculation", {
|
|
31287
|
-
studentId: data.studentId,
|
|
31288
|
-
courseId: data.courseId
|
|
31313
|
+
if (typeof masterableUnits === "number" && masterableUnits > 0) {
|
|
31314
|
+
const wasMastered = currentMastered >= masterableUnits;
|
|
31315
|
+
const willBeMastered = currentMastered + data.units >= masterableUnits;
|
|
31316
|
+
if (wasMastered !== willBeMastered) {
|
|
31317
|
+
const ids = deriveSourcedIds(data.courseId);
|
|
31318
|
+
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
31319
|
+
const resultId = `${lineItemId}:${data.studentId}:completion`;
|
|
31320
|
+
if (willBeMastered) {
|
|
31321
|
+
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
31322
|
+
sourcedId: lineItemId,
|
|
31323
|
+
title: "Mastery Completion",
|
|
31324
|
+
status: ONEROSTER_STATUS.active,
|
|
31325
|
+
course: { sourcedId: ids.course },
|
|
31326
|
+
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
31289
31327
|
});
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
|
|
31297
|
-
|
|
31298
|
-
|
|
31299
|
-
|
|
31300
|
-
|
|
31301
|
-
|
|
31328
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31329
|
+
sourcedId: resultId,
|
|
31330
|
+
status: ONEROSTER_STATUS.active,
|
|
31331
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31332
|
+
student: { sourcedId: data.studentId },
|
|
31333
|
+
score: 100,
|
|
31334
|
+
scoreDate: new Date().toISOString(),
|
|
31335
|
+
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31336
|
+
inProgress: "false",
|
|
31337
|
+
metadata: {
|
|
31338
|
+
isMasteryCompletion: true,
|
|
31339
|
+
adminAction: true,
|
|
31340
|
+
appName
|
|
31341
|
+
}
|
|
31302
31342
|
});
|
|
31343
|
+
} else {
|
|
31344
|
+
try {
|
|
31345
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31346
|
+
sourcedId: resultId,
|
|
31347
|
+
status: ONEROSTER_STATUS.active,
|
|
31348
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31349
|
+
student: { sourcedId: data.studentId },
|
|
31350
|
+
score: 0,
|
|
31351
|
+
scoreDate: new Date().toISOString(),
|
|
31352
|
+
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31353
|
+
inProgress: "true",
|
|
31354
|
+
metadata: {
|
|
31355
|
+
isMasteryCompletion: true,
|
|
31356
|
+
adminAction: true,
|
|
31357
|
+
appName
|
|
31358
|
+
}
|
|
31359
|
+
});
|
|
31360
|
+
} catch {
|
|
31361
|
+
logger16.debug("No completion entry to revoke", {
|
|
31362
|
+
studentId: data.studentId,
|
|
31363
|
+
courseId: data.courseId
|
|
31364
|
+
});
|
|
31365
|
+
}
|
|
31303
31366
|
}
|
|
31304
31367
|
}
|
|
31305
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31306
|
-
sourcedId: resultId,
|
|
31307
|
-
status: ONEROSTER_STATUS.active,
|
|
31308
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
31309
|
-
student: { sourcedId: data.studentId },
|
|
31310
|
-
score: 100,
|
|
31311
|
-
scoreDate: new Date().toISOString(),
|
|
31312
|
-
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31313
|
-
inProgress: "false",
|
|
31314
|
-
metadata: {
|
|
31315
|
-
isMasteryCompletion: true,
|
|
31316
|
-
adminAction: true,
|
|
31317
|
-
appName
|
|
31318
|
-
}
|
|
31319
|
-
});
|
|
31320
|
-
await this.recordCourseCompletionHistory(historyClient, {
|
|
31321
|
-
gameId: data.gameId,
|
|
31322
|
-
courseId: data.courseId,
|
|
31323
|
-
studentId: data.studentId,
|
|
31324
|
-
action: "complete",
|
|
31325
|
-
actor,
|
|
31326
|
-
appName,
|
|
31327
|
-
sensorUrl
|
|
31328
|
-
});
|
|
31329
|
-
} else {
|
|
31330
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31331
|
-
sourcedId: resultId,
|
|
31332
|
-
status: ONEROSTER_STATUS.active,
|
|
31333
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
31334
|
-
student: { sourcedId: data.studentId },
|
|
31335
|
-
score: 0,
|
|
31336
|
-
scoreDate: new Date().toISOString(),
|
|
31337
|
-
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31338
|
-
inProgress: "true",
|
|
31339
|
-
metadata: {
|
|
31340
|
-
isMasteryCompletion: true,
|
|
31341
|
-
adminAction: true,
|
|
31342
|
-
appName
|
|
31343
|
-
}
|
|
31344
|
-
});
|
|
31345
|
-
await this.recordCourseCompletionHistory(historyClient, {
|
|
31346
|
-
gameId: data.gameId,
|
|
31347
|
-
courseId: data.courseId,
|
|
31348
|
-
studentId: data.studentId,
|
|
31349
|
-
action: "resume",
|
|
31350
|
-
actor,
|
|
31351
|
-
appName,
|
|
31352
|
-
sensorUrl
|
|
31353
|
-
});
|
|
31354
31368
|
}
|
|
31355
31369
|
return { status: "ok" };
|
|
31356
31370
|
}
|
|
@@ -31386,17 +31400,34 @@ class TimebackAdminService {
|
|
|
31386
31400
|
});
|
|
31387
31401
|
return { students: [] };
|
|
31388
31402
|
}
|
|
31389
|
-
const
|
|
31403
|
+
const fullRoster = await client.oneroster.enrollments.listByCourse(courseId, {
|
|
31390
31404
|
role: "student",
|
|
31405
|
+
includeInactive: true,
|
|
31391
31406
|
includeUsers: false
|
|
31392
31407
|
});
|
|
31393
|
-
const enrolledStudentIds = new Set(
|
|
31394
|
-
const
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
|
|
31399
|
-
|
|
31408
|
+
const enrolledStudentIds = new Set(fullRoster.filter((entry) => entry.enrollment.status === "active").map((entry) => entry.enrollment.user.sourcedId));
|
|
31409
|
+
const pastEnrollmentsByStudent = new Map;
|
|
31410
|
+
const inactiveEntries = fullRoster.filter((entry) => entry.enrollment.status === "tobedeleted").toSorted((a, b) => compareEnrollmentsByRecency(a.enrollment, b.enrollment));
|
|
31411
|
+
for (const entry of inactiveEntries) {
|
|
31412
|
+
const studentId = entry.enrollment.user.sourcedId;
|
|
31413
|
+
const list = pastEnrollmentsByStudent.get(studentId) ?? [];
|
|
31414
|
+
list.push({
|
|
31415
|
+
enrollmentId: entry.enrollment.sourcedId,
|
|
31416
|
+
beginDate: entry.enrollment.beginDate ?? null,
|
|
31417
|
+
endDate: entry.enrollment.endDate ?? null
|
|
31418
|
+
});
|
|
31419
|
+
pastEnrollmentsByStudent.set(studentId, list);
|
|
31420
|
+
}
|
|
31421
|
+
const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => {
|
|
31422
|
+
const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
|
|
31423
|
+
return {
|
|
31424
|
+
studentId: entry.sourcedId,
|
|
31425
|
+
name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
|
|
31426
|
+
email: entry.email || null,
|
|
31427
|
+
alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
|
|
31428
|
+
...past.length > 0 ? { pastEnrollments: past } : {}
|
|
31429
|
+
};
|
|
31430
|
+
});
|
|
31400
31431
|
return { students };
|
|
31401
31432
|
}
|
|
31402
31433
|
async enrollStudent(data, user) {
|
|
@@ -31427,6 +31458,48 @@ class TimebackAdminService {
|
|
|
31427
31458
|
client.invalidateEnrollments(data.studentId);
|
|
31428
31459
|
return { status: "ok" };
|
|
31429
31460
|
}
|
|
31461
|
+
async reactivateEnrollment(data, user) {
|
|
31462
|
+
const client = this.requireClient();
|
|
31463
|
+
await this.deps.validateGameManagementAccess(user, data.gameId);
|
|
31464
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
31465
|
+
where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
|
|
31466
|
+
});
|
|
31467
|
+
if (!integration) {
|
|
31468
|
+
throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
|
|
31469
|
+
}
|
|
31470
|
+
const enrollment = await client.oneroster.enrollments.get(data.enrollmentId);
|
|
31471
|
+
if (!enrollment) {
|
|
31472
|
+
throw new NotFoundError("Enrollment", data.enrollmentId);
|
|
31473
|
+
}
|
|
31474
|
+
if (enrollment.user.sourcedId !== data.studentId) {
|
|
31475
|
+
throw new ValidationError("Enrollment does not belong to the specified student");
|
|
31476
|
+
}
|
|
31477
|
+
if (enrollment.status === "active") {
|
|
31478
|
+
throw new ValidationError("Enrollment is already active");
|
|
31479
|
+
}
|
|
31480
|
+
const { allEnrollments } = await this.getStudentEnrollmentsByCourseId(client, data.studentId, [data.courseId], { includeInactive: true });
|
|
31481
|
+
const courseEnrollmentIds = new Set((allEnrollments.get(data.courseId) ?? []).map((e) => e.id));
|
|
31482
|
+
if (!courseEnrollmentIds.has(data.enrollmentId)) {
|
|
31483
|
+
throw new ValidationError("Enrollment does not belong to the specified course");
|
|
31484
|
+
}
|
|
31485
|
+
const activeEnrollments = await client.edubridge.enrollments.listByUser(data.studentId);
|
|
31486
|
+
if (activeEnrollments.some((e) => e.course.id === data.courseId)) {
|
|
31487
|
+
throw new ValidationError("Student already has an active enrollment for this course. Unenroll from the current enrollment before reactivating a past one.");
|
|
31488
|
+
}
|
|
31489
|
+
await client.oneroster.enrollments.update(data.enrollmentId, {
|
|
31490
|
+
role: enrollment.role,
|
|
31491
|
+
primary: enrollment.primary,
|
|
31492
|
+
beginDate: enrollment.beginDate,
|
|
31493
|
+
endDate: enrollment.endDate,
|
|
31494
|
+
user: enrollment.user,
|
|
31495
|
+
class: enrollment.class,
|
|
31496
|
+
school: enrollment.school,
|
|
31497
|
+
sourcedId: data.enrollmentId,
|
|
31498
|
+
status: "active"
|
|
31499
|
+
});
|
|
31500
|
+
client.invalidateEnrollments(data.studentId);
|
|
31501
|
+
return { status: "ok" };
|
|
31502
|
+
}
|
|
31430
31503
|
async getCompletionStatus(client, courseId, studentId) {
|
|
31431
31504
|
const ids = deriveSourcedIds(courseId);
|
|
31432
31505
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
@@ -36517,6 +36590,10 @@ function createOneRosterNamespace(client) {
|
|
|
36517
36590
|
}
|
|
36518
36591
|
},
|
|
36519
36592
|
enrollments: {
|
|
36593
|
+
get: async (sourcedId) => {
|
|
36594
|
+
const response = await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "GET");
|
|
36595
|
+
return response.enrollment;
|
|
36596
|
+
},
|
|
36520
36597
|
listByClass: async (classSourcedId, options) => {
|
|
36521
36598
|
const queryParams = new URLSearchParams;
|
|
36522
36599
|
const filters = [`class.sourcedId='${escapeFilterValue2(classSourcedId)}'`];
|
|
@@ -36594,6 +36671,11 @@ function createOneRosterNamespace(client) {
|
|
|
36594
36671
|
}
|
|
36595
36672
|
},
|
|
36596
36673
|
create: async (data) => client["request"](ONEROSTER_ENDPOINTS5.enrollments, "POST", { enrollment: data }),
|
|
36674
|
+
update: async (sourcedId, data) => {
|
|
36675
|
+
await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "PUT", {
|
|
36676
|
+
enrollment: data
|
|
36677
|
+
});
|
|
36678
|
+
},
|
|
36597
36679
|
delete: async (sourcedId) => {
|
|
36598
36680
|
await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "DELETE");
|
|
36599
36681
|
}
|
|
@@ -36943,6 +37025,7 @@ class AdminEventRecorder {
|
|
|
36943
37025
|
defaultActivityId: "playcademy-admin-mastery-adjustment",
|
|
36944
37026
|
eventKind: "remediation-mastery"
|
|
36945
37027
|
});
|
|
37028
|
+
const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS5[this.environment]).course(data.courseId);
|
|
36946
37029
|
await this.caliper.emitActivityEvent({
|
|
36947
37030
|
studentId: ctx.student.id,
|
|
36948
37031
|
studentEmail: ctx.student.email,
|
|
@@ -36957,32 +37040,13 @@ class AdminEventRecorder {
|
|
|
36957
37040
|
appName: ctx.appName,
|
|
36958
37041
|
sensorUrl: ctx.sensorUrl,
|
|
36959
37042
|
process: true,
|
|
36960
|
-
generatedExtensions: ctx.metadata,
|
|
36961
|
-
eventExtensions: ctx.metadata
|
|
36962
|
-
});
|
|
36963
|
-
}
|
|
36964
|
-
async recordCourseCompletionChange(data) {
|
|
36965
|
-
const isResume = data.action === "resume";
|
|
36966
|
-
const ctx = await this.prepareAdminEvent({
|
|
36967
|
-
...data,
|
|
36968
|
-
defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
|
|
36969
|
-
reason: "Admin action",
|
|
36970
|
-
eventKind: isResume ? "course-resumed" : "course-completed"
|
|
36971
|
-
});
|
|
36972
|
-
await this.caliper.emitActivityEvent({
|
|
36973
|
-
studentId: ctx.student.id,
|
|
36974
|
-
studentEmail: ctx.student.email,
|
|
36975
|
-
gameId: data.gameId,
|
|
36976
|
-
activityId: ctx.activityId,
|
|
36977
|
-
activityName: isResume ? "Course resumed" : "Course marked complete",
|
|
36978
|
-
courseId: data.courseId,
|
|
36979
|
-
courseName: ctx.courseContext.courseName,
|
|
36980
|
-
subject: ctx.courseContext.subject,
|
|
36981
|
-
appName: ctx.appName,
|
|
36982
|
-
sensorUrl: ctx.sensorUrl,
|
|
36983
|
-
process: false,
|
|
36984
37043
|
includeAttempt: false,
|
|
36985
|
-
|
|
37044
|
+
objectId: `${ctx.sensorUrl.replace(/\/$/, "")}/urn:uuid:${crypto.randomUUID()}`,
|
|
37045
|
+
generatedId: courseUrl,
|
|
37046
|
+
generatedExtensions: {
|
|
37047
|
+
...ctx.metadata,
|
|
37048
|
+
...data.pctCompleteApp !== undefined ? { pctCompleteApp: data.pctCompleteApp } : {}
|
|
37049
|
+
},
|
|
36986
37050
|
eventExtensions: ctx.metadata
|
|
36987
37051
|
});
|
|
36988
37052
|
}
|
|
@@ -38173,10 +38237,6 @@ class TimebackClient {
|
|
|
38173
38237
|
await this._ensureAuthenticated();
|
|
38174
38238
|
return this.adminEventRecorder.recordMasteryAdjustment(data);
|
|
38175
38239
|
}
|
|
38176
|
-
async recordAdminCourseCompletionChange(data) {
|
|
38177
|
-
await this._ensureAuthenticated();
|
|
38178
|
-
return this.adminEventRecorder.recordCourseCompletionChange(data);
|
|
38179
|
-
}
|
|
38180
38240
|
clearCaches() {
|
|
38181
38241
|
this.cacheManager.clearAll();
|
|
38182
38242
|
}
|
|
@@ -95107,7 +95167,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
95107
95167
|
const date4 = new Date(Date.UTC(year3, month - 1, day, 12, 0, 0));
|
|
95108
95168
|
return date4.getUTCFullYear() === year3 && date4.getUTCMonth() + 1 === month && date4.getUTCDate() === day;
|
|
95109
95169
|
}
|
|
95110
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS6, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema,
|
|
95170
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS6, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
95111
95171
|
var init_schemas11 = __esm(() => {
|
|
95112
95172
|
init_drizzle_zod();
|
|
95113
95173
|
init_esm();
|
|
@@ -95278,12 +95338,6 @@ var init_schemas11 = __esm(() => {
|
|
|
95278
95338
|
date: AdminAttributionDateSchema.optional(),
|
|
95279
95339
|
useCurrentTime: exports_external.boolean().optional()
|
|
95280
95340
|
});
|
|
95281
|
-
ToggleCourseCompletionRequestSchema = exports_external.object({
|
|
95282
|
-
gameId: exports_external.string().uuid(),
|
|
95283
|
-
courseId: exports_external.string().min(1),
|
|
95284
|
-
studentId: exports_external.string().min(1),
|
|
95285
|
-
action: exports_external.enum(["complete", "resume"])
|
|
95286
|
-
});
|
|
95287
95341
|
EnrollStudentRequestSchema = exports_external.object({
|
|
95288
95342
|
gameId: exports_external.string().uuid(),
|
|
95289
95343
|
courseId: exports_external.string().min(1),
|
|
@@ -95294,6 +95348,12 @@ var init_schemas11 = __esm(() => {
|
|
|
95294
95348
|
courseId: exports_external.string().min(1),
|
|
95295
95349
|
studentId: exports_external.string().min(1)
|
|
95296
95350
|
});
|
|
95351
|
+
ReactivateEnrollmentRequestSchema = exports_external.object({
|
|
95352
|
+
gameId: exports_external.string().uuid(),
|
|
95353
|
+
courseId: exports_external.string().min(1),
|
|
95354
|
+
studentId: exports_external.string().min(1),
|
|
95355
|
+
enrollmentId: exports_external.string().min(1)
|
|
95356
|
+
});
|
|
95297
95357
|
InsertAssessmentTestSchema = createInsertSchema(gameTimebackAssessmentTests).omit({
|
|
95298
95358
|
id: true,
|
|
95299
95359
|
createdAt: true
|
|
@@ -97473,7 +97533,7 @@ var init_sprite_controller = __esm(() => {
|
|
|
97473
97533
|
});
|
|
97474
97534
|
|
|
97475
97535
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
97476
|
-
var logger65, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery,
|
|
97536
|
+
var logger65, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
97477
97537
|
var init_timeback_controller = __esm(() => {
|
|
97478
97538
|
init_esm();
|
|
97479
97539
|
init_schemas_index();
|
|
@@ -97874,17 +97934,6 @@ var init_timeback_controller = __esm(() => {
|
|
|
97874
97934
|
});
|
|
97875
97935
|
return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
|
|
97876
97936
|
});
|
|
97877
|
-
toggleCompletion = requireGameManagementAccess(async (ctx) => {
|
|
97878
|
-
const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
|
|
97879
|
-
logger65.debug("Toggling course completion", {
|
|
97880
|
-
requesterId: ctx.user.id,
|
|
97881
|
-
gameId: body2.gameId,
|
|
97882
|
-
courseId: body2.courseId,
|
|
97883
|
-
studentId: body2.studentId,
|
|
97884
|
-
action: body2.action
|
|
97885
|
-
});
|
|
97886
|
-
return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
|
|
97887
|
-
});
|
|
97888
97937
|
searchStudents = requireGameManagementAccess(async (ctx) => {
|
|
97889
97938
|
const gameId = ctx.params.gameId;
|
|
97890
97939
|
const courseId = ctx.params.courseId;
|
|
@@ -97920,6 +97969,17 @@ var init_timeback_controller = __esm(() => {
|
|
|
97920
97969
|
});
|
|
97921
97970
|
return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
|
|
97922
97971
|
});
|
|
97972
|
+
reactivateEnrollment = requireGameManagementAccess(async (ctx) => {
|
|
97973
|
+
const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
|
|
97974
|
+
logger65.debug("Reactivating enrollment", {
|
|
97975
|
+
requesterId: ctx.user.id,
|
|
97976
|
+
gameId: body2.gameId,
|
|
97977
|
+
courseId: body2.courseId,
|
|
97978
|
+
studentId: body2.studentId,
|
|
97979
|
+
enrollmentId: body2.enrollmentId
|
|
97980
|
+
});
|
|
97981
|
+
return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
|
|
97982
|
+
});
|
|
97923
97983
|
listAssessments = requireGameManagementAccess(async (ctx) => {
|
|
97924
97984
|
const { gameId, courseId } = ctx.params;
|
|
97925
97985
|
if (!gameId || !courseId) {
|
|
@@ -98065,10 +98125,10 @@ var init_timeback_controller = __esm(() => {
|
|
|
98065
98125
|
grantXp,
|
|
98066
98126
|
adjustTime,
|
|
98067
98127
|
adjustMastery,
|
|
98068
|
-
toggleCompletion,
|
|
98069
98128
|
searchStudents,
|
|
98070
98129
|
enrollStudent,
|
|
98071
98130
|
unenrollStudent,
|
|
98131
|
+
reactivateEnrollment,
|
|
98072
98132
|
listAssessments,
|
|
98073
98133
|
createAssessment,
|
|
98074
98134
|
deleteAssessment,
|
package/dist/server.js
CHANGED
|
@@ -1329,7 +1329,7 @@ var package_default;
|
|
|
1329
1329
|
var init_package = __esm(() => {
|
|
1330
1330
|
package_default = {
|
|
1331
1331
|
name: "@playcademy/sandbox",
|
|
1332
|
-
version: "0.3.17-beta.
|
|
1332
|
+
version: "0.3.17-beta.35",
|
|
1333
1333
|
description: "Local development server for Playcademy game development",
|
|
1334
1334
|
type: "module",
|
|
1335
1335
|
exports: {
|
|
@@ -30432,6 +30432,31 @@ function resolveAdminEventTime(data) {
|
|
|
30432
30432
|
}
|
|
30433
30433
|
return toAttributionEventTime(data.date);
|
|
30434
30434
|
}
|
|
30435
|
+
function validateMasteryAdjustment(delta, currentMastered, masterableUnits) {
|
|
30436
|
+
if (delta < 0 && currentMastered + delta < 0) {
|
|
30437
|
+
throw new ValidationError(`Adjustment would go below 0. Current: ${currentMastered}, adjustment: ${delta}`);
|
|
30438
|
+
}
|
|
30439
|
+
if (delta < 0 && typeof masterableUnits === "number" && masterableUnits > 0 && currentMastered > masterableUnits && currentMastered + delta > masterableUnits) {
|
|
30440
|
+
const minDelta = masterableUnits - currentMastered;
|
|
30441
|
+
throw new ValidationError(`Adjustment must reduce mastery to at most ${masterableUnits}. Current: ${currentMastered}/${masterableUnits}, minimum adjustment: ${minDelta}`);
|
|
30442
|
+
}
|
|
30443
|
+
if (delta > 0 && typeof masterableUnits === "number" && masterableUnits > 0) {
|
|
30444
|
+
if (currentMastered >= masterableUnits) {
|
|
30445
|
+
throw new ValidationError(`Mastery is already at maximum (${currentMastered}/${masterableUnits}). Only negative adjustments are allowed.`);
|
|
30446
|
+
}
|
|
30447
|
+
if (currentMastered + delta > masterableUnits) {
|
|
30448
|
+
const remaining = masterableUnits - currentMastered;
|
|
30449
|
+
throw new ValidationError(`Adjustment would exceed maximum. Current: ${currentMastered}/${masterableUnits}, max adjustment: +${remaining}`);
|
|
30450
|
+
}
|
|
30451
|
+
}
|
|
30452
|
+
}
|
|
30453
|
+
function compareEnrollmentsByRecency(a, b) {
|
|
30454
|
+
const dateCompare = (b.beginDate ?? "").localeCompare(a.beginDate ?? "");
|
|
30455
|
+
if (dateCompare !== 0) {
|
|
30456
|
+
return dateCompare;
|
|
30457
|
+
}
|
|
30458
|
+
return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
|
|
30459
|
+
}
|
|
30435
30460
|
var init_timeback_admin_util = __esm(() => {
|
|
30436
30461
|
init_errors();
|
|
30437
30462
|
});
|
|
@@ -30784,17 +30809,6 @@ class TimebackAdminService {
|
|
|
30784
30809
|
}
|
|
30785
30810
|
return this.deps.timeback;
|
|
30786
30811
|
}
|
|
30787
|
-
async recordCourseCompletionHistory(client, data) {
|
|
30788
|
-
await client.recordAdminCourseCompletionChange(data).catch((error) => {
|
|
30789
|
-
logger16.error("Failed to record admin course completion history event", {
|
|
30790
|
-
gameId: data.gameId,
|
|
30791
|
-
courseId: data.courseId,
|
|
30792
|
-
studentId: data.studentId,
|
|
30793
|
-
action: data.action,
|
|
30794
|
-
error: error instanceof Error ? error.message : String(error)
|
|
30795
|
-
});
|
|
30796
|
-
});
|
|
30797
|
-
}
|
|
30798
30812
|
async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
|
|
30799
30813
|
const client = this.requireClient();
|
|
30800
30814
|
if (accessLevel === "dashboard") {
|
|
@@ -30893,7 +30907,7 @@ class TimebackAdminService {
|
|
|
30893
30907
|
if (typeof masterableUnits !== "number" || masterableUnits <= 0) {
|
|
30894
30908
|
return;
|
|
30895
30909
|
}
|
|
30896
|
-
return Math.
|
|
30910
|
+
return Math.round(masteredUnits / masterableUnits * 100);
|
|
30897
30911
|
}
|
|
30898
30912
|
async getMasterableUnits(courseId) {
|
|
30899
30913
|
const client = this.requireClient();
|
|
@@ -30946,6 +30960,7 @@ class TimebackAdminService {
|
|
|
30946
30960
|
}
|
|
30947
30961
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
30948
30962
|
const enrollments = new Map;
|
|
30963
|
+
const allEnrollments = new Map;
|
|
30949
30964
|
const entries = await Promise.all(courseIds.map(async (courseId) => {
|
|
30950
30965
|
const roster = await client.oneroster.enrollments.listByCourse(courseId, {
|
|
30951
30966
|
includeInactive: options?.includeInactive,
|
|
@@ -30957,28 +30972,30 @@ class TimebackAdminService {
|
|
|
30957
30972
|
if (aActive !== bActive) {
|
|
30958
30973
|
return aActive ? -1 : 1;
|
|
30959
30974
|
}
|
|
30960
|
-
return (
|
|
30975
|
+
return compareEnrollmentsByRecency(a.enrollment, b.enrollment);
|
|
30961
30976
|
});
|
|
30962
|
-
return { courseId,
|
|
30977
|
+
return { courseId, matches };
|
|
30963
30978
|
}));
|
|
30964
|
-
for (const { courseId,
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
|
|
30969
|
-
|
|
30970
|
-
|
|
30971
|
-
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
30975
|
-
|
|
30976
|
-
|
|
30977
|
-
|
|
30978
|
-
|
|
30979
|
+
for (const { courseId, matches } of entries) {
|
|
30980
|
+
const records = matches.map((match) => ({
|
|
30981
|
+
id: match.enrollment.sourcedId,
|
|
30982
|
+
status: match.enrollment.status ?? "active",
|
|
30983
|
+
role: match.enrollment.role ?? "student",
|
|
30984
|
+
beginDate: match.enrollment.beginDate ?? null,
|
|
30985
|
+
endDate: match.enrollment.endDate ?? null,
|
|
30986
|
+
course: {
|
|
30987
|
+
id: courseId,
|
|
30988
|
+
title: match.class?.title ?? "",
|
|
30989
|
+
subjects: null,
|
|
30990
|
+
grades: null
|
|
30991
|
+
}
|
|
30992
|
+
}));
|
|
30993
|
+
if (records.length > 0) {
|
|
30994
|
+
enrollments.set(courseId, records[0]);
|
|
30979
30995
|
}
|
|
30996
|
+
allEnrollments.set(courseId, records);
|
|
30980
30997
|
}
|
|
30981
|
-
return { enrollments };
|
|
30998
|
+
return { enrollments, allEnrollments };
|
|
30982
30999
|
}
|
|
30983
31000
|
async assertStudentEnrolledInCourse(client, studentId, courseId) {
|
|
30984
31001
|
const enrollments = await client.edubridge.enrollments.listByUser(studentId);
|
|
@@ -31065,7 +31082,7 @@ class TimebackAdminService {
|
|
|
31065
31082
|
const enrollmentId = rosterEntry.enrollment.sourcedId || null;
|
|
31066
31083
|
const summary = enrollmentId ? analyticsByEnrollmentId.get(enrollmentId) : undefined;
|
|
31067
31084
|
const analyticsUnavailable = Boolean(enrollmentId) && summary?.analyticsAvailable !== true;
|
|
31068
|
-
const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() :
|
|
31085
|
+
const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() || "No name specified" : "No name specified";
|
|
31069
31086
|
const inactive = rosterEntry.enrollment.status === "tobedeleted";
|
|
31070
31087
|
return {
|
|
31071
31088
|
studentId: rosterEntry.enrollment.user.sourcedId,
|
|
@@ -31110,14 +31127,15 @@ class TimebackAdminService {
|
|
|
31110
31127
|
throw new NotFoundError("Timeback integration", gameId);
|
|
31111
31128
|
}
|
|
31112
31129
|
const courseIds = new Set(integrations.map((integration) => integration.courseId));
|
|
31113
|
-
const { enrollments: enrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
|
|
31130
|
+
const { enrollments: enrollmentsByCourseId, allEnrollments: allEnrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
|
|
31114
31131
|
includeInactive: true
|
|
31115
31132
|
});
|
|
31116
31133
|
if (enrollmentsByCourseId.size === 0) {
|
|
31117
31134
|
throw new NotFoundError("Student enrollment", courseId ? `${studentId}:${courseId}` : `${studentId}:${gameId}`);
|
|
31118
31135
|
}
|
|
31136
|
+
const allEnrollmentIds = [...allEnrollmentsByCourseId.values()].flat().map((enrollment) => enrollment.id);
|
|
31119
31137
|
const studentProfile = await client.oneroster.users.get(studentId);
|
|
31120
|
-
const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries(
|
|
31138
|
+
const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries(allEnrollmentIds);
|
|
31121
31139
|
const [masterableUnitsByCourse, completionStatusByCourse] = await Promise.all([
|
|
31122
31140
|
this.getMasterableUnitsByCourse(integrations.map((integration) => integration.courseId)),
|
|
31123
31141
|
this.getCompletionStatusByCourse(client, integrations.map((integration) => integration.courseId), studentId)
|
|
@@ -31128,6 +31146,24 @@ class TimebackAdminService {
|
|
|
31128
31146
|
const masterableUnits = masterableUnitsByCourse.get(integration.courseId);
|
|
31129
31147
|
const analyticsUnavailable = Boolean(enrollment?.id) && summary?.analyticsAvailable !== true;
|
|
31130
31148
|
const inactive = enrollment?.status === "tobedeleted";
|
|
31149
|
+
const courseEnrollments = allEnrollmentsByCourseId.get(integration.courseId) ?? [];
|
|
31150
|
+
const enrollmentSummaries = courseEnrollments.length > 1 ? courseEnrollments.map((record) => {
|
|
31151
|
+
const recordSummary = analyticsByEnrollmentId.get(record.id);
|
|
31152
|
+
const recordAnalyticsUnavailable = recordSummary?.analyticsAvailable !== true;
|
|
31153
|
+
return {
|
|
31154
|
+
enrollmentId: record.id,
|
|
31155
|
+
status: record.status === "tobedeleted" ? "tobedeleted" : "active",
|
|
31156
|
+
beginDate: record.beginDate,
|
|
31157
|
+
endDate: record.endDate,
|
|
31158
|
+
analyticsUnavailable: recordAnalyticsUnavailable,
|
|
31159
|
+
totalXp: recordSummary?.totalXp ?? 0,
|
|
31160
|
+
todayXp: recordSummary?.todayXp ?? 0,
|
|
31161
|
+
activeTimeSeconds: recordSummary?.activeTimeSeconds ?? 0,
|
|
31162
|
+
masteredUnits: recordSummary?.masteredUnits ?? 0,
|
|
31163
|
+
pctCompleteApp: TimebackAdminService.computeCompletionPct(recordSummary?.masteredUnits ?? 0, masterableUnits),
|
|
31164
|
+
history: recordSummary?.history ?? []
|
|
31165
|
+
};
|
|
31166
|
+
}) : undefined;
|
|
31131
31167
|
return {
|
|
31132
31168
|
courseId: integration.courseId,
|
|
31133
31169
|
title: enrollment?.course.title || `${integration.subject} Grade ${integration.grade}`,
|
|
@@ -31143,9 +31179,11 @@ class TimebackAdminService {
|
|
|
31143
31179
|
pctCompleteApp: TimebackAdminService.computeCompletionPct(summary?.masteredUnits ?? 0, masterableUnits),
|
|
31144
31180
|
completionStatus: completionStatusByCourse.get(integration.courseId) ?? "none",
|
|
31145
31181
|
history: summary?.history ?? [],
|
|
31146
|
-
...inactive ? { inactive } : {}
|
|
31182
|
+
...inactive ? { inactive } : {},
|
|
31183
|
+
...enrollmentSummaries ? { enrollments: enrollmentSummaries } : {}
|
|
31147
31184
|
};
|
|
31148
31185
|
});
|
|
31186
|
+
courses.sort((a, b) => a.grade - b.grade);
|
|
31149
31187
|
return {
|
|
31150
31188
|
student: {
|
|
31151
31189
|
studentId,
|
|
@@ -31245,111 +31283,87 @@ class TimebackAdminService {
|
|
|
31245
31283
|
return { status: "ok" };
|
|
31246
31284
|
}
|
|
31247
31285
|
async adjustMasteredUnits(data, user) {
|
|
31248
|
-
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user
|
|
31286
|
+
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user);
|
|
31287
|
+
let currentMastered = 0;
|
|
31288
|
+
const masterableUnits = await this.getMasterableUnits(data.courseId);
|
|
31289
|
+
if (data.units !== 0) {
|
|
31290
|
+
const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
|
|
31291
|
+
try {
|
|
31292
|
+
const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
|
|
31293
|
+
currentMastered = this.summarizeAnalyticsFacts(analytics.facts).masteredUnits;
|
|
31294
|
+
} catch {
|
|
31295
|
+
throw new ValidationError("Unable to validate mastery bounds — analytics unavailable. Please retry.");
|
|
31296
|
+
}
|
|
31297
|
+
validateMasteryAdjustment(data.units, currentMastered, masterableUnits);
|
|
31298
|
+
}
|
|
31299
|
+
const pctCompleteApp = typeof masterableUnits === "number" && masterableUnits > 0 ? Math.min(100, Math.max(0, Math.round((currentMastered + data.units) / masterableUnits * 100))) : undefined;
|
|
31249
31300
|
await client.recordAdminMasteryAdjustment({
|
|
31250
31301
|
gameId: data.gameId,
|
|
31251
31302
|
courseId: data.courseId,
|
|
31252
31303
|
studentId: data.studentId,
|
|
31253
31304
|
masteredUnits: data.units,
|
|
31305
|
+
pctCompleteApp,
|
|
31254
31306
|
eventTime: resolveAdminEventTime(data),
|
|
31255
31307
|
reason: data.reason,
|
|
31256
31308
|
actor,
|
|
31257
31309
|
appName,
|
|
31258
31310
|
sensorUrl
|
|
31259
31311
|
});
|
|
31260
|
-
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
});
|
|
31275
|
-
if (data.action === "complete") {
|
|
31276
|
-
const masterableUnits = await this.getMasterableUnits(data.courseId);
|
|
31277
|
-
if (masterableUnits && masterableUnits > 0) {
|
|
31278
|
-
const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
|
|
31279
|
-
let currentMastered = 0;
|
|
31280
|
-
try {
|
|
31281
|
-
const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
|
|
31282
|
-
const summary = this.summarizeAnalyticsFacts(analytics.facts);
|
|
31283
|
-
currentMastered = summary.masteredUnits;
|
|
31284
|
-
} catch {
|
|
31285
|
-
logger16.warn("Failed to load analytics for mastery gap calculation", {
|
|
31286
|
-
studentId: data.studentId,
|
|
31287
|
-
courseId: data.courseId
|
|
31312
|
+
if (typeof masterableUnits === "number" && masterableUnits > 0) {
|
|
31313
|
+
const wasMastered = currentMastered >= masterableUnits;
|
|
31314
|
+
const willBeMastered = currentMastered + data.units >= masterableUnits;
|
|
31315
|
+
if (wasMastered !== willBeMastered) {
|
|
31316
|
+
const ids = deriveSourcedIds(data.courseId);
|
|
31317
|
+
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
31318
|
+
const resultId = `${lineItemId}:${data.studentId}:completion`;
|
|
31319
|
+
if (willBeMastered) {
|
|
31320
|
+
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
31321
|
+
sourcedId: lineItemId,
|
|
31322
|
+
title: "Mastery Completion",
|
|
31323
|
+
status: ONEROSTER_STATUS.active,
|
|
31324
|
+
course: { sourcedId: ids.course },
|
|
31325
|
+
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
31288
31326
|
});
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
|
|
31297
|
-
|
|
31298
|
-
|
|
31299
|
-
|
|
31300
|
-
|
|
31327
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31328
|
+
sourcedId: resultId,
|
|
31329
|
+
status: ONEROSTER_STATUS.active,
|
|
31330
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31331
|
+
student: { sourcedId: data.studentId },
|
|
31332
|
+
score: 100,
|
|
31333
|
+
scoreDate: new Date().toISOString(),
|
|
31334
|
+
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31335
|
+
inProgress: "false",
|
|
31336
|
+
metadata: {
|
|
31337
|
+
isMasteryCompletion: true,
|
|
31338
|
+
adminAction: true,
|
|
31339
|
+
appName
|
|
31340
|
+
}
|
|
31301
31341
|
});
|
|
31342
|
+
} else {
|
|
31343
|
+
try {
|
|
31344
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31345
|
+
sourcedId: resultId,
|
|
31346
|
+
status: ONEROSTER_STATUS.active,
|
|
31347
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31348
|
+
student: { sourcedId: data.studentId },
|
|
31349
|
+
score: 0,
|
|
31350
|
+
scoreDate: new Date().toISOString(),
|
|
31351
|
+
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31352
|
+
inProgress: "true",
|
|
31353
|
+
metadata: {
|
|
31354
|
+
isMasteryCompletion: true,
|
|
31355
|
+
adminAction: true,
|
|
31356
|
+
appName
|
|
31357
|
+
}
|
|
31358
|
+
});
|
|
31359
|
+
} catch {
|
|
31360
|
+
logger16.debug("No completion entry to revoke", {
|
|
31361
|
+
studentId: data.studentId,
|
|
31362
|
+
courseId: data.courseId
|
|
31363
|
+
});
|
|
31364
|
+
}
|
|
31302
31365
|
}
|
|
31303
31366
|
}
|
|
31304
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31305
|
-
sourcedId: resultId,
|
|
31306
|
-
status: ONEROSTER_STATUS.active,
|
|
31307
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
31308
|
-
student: { sourcedId: data.studentId },
|
|
31309
|
-
score: 100,
|
|
31310
|
-
scoreDate: new Date().toISOString(),
|
|
31311
|
-
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31312
|
-
inProgress: "false",
|
|
31313
|
-
metadata: {
|
|
31314
|
-
isMasteryCompletion: true,
|
|
31315
|
-
adminAction: true,
|
|
31316
|
-
appName
|
|
31317
|
-
}
|
|
31318
|
-
});
|
|
31319
|
-
await this.recordCourseCompletionHistory(historyClient, {
|
|
31320
|
-
gameId: data.gameId,
|
|
31321
|
-
courseId: data.courseId,
|
|
31322
|
-
studentId: data.studentId,
|
|
31323
|
-
action: "complete",
|
|
31324
|
-
actor,
|
|
31325
|
-
appName,
|
|
31326
|
-
sensorUrl
|
|
31327
|
-
});
|
|
31328
|
-
} else {
|
|
31329
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31330
|
-
sourcedId: resultId,
|
|
31331
|
-
status: ONEROSTER_STATUS.active,
|
|
31332
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
31333
|
-
student: { sourcedId: data.studentId },
|
|
31334
|
-
score: 0,
|
|
31335
|
-
scoreDate: new Date().toISOString(),
|
|
31336
|
-
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31337
|
-
inProgress: "true",
|
|
31338
|
-
metadata: {
|
|
31339
|
-
isMasteryCompletion: true,
|
|
31340
|
-
adminAction: true,
|
|
31341
|
-
appName
|
|
31342
|
-
}
|
|
31343
|
-
});
|
|
31344
|
-
await this.recordCourseCompletionHistory(historyClient, {
|
|
31345
|
-
gameId: data.gameId,
|
|
31346
|
-
courseId: data.courseId,
|
|
31347
|
-
studentId: data.studentId,
|
|
31348
|
-
action: "resume",
|
|
31349
|
-
actor,
|
|
31350
|
-
appName,
|
|
31351
|
-
sensorUrl
|
|
31352
|
-
});
|
|
31353
31367
|
}
|
|
31354
31368
|
return { status: "ok" };
|
|
31355
31369
|
}
|
|
@@ -31385,17 +31399,34 @@ class TimebackAdminService {
|
|
|
31385
31399
|
});
|
|
31386
31400
|
return { students: [] };
|
|
31387
31401
|
}
|
|
31388
|
-
const
|
|
31402
|
+
const fullRoster = await client.oneroster.enrollments.listByCourse(courseId, {
|
|
31389
31403
|
role: "student",
|
|
31404
|
+
includeInactive: true,
|
|
31390
31405
|
includeUsers: false
|
|
31391
31406
|
});
|
|
31392
|
-
const enrolledStudentIds = new Set(
|
|
31393
|
-
const
|
|
31394
|
-
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
|
|
31407
|
+
const enrolledStudentIds = new Set(fullRoster.filter((entry) => entry.enrollment.status === "active").map((entry) => entry.enrollment.user.sourcedId));
|
|
31408
|
+
const pastEnrollmentsByStudent = new Map;
|
|
31409
|
+
const inactiveEntries = fullRoster.filter((entry) => entry.enrollment.status === "tobedeleted").toSorted((a, b) => compareEnrollmentsByRecency(a.enrollment, b.enrollment));
|
|
31410
|
+
for (const entry of inactiveEntries) {
|
|
31411
|
+
const studentId = entry.enrollment.user.sourcedId;
|
|
31412
|
+
const list = pastEnrollmentsByStudent.get(studentId) ?? [];
|
|
31413
|
+
list.push({
|
|
31414
|
+
enrollmentId: entry.enrollment.sourcedId,
|
|
31415
|
+
beginDate: entry.enrollment.beginDate ?? null,
|
|
31416
|
+
endDate: entry.enrollment.endDate ?? null
|
|
31417
|
+
});
|
|
31418
|
+
pastEnrollmentsByStudent.set(studentId, list);
|
|
31419
|
+
}
|
|
31420
|
+
const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => {
|
|
31421
|
+
const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
|
|
31422
|
+
return {
|
|
31423
|
+
studentId: entry.sourcedId,
|
|
31424
|
+
name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
|
|
31425
|
+
email: entry.email || null,
|
|
31426
|
+
alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
|
|
31427
|
+
...past.length > 0 ? { pastEnrollments: past } : {}
|
|
31428
|
+
};
|
|
31429
|
+
});
|
|
31399
31430
|
return { students };
|
|
31400
31431
|
}
|
|
31401
31432
|
async enrollStudent(data, user) {
|
|
@@ -31426,6 +31457,48 @@ class TimebackAdminService {
|
|
|
31426
31457
|
client.invalidateEnrollments(data.studentId);
|
|
31427
31458
|
return { status: "ok" };
|
|
31428
31459
|
}
|
|
31460
|
+
async reactivateEnrollment(data, user) {
|
|
31461
|
+
const client = this.requireClient();
|
|
31462
|
+
await this.deps.validateGameManagementAccess(user, data.gameId);
|
|
31463
|
+
const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
31464
|
+
where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
|
|
31465
|
+
});
|
|
31466
|
+
if (!integration) {
|
|
31467
|
+
throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
|
|
31468
|
+
}
|
|
31469
|
+
const enrollment = await client.oneroster.enrollments.get(data.enrollmentId);
|
|
31470
|
+
if (!enrollment) {
|
|
31471
|
+
throw new NotFoundError("Enrollment", data.enrollmentId);
|
|
31472
|
+
}
|
|
31473
|
+
if (enrollment.user.sourcedId !== data.studentId) {
|
|
31474
|
+
throw new ValidationError("Enrollment does not belong to the specified student");
|
|
31475
|
+
}
|
|
31476
|
+
if (enrollment.status === "active") {
|
|
31477
|
+
throw new ValidationError("Enrollment is already active");
|
|
31478
|
+
}
|
|
31479
|
+
const { allEnrollments } = await this.getStudentEnrollmentsByCourseId(client, data.studentId, [data.courseId], { includeInactive: true });
|
|
31480
|
+
const courseEnrollmentIds = new Set((allEnrollments.get(data.courseId) ?? []).map((e) => e.id));
|
|
31481
|
+
if (!courseEnrollmentIds.has(data.enrollmentId)) {
|
|
31482
|
+
throw new ValidationError("Enrollment does not belong to the specified course");
|
|
31483
|
+
}
|
|
31484
|
+
const activeEnrollments = await client.edubridge.enrollments.listByUser(data.studentId);
|
|
31485
|
+
if (activeEnrollments.some((e) => e.course.id === data.courseId)) {
|
|
31486
|
+
throw new ValidationError("Student already has an active enrollment for this course. Unenroll from the current enrollment before reactivating a past one.");
|
|
31487
|
+
}
|
|
31488
|
+
await client.oneroster.enrollments.update(data.enrollmentId, {
|
|
31489
|
+
role: enrollment.role,
|
|
31490
|
+
primary: enrollment.primary,
|
|
31491
|
+
beginDate: enrollment.beginDate,
|
|
31492
|
+
endDate: enrollment.endDate,
|
|
31493
|
+
user: enrollment.user,
|
|
31494
|
+
class: enrollment.class,
|
|
31495
|
+
school: enrollment.school,
|
|
31496
|
+
sourcedId: data.enrollmentId,
|
|
31497
|
+
status: "active"
|
|
31498
|
+
});
|
|
31499
|
+
client.invalidateEnrollments(data.studentId);
|
|
31500
|
+
return { status: "ok" };
|
|
31501
|
+
}
|
|
31429
31502
|
async getCompletionStatus(client, courseId, studentId) {
|
|
31430
31503
|
const ids = deriveSourcedIds(courseId);
|
|
31431
31504
|
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
@@ -36516,6 +36589,10 @@ function createOneRosterNamespace(client) {
|
|
|
36516
36589
|
}
|
|
36517
36590
|
},
|
|
36518
36591
|
enrollments: {
|
|
36592
|
+
get: async (sourcedId) => {
|
|
36593
|
+
const response = await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "GET");
|
|
36594
|
+
return response.enrollment;
|
|
36595
|
+
},
|
|
36519
36596
|
listByClass: async (classSourcedId, options) => {
|
|
36520
36597
|
const queryParams = new URLSearchParams;
|
|
36521
36598
|
const filters = [`class.sourcedId='${escapeFilterValue2(classSourcedId)}'`];
|
|
@@ -36593,6 +36670,11 @@ function createOneRosterNamespace(client) {
|
|
|
36593
36670
|
}
|
|
36594
36671
|
},
|
|
36595
36672
|
create: async (data) => client["request"](ONEROSTER_ENDPOINTS5.enrollments, "POST", { enrollment: data }),
|
|
36673
|
+
update: async (sourcedId, data) => {
|
|
36674
|
+
await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "PUT", {
|
|
36675
|
+
enrollment: data
|
|
36676
|
+
});
|
|
36677
|
+
},
|
|
36596
36678
|
delete: async (sourcedId) => {
|
|
36597
36679
|
await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "DELETE");
|
|
36598
36680
|
}
|
|
@@ -36942,6 +37024,7 @@ class AdminEventRecorder {
|
|
|
36942
37024
|
defaultActivityId: "playcademy-admin-mastery-adjustment",
|
|
36943
37025
|
eventKind: "remediation-mastery"
|
|
36944
37026
|
});
|
|
37027
|
+
const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS5[this.environment]).course(data.courseId);
|
|
36945
37028
|
await this.caliper.emitActivityEvent({
|
|
36946
37029
|
studentId: ctx.student.id,
|
|
36947
37030
|
studentEmail: ctx.student.email,
|
|
@@ -36956,32 +37039,13 @@ class AdminEventRecorder {
|
|
|
36956
37039
|
appName: ctx.appName,
|
|
36957
37040
|
sensorUrl: ctx.sensorUrl,
|
|
36958
37041
|
process: true,
|
|
36959
|
-
generatedExtensions: ctx.metadata,
|
|
36960
|
-
eventExtensions: ctx.metadata
|
|
36961
|
-
});
|
|
36962
|
-
}
|
|
36963
|
-
async recordCourseCompletionChange(data) {
|
|
36964
|
-
const isResume = data.action === "resume";
|
|
36965
|
-
const ctx = await this.prepareAdminEvent({
|
|
36966
|
-
...data,
|
|
36967
|
-
defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
|
|
36968
|
-
reason: "Admin action",
|
|
36969
|
-
eventKind: isResume ? "course-resumed" : "course-completed"
|
|
36970
|
-
});
|
|
36971
|
-
await this.caliper.emitActivityEvent({
|
|
36972
|
-
studentId: ctx.student.id,
|
|
36973
|
-
studentEmail: ctx.student.email,
|
|
36974
|
-
gameId: data.gameId,
|
|
36975
|
-
activityId: ctx.activityId,
|
|
36976
|
-
activityName: isResume ? "Course resumed" : "Course marked complete",
|
|
36977
|
-
courseId: data.courseId,
|
|
36978
|
-
courseName: ctx.courseContext.courseName,
|
|
36979
|
-
subject: ctx.courseContext.subject,
|
|
36980
|
-
appName: ctx.appName,
|
|
36981
|
-
sensorUrl: ctx.sensorUrl,
|
|
36982
|
-
process: false,
|
|
36983
37042
|
includeAttempt: false,
|
|
36984
|
-
|
|
37043
|
+
objectId: `${ctx.sensorUrl.replace(/\/$/, "")}/urn:uuid:${crypto.randomUUID()}`,
|
|
37044
|
+
generatedId: courseUrl,
|
|
37045
|
+
generatedExtensions: {
|
|
37046
|
+
...ctx.metadata,
|
|
37047
|
+
...data.pctCompleteApp !== undefined ? { pctCompleteApp: data.pctCompleteApp } : {}
|
|
37048
|
+
},
|
|
36985
37049
|
eventExtensions: ctx.metadata
|
|
36986
37050
|
});
|
|
36987
37051
|
}
|
|
@@ -38172,10 +38236,6 @@ class TimebackClient {
|
|
|
38172
38236
|
await this._ensureAuthenticated();
|
|
38173
38237
|
return this.adminEventRecorder.recordMasteryAdjustment(data);
|
|
38174
38238
|
}
|
|
38175
|
-
async recordAdminCourseCompletionChange(data) {
|
|
38176
|
-
await this._ensureAuthenticated();
|
|
38177
|
-
return this.adminEventRecorder.recordCourseCompletionChange(data);
|
|
38178
|
-
}
|
|
38179
38239
|
clearCaches() {
|
|
38180
38240
|
this.cacheManager.clearAll();
|
|
38181
38241
|
}
|
|
@@ -95106,7 +95166,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
95106
95166
|
const date4 = new Date(Date.UTC(year3, month - 1, day, 12, 0, 0));
|
|
95107
95167
|
return date4.getUTCFullYear() === year3 && date4.getUTCMonth() + 1 === month && date4.getUTCDate() === day;
|
|
95108
95168
|
}
|
|
95109
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS6, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema,
|
|
95169
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS6, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
95110
95170
|
var init_schemas11 = __esm(() => {
|
|
95111
95171
|
init_drizzle_zod();
|
|
95112
95172
|
init_esm();
|
|
@@ -95277,12 +95337,6 @@ var init_schemas11 = __esm(() => {
|
|
|
95277
95337
|
date: AdminAttributionDateSchema.optional(),
|
|
95278
95338
|
useCurrentTime: exports_external.boolean().optional()
|
|
95279
95339
|
});
|
|
95280
|
-
ToggleCourseCompletionRequestSchema = exports_external.object({
|
|
95281
|
-
gameId: exports_external.string().uuid(),
|
|
95282
|
-
courseId: exports_external.string().min(1),
|
|
95283
|
-
studentId: exports_external.string().min(1),
|
|
95284
|
-
action: exports_external.enum(["complete", "resume"])
|
|
95285
|
-
});
|
|
95286
95340
|
EnrollStudentRequestSchema = exports_external.object({
|
|
95287
95341
|
gameId: exports_external.string().uuid(),
|
|
95288
95342
|
courseId: exports_external.string().min(1),
|
|
@@ -95293,6 +95347,12 @@ var init_schemas11 = __esm(() => {
|
|
|
95293
95347
|
courseId: exports_external.string().min(1),
|
|
95294
95348
|
studentId: exports_external.string().min(1)
|
|
95295
95349
|
});
|
|
95350
|
+
ReactivateEnrollmentRequestSchema = exports_external.object({
|
|
95351
|
+
gameId: exports_external.string().uuid(),
|
|
95352
|
+
courseId: exports_external.string().min(1),
|
|
95353
|
+
studentId: exports_external.string().min(1),
|
|
95354
|
+
enrollmentId: exports_external.string().min(1)
|
|
95355
|
+
});
|
|
95296
95356
|
InsertAssessmentTestSchema = createInsertSchema(gameTimebackAssessmentTests).omit({
|
|
95297
95357
|
id: true,
|
|
95298
95358
|
createdAt: true
|
|
@@ -97472,7 +97532,7 @@ var init_sprite_controller = __esm(() => {
|
|
|
97472
97532
|
});
|
|
97473
97533
|
|
|
97474
97534
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
97475
|
-
var logger65, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery,
|
|
97535
|
+
var logger65, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
97476
97536
|
var init_timeback_controller = __esm(() => {
|
|
97477
97537
|
init_esm();
|
|
97478
97538
|
init_schemas_index();
|
|
@@ -97873,17 +97933,6 @@ var init_timeback_controller = __esm(() => {
|
|
|
97873
97933
|
});
|
|
97874
97934
|
return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
|
|
97875
97935
|
});
|
|
97876
|
-
toggleCompletion = requireGameManagementAccess(async (ctx) => {
|
|
97877
|
-
const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
|
|
97878
|
-
logger65.debug("Toggling course completion", {
|
|
97879
|
-
requesterId: ctx.user.id,
|
|
97880
|
-
gameId: body2.gameId,
|
|
97881
|
-
courseId: body2.courseId,
|
|
97882
|
-
studentId: body2.studentId,
|
|
97883
|
-
action: body2.action
|
|
97884
|
-
});
|
|
97885
|
-
return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
|
|
97886
|
-
});
|
|
97887
97936
|
searchStudents = requireGameManagementAccess(async (ctx) => {
|
|
97888
97937
|
const gameId = ctx.params.gameId;
|
|
97889
97938
|
const courseId = ctx.params.courseId;
|
|
@@ -97919,6 +97968,17 @@ var init_timeback_controller = __esm(() => {
|
|
|
97919
97968
|
});
|
|
97920
97969
|
return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
|
|
97921
97970
|
});
|
|
97971
|
+
reactivateEnrollment = requireGameManagementAccess(async (ctx) => {
|
|
97972
|
+
const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
|
|
97973
|
+
logger65.debug("Reactivating enrollment", {
|
|
97974
|
+
requesterId: ctx.user.id,
|
|
97975
|
+
gameId: body2.gameId,
|
|
97976
|
+
courseId: body2.courseId,
|
|
97977
|
+
studentId: body2.studentId,
|
|
97978
|
+
enrollmentId: body2.enrollmentId
|
|
97979
|
+
});
|
|
97980
|
+
return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
|
|
97981
|
+
});
|
|
97922
97982
|
listAssessments = requireGameManagementAccess(async (ctx) => {
|
|
97923
97983
|
const { gameId, courseId } = ctx.params;
|
|
97924
97984
|
if (!gameId || !courseId) {
|
|
@@ -98064,10 +98124,10 @@ var init_timeback_controller = __esm(() => {
|
|
|
98064
98124
|
grantXp,
|
|
98065
98125
|
adjustTime,
|
|
98066
98126
|
adjustMastery,
|
|
98067
|
-
toggleCompletion,
|
|
98068
98127
|
searchStudents,
|
|
98069
98128
|
enrollStudent,
|
|
98070
98129
|
unenrollStudent,
|
|
98130
|
+
reactivateEnrollment,
|
|
98071
98131
|
listAssessments,
|
|
98072
98132
|
createAssessment,
|
|
98073
98133
|
deleteAssessment,
|