@playcademy/sandbox 0.4.0 → 0.4.1-beta.1
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 +253 -15
- package/dist/constants.js +1 -0
- package/dist/server.js +253 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -248,6 +248,7 @@ var init_timeback2 = __esm(() => {
|
|
|
248
248
|
TIMEBACK_ROUTES = {
|
|
249
249
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
250
250
|
GET_XP: "/integrations/timeback/xp",
|
|
251
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
251
252
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
252
253
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
253
254
|
};
|
|
@@ -1064,7 +1065,7 @@ var package_default;
|
|
|
1064
1065
|
var init_package = __esm(() => {
|
|
1065
1066
|
package_default = {
|
|
1066
1067
|
name: "@playcademy/sandbox",
|
|
1067
|
-
version: "0.4.
|
|
1068
|
+
version: "0.4.1-beta.1",
|
|
1068
1069
|
description: "Local development server for Playcademy game development",
|
|
1069
1070
|
type: "module",
|
|
1070
1071
|
exports: {
|
|
@@ -28234,6 +28235,7 @@ var init_constants3 = __esm(() => {
|
|
|
28234
28235
|
TIMEBACK: {
|
|
28235
28236
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28236
28237
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28238
|
+
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28237
28239
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28238
28240
|
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
|
|
28239
28241
|
}
|
|
@@ -29958,7 +29960,11 @@ var init_schemas4 = __esm(() => {
|
|
|
29958
29960
|
}).optional(),
|
|
29959
29961
|
xpEarned: exports_external.number().optional(),
|
|
29960
29962
|
masteredUnits: exports_external.number().optional(),
|
|
29963
|
+
masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
|
|
29961
29964
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
29965
|
+
}).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
|
|
29966
|
+
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
29967
|
+
path: ["masteredUnitsAbsolute"]
|
|
29962
29968
|
});
|
|
29963
29969
|
GameActivityMetricsSchema = exports_external.object({
|
|
29964
29970
|
activityId: exports_external.string().min(1),
|
|
@@ -30329,7 +30335,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
30329
30335
|
subject: integration.subject,
|
|
30330
30336
|
courseId: integration.courseId,
|
|
30331
30337
|
orgId: courseToSchool.get(integration.courseId),
|
|
30332
|
-
...enrollment ? {
|
|
30338
|
+
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
30333
30339
|
};
|
|
30334
30340
|
});
|
|
30335
30341
|
}
|
|
@@ -32886,6 +32892,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32886
32892
|
sessionTimingData,
|
|
32887
32893
|
xpEarned,
|
|
32888
32894
|
masteredUnits,
|
|
32895
|
+
masteredUnitsAbsolute,
|
|
32889
32896
|
extensions,
|
|
32890
32897
|
user
|
|
32891
32898
|
}) {
|
|
@@ -32909,6 +32916,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32909
32916
|
durationSeconds: timingData.durationSeconds,
|
|
32910
32917
|
xpEarned,
|
|
32911
32918
|
masteredUnits,
|
|
32919
|
+
masteredUnitsAbsolute,
|
|
32912
32920
|
extensions: extensionsWithResumeId,
|
|
32913
32921
|
activityId: activityData.activityId,
|
|
32914
32922
|
activityName: activityData.activityName,
|
|
@@ -33157,6 +33165,46 @@ var init_timeback_service = __esm(() => {
|
|
|
33157
33165
|
});
|
|
33158
33166
|
return result;
|
|
33159
33167
|
}
|
|
33168
|
+
async getStudentMastery(timebackId, user, options) {
|
|
33169
|
+
const client = this.requireClient();
|
|
33170
|
+
const db2 = this.deps.db;
|
|
33171
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
33172
|
+
const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
|
|
33173
|
+
if (options.grade !== undefined && options.subject) {
|
|
33174
|
+
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
33175
|
+
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
33176
|
+
}
|
|
33177
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
33178
|
+
where: and(...conditions2)
|
|
33179
|
+
});
|
|
33180
|
+
const courseIds = integrations.map((i2) => i2.courseId);
|
|
33181
|
+
if (courseIds.length === 0) {
|
|
33182
|
+
logger20.debug("No integrations found for game, returning empty mastery", {
|
|
33183
|
+
timebackId,
|
|
33184
|
+
gameId: options.gameId,
|
|
33185
|
+
grade: options.grade,
|
|
33186
|
+
subject: options.subject
|
|
33187
|
+
});
|
|
33188
|
+
return {
|
|
33189
|
+
totalMasteredUnits: 0,
|
|
33190
|
+
totalMasterableUnits: 0,
|
|
33191
|
+
...options.include?.perCourse && { courses: [] }
|
|
33192
|
+
};
|
|
33193
|
+
}
|
|
33194
|
+
const result = await client.getStudentMastery(timebackId, {
|
|
33195
|
+
courseIds,
|
|
33196
|
+
include: options.include
|
|
33197
|
+
});
|
|
33198
|
+
logger20.debug("Retrieved student mastery", {
|
|
33199
|
+
timebackId,
|
|
33200
|
+
gameId: options.gameId,
|
|
33201
|
+
grade: options.grade,
|
|
33202
|
+
subject: options.subject,
|
|
33203
|
+
totalMasteredUnits: result.totalMasteredUnits,
|
|
33204
|
+
courseCount: result.courses?.length
|
|
33205
|
+
});
|
|
33206
|
+
return result;
|
|
33207
|
+
}
|
|
33160
33208
|
};
|
|
33161
33209
|
});
|
|
33162
33210
|
|
|
@@ -35482,15 +35530,18 @@ class MasteryTracker {
|
|
|
35482
35530
|
this.edubridgeNamespace = edubridgeNamespace;
|
|
35483
35531
|
}
|
|
35484
35532
|
async checkProgress(input) {
|
|
35485
|
-
const { studentId, courseId, resourceId, masteredUnits } = input;
|
|
35486
|
-
|
|
35533
|
+
const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
|
|
35534
|
+
const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
|
|
35535
|
+
const hasAbsolute = typeof masteredUnitsAbsolute === "number";
|
|
35536
|
+
if (!hasIncremental && !hasAbsolute) {
|
|
35487
35537
|
return;
|
|
35488
35538
|
}
|
|
35489
35539
|
const status = await this.calculateStatus({
|
|
35490
35540
|
studentId,
|
|
35491
35541
|
courseId,
|
|
35492
35542
|
resourceId,
|
|
35493
|
-
additionalMasteredUnits: masteredUnits
|
|
35543
|
+
additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
|
|
35544
|
+
absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
|
|
35494
35545
|
});
|
|
35495
35546
|
if (!status) {
|
|
35496
35547
|
return;
|
|
@@ -35499,7 +35550,8 @@ class MasteryTracker {
|
|
|
35499
35550
|
return {
|
|
35500
35551
|
pctCompleteApp: status.pctCompleteApp,
|
|
35501
35552
|
masteryAchieved: !wasComplete && status.isComplete,
|
|
35502
|
-
masteryRevoked: wasComplete && !status.isComplete
|
|
35553
|
+
masteryRevoked: wasComplete && !status.isComplete,
|
|
35554
|
+
effectiveDelta: status.effectiveDelta
|
|
35503
35555
|
};
|
|
35504
35556
|
}
|
|
35505
35557
|
async getStatus(input) {
|
|
@@ -35521,7 +35573,8 @@ class MasteryTracker {
|
|
|
35521
35573
|
studentId,
|
|
35522
35574
|
courseId,
|
|
35523
35575
|
resourceId,
|
|
35524
|
-
additionalMasteredUnits
|
|
35576
|
+
additionalMasteredUnits,
|
|
35577
|
+
absoluteMasteredUnits
|
|
35525
35578
|
}) {
|
|
35526
35579
|
const masterableUnits = await this.resolveMasterableUnits(resourceId);
|
|
35527
35580
|
if (!masterableUnits || masterableUnits <= 0) {
|
|
@@ -35540,7 +35593,15 @@ class MasteryTracker {
|
|
|
35540
35593
|
return;
|
|
35541
35594
|
}
|
|
35542
35595
|
const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
|
|
35543
|
-
|
|
35596
|
+
let totalMastered;
|
|
35597
|
+
let effectiveDelta;
|
|
35598
|
+
if (absoluteMasteredUnits !== undefined) {
|
|
35599
|
+
totalMastered = Math.max(0, absoluteMasteredUnits);
|
|
35600
|
+
effectiveDelta = totalMastered - historicalMasteredUnits;
|
|
35601
|
+
} else {
|
|
35602
|
+
effectiveDelta = additionalMasteredUnits;
|
|
35603
|
+
totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
|
|
35604
|
+
}
|
|
35544
35605
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
35545
35606
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
35546
35607
|
return {
|
|
@@ -35548,7 +35609,8 @@ class MasteryTracker {
|
|
|
35548
35609
|
masterableUnits,
|
|
35549
35610
|
pctCompleteApp,
|
|
35550
35611
|
isComplete: totalMastered >= masterableUnits,
|
|
35551
|
-
historicalMasteredUnits
|
|
35612
|
+
historicalMasteredUnits,
|
|
35613
|
+
effectiveDelta
|
|
35552
35614
|
};
|
|
35553
35615
|
}
|
|
35554
35616
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -35777,7 +35839,7 @@ class ProgressRecorder {
|
|
|
35777
35839
|
validateProgressData(progressData);
|
|
35778
35840
|
const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
|
|
35779
35841
|
const { id: studentId, email: studentEmail } = student;
|
|
35780
|
-
const { score, totalQuestions, correctQuestions, xpEarned,
|
|
35842
|
+
const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
|
|
35781
35843
|
const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
|
|
35782
35844
|
const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
|
|
35783
35845
|
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
|
|
@@ -35786,8 +35848,10 @@ class ProgressRecorder {
|
|
|
35786
35848
|
studentId,
|
|
35787
35849
|
courseId,
|
|
35788
35850
|
resourceId: ids.resource,
|
|
35789
|
-
masteredUnits: progressData.masteredUnits ?? 0
|
|
35851
|
+
masteredUnits: progressData.masteredUnits ?? 0,
|
|
35852
|
+
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
35790
35853
|
});
|
|
35854
|
+
const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
|
|
35791
35855
|
let pctCompleteApp;
|
|
35792
35856
|
let masteryAchieved = false;
|
|
35793
35857
|
let scoreStatus = SCORE_STATUS5.fullyGraded;
|
|
@@ -35815,7 +35879,7 @@ class ProgressRecorder {
|
|
|
35815
35879
|
appName: progressData.appName,
|
|
35816
35880
|
totalQuestions,
|
|
35817
35881
|
correctQuestions,
|
|
35818
|
-
masteredUnits,
|
|
35882
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35819
35883
|
pctCompleteApp
|
|
35820
35884
|
});
|
|
35821
35885
|
} else {
|
|
@@ -35853,7 +35917,7 @@ class ProgressRecorder {
|
|
|
35853
35917
|
totalQuestions,
|
|
35854
35918
|
correctQuestions,
|
|
35855
35919
|
xpEarned: calculatedXp,
|
|
35856
|
-
masteredUnits,
|
|
35920
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35857
35921
|
attemptNumber: currentAttemptNumber,
|
|
35858
35922
|
progressData,
|
|
35859
35923
|
extensions,
|
|
@@ -35862,7 +35926,7 @@ class ProgressRecorder {
|
|
|
35862
35926
|
return {
|
|
35863
35927
|
xpAwarded: calculatedXp,
|
|
35864
35928
|
attemptNumber: currentAttemptNumber,
|
|
35865
|
-
masteredUnitsApplied:
|
|
35929
|
+
masteredUnitsApplied: effectiveMasteredUnits,
|
|
35866
35930
|
pctCompleteApp,
|
|
35867
35931
|
scoreStatus,
|
|
35868
35932
|
inProgress
|
|
@@ -36397,6 +36461,65 @@ class TimebackClient {
|
|
|
36397
36461
|
resourceId: ids.resource
|
|
36398
36462
|
});
|
|
36399
36463
|
}
|
|
36464
|
+
async getStudentMastery(studentId, options) {
|
|
36465
|
+
await this._ensureAuthenticated();
|
|
36466
|
+
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
36467
|
+
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
36468
|
+
if (filteredEnrollments.length === 0) {
|
|
36469
|
+
return {
|
|
36470
|
+
totalMasteredUnits: 0,
|
|
36471
|
+
totalMasterableUnits: 0,
|
|
36472
|
+
...options?.include?.perCourse && { courses: [] }
|
|
36473
|
+
};
|
|
36474
|
+
}
|
|
36475
|
+
const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
|
|
36476
|
+
try {
|
|
36477
|
+
const ids = deriveSourcedIds2(enrollment.course.id);
|
|
36478
|
+
const status = await this.masteryTracker.getStatus({
|
|
36479
|
+
studentId,
|
|
36480
|
+
courseId: enrollment.course.id,
|
|
36481
|
+
resourceId: ids.resource
|
|
36482
|
+
});
|
|
36483
|
+
return { enrollment, status };
|
|
36484
|
+
} catch (error) {
|
|
36485
|
+
log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
|
|
36486
|
+
enrollmentId: enrollment.id,
|
|
36487
|
+
error
|
|
36488
|
+
});
|
|
36489
|
+
return { enrollment, status: undefined };
|
|
36490
|
+
}
|
|
36491
|
+
}));
|
|
36492
|
+
let totalMasteredUnits = 0;
|
|
36493
|
+
let totalMasterableUnits = 0;
|
|
36494
|
+
const courses = [];
|
|
36495
|
+
for (const { enrollment, status } of masteryResults) {
|
|
36496
|
+
const masteredUnits = status?.masteredUnits ?? 0;
|
|
36497
|
+
const masterableUnits = status?.masterableUnits ?? 0;
|
|
36498
|
+
totalMasteredUnits += masteredUnits;
|
|
36499
|
+
totalMasterableUnits += masterableUnits;
|
|
36500
|
+
if (options?.include?.perCourse) {
|
|
36501
|
+
const gradeStr = enrollment.course.grades?.[0];
|
|
36502
|
+
const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
|
|
36503
|
+
const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
|
|
36504
|
+
const subjectStr = enrollment.course.subjects?.[0];
|
|
36505
|
+
const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
|
|
36506
|
+
courses.push({
|
|
36507
|
+
grade,
|
|
36508
|
+
subject,
|
|
36509
|
+
title: enrollment.course.title,
|
|
36510
|
+
masteredUnits,
|
|
36511
|
+
masterableUnits,
|
|
36512
|
+
pctComplete: status?.pctCompleteApp ?? 0,
|
|
36513
|
+
isComplete: status?.isComplete ?? false
|
|
36514
|
+
});
|
|
36515
|
+
}
|
|
36516
|
+
}
|
|
36517
|
+
return {
|
|
36518
|
+
totalMasteredUnits,
|
|
36519
|
+
totalMasterableUnits,
|
|
36520
|
+
...options?.include?.perCourse && { courses }
|
|
36521
|
+
};
|
|
36522
|
+
}
|
|
36400
36523
|
async getStudentXp(studentId, options) {
|
|
36401
36524
|
await this._ensureAuthenticated();
|
|
36402
36525
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -93719,7 +93842,7 @@ var init_session_controller = __esm(() => {
|
|
|
93719
93842
|
});
|
|
93720
93843
|
|
|
93721
93844
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
93722
|
-
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
93845
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getStudentMastery, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
93723
93846
|
var init_timeback_controller = __esm(() => {
|
|
93724
93847
|
init_esm();
|
|
93725
93848
|
init_schemas_index();
|
|
@@ -93877,6 +94000,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93877
94000
|
sessionTimingData,
|
|
93878
94001
|
xpEarned,
|
|
93879
94002
|
masteredUnits,
|
|
94003
|
+
masteredUnitsAbsolute,
|
|
93880
94004
|
extensions
|
|
93881
94005
|
} = body2;
|
|
93882
94006
|
logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
|
|
@@ -93891,6 +94015,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93891
94015
|
sessionTimingData,
|
|
93892
94016
|
xpEarned,
|
|
93893
94017
|
masteredUnits,
|
|
94018
|
+
masteredUnitsAbsolute,
|
|
93894
94019
|
extensions,
|
|
93895
94020
|
user: ctx.user
|
|
93896
94021
|
});
|
|
@@ -94000,6 +94125,53 @@ var init_timeback_controller = __esm(() => {
|
|
|
94000
94125
|
include
|
|
94001
94126
|
});
|
|
94002
94127
|
});
|
|
94128
|
+
getStudentMastery = requireDeveloper(async (ctx) => {
|
|
94129
|
+
const timebackId = ctx.params.timebackId;
|
|
94130
|
+
if (!timebackId) {
|
|
94131
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
94132
|
+
}
|
|
94133
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
94134
|
+
if (!gameId) {
|
|
94135
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
94136
|
+
}
|
|
94137
|
+
const gradeParam = ctx.url.searchParams.get("grade");
|
|
94138
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
94139
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
94140
|
+
throw ApiError.badRequest("Both grade and subject must be provided together");
|
|
94141
|
+
}
|
|
94142
|
+
let grade;
|
|
94143
|
+
let subject;
|
|
94144
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
94145
|
+
const parsedGrade = parseInt(gradeParam, 10);
|
|
94146
|
+
if (!isTimebackGrade2(parsedGrade)) {
|
|
94147
|
+
throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
94148
|
+
}
|
|
94149
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
94150
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
94151
|
+
}
|
|
94152
|
+
grade = parsedGrade;
|
|
94153
|
+
subject = subjectParam;
|
|
94154
|
+
}
|
|
94155
|
+
const includeParam = ctx.url.searchParams.get("include");
|
|
94156
|
+
const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
|
|
94157
|
+
const include = {
|
|
94158
|
+
perCourse: includeOptions.includes("percourse")
|
|
94159
|
+
};
|
|
94160
|
+
logger45.debug("Getting student mastery", {
|
|
94161
|
+
requesterId: ctx.user.id,
|
|
94162
|
+
timebackId,
|
|
94163
|
+
gameId,
|
|
94164
|
+
grade,
|
|
94165
|
+
subject,
|
|
94166
|
+
include
|
|
94167
|
+
});
|
|
94168
|
+
return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
|
|
94169
|
+
gameId,
|
|
94170
|
+
grade,
|
|
94171
|
+
subject,
|
|
94172
|
+
include
|
|
94173
|
+
});
|
|
94174
|
+
});
|
|
94003
94175
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94004
94176
|
const gameId = ctx.params.gameId;
|
|
94005
94177
|
const courseId = ctx.params.courseId;
|
|
@@ -94332,6 +94504,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
94332
94504
|
heartbeat,
|
|
94333
94505
|
advanceCourse,
|
|
94334
94506
|
getStudentXp,
|
|
94507
|
+
getStudentMastery,
|
|
94335
94508
|
getRoster,
|
|
94336
94509
|
getStudentOverview,
|
|
94337
94510
|
getGameMetrics,
|
|
@@ -95218,6 +95391,7 @@ var init_timeback6 = __esm(() => {
|
|
|
95218
95391
|
init_controllers();
|
|
95219
95392
|
init_errors();
|
|
95220
95393
|
init_utils11();
|
|
95394
|
+
init_schemas_index();
|
|
95221
95395
|
init_api();
|
|
95222
95396
|
init_error_handler();
|
|
95223
95397
|
init_timeback5();
|
|
@@ -95297,6 +95471,14 @@ var init_timeback6 = __esm(() => {
|
|
|
95297
95471
|
}
|
|
95298
95472
|
if (gradeParam !== null && subjectParam !== null) {
|
|
95299
95473
|
const grade = parseInt(gradeParam, 10);
|
|
95474
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95475
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95476
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95477
|
+
}
|
|
95478
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95479
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95480
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95481
|
+
}
|
|
95300
95482
|
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95301
95483
|
}
|
|
95302
95484
|
const mockCourses = enrollments.map((e) => {
|
|
@@ -95321,6 +95503,62 @@ var init_timeback6 = __esm(() => {
|
|
|
95321
95503
|
}
|
|
95322
95504
|
return handle2(timeback2.getStudentXp)(c2);
|
|
95323
95505
|
});
|
|
95506
|
+
timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
|
|
95507
|
+
const user = c2.get("user");
|
|
95508
|
+
if (!user) {
|
|
95509
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
|
|
95510
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95511
|
+
}
|
|
95512
|
+
if (shouldMockTimeback()) {
|
|
95513
|
+
const url2 = new URL(c2.req.url);
|
|
95514
|
+
const gradeParam = url2.searchParams.get("grade");
|
|
95515
|
+
const subjectParam = url2.searchParams.get("subject");
|
|
95516
|
+
const includeParam = url2.searchParams.get("include") || "";
|
|
95517
|
+
const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
|
|
95518
|
+
const includePerCourse = includeOptions.includes("percourse");
|
|
95519
|
+
const db2 = c2.get("db");
|
|
95520
|
+
let enrollments = await getMockEnrollments(db2);
|
|
95521
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
95522
|
+
const error2 = ApiError.badRequest("Both grade and subject must be provided together");
|
|
95523
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95524
|
+
}
|
|
95525
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
95526
|
+
const grade = parseInt(gradeParam, 10);
|
|
95527
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95528
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95529
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95530
|
+
}
|
|
95531
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95532
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95533
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95534
|
+
}
|
|
95535
|
+
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95536
|
+
}
|
|
95537
|
+
const mockCourses = enrollments.map((e) => {
|
|
95538
|
+
const seed3 = hashCode(`${e.grade}-${e.subject}`);
|
|
95539
|
+
const masterableUnits = 5 + seed3 % 16;
|
|
95540
|
+
const masteredUnits = seed3 % (masterableUnits + 1);
|
|
95541
|
+
const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
|
|
95542
|
+
return {
|
|
95543
|
+
grade: e.grade,
|
|
95544
|
+
subject: e.subject,
|
|
95545
|
+
title: `${e.subject} ${formatGradeLabel(e.grade)}`,
|
|
95546
|
+
masteredUnits,
|
|
95547
|
+
masterableUnits,
|
|
95548
|
+
pctComplete,
|
|
95549
|
+
isComplete: masteredUnits >= masterableUnits
|
|
95550
|
+
};
|
|
95551
|
+
});
|
|
95552
|
+
const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
|
|
95553
|
+
const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
|
|
95554
|
+
return c2.json({
|
|
95555
|
+
totalMasteredUnits,
|
|
95556
|
+
totalMasterableUnits,
|
|
95557
|
+
...includePerCourse && { courses: mockCourses }
|
|
95558
|
+
});
|
|
95559
|
+
}
|
|
95560
|
+
return handle2(timeback2.getStudentMastery)(c2);
|
|
95561
|
+
});
|
|
95324
95562
|
});
|
|
95325
95563
|
|
|
95326
95564
|
// src/routes/integrations/lti.ts
|
package/dist/constants.js
CHANGED
|
@@ -83,6 +83,7 @@ var init_timeback = __esm(() => {
|
|
|
83
83
|
TIMEBACK_ROUTES = {
|
|
84
84
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
85
85
|
GET_XP: "/integrations/timeback/xp",
|
|
86
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
86
87
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
87
88
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
88
89
|
};
|
package/dist/server.js
CHANGED
|
@@ -247,6 +247,7 @@ var init_timeback2 = __esm(() => {
|
|
|
247
247
|
TIMEBACK_ROUTES = {
|
|
248
248
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
249
249
|
GET_XP: "/integrations/timeback/xp",
|
|
250
|
+
GET_MASTERY: "/integrations/timeback/mastery",
|
|
250
251
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
251
252
|
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
252
253
|
};
|
|
@@ -1063,7 +1064,7 @@ var package_default;
|
|
|
1063
1064
|
var init_package = __esm(() => {
|
|
1064
1065
|
package_default = {
|
|
1065
1066
|
name: "@playcademy/sandbox",
|
|
1066
|
-
version: "0.4.
|
|
1067
|
+
version: "0.4.1-beta.1",
|
|
1067
1068
|
description: "Local development server for Playcademy game development",
|
|
1068
1069
|
type: "module",
|
|
1069
1070
|
exports: {
|
|
@@ -28233,6 +28234,7 @@ var init_constants3 = __esm(() => {
|
|
|
28233
28234
|
TIMEBACK: {
|
|
28234
28235
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28235
28236
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28237
|
+
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28236
28238
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28237
28239
|
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`
|
|
28238
28240
|
}
|
|
@@ -29957,7 +29959,11 @@ var init_schemas4 = __esm(() => {
|
|
|
29957
29959
|
}).optional(),
|
|
29958
29960
|
xpEarned: exports_external.number().optional(),
|
|
29959
29961
|
masteredUnits: exports_external.number().optional(),
|
|
29962
|
+
masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
|
|
29960
29963
|
extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
29964
|
+
}).refine((data) => !(data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined), {
|
|
29965
|
+
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
29966
|
+
path: ["masteredUnitsAbsolute"]
|
|
29961
29967
|
});
|
|
29962
29968
|
GameActivityMetricsSchema = exports_external.object({
|
|
29963
29969
|
activityId: exports_external.string().min(1),
|
|
@@ -30328,7 +30334,7 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
|
|
|
30328
30334
|
subject: integration.subject,
|
|
30329
30335
|
courseId: integration.courseId,
|
|
30330
30336
|
orgId: courseToSchool.get(integration.courseId),
|
|
30331
|
-
...enrollment ? {
|
|
30337
|
+
...enrollment ? { id: enrollment.sourcedId } : {}
|
|
30332
30338
|
};
|
|
30333
30339
|
});
|
|
30334
30340
|
}
|
|
@@ -32885,6 +32891,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32885
32891
|
sessionTimingData,
|
|
32886
32892
|
xpEarned,
|
|
32887
32893
|
masteredUnits,
|
|
32894
|
+
masteredUnitsAbsolute,
|
|
32888
32895
|
extensions,
|
|
32889
32896
|
user
|
|
32890
32897
|
}) {
|
|
@@ -32908,6 +32915,7 @@ var init_timeback_service = __esm(() => {
|
|
|
32908
32915
|
durationSeconds: timingData.durationSeconds,
|
|
32909
32916
|
xpEarned,
|
|
32910
32917
|
masteredUnits,
|
|
32918
|
+
masteredUnitsAbsolute,
|
|
32911
32919
|
extensions: extensionsWithResumeId,
|
|
32912
32920
|
activityId: activityData.activityId,
|
|
32913
32921
|
activityName: activityData.activityName,
|
|
@@ -33156,6 +33164,46 @@ var init_timeback_service = __esm(() => {
|
|
|
33156
33164
|
});
|
|
33157
33165
|
return result;
|
|
33158
33166
|
}
|
|
33167
|
+
async getStudentMastery(timebackId, user, options) {
|
|
33168
|
+
const client = this.requireClient();
|
|
33169
|
+
const db2 = this.deps.db;
|
|
33170
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
33171
|
+
const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
|
|
33172
|
+
if (options.grade !== undefined && options.subject) {
|
|
33173
|
+
conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
|
|
33174
|
+
conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
|
|
33175
|
+
}
|
|
33176
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
33177
|
+
where: and(...conditions2)
|
|
33178
|
+
});
|
|
33179
|
+
const courseIds = integrations.map((i2) => i2.courseId);
|
|
33180
|
+
if (courseIds.length === 0) {
|
|
33181
|
+
logger20.debug("No integrations found for game, returning empty mastery", {
|
|
33182
|
+
timebackId,
|
|
33183
|
+
gameId: options.gameId,
|
|
33184
|
+
grade: options.grade,
|
|
33185
|
+
subject: options.subject
|
|
33186
|
+
});
|
|
33187
|
+
return {
|
|
33188
|
+
totalMasteredUnits: 0,
|
|
33189
|
+
totalMasterableUnits: 0,
|
|
33190
|
+
...options.include?.perCourse && { courses: [] }
|
|
33191
|
+
};
|
|
33192
|
+
}
|
|
33193
|
+
const result = await client.getStudentMastery(timebackId, {
|
|
33194
|
+
courseIds,
|
|
33195
|
+
include: options.include
|
|
33196
|
+
});
|
|
33197
|
+
logger20.debug("Retrieved student mastery", {
|
|
33198
|
+
timebackId,
|
|
33199
|
+
gameId: options.gameId,
|
|
33200
|
+
grade: options.grade,
|
|
33201
|
+
subject: options.subject,
|
|
33202
|
+
totalMasteredUnits: result.totalMasteredUnits,
|
|
33203
|
+
courseCount: result.courses?.length
|
|
33204
|
+
});
|
|
33205
|
+
return result;
|
|
33206
|
+
}
|
|
33159
33207
|
};
|
|
33160
33208
|
});
|
|
33161
33209
|
|
|
@@ -35481,15 +35529,18 @@ class MasteryTracker {
|
|
|
35481
35529
|
this.edubridgeNamespace = edubridgeNamespace;
|
|
35482
35530
|
}
|
|
35483
35531
|
async checkProgress(input) {
|
|
35484
|
-
const { studentId, courseId, resourceId, masteredUnits } = input;
|
|
35485
|
-
|
|
35532
|
+
const { studentId, courseId, resourceId, masteredUnits, masteredUnitsAbsolute } = input;
|
|
35533
|
+
const hasIncremental = typeof masteredUnits === "number" && masteredUnits !== 0;
|
|
35534
|
+
const hasAbsolute = typeof masteredUnitsAbsolute === "number";
|
|
35535
|
+
if (!hasIncremental && !hasAbsolute) {
|
|
35486
35536
|
return;
|
|
35487
35537
|
}
|
|
35488
35538
|
const status = await this.calculateStatus({
|
|
35489
35539
|
studentId,
|
|
35490
35540
|
courseId,
|
|
35491
35541
|
resourceId,
|
|
35492
|
-
additionalMasteredUnits: masteredUnits
|
|
35542
|
+
additionalMasteredUnits: hasAbsolute ? 0 : masteredUnits,
|
|
35543
|
+
absoluteMasteredUnits: hasAbsolute ? masteredUnitsAbsolute : undefined
|
|
35493
35544
|
});
|
|
35494
35545
|
if (!status) {
|
|
35495
35546
|
return;
|
|
@@ -35498,7 +35549,8 @@ class MasteryTracker {
|
|
|
35498
35549
|
return {
|
|
35499
35550
|
pctCompleteApp: status.pctCompleteApp,
|
|
35500
35551
|
masteryAchieved: !wasComplete && status.isComplete,
|
|
35501
|
-
masteryRevoked: wasComplete && !status.isComplete
|
|
35552
|
+
masteryRevoked: wasComplete && !status.isComplete,
|
|
35553
|
+
effectiveDelta: status.effectiveDelta
|
|
35502
35554
|
};
|
|
35503
35555
|
}
|
|
35504
35556
|
async getStatus(input) {
|
|
@@ -35520,7 +35572,8 @@ class MasteryTracker {
|
|
|
35520
35572
|
studentId,
|
|
35521
35573
|
courseId,
|
|
35522
35574
|
resourceId,
|
|
35523
|
-
additionalMasteredUnits
|
|
35575
|
+
additionalMasteredUnits,
|
|
35576
|
+
absoluteMasteredUnits
|
|
35524
35577
|
}) {
|
|
35525
35578
|
const masterableUnits = await this.resolveMasterableUnits(resourceId);
|
|
35526
35579
|
if (!masterableUnits || masterableUnits <= 0) {
|
|
@@ -35539,7 +35592,15 @@ class MasteryTracker {
|
|
|
35539
35592
|
return;
|
|
35540
35593
|
}
|
|
35541
35594
|
const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
|
|
35542
|
-
|
|
35595
|
+
let totalMastered;
|
|
35596
|
+
let effectiveDelta;
|
|
35597
|
+
if (absoluteMasteredUnits !== undefined) {
|
|
35598
|
+
totalMastered = Math.max(0, absoluteMasteredUnits);
|
|
35599
|
+
effectiveDelta = totalMastered - historicalMasteredUnits;
|
|
35600
|
+
} else {
|
|
35601
|
+
effectiveDelta = additionalMasteredUnits;
|
|
35602
|
+
totalMastered = Math.max(0, historicalMasteredUnits + effectiveDelta);
|
|
35603
|
+
}
|
|
35543
35604
|
const rawPct = totalMastered / masterableUnits * 100;
|
|
35544
35605
|
const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
|
|
35545
35606
|
return {
|
|
@@ -35547,7 +35608,8 @@ class MasteryTracker {
|
|
|
35547
35608
|
masterableUnits,
|
|
35548
35609
|
pctCompleteApp,
|
|
35549
35610
|
isComplete: totalMastered >= masterableUnits,
|
|
35550
|
-
historicalMasteredUnits
|
|
35611
|
+
historicalMasteredUnits,
|
|
35612
|
+
effectiveDelta
|
|
35551
35613
|
};
|
|
35552
35614
|
}
|
|
35553
35615
|
async createCompletionEntry(studentId, courseId, classId, appName) {
|
|
@@ -35776,7 +35838,7 @@ class ProgressRecorder {
|
|
|
35776
35838
|
validateProgressData(progressData);
|
|
35777
35839
|
const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
|
|
35778
35840
|
const { id: studentId, email: studentEmail } = student;
|
|
35779
|
-
const { score, totalQuestions, correctQuestions, xpEarned,
|
|
35841
|
+
const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
|
|
35780
35842
|
const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
|
|
35781
35843
|
const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
|
|
35782
35844
|
const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
|
|
@@ -35785,8 +35847,10 @@ class ProgressRecorder {
|
|
|
35785
35847
|
studentId,
|
|
35786
35848
|
courseId,
|
|
35787
35849
|
resourceId: ids.resource,
|
|
35788
|
-
masteredUnits: progressData.masteredUnits ?? 0
|
|
35850
|
+
masteredUnits: progressData.masteredUnits ?? 0,
|
|
35851
|
+
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
35789
35852
|
});
|
|
35853
|
+
const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
|
|
35790
35854
|
let pctCompleteApp;
|
|
35791
35855
|
let masteryAchieved = false;
|
|
35792
35856
|
let scoreStatus = SCORE_STATUS5.fullyGraded;
|
|
@@ -35814,7 +35878,7 @@ class ProgressRecorder {
|
|
|
35814
35878
|
appName: progressData.appName,
|
|
35815
35879
|
totalQuestions,
|
|
35816
35880
|
correctQuestions,
|
|
35817
|
-
masteredUnits,
|
|
35881
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35818
35882
|
pctCompleteApp
|
|
35819
35883
|
});
|
|
35820
35884
|
} else {
|
|
@@ -35852,7 +35916,7 @@ class ProgressRecorder {
|
|
|
35852
35916
|
totalQuestions,
|
|
35853
35917
|
correctQuestions,
|
|
35854
35918
|
xpEarned: calculatedXp,
|
|
35855
|
-
masteredUnits,
|
|
35919
|
+
masteredUnits: effectiveMasteredUnits || undefined,
|
|
35856
35920
|
attemptNumber: currentAttemptNumber,
|
|
35857
35921
|
progressData,
|
|
35858
35922
|
extensions,
|
|
@@ -35861,7 +35925,7 @@ class ProgressRecorder {
|
|
|
35861
35925
|
return {
|
|
35862
35926
|
xpAwarded: calculatedXp,
|
|
35863
35927
|
attemptNumber: currentAttemptNumber,
|
|
35864
|
-
masteredUnitsApplied:
|
|
35928
|
+
masteredUnitsApplied: effectiveMasteredUnits,
|
|
35865
35929
|
pctCompleteApp,
|
|
35866
35930
|
scoreStatus,
|
|
35867
35931
|
inProgress
|
|
@@ -36396,6 +36460,65 @@ class TimebackClient {
|
|
|
36396
36460
|
resourceId: ids.resource
|
|
36397
36461
|
});
|
|
36398
36462
|
}
|
|
36463
|
+
async getStudentMastery(studentId, options) {
|
|
36464
|
+
await this._ensureAuthenticated();
|
|
36465
|
+
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
36466
|
+
const filteredEnrollments = options?.courseIds?.length ? enrollments.filter((e) => options.courseIds.includes(e.course.id)) : enrollments;
|
|
36467
|
+
if (filteredEnrollments.length === 0) {
|
|
36468
|
+
return {
|
|
36469
|
+
totalMasteredUnits: 0,
|
|
36470
|
+
totalMasterableUnits: 0,
|
|
36471
|
+
...options?.include?.perCourse && { courses: [] }
|
|
36472
|
+
};
|
|
36473
|
+
}
|
|
36474
|
+
const masteryResults = await Promise.all(filteredEnrollments.map(async (enrollment) => {
|
|
36475
|
+
try {
|
|
36476
|
+
const ids = deriveSourcedIds2(enrollment.course.id);
|
|
36477
|
+
const status = await this.masteryTracker.getStatus({
|
|
36478
|
+
studentId,
|
|
36479
|
+
courseId: enrollment.course.id,
|
|
36480
|
+
resourceId: ids.resource
|
|
36481
|
+
});
|
|
36482
|
+
return { enrollment, status };
|
|
36483
|
+
} catch (error) {
|
|
36484
|
+
log.warn("[TimebackClient] Failed to fetch mastery for enrollment", {
|
|
36485
|
+
enrollmentId: enrollment.id,
|
|
36486
|
+
error
|
|
36487
|
+
});
|
|
36488
|
+
return { enrollment, status: undefined };
|
|
36489
|
+
}
|
|
36490
|
+
}));
|
|
36491
|
+
let totalMasteredUnits = 0;
|
|
36492
|
+
let totalMasterableUnits = 0;
|
|
36493
|
+
const courses = [];
|
|
36494
|
+
for (const { enrollment, status } of masteryResults) {
|
|
36495
|
+
const masteredUnits = status?.masteredUnits ?? 0;
|
|
36496
|
+
const masterableUnits = status?.masterableUnits ?? 0;
|
|
36497
|
+
totalMasteredUnits += masteredUnits;
|
|
36498
|
+
totalMasterableUnits += masterableUnits;
|
|
36499
|
+
if (options?.include?.perCourse) {
|
|
36500
|
+
const gradeStr = enrollment.course.grades?.[0];
|
|
36501
|
+
const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
|
|
36502
|
+
const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
|
|
36503
|
+
const subjectStr = enrollment.course.subjects?.[0];
|
|
36504
|
+
const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
|
|
36505
|
+
courses.push({
|
|
36506
|
+
grade,
|
|
36507
|
+
subject,
|
|
36508
|
+
title: enrollment.course.title,
|
|
36509
|
+
masteredUnits,
|
|
36510
|
+
masterableUnits,
|
|
36511
|
+
pctComplete: status?.pctCompleteApp ?? 0,
|
|
36512
|
+
isComplete: status?.isComplete ?? false
|
|
36513
|
+
});
|
|
36514
|
+
}
|
|
36515
|
+
}
|
|
36516
|
+
return {
|
|
36517
|
+
totalMasteredUnits,
|
|
36518
|
+
totalMasterableUnits,
|
|
36519
|
+
...options?.include?.perCourse && { courses }
|
|
36520
|
+
};
|
|
36521
|
+
}
|
|
36399
36522
|
async getStudentXp(studentId, options) {
|
|
36400
36523
|
await this._ensureAuthenticated();
|
|
36401
36524
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -93718,7 +93841,7 @@ var init_session_controller = __esm(() => {
|
|
|
93718
93841
|
});
|
|
93719
93842
|
|
|
93720
93843
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
93721
|
-
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
93844
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, getStudentXp, getStudentMastery, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
93722
93845
|
var init_timeback_controller = __esm(() => {
|
|
93723
93846
|
init_esm();
|
|
93724
93847
|
init_schemas_index();
|
|
@@ -93876,6 +93999,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93876
93999
|
sessionTimingData,
|
|
93877
94000
|
xpEarned,
|
|
93878
94001
|
masteredUnits,
|
|
94002
|
+
masteredUnitsAbsolute,
|
|
93879
94003
|
extensions
|
|
93880
94004
|
} = body2;
|
|
93881
94005
|
logger45.debug("Ending activity", { userId: ctx.user.id, gameId });
|
|
@@ -93890,6 +94014,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
93890
94014
|
sessionTimingData,
|
|
93891
94015
|
xpEarned,
|
|
93892
94016
|
masteredUnits,
|
|
94017
|
+
masteredUnitsAbsolute,
|
|
93893
94018
|
extensions,
|
|
93894
94019
|
user: ctx.user
|
|
93895
94020
|
});
|
|
@@ -93999,6 +94124,53 @@ var init_timeback_controller = __esm(() => {
|
|
|
93999
94124
|
include
|
|
94000
94125
|
});
|
|
94001
94126
|
});
|
|
94127
|
+
getStudentMastery = requireDeveloper(async (ctx) => {
|
|
94128
|
+
const timebackId = ctx.params.timebackId;
|
|
94129
|
+
if (!timebackId) {
|
|
94130
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
94131
|
+
}
|
|
94132
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
94133
|
+
if (!gameId) {
|
|
94134
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
94135
|
+
}
|
|
94136
|
+
const gradeParam = ctx.url.searchParams.get("grade");
|
|
94137
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
94138
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
94139
|
+
throw ApiError.badRequest("Both grade and subject must be provided together");
|
|
94140
|
+
}
|
|
94141
|
+
let grade;
|
|
94142
|
+
let subject;
|
|
94143
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
94144
|
+
const parsedGrade = parseInt(gradeParam, 10);
|
|
94145
|
+
if (!isTimebackGrade2(parsedGrade)) {
|
|
94146
|
+
throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
94147
|
+
}
|
|
94148
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
94149
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
94150
|
+
}
|
|
94151
|
+
grade = parsedGrade;
|
|
94152
|
+
subject = subjectParam;
|
|
94153
|
+
}
|
|
94154
|
+
const includeParam = ctx.url.searchParams.get("include");
|
|
94155
|
+
const includeOptions = includeParam ? includeParam.split(",").map((opt) => opt.trim().toLowerCase()) : [];
|
|
94156
|
+
const include = {
|
|
94157
|
+
perCourse: includeOptions.includes("percourse")
|
|
94158
|
+
};
|
|
94159
|
+
logger45.debug("Getting student mastery", {
|
|
94160
|
+
requesterId: ctx.user.id,
|
|
94161
|
+
timebackId,
|
|
94162
|
+
gameId,
|
|
94163
|
+
grade,
|
|
94164
|
+
subject,
|
|
94165
|
+
include
|
|
94166
|
+
});
|
|
94167
|
+
return ctx.services.timeback.getStudentMastery(timebackId, ctx.user, {
|
|
94168
|
+
gameId,
|
|
94169
|
+
grade,
|
|
94170
|
+
subject,
|
|
94171
|
+
include
|
|
94172
|
+
});
|
|
94173
|
+
});
|
|
94002
94174
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94003
94175
|
const gameId = ctx.params.gameId;
|
|
94004
94176
|
const courseId = ctx.params.courseId;
|
|
@@ -94331,6 +94503,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
94331
94503
|
heartbeat,
|
|
94332
94504
|
advanceCourse,
|
|
94333
94505
|
getStudentXp,
|
|
94506
|
+
getStudentMastery,
|
|
94334
94507
|
getRoster,
|
|
94335
94508
|
getStudentOverview,
|
|
94336
94509
|
getGameMetrics,
|
|
@@ -95217,6 +95390,7 @@ var init_timeback6 = __esm(() => {
|
|
|
95217
95390
|
init_controllers();
|
|
95218
95391
|
init_errors();
|
|
95219
95392
|
init_utils11();
|
|
95393
|
+
init_schemas_index();
|
|
95220
95394
|
init_api();
|
|
95221
95395
|
init_error_handler();
|
|
95222
95396
|
init_timeback5();
|
|
@@ -95296,6 +95470,14 @@ var init_timeback6 = __esm(() => {
|
|
|
95296
95470
|
}
|
|
95297
95471
|
if (gradeParam !== null && subjectParam !== null) {
|
|
95298
95472
|
const grade = parseInt(gradeParam, 10);
|
|
95473
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95474
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95475
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95476
|
+
}
|
|
95477
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95478
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95479
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95480
|
+
}
|
|
95299
95481
|
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95300
95482
|
}
|
|
95301
95483
|
const mockCourses = enrollments.map((e) => {
|
|
@@ -95320,6 +95502,62 @@ var init_timeback6 = __esm(() => {
|
|
|
95320
95502
|
}
|
|
95321
95503
|
return handle2(timeback2.getStudentXp)(c2);
|
|
95322
95504
|
});
|
|
95505
|
+
timebackRouter.get("/student-mastery/:timebackId", async (c2) => {
|
|
95506
|
+
const user = c2.get("user");
|
|
95507
|
+
if (!user) {
|
|
95508
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student mastery");
|
|
95509
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95510
|
+
}
|
|
95511
|
+
if (shouldMockTimeback()) {
|
|
95512
|
+
const url2 = new URL(c2.req.url);
|
|
95513
|
+
const gradeParam = url2.searchParams.get("grade");
|
|
95514
|
+
const subjectParam = url2.searchParams.get("subject");
|
|
95515
|
+
const includeParam = url2.searchParams.get("include") || "";
|
|
95516
|
+
const includeOptions = includeParam.split(",").map((opt) => opt.trim().toLowerCase());
|
|
95517
|
+
const includePerCourse = includeOptions.includes("percourse");
|
|
95518
|
+
const db2 = c2.get("db");
|
|
95519
|
+
let enrollments = await getMockEnrollments(db2);
|
|
95520
|
+
if (gradeParam !== null !== (subjectParam !== null)) {
|
|
95521
|
+
const error2 = ApiError.badRequest("Both grade and subject must be provided together");
|
|
95522
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95523
|
+
}
|
|
95524
|
+
if (gradeParam !== null && subjectParam !== null) {
|
|
95525
|
+
const grade = parseInt(gradeParam, 10);
|
|
95526
|
+
if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
|
|
95527
|
+
const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
|
|
95528
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95529
|
+
}
|
|
95530
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
95531
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
95532
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
95533
|
+
}
|
|
95534
|
+
enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
|
|
95535
|
+
}
|
|
95536
|
+
const mockCourses = enrollments.map((e) => {
|
|
95537
|
+
const seed3 = hashCode(`${e.grade}-${e.subject}`);
|
|
95538
|
+
const masterableUnits = 5 + seed3 % 16;
|
|
95539
|
+
const masteredUnits = seed3 % (masterableUnits + 1);
|
|
95540
|
+
const pctComplete = masterableUnits > 0 ? Math.round(masteredUnits / masterableUnits * 1e4) / 100 : 0;
|
|
95541
|
+
return {
|
|
95542
|
+
grade: e.grade,
|
|
95543
|
+
subject: e.subject,
|
|
95544
|
+
title: `${e.subject} ${formatGradeLabel(e.grade)}`,
|
|
95545
|
+
masteredUnits,
|
|
95546
|
+
masterableUnits,
|
|
95547
|
+
pctComplete,
|
|
95548
|
+
isComplete: masteredUnits >= masterableUnits
|
|
95549
|
+
};
|
|
95550
|
+
});
|
|
95551
|
+
const totalMasteredUnits = mockCourses.reduce((sum2, course) => sum2 + course.masteredUnits, 0);
|
|
95552
|
+
const totalMasterableUnits = mockCourses.reduce((sum2, course) => sum2 + course.masterableUnits, 0);
|
|
95553
|
+
return c2.json({
|
|
95554
|
+
totalMasteredUnits,
|
|
95555
|
+
totalMasterableUnits,
|
|
95556
|
+
...includePerCourse && { courses: mockCourses }
|
|
95557
|
+
});
|
|
95558
|
+
}
|
|
95559
|
+
return handle2(timeback2.getStudentMastery)(c2);
|
|
95560
|
+
});
|
|
95323
95561
|
});
|
|
95324
95562
|
|
|
95325
95563
|
// src/routes/integrations/lti.ts
|