@playcademy/sandbox 0.4.1 → 0.4.2-beta.2
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 +247 -12
- package/dist/constants.js +3 -1
- package/dist/mocks/timeback.d.ts +1 -0
- package/dist/server.js +245 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -249,8 +249,10 @@ var init_timeback2 = __esm(() => {
|
|
|
249
249
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
250
250
|
GET_XP: "/integrations/timeback/xp",
|
|
251
251
|
GET_MASTERY: "/integrations/timeback/mastery",
|
|
252
|
+
GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
|
|
252
253
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
253
|
-
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
254
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course",
|
|
255
|
+
UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
|
|
254
256
|
};
|
|
255
257
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
256
258
|
gradingScheme: "STANDARD",
|
|
@@ -1076,7 +1078,7 @@ var package_default;
|
|
|
1076
1078
|
var init_package = __esm(() => {
|
|
1077
1079
|
package_default = {
|
|
1078
1080
|
name: "@playcademy/sandbox",
|
|
1079
|
-
version: "0.4.
|
|
1081
|
+
version: "0.4.2-beta.2",
|
|
1080
1082
|
description: "Local development server for Playcademy game development",
|
|
1081
1083
|
type: "module",
|
|
1082
1084
|
exports: {
|
|
@@ -28247,8 +28249,10 @@ var init_constants3 = __esm(() => {
|
|
|
28247
28249
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28248
28250
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28249
28251
|
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28252
|
+
GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
|
|
28250
28253
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28251
|
-
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}
|
|
28254
|
+
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
|
|
28255
|
+
UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
|
|
28252
28256
|
}
|
|
28253
28257
|
};
|
|
28254
28258
|
});
|
|
@@ -29126,6 +29130,7 @@ var init_utils6 = __esm(() => {
|
|
|
29126
29130
|
};
|
|
29127
29131
|
});
|
|
29128
29132
|
init_constants7();
|
|
29133
|
+
init_constants7();
|
|
29129
29134
|
if (process.env.DEBUG === "true") {
|
|
29130
29135
|
process.env.TERM = "dumb";
|
|
29131
29136
|
}
|
|
@@ -29902,7 +29907,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29902
29907
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29903
29908
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29904
29909
|
}
|
|
29905
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29910
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29906
29911
|
var init_schemas4 = __esm(() => {
|
|
29907
29912
|
init_drizzle_zod();
|
|
29908
29913
|
init_esm();
|
|
@@ -30004,6 +30009,12 @@ var init_schemas4 = __esm(() => {
|
|
|
30004
30009
|
studentId: exports_external.string().min(1),
|
|
30005
30010
|
subject: TimebackSubjectSchema.optional()
|
|
30006
30011
|
});
|
|
30012
|
+
UnenrollCourseRequestSchema = exports_external.object({
|
|
30013
|
+
gameId: exports_external.string().uuid(),
|
|
30014
|
+
studentId: exports_external.string().min(1),
|
|
30015
|
+
subject: TimebackSubjectSchema.optional(),
|
|
30016
|
+
force: exports_external.boolean().optional()
|
|
30017
|
+
});
|
|
30007
30018
|
HeartbeatRequestSchema = exports_external.object({
|
|
30008
30019
|
gameId: exports_external.string().uuid(),
|
|
30009
30020
|
studentId: exports_external.string().min(1),
|
|
@@ -33208,15 +33219,14 @@ var init_timeback_service = __esm(() => {
|
|
|
33208
33219
|
inProgress: result.inProgress
|
|
33209
33220
|
};
|
|
33210
33221
|
}
|
|
33211
|
-
async
|
|
33222
|
+
async resolveActiveGameCourse({
|
|
33212
33223
|
gameId,
|
|
33213
33224
|
studentId,
|
|
33214
33225
|
subject,
|
|
33215
|
-
|
|
33226
|
+
action
|
|
33216
33227
|
}) {
|
|
33217
33228
|
const client = this.requireClient();
|
|
33218
33229
|
const db2 = this.deps.db;
|
|
33219
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33220
33230
|
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
33221
33231
|
where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject)) : eq(gameTimebackIntegrations.gameId, gameId)
|
|
33222
33232
|
});
|
|
@@ -33231,12 +33241,30 @@ var init_timeback_service = __esm(() => {
|
|
|
33231
33241
|
}
|
|
33232
33242
|
const subjectsInPlay = new Set(enrolledIntegrations.map((i2) => i2.subject));
|
|
33233
33243
|
if (subjectsInPlay.size > 1) {
|
|
33234
|
-
throw new ValidationError(`Ambiguous Timeback
|
|
33244
|
+
throw new ValidationError(`Ambiguous Timeback ${action}: student is enrolled in ${subjectsInPlay.size} parallel ladders (${[...subjectsInPlay].join(", ")}); pass { subject } to disambiguate`);
|
|
33235
33245
|
}
|
|
33236
33246
|
const currentIntegration = enrolledIntegrations.toSorted((left, right) => left.grade - right.grade)[0];
|
|
33247
|
+
const currentEnrollment = enrollments.find((enrollment) => enrollment.course.id === currentIntegration.courseId);
|
|
33248
|
+
return { currentIntegration, currentEnrollment, enrollments };
|
|
33249
|
+
}
|
|
33250
|
+
async advanceCourse({
|
|
33251
|
+
gameId,
|
|
33252
|
+
studentId,
|
|
33253
|
+
subject,
|
|
33254
|
+
user
|
|
33255
|
+
}) {
|
|
33256
|
+
const client = this.requireClient();
|
|
33257
|
+
const db2 = this.deps.db;
|
|
33258
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33259
|
+
const { currentIntegration, enrollments } = await this.resolveActiveGameCourse({
|
|
33260
|
+
gameId,
|
|
33261
|
+
studentId,
|
|
33262
|
+
subject,
|
|
33263
|
+
action: "advance"
|
|
33264
|
+
});
|
|
33237
33265
|
const masteryStatus = await client.getMasteryStatus(currentIntegration.courseId, studentId);
|
|
33238
33266
|
if (!masteryStatus) {
|
|
33239
|
-
throw new ValidationError(`Cannot advance course: mastery status is unavailable for course ${currentIntegration.courseId}. Ensure the course has mastery configuration and the student has enrollment analytics before calling client.timeback.
|
|
33267
|
+
throw new ValidationError(`Cannot advance course: mastery status is unavailable for course ${currentIntegration.courseId}. Ensure the course has mastery configuration and the student has enrollment analytics before calling client.timeback.course.advance().`);
|
|
33240
33268
|
}
|
|
33241
33269
|
if (!masteryStatus.isComplete) {
|
|
33242
33270
|
const promotion2 = {
|
|
@@ -33274,6 +33302,93 @@ var init_timeback_service = __esm(() => {
|
|
|
33274
33302
|
});
|
|
33275
33303
|
return { status: "ok", promotion };
|
|
33276
33304
|
}
|
|
33305
|
+
async unenrollCourse({
|
|
33306
|
+
gameId,
|
|
33307
|
+
studentId,
|
|
33308
|
+
subject,
|
|
33309
|
+
force,
|
|
33310
|
+
user
|
|
33311
|
+
}) {
|
|
33312
|
+
const client = this.requireClient();
|
|
33313
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33314
|
+
const { currentIntegration, currentEnrollment } = await this.resolveActiveGameCourse({
|
|
33315
|
+
gameId,
|
|
33316
|
+
studentId,
|
|
33317
|
+
subject,
|
|
33318
|
+
action: "unenroll"
|
|
33319
|
+
});
|
|
33320
|
+
const schoolId = currentEnrollment.school.id;
|
|
33321
|
+
const masteryStatus = await client.getMasteryStatus(currentIntegration.courseId, studentId);
|
|
33322
|
+
if (!masteryStatus && !force) {
|
|
33323
|
+
throw new ValidationError(`Cannot unenroll course: mastery status is unavailable for course ${currentIntegration.courseId}. Pass { force: true } to unenroll without mastery data.`);
|
|
33324
|
+
}
|
|
33325
|
+
if (masteryStatus && !masteryStatus.isComplete && !force) {
|
|
33326
|
+
const unenrollment = {
|
|
33327
|
+
status: "not-mastered",
|
|
33328
|
+
currentCourseId: currentIntegration.courseId,
|
|
33329
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33330
|
+
masterableUnits: masteryStatus.masterableUnits
|
|
33331
|
+
};
|
|
33332
|
+
logger20.debug("Skipping game-initiated course unenroll because mastery is incomplete", {
|
|
33333
|
+
event: "timeback.course.unenroll",
|
|
33334
|
+
outcome: "not-mastered",
|
|
33335
|
+
gameId,
|
|
33336
|
+
studentId,
|
|
33337
|
+
courseId: currentIntegration.courseId,
|
|
33338
|
+
subject: currentIntegration.subject,
|
|
33339
|
+
grade: currentIntegration.grade,
|
|
33340
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33341
|
+
masterableUnits: masteryStatus.masterableUnits,
|
|
33342
|
+
forced: false,
|
|
33343
|
+
requesterUserId: user.id
|
|
33344
|
+
});
|
|
33345
|
+
return { status: "ok", unenrollment };
|
|
33346
|
+
}
|
|
33347
|
+
const forcedEarlyExit = Boolean(force && (!masteryStatus || !masteryStatus.isComplete));
|
|
33348
|
+
if (forcedEarlyExit) {
|
|
33349
|
+
logger20.warn("Force-unenrolled student before mastery completion", {
|
|
33350
|
+
event: "timeback.course.unenroll",
|
|
33351
|
+
outcome: "force-unenrolled",
|
|
33352
|
+
gameId,
|
|
33353
|
+
studentId,
|
|
33354
|
+
courseId: currentIntegration.courseId,
|
|
33355
|
+
subject: currentIntegration.subject,
|
|
33356
|
+
grade: currentIntegration.grade,
|
|
33357
|
+
masteredUnits: masteryStatus?.masteredUnits,
|
|
33358
|
+
masterableUnits: masteryStatus?.masterableUnits,
|
|
33359
|
+
masteryStatusAvailable: Boolean(masteryStatus),
|
|
33360
|
+
forced: true,
|
|
33361
|
+
requesterUserId: user.id
|
|
33362
|
+
});
|
|
33363
|
+
}
|
|
33364
|
+
await client.edubridge.enrollments.unenroll(studentId, currentIntegration.courseId, schoolId ? { schoolId } : {});
|
|
33365
|
+
client.invalidateEnrollments(studentId);
|
|
33366
|
+
logger20.info("Game-initiated course unenroll completed", {
|
|
33367
|
+
event: "timeback.course.unenroll",
|
|
33368
|
+
outcome: "unenrolled",
|
|
33369
|
+
gameId,
|
|
33370
|
+
studentId,
|
|
33371
|
+
courseId: currentIntegration.courseId,
|
|
33372
|
+
subject: currentIntegration.subject,
|
|
33373
|
+
grade: currentIntegration.grade,
|
|
33374
|
+
masteredUnits: masteryStatus?.masteredUnits,
|
|
33375
|
+
masterableUnits: masteryStatus?.masterableUnits,
|
|
33376
|
+
forced: forcedEarlyExit,
|
|
33377
|
+
requesterUserId: user.id
|
|
33378
|
+
});
|
|
33379
|
+
return {
|
|
33380
|
+
status: "ok",
|
|
33381
|
+
unenrollment: {
|
|
33382
|
+
status: "unenrolled",
|
|
33383
|
+
currentCourseId: currentIntegration.courseId,
|
|
33384
|
+
...masteryStatus ? {
|
|
33385
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33386
|
+
masterableUnits: masteryStatus.masterableUnits
|
|
33387
|
+
} : {},
|
|
33388
|
+
...forcedEarlyExit ? { forced: true } : {}
|
|
33389
|
+
}
|
|
33390
|
+
};
|
|
33391
|
+
}
|
|
33277
33392
|
async recordHeartbeat({
|
|
33278
33393
|
gameId,
|
|
33279
33394
|
studentId,
|
|
@@ -33448,6 +33563,25 @@ var init_timeback_service = __esm(() => {
|
|
|
33448
33563
|
});
|
|
33449
33564
|
return result;
|
|
33450
33565
|
}
|
|
33566
|
+
async getStudentHighestGradeMastered(timebackId, user, options) {
|
|
33567
|
+
const client = this.requireClient();
|
|
33568
|
+
const db2 = this.deps.db;
|
|
33569
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
33570
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
33571
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
|
|
33572
|
+
});
|
|
33573
|
+
if (!integration) {
|
|
33574
|
+
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
33575
|
+
}
|
|
33576
|
+
const result = await client.getHighestGradeMastered(timebackId, options.subject);
|
|
33577
|
+
logger20.debug("Retrieved student highest grade mastered", {
|
|
33578
|
+
timebackId,
|
|
33579
|
+
gameId: options.gameId,
|
|
33580
|
+
subject: options.subject,
|
|
33581
|
+
highestGradeMastered: result.highestGradeMastered
|
|
33582
|
+
});
|
|
33583
|
+
return result;
|
|
33584
|
+
}
|
|
33451
33585
|
};
|
|
33452
33586
|
});
|
|
33453
33587
|
|
|
@@ -34677,6 +34811,19 @@ async function getTimebackTokenResponse(config2) {
|
|
|
34677
34811
|
function getAuthUrl(environment = "production") {
|
|
34678
34812
|
return TIMEBACK_AUTH_URLS5[environment];
|
|
34679
34813
|
}
|
|
34814
|
+
function parseEduBridgeGrade(value) {
|
|
34815
|
+
if (value === null || value === undefined || value.trim() === "") {
|
|
34816
|
+
return null;
|
|
34817
|
+
}
|
|
34818
|
+
const parsed = Number(value);
|
|
34819
|
+
return isTimebackGrade3(parsed) ? parsed : null;
|
|
34820
|
+
}
|
|
34821
|
+
function normalizeHighestGradeMastered(response, subject) {
|
|
34822
|
+
return {
|
|
34823
|
+
subject,
|
|
34824
|
+
highestGradeMastered: parseEduBridgeGrade(response.grades.highestGradeOverall)
|
|
34825
|
+
};
|
|
34826
|
+
}
|
|
34680
34827
|
function handleHttpError(res, errorBody, attempt, retries, url2) {
|
|
34681
34828
|
const error = new TimebackApiError2(res.status, res.statusText, errorBody);
|
|
34682
34829
|
if (res.status >= HTTP_STATUS5.CLIENT_ERROR_MIN && res.status < HTTP_STATUS5.CLIENT_ERROR_MAX) {
|
|
@@ -35021,7 +35168,8 @@ function createEduBridgeNamespace(client) {
|
|
|
35021
35168
|
const analytics = {
|
|
35022
35169
|
getEnrollmentFacts: async (enrollmentId, options) => client["request"](buildPath(`/edubridge/analytics/enrollment/${enrollmentId}`, {
|
|
35023
35170
|
timezone: options?.timezone
|
|
35024
|
-
}), "GET")
|
|
35171
|
+
}), "GET"),
|
|
35172
|
+
getHighestGradeMastered: async (studentId, subject) => client["request"](`/edubridge/analytics/highestGradeMastered/${encodeURIComponent(studentId)}/${encodeURIComponent(subject)}`, "GET")
|
|
35025
35173
|
};
|
|
35026
35174
|
return {
|
|
35027
35175
|
enrollments,
|
|
@@ -36763,6 +36911,11 @@ class TimebackClient {
|
|
|
36763
36911
|
...options?.include?.perCourse && { courses }
|
|
36764
36912
|
};
|
|
36765
36913
|
}
|
|
36914
|
+
async getHighestGradeMastered(studentId, subject) {
|
|
36915
|
+
await this._ensureAuthenticated();
|
|
36916
|
+
const response = await this.edubridge.analytics.getHighestGradeMastered(studentId, subject);
|
|
36917
|
+
return normalizeHighestGradeMastered(response, subject);
|
|
36918
|
+
}
|
|
36766
36919
|
async getStudentXp(studentId, options) {
|
|
36767
36920
|
await this._ensureAuthenticated();
|
|
36768
36921
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -94085,7 +94238,7 @@ var init_session_controller = __esm(() => {
|
|
|
94085
94238
|
});
|
|
94086
94239
|
|
|
94087
94240
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
94088
|
-
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;
|
|
94241
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, 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;
|
|
94089
94242
|
var init_timeback_controller = __esm(() => {
|
|
94090
94243
|
init_esm();
|
|
94091
94244
|
init_schemas_index();
|
|
@@ -94323,6 +94476,20 @@ var init_timeback_controller = __esm(() => {
|
|
|
94323
94476
|
user: ctx.user
|
|
94324
94477
|
});
|
|
94325
94478
|
});
|
|
94479
|
+
unenrollCourse = requireDeveloper(async (ctx) => {
|
|
94480
|
+
const body2 = await parseRequestBody(ctx.request, UnenrollCourseRequestSchema);
|
|
94481
|
+
logger45.debug("Unenrolling student from current course", {
|
|
94482
|
+
userId: ctx.user.id,
|
|
94483
|
+
gameId: body2.gameId,
|
|
94484
|
+
studentId: body2.studentId,
|
|
94485
|
+
subject: body2.subject,
|
|
94486
|
+
force: body2.force
|
|
94487
|
+
});
|
|
94488
|
+
return ctx.services.timeback.unenrollCourse({
|
|
94489
|
+
...body2,
|
|
94490
|
+
user: ctx.user
|
|
94491
|
+
});
|
|
94492
|
+
});
|
|
94326
94493
|
getStudentXp = requireDeveloper(async (ctx) => {
|
|
94327
94494
|
const timebackId = ctx.params.timebackId;
|
|
94328
94495
|
if (!timebackId) {
|
|
@@ -94415,6 +94582,33 @@ var init_timeback_controller = __esm(() => {
|
|
|
94415
94582
|
include
|
|
94416
94583
|
});
|
|
94417
94584
|
});
|
|
94585
|
+
getStudentHighestGradeMastered = requireDeveloper(async (ctx) => {
|
|
94586
|
+
const timebackId = ctx.params.timebackId;
|
|
94587
|
+
if (!timebackId) {
|
|
94588
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
94589
|
+
}
|
|
94590
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
94591
|
+
if (!gameId) {
|
|
94592
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
94593
|
+
}
|
|
94594
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
94595
|
+
if (!subjectParam) {
|
|
94596
|
+
throw ApiError.badRequest("Missing required subject query parameter");
|
|
94597
|
+
}
|
|
94598
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
94599
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
94600
|
+
}
|
|
94601
|
+
logger45.debug("Getting student highest grade mastered", {
|
|
94602
|
+
requesterId: ctx.user.id,
|
|
94603
|
+
timebackId,
|
|
94604
|
+
gameId,
|
|
94605
|
+
subject: subjectParam
|
|
94606
|
+
});
|
|
94607
|
+
return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
|
|
94608
|
+
gameId,
|
|
94609
|
+
subject: subjectParam
|
|
94610
|
+
});
|
|
94611
|
+
});
|
|
94418
94612
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94419
94613
|
const gameId = ctx.params.gameId;
|
|
94420
94614
|
const courseId = ctx.params.courseId;
|
|
@@ -94750,8 +94944,10 @@ var init_timeback_controller = __esm(() => {
|
|
|
94750
94944
|
endActivity,
|
|
94751
94945
|
heartbeat,
|
|
94752
94946
|
advanceCourse,
|
|
94947
|
+
unenrollCourse,
|
|
94753
94948
|
getStudentXp,
|
|
94754
94949
|
getStudentMastery,
|
|
94950
|
+
getStudentHighestGradeMastered,
|
|
94755
94951
|
getRoster,
|
|
94756
94952
|
getStudentOverview,
|
|
94757
94953
|
getGameMetrics,
|
|
@@ -94960,6 +95156,10 @@ async function getMockTimebackUser(db2, gameId) {
|
|
|
94960
95156
|
const timebackId = config.timeback.timebackId || "mock-student-00000001";
|
|
94961
95157
|
return getMockTimebackData(db2, timebackId, gameId);
|
|
94962
95158
|
}
|
|
95159
|
+
function getMockHighestGradeMastered(enrollments, subject) {
|
|
95160
|
+
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
95161
|
+
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
95162
|
+
}
|
|
94963
95163
|
async function buildMockUserResponse(db2, user, gameId) {
|
|
94964
95164
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
94965
95165
|
if (gameId) {
|
|
@@ -95654,6 +95854,7 @@ var init_timeback6 = __esm(() => {
|
|
|
95654
95854
|
timebackRouter.post("/end-activity", handle2(timeback2.endActivity));
|
|
95655
95855
|
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
95656
95856
|
timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
|
|
95857
|
+
timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
|
|
95657
95858
|
timebackRouter.get("/user", async (c2) => {
|
|
95658
95859
|
const user = c2.get("user");
|
|
95659
95860
|
const gameId = c2.get("gameId");
|
|
@@ -95806,6 +96007,39 @@ var init_timeback6 = __esm(() => {
|
|
|
95806
96007
|
}
|
|
95807
96008
|
return handle2(timeback2.getStudentMastery)(c2);
|
|
95808
96009
|
});
|
|
96010
|
+
timebackRouter.get("/student-highest-grade-mastered/:timebackId", async (c2) => {
|
|
96011
|
+
const user = c2.get("user");
|
|
96012
|
+
if (!user) {
|
|
96013
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student highest grade mastered");
|
|
96014
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96015
|
+
}
|
|
96016
|
+
if (shouldMockTimeback()) {
|
|
96017
|
+
const url2 = new URL(c2.req.url);
|
|
96018
|
+
const subject = url2.searchParams.get("subject");
|
|
96019
|
+
const contextGameId = c2.get("gameId");
|
|
96020
|
+
const gameId = url2.searchParams.get("gameId") || (typeof contextGameId === "string" ? contextGameId : undefined);
|
|
96021
|
+
if (!subject) {
|
|
96022
|
+
const error2 = ApiError.badRequest("Missing required subject query parameter");
|
|
96023
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96024
|
+
}
|
|
96025
|
+
if (!gameId) {
|
|
96026
|
+
const error2 = ApiError.badRequest("Missing required gameId query parameter");
|
|
96027
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96028
|
+
}
|
|
96029
|
+
if (!isTimebackSubject2(subject)) {
|
|
96030
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
96031
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96032
|
+
}
|
|
96033
|
+
const db2 = c2.get("db");
|
|
96034
|
+
const enrollments = await getMockEnrollments(db2);
|
|
96035
|
+
const gameScopedEnrollments = filterEnrollmentsByGame(enrollments, gameId);
|
|
96036
|
+
return c2.json({
|
|
96037
|
+
subject,
|
|
96038
|
+
highestGradeMastered: getMockHighestGradeMastered(gameScopedEnrollments, subject)
|
|
96039
|
+
});
|
|
96040
|
+
}
|
|
96041
|
+
return handle2(timeback2.getStudentHighestGradeMastered)(c2);
|
|
96042
|
+
});
|
|
95809
96043
|
});
|
|
95810
96044
|
|
|
95811
96045
|
// src/routes/integrations/lti.ts
|
|
@@ -98183,7 +98417,8 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
|
|
|
98183
98417
|
port,
|
|
98184
98418
|
url: `http://localhost:${port}/api`,
|
|
98185
98419
|
startedAt: Date.now(),
|
|
98186
|
-
projectRoot: process.cwd()
|
|
98420
|
+
projectRoot: process.cwd(),
|
|
98421
|
+
gameId: server.gameId
|
|
98187
98422
|
});
|
|
98188
98423
|
const totalCourses = project?.timebackCourses?.length ?? 0;
|
|
98189
98424
|
const excludedCount = options.timebackExcludedCourses ? options.timebackExcludedCourses.split(",").filter(Boolean).length : 0;
|
package/dist/constants.js
CHANGED
|
@@ -84,8 +84,10 @@ var init_timeback = __esm(() => {
|
|
|
84
84
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
85
85
|
GET_XP: "/integrations/timeback/xp",
|
|
86
86
|
GET_MASTERY: "/integrations/timeback/mastery",
|
|
87
|
+
GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
|
|
87
88
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
88
|
-
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
89
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course",
|
|
90
|
+
UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
|
|
89
91
|
};
|
|
90
92
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
91
93
|
gradingScheme: "STANDARD",
|
package/dist/mocks/timeback.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function getMockTimebackData(db: DatabaseInstance, timebackId: st
|
|
|
30
30
|
* Uses the configured timebackId from sandbox config.
|
|
31
31
|
*/
|
|
32
32
|
export declare function getMockTimebackUser(db: DatabaseInstance, gameId?: string): Promise<UserTimebackData>;
|
|
33
|
+
export declare function getMockHighestGradeMastered(enrollments: UserEnrollment[], subject: string): number | null;
|
|
33
34
|
/**
|
|
34
35
|
* Build a complete user response with mock timeback data.
|
|
35
36
|
* Used to bypass api-core when in mock mode (avoids real API calls).
|
package/dist/server.js
CHANGED
|
@@ -248,8 +248,10 @@ var init_timeback2 = __esm(() => {
|
|
|
248
248
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
249
249
|
GET_XP: "/integrations/timeback/xp",
|
|
250
250
|
GET_MASTERY: "/integrations/timeback/mastery",
|
|
251
|
+
GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
|
|
251
252
|
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
252
|
-
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
253
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course",
|
|
254
|
+
UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
|
|
253
255
|
};
|
|
254
256
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
255
257
|
gradingScheme: "STANDARD",
|
|
@@ -1075,7 +1077,7 @@ var package_default;
|
|
|
1075
1077
|
var init_package = __esm(() => {
|
|
1076
1078
|
package_default = {
|
|
1077
1079
|
name: "@playcademy/sandbox",
|
|
1078
|
-
version: "0.4.
|
|
1080
|
+
version: "0.4.2-beta.2",
|
|
1079
1081
|
description: "Local development server for Playcademy game development",
|
|
1080
1082
|
type: "module",
|
|
1081
1083
|
exports: {
|
|
@@ -28246,8 +28248,10 @@ var init_constants3 = __esm(() => {
|
|
|
28246
28248
|
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
|
|
28247
28249
|
GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
|
|
28248
28250
|
GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
|
|
28251
|
+
GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
|
|
28249
28252
|
HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
|
|
28250
|
-
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}
|
|
28253
|
+
ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
|
|
28254
|
+
UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
|
|
28251
28255
|
}
|
|
28252
28256
|
};
|
|
28253
28257
|
});
|
|
@@ -29125,6 +29129,7 @@ var init_utils6 = __esm(() => {
|
|
|
29125
29129
|
};
|
|
29126
29130
|
});
|
|
29127
29131
|
init_constants7();
|
|
29132
|
+
init_constants7();
|
|
29128
29133
|
if (process.env.DEBUG === "true") {
|
|
29129
29134
|
process.env.TERM = "dumb";
|
|
29130
29135
|
}
|
|
@@ -29901,7 +29906,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29901
29906
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29902
29907
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29903
29908
|
}
|
|
29904
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29909
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29905
29910
|
var init_schemas4 = __esm(() => {
|
|
29906
29911
|
init_drizzle_zod();
|
|
29907
29912
|
init_esm();
|
|
@@ -30003,6 +30008,12 @@ var init_schemas4 = __esm(() => {
|
|
|
30003
30008
|
studentId: exports_external.string().min(1),
|
|
30004
30009
|
subject: TimebackSubjectSchema.optional()
|
|
30005
30010
|
});
|
|
30011
|
+
UnenrollCourseRequestSchema = exports_external.object({
|
|
30012
|
+
gameId: exports_external.string().uuid(),
|
|
30013
|
+
studentId: exports_external.string().min(1),
|
|
30014
|
+
subject: TimebackSubjectSchema.optional(),
|
|
30015
|
+
force: exports_external.boolean().optional()
|
|
30016
|
+
});
|
|
30006
30017
|
HeartbeatRequestSchema = exports_external.object({
|
|
30007
30018
|
gameId: exports_external.string().uuid(),
|
|
30008
30019
|
studentId: exports_external.string().min(1),
|
|
@@ -33207,15 +33218,14 @@ var init_timeback_service = __esm(() => {
|
|
|
33207
33218
|
inProgress: result.inProgress
|
|
33208
33219
|
};
|
|
33209
33220
|
}
|
|
33210
|
-
async
|
|
33221
|
+
async resolveActiveGameCourse({
|
|
33211
33222
|
gameId,
|
|
33212
33223
|
studentId,
|
|
33213
33224
|
subject,
|
|
33214
|
-
|
|
33225
|
+
action
|
|
33215
33226
|
}) {
|
|
33216
33227
|
const client = this.requireClient();
|
|
33217
33228
|
const db2 = this.deps.db;
|
|
33218
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33219
33229
|
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
33220
33230
|
where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject)) : eq(gameTimebackIntegrations.gameId, gameId)
|
|
33221
33231
|
});
|
|
@@ -33230,12 +33240,30 @@ var init_timeback_service = __esm(() => {
|
|
|
33230
33240
|
}
|
|
33231
33241
|
const subjectsInPlay = new Set(enrolledIntegrations.map((i2) => i2.subject));
|
|
33232
33242
|
if (subjectsInPlay.size > 1) {
|
|
33233
|
-
throw new ValidationError(`Ambiguous Timeback
|
|
33243
|
+
throw new ValidationError(`Ambiguous Timeback ${action}: student is enrolled in ${subjectsInPlay.size} parallel ladders (${[...subjectsInPlay].join(", ")}); pass { subject } to disambiguate`);
|
|
33234
33244
|
}
|
|
33235
33245
|
const currentIntegration = enrolledIntegrations.toSorted((left, right) => left.grade - right.grade)[0];
|
|
33246
|
+
const currentEnrollment = enrollments.find((enrollment) => enrollment.course.id === currentIntegration.courseId);
|
|
33247
|
+
return { currentIntegration, currentEnrollment, enrollments };
|
|
33248
|
+
}
|
|
33249
|
+
async advanceCourse({
|
|
33250
|
+
gameId,
|
|
33251
|
+
studentId,
|
|
33252
|
+
subject,
|
|
33253
|
+
user
|
|
33254
|
+
}) {
|
|
33255
|
+
const client = this.requireClient();
|
|
33256
|
+
const db2 = this.deps.db;
|
|
33257
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33258
|
+
const { currentIntegration, enrollments } = await this.resolveActiveGameCourse({
|
|
33259
|
+
gameId,
|
|
33260
|
+
studentId,
|
|
33261
|
+
subject,
|
|
33262
|
+
action: "advance"
|
|
33263
|
+
});
|
|
33236
33264
|
const masteryStatus = await client.getMasteryStatus(currentIntegration.courseId, studentId);
|
|
33237
33265
|
if (!masteryStatus) {
|
|
33238
|
-
throw new ValidationError(`Cannot advance course: mastery status is unavailable for course ${currentIntegration.courseId}. Ensure the course has mastery configuration and the student has enrollment analytics before calling client.timeback.
|
|
33266
|
+
throw new ValidationError(`Cannot advance course: mastery status is unavailable for course ${currentIntegration.courseId}. Ensure the course has mastery configuration and the student has enrollment analytics before calling client.timeback.course.advance().`);
|
|
33239
33267
|
}
|
|
33240
33268
|
if (!masteryStatus.isComplete) {
|
|
33241
33269
|
const promotion2 = {
|
|
@@ -33273,6 +33301,93 @@ var init_timeback_service = __esm(() => {
|
|
|
33273
33301
|
});
|
|
33274
33302
|
return { status: "ok", promotion };
|
|
33275
33303
|
}
|
|
33304
|
+
async unenrollCourse({
|
|
33305
|
+
gameId,
|
|
33306
|
+
studentId,
|
|
33307
|
+
subject,
|
|
33308
|
+
force,
|
|
33309
|
+
user
|
|
33310
|
+
}) {
|
|
33311
|
+
const client = this.requireClient();
|
|
33312
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
33313
|
+
const { currentIntegration, currentEnrollment } = await this.resolveActiveGameCourse({
|
|
33314
|
+
gameId,
|
|
33315
|
+
studentId,
|
|
33316
|
+
subject,
|
|
33317
|
+
action: "unenroll"
|
|
33318
|
+
});
|
|
33319
|
+
const schoolId = currentEnrollment.school.id;
|
|
33320
|
+
const masteryStatus = await client.getMasteryStatus(currentIntegration.courseId, studentId);
|
|
33321
|
+
if (!masteryStatus && !force) {
|
|
33322
|
+
throw new ValidationError(`Cannot unenroll course: mastery status is unavailable for course ${currentIntegration.courseId}. Pass { force: true } to unenroll without mastery data.`);
|
|
33323
|
+
}
|
|
33324
|
+
if (masteryStatus && !masteryStatus.isComplete && !force) {
|
|
33325
|
+
const unenrollment = {
|
|
33326
|
+
status: "not-mastered",
|
|
33327
|
+
currentCourseId: currentIntegration.courseId,
|
|
33328
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33329
|
+
masterableUnits: masteryStatus.masterableUnits
|
|
33330
|
+
};
|
|
33331
|
+
logger20.debug("Skipping game-initiated course unenroll because mastery is incomplete", {
|
|
33332
|
+
event: "timeback.course.unenroll",
|
|
33333
|
+
outcome: "not-mastered",
|
|
33334
|
+
gameId,
|
|
33335
|
+
studentId,
|
|
33336
|
+
courseId: currentIntegration.courseId,
|
|
33337
|
+
subject: currentIntegration.subject,
|
|
33338
|
+
grade: currentIntegration.grade,
|
|
33339
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33340
|
+
masterableUnits: masteryStatus.masterableUnits,
|
|
33341
|
+
forced: false,
|
|
33342
|
+
requesterUserId: user.id
|
|
33343
|
+
});
|
|
33344
|
+
return { status: "ok", unenrollment };
|
|
33345
|
+
}
|
|
33346
|
+
const forcedEarlyExit = Boolean(force && (!masteryStatus || !masteryStatus.isComplete));
|
|
33347
|
+
if (forcedEarlyExit) {
|
|
33348
|
+
logger20.warn("Force-unenrolled student before mastery completion", {
|
|
33349
|
+
event: "timeback.course.unenroll",
|
|
33350
|
+
outcome: "force-unenrolled",
|
|
33351
|
+
gameId,
|
|
33352
|
+
studentId,
|
|
33353
|
+
courseId: currentIntegration.courseId,
|
|
33354
|
+
subject: currentIntegration.subject,
|
|
33355
|
+
grade: currentIntegration.grade,
|
|
33356
|
+
masteredUnits: masteryStatus?.masteredUnits,
|
|
33357
|
+
masterableUnits: masteryStatus?.masterableUnits,
|
|
33358
|
+
masteryStatusAvailable: Boolean(masteryStatus),
|
|
33359
|
+
forced: true,
|
|
33360
|
+
requesterUserId: user.id
|
|
33361
|
+
});
|
|
33362
|
+
}
|
|
33363
|
+
await client.edubridge.enrollments.unenroll(studentId, currentIntegration.courseId, schoolId ? { schoolId } : {});
|
|
33364
|
+
client.invalidateEnrollments(studentId);
|
|
33365
|
+
logger20.info("Game-initiated course unenroll completed", {
|
|
33366
|
+
event: "timeback.course.unenroll",
|
|
33367
|
+
outcome: "unenrolled",
|
|
33368
|
+
gameId,
|
|
33369
|
+
studentId,
|
|
33370
|
+
courseId: currentIntegration.courseId,
|
|
33371
|
+
subject: currentIntegration.subject,
|
|
33372
|
+
grade: currentIntegration.grade,
|
|
33373
|
+
masteredUnits: masteryStatus?.masteredUnits,
|
|
33374
|
+
masterableUnits: masteryStatus?.masterableUnits,
|
|
33375
|
+
forced: forcedEarlyExit,
|
|
33376
|
+
requesterUserId: user.id
|
|
33377
|
+
});
|
|
33378
|
+
return {
|
|
33379
|
+
status: "ok",
|
|
33380
|
+
unenrollment: {
|
|
33381
|
+
status: "unenrolled",
|
|
33382
|
+
currentCourseId: currentIntegration.courseId,
|
|
33383
|
+
...masteryStatus ? {
|
|
33384
|
+
masteredUnits: masteryStatus.masteredUnits,
|
|
33385
|
+
masterableUnits: masteryStatus.masterableUnits
|
|
33386
|
+
} : {},
|
|
33387
|
+
...forcedEarlyExit ? { forced: true } : {}
|
|
33388
|
+
}
|
|
33389
|
+
};
|
|
33390
|
+
}
|
|
33276
33391
|
async recordHeartbeat({
|
|
33277
33392
|
gameId,
|
|
33278
33393
|
studentId,
|
|
@@ -33447,6 +33562,25 @@ var init_timeback_service = __esm(() => {
|
|
|
33447
33562
|
});
|
|
33448
33563
|
return result;
|
|
33449
33564
|
}
|
|
33565
|
+
async getStudentHighestGradeMastered(timebackId, user, options) {
|
|
33566
|
+
const client = this.requireClient();
|
|
33567
|
+
const db2 = this.deps.db;
|
|
33568
|
+
await this.deps.validateDeveloperAccess(user, options.gameId);
|
|
33569
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
33570
|
+
where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
|
|
33571
|
+
});
|
|
33572
|
+
if (!integration) {
|
|
33573
|
+
throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
|
|
33574
|
+
}
|
|
33575
|
+
const result = await client.getHighestGradeMastered(timebackId, options.subject);
|
|
33576
|
+
logger20.debug("Retrieved student highest grade mastered", {
|
|
33577
|
+
timebackId,
|
|
33578
|
+
gameId: options.gameId,
|
|
33579
|
+
subject: options.subject,
|
|
33580
|
+
highestGradeMastered: result.highestGradeMastered
|
|
33581
|
+
});
|
|
33582
|
+
return result;
|
|
33583
|
+
}
|
|
33450
33584
|
};
|
|
33451
33585
|
});
|
|
33452
33586
|
|
|
@@ -34676,6 +34810,19 @@ async function getTimebackTokenResponse(config2) {
|
|
|
34676
34810
|
function getAuthUrl(environment = "production") {
|
|
34677
34811
|
return TIMEBACK_AUTH_URLS5[environment];
|
|
34678
34812
|
}
|
|
34813
|
+
function parseEduBridgeGrade(value) {
|
|
34814
|
+
if (value === null || value === undefined || value.trim() === "") {
|
|
34815
|
+
return null;
|
|
34816
|
+
}
|
|
34817
|
+
const parsed = Number(value);
|
|
34818
|
+
return isTimebackGrade3(parsed) ? parsed : null;
|
|
34819
|
+
}
|
|
34820
|
+
function normalizeHighestGradeMastered(response, subject) {
|
|
34821
|
+
return {
|
|
34822
|
+
subject,
|
|
34823
|
+
highestGradeMastered: parseEduBridgeGrade(response.grades.highestGradeOverall)
|
|
34824
|
+
};
|
|
34825
|
+
}
|
|
34679
34826
|
function handleHttpError(res, errorBody, attempt, retries, url2) {
|
|
34680
34827
|
const error = new TimebackApiError2(res.status, res.statusText, errorBody);
|
|
34681
34828
|
if (res.status >= HTTP_STATUS5.CLIENT_ERROR_MIN && res.status < HTTP_STATUS5.CLIENT_ERROR_MAX) {
|
|
@@ -35020,7 +35167,8 @@ function createEduBridgeNamespace(client) {
|
|
|
35020
35167
|
const analytics = {
|
|
35021
35168
|
getEnrollmentFacts: async (enrollmentId, options) => client["request"](buildPath(`/edubridge/analytics/enrollment/${enrollmentId}`, {
|
|
35022
35169
|
timezone: options?.timezone
|
|
35023
|
-
}), "GET")
|
|
35170
|
+
}), "GET"),
|
|
35171
|
+
getHighestGradeMastered: async (studentId, subject) => client["request"](`/edubridge/analytics/highestGradeMastered/${encodeURIComponent(studentId)}/${encodeURIComponent(subject)}`, "GET")
|
|
35024
35172
|
};
|
|
35025
35173
|
return {
|
|
35026
35174
|
enrollments,
|
|
@@ -36762,6 +36910,11 @@ class TimebackClient {
|
|
|
36762
36910
|
...options?.include?.perCourse && { courses }
|
|
36763
36911
|
};
|
|
36764
36912
|
}
|
|
36913
|
+
async getHighestGradeMastered(studentId, subject) {
|
|
36914
|
+
await this._ensureAuthenticated();
|
|
36915
|
+
const response = await this.edubridge.analytics.getHighestGradeMastered(studentId, subject);
|
|
36916
|
+
return normalizeHighestGradeMastered(response, subject);
|
|
36917
|
+
}
|
|
36765
36918
|
async getStudentXp(studentId, options) {
|
|
36766
36919
|
await this._ensureAuthenticated();
|
|
36767
36920
|
const enrollments = await this.edubridge.enrollments.listByUser(studentId);
|
|
@@ -94084,7 +94237,7 @@ var init_session_controller = __esm(() => {
|
|
|
94084
94237
|
});
|
|
94085
94238
|
|
|
94086
94239
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
94087
|
-
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;
|
|
94240
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, 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;
|
|
94088
94241
|
var init_timeback_controller = __esm(() => {
|
|
94089
94242
|
init_esm();
|
|
94090
94243
|
init_schemas_index();
|
|
@@ -94322,6 +94475,20 @@ var init_timeback_controller = __esm(() => {
|
|
|
94322
94475
|
user: ctx.user
|
|
94323
94476
|
});
|
|
94324
94477
|
});
|
|
94478
|
+
unenrollCourse = requireDeveloper(async (ctx) => {
|
|
94479
|
+
const body2 = await parseRequestBody(ctx.request, UnenrollCourseRequestSchema);
|
|
94480
|
+
logger45.debug("Unenrolling student from current course", {
|
|
94481
|
+
userId: ctx.user.id,
|
|
94482
|
+
gameId: body2.gameId,
|
|
94483
|
+
studentId: body2.studentId,
|
|
94484
|
+
subject: body2.subject,
|
|
94485
|
+
force: body2.force
|
|
94486
|
+
});
|
|
94487
|
+
return ctx.services.timeback.unenrollCourse({
|
|
94488
|
+
...body2,
|
|
94489
|
+
user: ctx.user
|
|
94490
|
+
});
|
|
94491
|
+
});
|
|
94325
94492
|
getStudentXp = requireDeveloper(async (ctx) => {
|
|
94326
94493
|
const timebackId = ctx.params.timebackId;
|
|
94327
94494
|
if (!timebackId) {
|
|
@@ -94414,6 +94581,33 @@ var init_timeback_controller = __esm(() => {
|
|
|
94414
94581
|
include
|
|
94415
94582
|
});
|
|
94416
94583
|
});
|
|
94584
|
+
getStudentHighestGradeMastered = requireDeveloper(async (ctx) => {
|
|
94585
|
+
const timebackId = ctx.params.timebackId;
|
|
94586
|
+
if (!timebackId) {
|
|
94587
|
+
throw ApiError.badRequest("Missing timebackId parameter");
|
|
94588
|
+
}
|
|
94589
|
+
const gameId = ctx.url.searchParams.get("gameId");
|
|
94590
|
+
if (!gameId) {
|
|
94591
|
+
throw ApiError.badRequest("Missing required gameId query parameter");
|
|
94592
|
+
}
|
|
94593
|
+
const subjectParam = ctx.url.searchParams.get("subject");
|
|
94594
|
+
if (!subjectParam) {
|
|
94595
|
+
throw ApiError.badRequest("Missing required subject query parameter");
|
|
94596
|
+
}
|
|
94597
|
+
if (!isTimebackSubject2(subjectParam)) {
|
|
94598
|
+
throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
94599
|
+
}
|
|
94600
|
+
logger45.debug("Getting student highest grade mastered", {
|
|
94601
|
+
requesterId: ctx.user.id,
|
|
94602
|
+
timebackId,
|
|
94603
|
+
gameId,
|
|
94604
|
+
subject: subjectParam
|
|
94605
|
+
});
|
|
94606
|
+
return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
|
|
94607
|
+
gameId,
|
|
94608
|
+
subject: subjectParam
|
|
94609
|
+
});
|
|
94610
|
+
});
|
|
94417
94611
|
getRoster = requireGameManagementAccess(async (ctx) => {
|
|
94418
94612
|
const gameId = ctx.params.gameId;
|
|
94419
94613
|
const courseId = ctx.params.courseId;
|
|
@@ -94749,8 +94943,10 @@ var init_timeback_controller = __esm(() => {
|
|
|
94749
94943
|
endActivity,
|
|
94750
94944
|
heartbeat,
|
|
94751
94945
|
advanceCourse,
|
|
94946
|
+
unenrollCourse,
|
|
94752
94947
|
getStudentXp,
|
|
94753
94948
|
getStudentMastery,
|
|
94949
|
+
getStudentHighestGradeMastered,
|
|
94754
94950
|
getRoster,
|
|
94755
94951
|
getStudentOverview,
|
|
94756
94952
|
getGameMetrics,
|
|
@@ -94959,6 +95155,10 @@ async function getMockTimebackUser(db2, gameId) {
|
|
|
94959
95155
|
const timebackId = config.timeback.timebackId || "mock-student-00000001";
|
|
94960
95156
|
return getMockTimebackData(db2, timebackId, gameId);
|
|
94961
95157
|
}
|
|
95158
|
+
function getMockHighestGradeMastered(enrollments, subject) {
|
|
95159
|
+
const subjectGrades = enrollments.filter((enrollment) => enrollment.subject === subject).map((enrollment) => enrollment.grade);
|
|
95160
|
+
return subjectGrades.length > 0 ? Math.max(...subjectGrades) : null;
|
|
95161
|
+
}
|
|
94962
95162
|
async function buildMockUserResponse(db2, user, gameId) {
|
|
94963
95163
|
const timeback3 = user.timebackId ? await getMockTimebackData(db2, user.timebackId, gameId) : undefined;
|
|
94964
95164
|
if (gameId) {
|
|
@@ -95653,6 +95853,7 @@ var init_timeback6 = __esm(() => {
|
|
|
95653
95853
|
timebackRouter.post("/end-activity", handle2(timeback2.endActivity));
|
|
95654
95854
|
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
95655
95855
|
timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
|
|
95856
|
+
timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
|
|
95656
95857
|
timebackRouter.get("/user", async (c2) => {
|
|
95657
95858
|
const user = c2.get("user");
|
|
95658
95859
|
const gameId = c2.get("gameId");
|
|
@@ -95805,6 +96006,39 @@ var init_timeback6 = __esm(() => {
|
|
|
95805
96006
|
}
|
|
95806
96007
|
return handle2(timeback2.getStudentMastery)(c2);
|
|
95807
96008
|
});
|
|
96009
|
+
timebackRouter.get("/student-highest-grade-mastered/:timebackId", async (c2) => {
|
|
96010
|
+
const user = c2.get("user");
|
|
96011
|
+
if (!user) {
|
|
96012
|
+
const error2 = ApiError.unauthorized("Must be logged in to get student highest grade mastered");
|
|
96013
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96014
|
+
}
|
|
96015
|
+
if (shouldMockTimeback()) {
|
|
96016
|
+
const url2 = new URL(c2.req.url);
|
|
96017
|
+
const subject = url2.searchParams.get("subject");
|
|
96018
|
+
const contextGameId = c2.get("gameId");
|
|
96019
|
+
const gameId = url2.searchParams.get("gameId") || (typeof contextGameId === "string" ? contextGameId : undefined);
|
|
96020
|
+
if (!subject) {
|
|
96021
|
+
const error2 = ApiError.badRequest("Missing required subject query parameter");
|
|
96022
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96023
|
+
}
|
|
96024
|
+
if (!gameId) {
|
|
96025
|
+
const error2 = ApiError.badRequest("Missing required gameId query parameter");
|
|
96026
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96027
|
+
}
|
|
96028
|
+
if (!isTimebackSubject2(subject)) {
|
|
96029
|
+
const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS4.join(", ")}`);
|
|
96030
|
+
return c2.json(createErrorResponse(error2), error2.status);
|
|
96031
|
+
}
|
|
96032
|
+
const db2 = c2.get("db");
|
|
96033
|
+
const enrollments = await getMockEnrollments(db2);
|
|
96034
|
+
const gameScopedEnrollments = filterEnrollmentsByGame(enrollments, gameId);
|
|
96035
|
+
return c2.json({
|
|
96036
|
+
subject,
|
|
96037
|
+
highestGradeMastered: getMockHighestGradeMastered(gameScopedEnrollments, subject)
|
|
96038
|
+
});
|
|
96039
|
+
}
|
|
96040
|
+
return handle2(timeback2.getStudentHighestGradeMastered)(c2);
|
|
96041
|
+
});
|
|
95808
96042
|
});
|
|
95809
96043
|
|
|
95810
96044
|
// src/routes/integrations/lti.ts
|