@playcademy/sandbox 0.6.1-beta.8 → 0.7.0
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 +1660 -581
- package/dist/server.js +1660 -581
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1083,7 +1083,7 @@ var package_default;
|
|
|
1083
1083
|
var init_package = __esm(() => {
|
|
1084
1084
|
package_default = {
|
|
1085
1085
|
name: "@playcademy/sandbox",
|
|
1086
|
-
version: "0.
|
|
1086
|
+
version: "0.7.0",
|
|
1087
1087
|
description: "Local development server for Playcademy game development",
|
|
1088
1088
|
type: "module",
|
|
1089
1089
|
exports: {
|
|
@@ -11244,7 +11244,7 @@ var init_pg_core = __esm(() => {
|
|
|
11244
11244
|
});
|
|
11245
11245
|
|
|
11246
11246
|
// ../data/src/domains/user/table.ts
|
|
11247
|
-
var userRoleEnum, developerStatusEnum, users, accounts, sessions, verification
|
|
11247
|
+
var userRoleEnum, developerStatusEnum, users, accounts, sessions, verification;
|
|
11248
11248
|
var init_table3 = __esm(() => {
|
|
11249
11249
|
init_pg_core();
|
|
11250
11250
|
userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
|
|
@@ -11337,16 +11337,6 @@ var init_table3 = __esm(() => {
|
|
|
11337
11337
|
withTimezone: true
|
|
11338
11338
|
}).notNull()
|
|
11339
11339
|
});
|
|
11340
|
-
ssoProvider = pgTable("sso_provider", {
|
|
11341
|
-
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
11342
|
-
issuer: text("issuer").notNull(),
|
|
11343
|
-
oidcConfig: text("oidc_config"),
|
|
11344
|
-
samlConfig: text("saml_config"),
|
|
11345
|
-
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
11346
|
-
providerId: text("provider_id").notNull().unique(),
|
|
11347
|
-
organizationId: text("organization_id"),
|
|
11348
|
-
domain: text("domain").notNull()
|
|
11349
|
-
});
|
|
11350
11340
|
});
|
|
11351
11341
|
|
|
11352
11342
|
// ../data/src/domains/developer/table.ts
|
|
@@ -11586,7 +11576,7 @@ var init_table6 = __esm(() => {
|
|
|
11586
11576
|
});
|
|
11587
11577
|
|
|
11588
11578
|
// ../data/src/domains/timeback/table.ts
|
|
11589
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11579
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11590
11580
|
var init_table7 = __esm(() => {
|
|
11591
11581
|
init_drizzle_orm();
|
|
11592
11582
|
init_pg_core();
|
|
@@ -11596,6 +11586,15 @@ var init_table7 = __esm(() => {
|
|
|
11596
11586
|
"active",
|
|
11597
11587
|
"deactivated"
|
|
11598
11588
|
]);
|
|
11589
|
+
gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", [
|
|
11590
|
+
"end_of_course",
|
|
11591
|
+
"diagnostic"
|
|
11592
|
+
]);
|
|
11593
|
+
gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
|
|
11594
|
+
"draft",
|
|
11595
|
+
"live",
|
|
11596
|
+
"archived"
|
|
11597
|
+
]);
|
|
11599
11598
|
gameTimebackIntegrations = pgTable("game_timeback_integrations", {
|
|
11600
11599
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11601
11600
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
@@ -11616,10 +11615,11 @@ var init_table7 = __esm(() => {
|
|
|
11616
11615
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11617
11616
|
integrationId: uuid("integration_id").notNull().references(() => gameTimebackIntegrations.id, { onDelete: "cascade" }),
|
|
11618
11617
|
qtiTestIdentifier: text("qti_test_identifier").notNull(),
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
sortOrder: integer("sort_order")
|
|
11622
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
11618
|
+
purpose: gameTimebackAssessmentPurposeEnum("purpose").notNull().default("end_of_course"),
|
|
11619
|
+
status: gameTimebackAssessmentStatusEnum("status").notNull().default("draft"),
|
|
11620
|
+
sortOrder: integer("sort_order"),
|
|
11621
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
11622
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
11623
11623
|
}, (table3) => [
|
|
11624
11624
|
uniqueIndex("game_timeback_assessment_tests_integration_qti_idx").on(table3.integrationId, table3.qtiTestIdentifier)
|
|
11625
11625
|
]);
|
|
@@ -11662,7 +11662,6 @@ __export(exports_tables_index, {
|
|
|
11662
11662
|
verification: () => verification,
|
|
11663
11663
|
users: () => users,
|
|
11664
11664
|
userRoleEnum: () => userRoleEnum,
|
|
11665
|
-
ssoProvider: () => ssoProvider,
|
|
11666
11665
|
sessions: () => sessions,
|
|
11667
11666
|
games: () => games,
|
|
11668
11667
|
gameVisibilityEnum: () => gameVisibilityEnum,
|
|
@@ -11671,6 +11670,8 @@ __export(exports_tables_index, {
|
|
|
11671
11670
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11672
11671
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
11673
11672
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11673
|
+
gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
|
|
11674
|
+
gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
|
|
11674
11675
|
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
11675
11676
|
gameScoresRelations: () => gameScoresRelations,
|
|
11676
11677
|
gameScores: () => gameScores,
|
|
@@ -28500,7 +28501,7 @@ function evaluateBaselineGuardrails(input) {
|
|
|
28500
28501
|
}
|
|
28501
28502
|
|
|
28502
28503
|
// ../api-core/src/utils/migration.util.ts
|
|
28503
|
-
function
|
|
28504
|
+
function planMigrations2(journal, ledger) {
|
|
28504
28505
|
const ledgerByTag = new Map(ledger.map((row) => [row.tag, row]));
|
|
28505
28506
|
const journalTags = new Set(journal.map((entry) => entry.tag));
|
|
28506
28507
|
const pendingTags = [];
|
|
@@ -29085,7 +29086,7 @@ class DeployService {
|
|
|
29085
29086
|
await this.assertNoSchemaDrift(databaseId, state);
|
|
29086
29087
|
await cf.d1.ensureMigrationLedger(databaseId);
|
|
29087
29088
|
const ledger = await cf.d1.readMigrationLedger(databaseId);
|
|
29088
|
-
const plan =
|
|
29089
|
+
const plan = planMigrations2(migrations.map((migration) => ({ tag: migration.tag, checksum: migration.checksum })), ledger.map((row) => ({
|
|
29089
29090
|
tag: row.tag,
|
|
29090
29091
|
checksum: row.checksum,
|
|
29091
29092
|
checksumAlgo: row.checksum_algo
|
|
@@ -29438,7 +29439,7 @@ class DeployService {
|
|
|
29438
29439
|
}
|
|
29439
29440
|
await cf.d1.ensureMigrationLedger(databaseId);
|
|
29440
29441
|
const ledger = await cf.d1.readMigrationLedger(databaseId);
|
|
29441
|
-
const plan =
|
|
29442
|
+
const plan = planMigrations2(slice, ledger.map((row) => ({
|
|
29442
29443
|
tag: row.tag,
|
|
29443
29444
|
checksum: row.checksum,
|
|
29444
29445
|
checksumAlgo: row.checksum_algo
|
|
@@ -30100,6 +30101,25 @@ function sleep(ms) {
|
|
|
30100
30101
|
}
|
|
30101
30102
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30102
30103
|
}
|
|
30104
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
30105
|
+
if (items.length === 0) {
|
|
30106
|
+
return [];
|
|
30107
|
+
}
|
|
30108
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
30109
|
+
const results = Array.from({ length: items.length });
|
|
30110
|
+
let nextIndex = 0;
|
|
30111
|
+
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
30112
|
+
while (true) {
|
|
30113
|
+
const currentIndex = nextIndex;
|
|
30114
|
+
nextIndex++;
|
|
30115
|
+
if (currentIndex >= items.length) {
|
|
30116
|
+
return;
|
|
30117
|
+
}
|
|
30118
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
30119
|
+
}
|
|
30120
|
+
}));
|
|
30121
|
+
return results;
|
|
30122
|
+
}
|
|
30103
30123
|
|
|
30104
30124
|
// ../timeback/dist/types.js
|
|
30105
30125
|
function isObject(value) {
|
|
@@ -31634,7 +31654,7 @@ class KVBackupService {
|
|
|
31634
31654
|
"app.kv_backup.dry_run": dryRun,
|
|
31635
31655
|
"app.kv_backup.game_slug": options.gameSlug
|
|
31636
31656
|
});
|
|
31637
|
-
const results = await
|
|
31657
|
+
const results = await runWithConcurrency(targets, namespaceConcurrency, (target) => this.backupNamespace(target, {
|
|
31638
31658
|
bucketName,
|
|
31639
31659
|
stage,
|
|
31640
31660
|
runId,
|
|
@@ -31700,7 +31720,7 @@ class KVBackupService {
|
|
|
31700
31720
|
}
|
|
31701
31721
|
static async fetchNamespaceEntries(cloudflare2, namespaceId, keyConcurrency = DEFAULT_KEY_CONCURRENCY) {
|
|
31702
31722
|
const keys = await KVBackupService.withRetries(`List KV keys for namespace ${namespaceId}`, () => cloudflare2.kv.listKeys(namespaceId));
|
|
31703
|
-
return
|
|
31723
|
+
return runWithConcurrency(keys, keyConcurrency, async (key) => {
|
|
31704
31724
|
const safeLabel = KVBackupService.redactKeyForLog(key.name);
|
|
31705
31725
|
const value = await KVBackupService.withRetries(`Fetch KV value ${safeLabel}`, () => cloudflare2.kv.getValue(namespaceId, key.name));
|
|
31706
31726
|
const metadata2 = KVBackupService.parseMetadata(key.metadata);
|
|
@@ -31877,25 +31897,6 @@ class KVBackupService {
|
|
|
31877
31897
|
}
|
|
31878
31898
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
31879
31899
|
}
|
|
31880
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
31881
|
-
if (items.length === 0) {
|
|
31882
|
-
return [];
|
|
31883
|
-
}
|
|
31884
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
31885
|
-
const results = Array.from({ length: items.length });
|
|
31886
|
-
let nextIndex = 0;
|
|
31887
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
31888
|
-
while (true) {
|
|
31889
|
-
const currentIndex = nextIndex;
|
|
31890
|
-
nextIndex++;
|
|
31891
|
-
if (currentIndex >= items.length) {
|
|
31892
|
-
return;
|
|
31893
|
-
}
|
|
31894
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
31895
|
-
}
|
|
31896
|
-
}));
|
|
31897
|
-
return results;
|
|
31898
|
-
}
|
|
31899
31900
|
}
|
|
31900
31901
|
var BACKUP_SCHEMA_VERSION = 1, DEFAULT_NAMESPACE_CONCURRENCY = 2, DEFAULT_KEY_CONCURRENCY = 10, DEFAULT_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 500;
|
|
31901
31902
|
var init_kv_backup_service = __esm(() => {
|
|
@@ -34202,9 +34203,8 @@ function isValidAdminAttributionDate(value) {
|
|
|
34202
34203
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
34203
34204
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
34204
34205
|
}
|
|
34205
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS2, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, 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, VerifyTimebackMetricDiscrepancyRequestSchema,
|
|
34206
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS2, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, 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, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
34206
34207
|
var init_schemas4 = __esm(() => {
|
|
34207
|
-
init_drizzle_zod();
|
|
34208
34208
|
init_esm();
|
|
34209
34209
|
init_table7();
|
|
34210
34210
|
TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
@@ -34457,15 +34457,26 @@ var init_schemas4 = __esm(() => {
|
|
|
34457
34457
|
runId: exports_external.string().uuid(),
|
|
34458
34458
|
activityId: exports_external.string().min(1).optional()
|
|
34459
34459
|
});
|
|
34460
|
-
|
|
34461
|
-
|
|
34462
|
-
createdAt: true
|
|
34463
|
-
});
|
|
34460
|
+
AssessmentPurposeSchema = exports_external.enum(gameTimebackAssessmentPurposeEnum.enumValues);
|
|
34461
|
+
AssessmentStatusSchema = exports_external.enum(gameTimebackAssessmentStatusEnum.enumValues);
|
|
34464
34462
|
CreateAssessmentRequestSchema = exports_external.object({
|
|
34465
|
-
title: exports_external.string().min(1, "Assessment title is required")
|
|
34463
|
+
title: exports_external.string().min(1, "Assessment title is required"),
|
|
34464
|
+
purpose: AssessmentPurposeSchema
|
|
34465
|
+
});
|
|
34466
|
+
UpdateAssessmentRequestSchema = exports_external.object({
|
|
34467
|
+
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
34468
|
+
purpose: AssessmentPurposeSchema.optional(),
|
|
34469
|
+
status: AssessmentStatusSchema.optional()
|
|
34470
|
+
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.status !== undefined, {
|
|
34471
|
+
message: "Title, purpose, or status is required"
|
|
34472
|
+
});
|
|
34473
|
+
CopyAssessmentRequestSchema = exports_external.object({
|
|
34474
|
+
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
34475
|
+
purpose: AssessmentPurposeSchema
|
|
34466
34476
|
});
|
|
34467
34477
|
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
34468
|
-
|
|
34478
|
+
purpose: AssessmentPurposeSchema,
|
|
34479
|
+
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
34469
34480
|
});
|
|
34470
34481
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
34471
34482
|
items: exports_external.array(exports_external.object({
|
|
@@ -53964,10 +53975,10 @@ var init_dist2 = __esm(async () => {
|
|
|
53964
53975
|
};
|
|
53965
53976
|
});
|
|
53966
53977
|
|
|
53967
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
53978
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-6jf1natv.js
|
|
53968
53979
|
var init_chunk_6jf1natv = () => {};
|
|
53969
53980
|
|
|
53970
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
53981
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-dd0pbbq3.js
|
|
53971
53982
|
var exports_chunk_dd0pbbq3 = {};
|
|
53972
53983
|
__export(exports_chunk_dd0pbbq3, {
|
|
53973
53984
|
types: () => types6,
|
|
@@ -54378,7 +54389,7 @@ var init_chunk_dd0pbbq3 = __esm(() => {
|
|
|
54378
54389
|
util_default2 = { TextEncoder: TextEncoder3, TextDecoder: TextDecoder3, promisify: promisify2, log: log9, inherits: inherits2, _extend: _extend2, callbackifyOnRejected: callbackifyOnRejected2, callbackify: callbackify2 };
|
|
54379
54390
|
});
|
|
54380
54391
|
|
|
54381
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
54392
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-pt7w63g2.js
|
|
54382
54393
|
function createInputValidationError2(message, issues) {
|
|
54383
54394
|
return new InputValidationError2(message, issues);
|
|
54384
54395
|
}
|
|
@@ -55429,7 +55440,7 @@ var ApiError3, UnauthorizedError3, ForbiddenError2, NotFoundError3, ValidationEr
|
|
|
55429
55440
|
console[method](label, style);
|
|
55430
55441
|
}
|
|
55431
55442
|
}, LOG_LEVELS2, GLOBAL_LOGGING_CONFIG_KEY2, BEYONDAI_PATHS2, LEARNWITHAI_PATHS2, PLATFORM_PATHS2, DEFAULT_PROVIDER_REGISTRY2, MODE_TRANSPORT2, MODE_PROVIDER2, MODE_ENV_CONFIG2, MODE_EXPLICIT_CONFIG2, MODE_ENV_FALLBACK_PLATFORM2, MODE_ENV_FALLBACK_EXPLICIT2, MODE_TOKEN_PROVIDER2, MODES2, MAX_RETRIES2 = 3, RETRY_STATUS_CODES2, INITIAL_RETRY_DELAY_MS2 = 1000, DEFAULT_LIMIT2 = 100, DEFAULT_MAX_ITEMS2 = 1e4, Paginator6;
|
|
55432
|
-
var
|
|
55443
|
+
var init_chunk_pt7w63g2 = __esm(async () => {
|
|
55433
55444
|
init_chunk_6jf1natv();
|
|
55434
55445
|
ApiError3 = class ApiError3 extends Error {
|
|
55435
55446
|
statusCode;
|
|
@@ -70575,15 +70586,14 @@ var init_v42 = __esm(() => {
|
|
|
70575
70586
|
init_classic2();
|
|
70576
70587
|
});
|
|
70577
70588
|
|
|
70578
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
70589
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/index.js
|
|
70579
70590
|
function resolveToProvider23(config4, registry3 = DEFAULT_PROVIDER_REGISTRY2) {
|
|
70580
70591
|
return resolveToProvider10(config4, QTI_ENV_VARS2, registry3);
|
|
70581
70592
|
}
|
|
70582
70593
|
function translateParams2(params) {
|
|
70583
70594
|
const {
|
|
70584
70595
|
orderBy,
|
|
70585
|
-
|
|
70586
|
-
fields: __,
|
|
70596
|
+
fields: _,
|
|
70587
70597
|
...rest
|
|
70588
70598
|
} = params;
|
|
70589
70599
|
return {
|
|
@@ -70591,6 +70601,23 @@ function translateParams2(params) {
|
|
|
70591
70601
|
order: orderBy
|
|
70592
70602
|
};
|
|
70593
70603
|
}
|
|
70604
|
+
function buildListQueryParams(params) {
|
|
70605
|
+
const queryParams = {};
|
|
70606
|
+
const filter = params.where ? whereToFilter2(params.where) : undefined;
|
|
70607
|
+
if (filter !== undefined)
|
|
70608
|
+
queryParams.filter = filter;
|
|
70609
|
+
if (params.query !== undefined)
|
|
70610
|
+
queryParams.query = params.query;
|
|
70611
|
+
if (params.page !== undefined)
|
|
70612
|
+
queryParams.page = params.page;
|
|
70613
|
+
if (params.limit !== undefined)
|
|
70614
|
+
queryParams.limit = params.limit;
|
|
70615
|
+
if (params.sort)
|
|
70616
|
+
queryParams.sort = params.sort;
|
|
70617
|
+
if (params.order)
|
|
70618
|
+
queryParams.order = params.order;
|
|
70619
|
+
return queryParams;
|
|
70620
|
+
}
|
|
70594
70621
|
|
|
70595
70622
|
class AssessmentItemsResource2 {
|
|
70596
70623
|
transport;
|
|
@@ -70599,19 +70626,8 @@ class AssessmentItemsResource2 {
|
|
|
70599
70626
|
}
|
|
70600
70627
|
list(params = {}) {
|
|
70601
70628
|
validatePageListParams2(params);
|
|
70602
|
-
const queryParams = {};
|
|
70603
|
-
if (params.query !== undefined)
|
|
70604
|
-
queryParams.query = params.query;
|
|
70605
|
-
if (params.page !== undefined)
|
|
70606
|
-
queryParams.page = params.page;
|
|
70607
|
-
if (params.limit !== undefined)
|
|
70608
|
-
queryParams.limit = params.limit;
|
|
70609
|
-
if (params.sort)
|
|
70610
|
-
queryParams.sort = params.sort;
|
|
70611
|
-
if (params.order)
|
|
70612
|
-
queryParams.order = params.order;
|
|
70613
70629
|
return this.transport.request("/assessment-items", {
|
|
70614
|
-
params:
|
|
70630
|
+
params: buildListQueryParams(params)
|
|
70615
70631
|
});
|
|
70616
70632
|
}
|
|
70617
70633
|
stream(params = {}) {
|
|
@@ -70734,20 +70750,9 @@ class TestPartSectionsHelper2 {
|
|
|
70734
70750
|
}
|
|
70735
70751
|
list(params = {}) {
|
|
70736
70752
|
validatePageListParams2(params);
|
|
70737
|
-
const queryParams = {};
|
|
70738
|
-
if (params.query !== undefined)
|
|
70739
|
-
queryParams.query = params.query;
|
|
70740
|
-
if (params.page !== undefined)
|
|
70741
|
-
queryParams.page = params.page;
|
|
70742
|
-
if (params.limit !== undefined)
|
|
70743
|
-
queryParams.limit = params.limit;
|
|
70744
|
-
if (params.sort)
|
|
70745
|
-
queryParams.sort = params.sort;
|
|
70746
|
-
if (params.order)
|
|
70747
|
-
queryParams.order = params.order;
|
|
70748
70753
|
const path = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts/${encodeURIComponent(this.testPartId)}/sections`;
|
|
70749
70754
|
return this.transport.request(path, {
|
|
70750
|
-
params:
|
|
70755
|
+
params: buildListQueryParams(params)
|
|
70751
70756
|
});
|
|
70752
70757
|
}
|
|
70753
70758
|
get(identifier) {
|
|
@@ -70794,19 +70799,10 @@ class AssessmentTestPartsHelper2 {
|
|
|
70794
70799
|
}
|
|
70795
70800
|
list(params = {}) {
|
|
70796
70801
|
validatePageListParams2(params);
|
|
70797
|
-
const queryParams = {};
|
|
70798
|
-
if (params.query !== undefined)
|
|
70799
|
-
queryParams.query = params.query;
|
|
70800
|
-
if (params.page !== undefined)
|
|
70801
|
-
queryParams.page = params.page;
|
|
70802
|
-
if (params.limit !== undefined)
|
|
70803
|
-
queryParams.limit = params.limit;
|
|
70804
|
-
if (params.sort)
|
|
70805
|
-
queryParams.sort = params.sort;
|
|
70806
|
-
if (params.order)
|
|
70807
|
-
queryParams.order = params.order;
|
|
70808
70802
|
const path = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts`;
|
|
70809
|
-
return this.transport.request(path, {
|
|
70803
|
+
return this.transport.request(path, {
|
|
70804
|
+
params: buildListQueryParams(params)
|
|
70805
|
+
});
|
|
70810
70806
|
}
|
|
70811
70807
|
get(identifier) {
|
|
70812
70808
|
validateNonEmptyString2(identifier, "testPartId");
|
|
@@ -70849,19 +70845,8 @@ class AssessmentTestsResource2 {
|
|
|
70849
70845
|
}
|
|
70850
70846
|
list(params = {}) {
|
|
70851
70847
|
validatePageListParams2(params);
|
|
70852
|
-
const queryParams = {};
|
|
70853
|
-
if (params.query !== undefined)
|
|
70854
|
-
queryParams.query = params.query;
|
|
70855
|
-
if (params.page !== undefined)
|
|
70856
|
-
queryParams.page = params.page;
|
|
70857
|
-
if (params.limit !== undefined)
|
|
70858
|
-
queryParams.limit = params.limit;
|
|
70859
|
-
if (params.sort)
|
|
70860
|
-
queryParams.sort = params.sort;
|
|
70861
|
-
if (params.order)
|
|
70862
|
-
queryParams.order = params.order;
|
|
70863
70848
|
return this.transport.request("/assessment-tests", {
|
|
70864
|
-
params:
|
|
70849
|
+
params: buildListQueryParams(params)
|
|
70865
70850
|
});
|
|
70866
70851
|
}
|
|
70867
70852
|
stream(params = {}) {
|
|
@@ -70967,19 +70952,8 @@ class StimuliResource2 {
|
|
|
70967
70952
|
}
|
|
70968
70953
|
list(params = {}) {
|
|
70969
70954
|
validatePageListParams2(params);
|
|
70970
|
-
const queryParams = {};
|
|
70971
|
-
if (params.query !== undefined)
|
|
70972
|
-
queryParams.query = params.query;
|
|
70973
|
-
if (params.page !== undefined)
|
|
70974
|
-
queryParams.page = params.page;
|
|
70975
|
-
if (params.limit !== undefined)
|
|
70976
|
-
queryParams.limit = params.limit;
|
|
70977
|
-
if (params.sort)
|
|
70978
|
-
queryParams.sort = params.sort;
|
|
70979
|
-
if (params.order)
|
|
70980
|
-
queryParams.order = params.order;
|
|
70981
70955
|
return this.transport.request("/stimuli", {
|
|
70982
|
-
params:
|
|
70956
|
+
params: buildListQueryParams(params)
|
|
70983
70957
|
});
|
|
70984
70958
|
}
|
|
70985
70959
|
stream(params = {}) {
|
|
@@ -71111,7 +71085,7 @@ var init_dist3 = __esm(async () => {
|
|
|
71111
71085
|
init_v42();
|
|
71112
71086
|
init_v42();
|
|
71113
71087
|
init_v42();
|
|
71114
|
-
await
|
|
71088
|
+
await init_chunk_pt7w63g2();
|
|
71115
71089
|
QTI_ENV_VARS2 = {
|
|
71116
71090
|
baseUrl: ["TIMEBACK_API_BASE_URL", "TIMEBACK_BASE_URL", "QTI_BASE_URL"],
|
|
71117
71091
|
clientId: ["TIMEBACK_API_CLIENT_ID", "TIMEBACK_CLIENT_ID", "QTI_CLIENT_ID"],
|
|
@@ -77952,13 +77926,6 @@ function deriveSourcedIds(courseId) {
|
|
|
77952
77926
|
componentResource: `${courseId}-cr`
|
|
77953
77927
|
};
|
|
77954
77928
|
}
|
|
77955
|
-
function deriveAssessmentBankIds(courseId) {
|
|
77956
|
-
return {
|
|
77957
|
-
component: `${courseId}-assessment-bank-component`,
|
|
77958
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
77959
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
77960
|
-
};
|
|
77961
|
-
}
|
|
77962
77929
|
function validateProgressData(progressData) {
|
|
77963
77930
|
if (!progressData.subject) {
|
|
77964
77931
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -79247,59 +79214,22 @@ class CourseAssessments {
|
|
|
79247
79214
|
constructor(core3) {
|
|
79248
79215
|
this.core = core3;
|
|
79249
79216
|
}
|
|
79250
|
-
|
|
79251
|
-
|
|
79252
|
-
|
|
79253
|
-
|
|
79254
|
-
|
|
79255
|
-
|
|
79256
|
-
|
|
79257
|
-
|
|
79258
|
-
|
|
79259
|
-
|
|
79260
|
-
|
|
79261
|
-
|
|
79262
|
-
|
|
79263
|
-
|
|
79264
|
-
throw error88;
|
|
79265
|
-
}
|
|
79266
|
-
}
|
|
79267
|
-
try {
|
|
79268
|
-
await oneroster.resources.create({
|
|
79269
|
-
sourcedId: bankIds.resource,
|
|
79270
|
-
status: "active",
|
|
79271
|
-
title: "Assessment Bank",
|
|
79272
|
-
vendorResourceId: "",
|
|
79273
|
-
vendorId: undefined,
|
|
79274
|
-
metadata: {
|
|
79275
|
-
type: "assessment-bank",
|
|
79276
|
-
resources: [],
|
|
79277
|
-
lessonType: "test-out",
|
|
79278
|
-
subject: input.subject,
|
|
79279
|
-
grade: String(input.grade)
|
|
79280
|
-
}
|
|
79281
|
-
});
|
|
79282
|
-
} catch (error88) {
|
|
79283
|
-
if (!isAlreadyExists(error88)) {
|
|
79284
|
-
throw error88;
|
|
79285
|
-
}
|
|
79286
|
-
}
|
|
79287
|
-
try {
|
|
79288
|
-
await oneroster.courses.createComponentResource({
|
|
79289
|
-
sourcedId: bankIds.componentResource,
|
|
79290
|
-
status: "active",
|
|
79291
|
-
title: "Test Out",
|
|
79292
|
-
resource: { sourcedId: bankIds.resource },
|
|
79293
|
-
courseComponent: { sourcedId: bankIds.component },
|
|
79294
|
-
sortOrder: 9999,
|
|
79295
|
-
metadata: { lessonType: "test-out" },
|
|
79296
|
-
lessonType: "test-out"
|
|
79297
|
-
});
|
|
79298
|
-
} catch (error88) {
|
|
79299
|
-
if (!isAlreadyExists(error88)) {
|
|
79300
|
-
throw error88;
|
|
79217
|
+
createItemFromXml(input) {
|
|
79218
|
+
return this.core.qti.assessmentItems.createFromXml({
|
|
79219
|
+
format: "xml",
|
|
79220
|
+
xml: input.xml,
|
|
79221
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
79222
|
+
});
|
|
79223
|
+
}
|
|
79224
|
+
updateItemFromXml(identifier, input) {
|
|
79225
|
+
return this.core.qti.getTransport().request(`/assessment-items/${encodeURIComponent(identifier)}`, {
|
|
79226
|
+
method: "PUT",
|
|
79227
|
+
body: {
|
|
79228
|
+
format: "xml",
|
|
79229
|
+
xml: input.xml,
|
|
79230
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
79301
79231
|
}
|
|
79302
|
-
}
|
|
79232
|
+
});
|
|
79303
79233
|
}
|
|
79304
79234
|
async createTestScaffold(input) {
|
|
79305
79235
|
const partId = `${input.identifier}-part1`;
|
|
@@ -79334,18 +79264,6 @@ class CourseAssessments {
|
|
|
79334
79264
|
}
|
|
79335
79265
|
}
|
|
79336
79266
|
}
|
|
79337
|
-
async teardownTest(qtiTestIdentifier) {
|
|
79338
|
-
const qti = this.core.qti;
|
|
79339
|
-
const partId = `${qtiTestIdentifier}-part1`;
|
|
79340
|
-
const sectionId = `${qtiTestIdentifier}-section1`;
|
|
79341
|
-
const questions = await qti.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
79342
|
-
const sectionItems = qti.assessmentTests.testParts(qtiTestIdentifier).sections(partId).items(sectionId);
|
|
79343
|
-
for (const q of questions.questions ?? []) {
|
|
79344
|
-
await sectionItems.remove(q.reference.identifier);
|
|
79345
|
-
await qti.assessmentItems.delete(q.reference.identifier);
|
|
79346
|
-
}
|
|
79347
|
-
await qti.assessmentTests.delete(qtiTestIdentifier);
|
|
79348
|
-
}
|
|
79349
79267
|
}
|
|
79350
79268
|
function buildCoursePayload(config4) {
|
|
79351
79269
|
return {
|
|
@@ -79575,9 +79493,9 @@ function createCourseNamespace(core3) {
|
|
|
79575
79493
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
79576
79494
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
79577
79495
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
79578
|
-
ensureAssessmentBank: (input) => assessments.ensureBank(input),
|
|
79579
79496
|
createAssessmentTest: (input) => assessments.createTestScaffold(input),
|
|
79580
|
-
|
|
79497
|
+
createAssessmentItemXml: (input) => assessments.createItemFromXml(input),
|
|
79498
|
+
updateAssessmentItemXml: (identifier, input) => assessments.updateItemFromXml(identifier, input)
|
|
79581
79499
|
};
|
|
79582
79500
|
}
|
|
79583
79501
|
function recordTimebackClientSummary(outcome) {
|
|
@@ -80060,13 +79978,6 @@ function deriveSourcedIds2(courseId) {
|
|
|
80060
79978
|
componentResource: `${courseId}-cr`
|
|
80061
79979
|
};
|
|
80062
79980
|
}
|
|
80063
|
-
function deriveAssessmentBankIds2(courseId) {
|
|
80064
|
-
return {
|
|
80065
|
-
component: `${courseId}-assessment-bank-component`,
|
|
80066
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
80067
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
80068
|
-
};
|
|
80069
|
-
}
|
|
80070
79981
|
var CACHE_DEFAULTS4, RESOURCE_DEFAULTS4;
|
|
80071
79982
|
var init_utils6 = __esm(() => {
|
|
80072
79983
|
init_src();
|
|
@@ -80101,6 +80012,96 @@ var init_utils6 = __esm(() => {
|
|
|
80101
80012
|
});
|
|
80102
80013
|
|
|
80103
80014
|
// ../utils/src/timeback.ts
|
|
80015
|
+
function playcademySupportedQtiInteractionType(value) {
|
|
80016
|
+
if (typeof value !== "string") {
|
|
80017
|
+
return;
|
|
80018
|
+
}
|
|
80019
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
80020
|
+
return PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET.has(normalized) ? normalized : undefined;
|
|
80021
|
+
}
|
|
80022
|
+
function parseNumericTextEntry(input) {
|
|
80023
|
+
const answer = typeof input.answer === "string" ? input.answer.trim() : "";
|
|
80024
|
+
const comparison = input.comparison;
|
|
80025
|
+
if (input.baseType !== "integer" && input.baseType !== "float") {
|
|
80026
|
+
return { success: false, message: "Numeric answers must be integer or float responses" };
|
|
80027
|
+
}
|
|
80028
|
+
if (!answer || (input.baseType === "integer" ? !INTEGER_ANSWER_PATTERN.test(answer) : !FLOAT_ANSWER_PATTERN.test(answer) || !Number.isFinite(Number(answer)))) {
|
|
80029
|
+
return { success: false, message: `The correct answer is not a valid ${input.baseType}` };
|
|
80030
|
+
}
|
|
80031
|
+
if (!comparison || typeof comparison !== "object" || !("kind" in comparison)) {
|
|
80032
|
+
return { success: false, message: "Numeric comparison is required" };
|
|
80033
|
+
}
|
|
80034
|
+
if (comparison.kind === "equal") {
|
|
80035
|
+
return {
|
|
80036
|
+
success: true,
|
|
80037
|
+
value: { baseType: input.baseType, answer, comparison: { kind: "equal" } }
|
|
80038
|
+
};
|
|
80039
|
+
}
|
|
80040
|
+
if (input.baseType === "integer") {
|
|
80041
|
+
return { success: false, message: "Integer answers cannot use decimal-place rounding" };
|
|
80042
|
+
}
|
|
80043
|
+
const figures = "figures" in comparison ? comparison.figures : undefined;
|
|
80044
|
+
const roundingMode = "roundingMode" in comparison ? comparison.roundingMode : undefined;
|
|
80045
|
+
if (comparison.kind !== "equal-rounded" || roundingMode !== "decimalPlaces" || !Number.isInteger(figures) || figures < 0 || figures > 15) {
|
|
80046
|
+
return { success: false, message: "Decimal places must be a whole number from 0 to 15" };
|
|
80047
|
+
}
|
|
80048
|
+
return {
|
|
80049
|
+
success: true,
|
|
80050
|
+
value: {
|
|
80051
|
+
baseType: "float",
|
|
80052
|
+
answer,
|
|
80053
|
+
comparison: { kind: "equal-rounded", roundingMode, figures }
|
|
80054
|
+
}
|
|
80055
|
+
};
|
|
80056
|
+
}
|
|
80057
|
+
function escapeXml(value) {
|
|
80058
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
80059
|
+
}
|
|
80060
|
+
function hottextMaxChoices(selectableCount, multiple) {
|
|
80061
|
+
return multiple ? selectableCount : 1;
|
|
80062
|
+
}
|
|
80063
|
+
function numericTextEntryXml(input) {
|
|
80064
|
+
const comparison = input.numeric.comparison.kind === "equal-rounded" ? `<qti-equal-rounded rounding-mode="decimalPlaces" figures="${input.numeric.comparison.figures}">` : "<qti-equal>";
|
|
80065
|
+
const closeComparison = input.numeric.comparison.kind === "equal-rounded" ? "</qti-equal-rounded>" : "</qti-equal>";
|
|
80066
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
80067
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
80068
|
+
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="${input.numeric.baseType}">
|
|
80069
|
+
<qti-correct-response><qti-value>${escapeXml(input.numeric.answer)}</qti-value></qti-correct-response>
|
|
80070
|
+
</qti-response-declaration>
|
|
80071
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
80072
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
80073
|
+
</qti-outcome-declaration>
|
|
80074
|
+
<qti-item-body>
|
|
80075
|
+
<p>${escapeXml(input.prompt)} <qti-text-entry-interaction response-identifier="RESPONSE" expected-length="15" /></p>
|
|
80076
|
+
</qti-item-body>
|
|
80077
|
+
<qti-response-processing>
|
|
80078
|
+
<qti-response-condition>
|
|
80079
|
+
<qti-response-if>
|
|
80080
|
+
${comparison}<qti-variable identifier="RESPONSE" /><qti-correct identifier="RESPONSE" />${closeComparison}
|
|
80081
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
80082
|
+
</qti-response-if>
|
|
80083
|
+
</qti-response-condition>
|
|
80084
|
+
</qti-response-processing>
|
|
80085
|
+
</qti-assessment-item>`;
|
|
80086
|
+
}
|
|
80087
|
+
function countQtiTestItems(test) {
|
|
80088
|
+
let itemCount = 0;
|
|
80089
|
+
for (const part of test["qti-test-part"] ?? []) {
|
|
80090
|
+
for (const section of part["qti-assessment-section"] ?? []) {
|
|
80091
|
+
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
80092
|
+
}
|
|
80093
|
+
}
|
|
80094
|
+
return itemCount;
|
|
80095
|
+
}
|
|
80096
|
+
function isQtiItemOwnedByTest(item, testIdentifier) {
|
|
80097
|
+
const hasOwnershipMetadata = Boolean(item.metadata && (("ownerSystem" in item.metadata) || (PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY in item.metadata)));
|
|
80098
|
+
if (hasOwnershipMetadata) {
|
|
80099
|
+
return item.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && item.metadata?.[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY] === testIdentifier;
|
|
80100
|
+
}
|
|
80101
|
+
const identifierPrefix = `${testIdentifier}-q`;
|
|
80102
|
+
const identifierSuffix = item.identifier?.startsWith(identifierPrefix) ? item.identifier.slice(identifierPrefix.length) : undefined;
|
|
80103
|
+
return identifierSuffix !== undefined && /^[0-9a-f]{8}$/i.test(identifierSuffix);
|
|
80104
|
+
}
|
|
80104
80105
|
function formatGradeLabel(grade) {
|
|
80105
80106
|
if (grade === null || grade === undefined) {
|
|
80106
80107
|
return "N/A";
|
|
@@ -80142,7 +80143,7 @@ function parseTimebackDiscrepancyQueueMetrics(values) {
|
|
|
80142
80143
|
}
|
|
80143
80144
|
return metrics;
|
|
80144
80145
|
}
|
|
80145
|
-
var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS, TIMEBACK_DISCREPANCY_QUEUE_METRICS, DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW = "this-week", TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES, TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES;
|
|
80146
|
+
var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS, TIMEBACK_DISCREPANCY_QUEUE_METRICS, PLAYCADEMY_QTI_OWNER_SYSTEM = "playcademy", PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY = "ownerTestIdentifier", PLAYCADEMY_QTI_EDITOR_MODE_KEY = "playcademyEditorMode", PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES, PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET, INTEGER_ANSWER_PATTERN, FLOAT_ANSWER_PATTERN, INLINE_CHOICE_BLANK = "_____", DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW = "this-week", TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES, TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES;
|
|
80146
80147
|
var init_timeback3 = __esm(() => {
|
|
80147
80148
|
TIMEBACK_DISCREPANCY_QUEUE_WINDOWS = [
|
|
80148
80149
|
"today",
|
|
@@ -80153,6 +80154,17 @@ var init_timeback3 = __esm(() => {
|
|
|
80153
80154
|
"custom"
|
|
80154
80155
|
];
|
|
80155
80156
|
TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
|
|
80157
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES = [
|
|
80158
|
+
"choice",
|
|
80159
|
+
"inline-choice",
|
|
80160
|
+
"text-entry",
|
|
80161
|
+
"order",
|
|
80162
|
+
"match",
|
|
80163
|
+
"hottext"
|
|
80164
|
+
];
|
|
80165
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET = new Set(PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES);
|
|
80166
|
+
INTEGER_ANSWER_PATTERN = /^[+-]?\d+$/;
|
|
80167
|
+
FLOAT_ANSWER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
80156
80168
|
TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
|
|
80157
80169
|
TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
|
|
80158
80170
|
});
|
|
@@ -81395,7 +81407,7 @@ class TimebackAdminService {
|
|
|
81395
81407
|
}
|
|
81396
81408
|
async getMasterableUnitsByCourse(courseIds) {
|
|
81397
81409
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
81398
|
-
const results = await
|
|
81410
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.MASTERABLE_UNITS_CONCURRENCY, async (courseId) => [courseId, await this.getMasterableUnits(courseId)]);
|
|
81399
81411
|
return new Map(results);
|
|
81400
81412
|
}
|
|
81401
81413
|
deriveGameSensorUrl(game2) {
|
|
@@ -81576,7 +81588,7 @@ class TimebackAdminService {
|
|
|
81576
81588
|
async loadEnrollmentAnalyticsSummaries(enrollmentIds) {
|
|
81577
81589
|
const client = this.requireClient();
|
|
81578
81590
|
const uniqueEnrollmentIds = [...new Set(enrollmentIds)];
|
|
81579
|
-
const results = await
|
|
81591
|
+
const results = await runWithConcurrency(uniqueEnrollmentIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (enrollmentId) => {
|
|
81580
81592
|
try {
|
|
81581
81593
|
const analytics = await client.api.edubridge.analytics.getEnrollmentFacts({
|
|
81582
81594
|
enrollmentId,
|
|
@@ -81670,7 +81682,7 @@ class TimebackAdminService {
|
|
|
81670
81682
|
if (options.runOwnersById.size === 0) {
|
|
81671
81683
|
return { events: [], runIds: new Set };
|
|
81672
81684
|
}
|
|
81673
|
-
const hydratedRuns = await
|
|
81685
|
+
const hydratedRuns = await runWithConcurrency([...options.runOwnersById.entries()], TimebackAdminService.DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY, async ([runId, studentId]) => {
|
|
81674
81686
|
try {
|
|
81675
81687
|
return {
|
|
81676
81688
|
runId,
|
|
@@ -81703,7 +81715,7 @@ class TimebackAdminService {
|
|
|
81703
81715
|
};
|
|
81704
81716
|
}
|
|
81705
81717
|
async buildMetricDiscrepancyQueueCandidates(user, options) {
|
|
81706
|
-
const comparisonResults = await
|
|
81718
|
+
const comparisonResults = await runWithConcurrency(options.activityGroups, TimebackAdminService.DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY, async (group) => {
|
|
81707
81719
|
try {
|
|
81708
81720
|
return {
|
|
81709
81721
|
group,
|
|
@@ -82703,7 +82715,7 @@ class TimebackAdminService {
|
|
|
82703
82715
|
const action = context2.newMasterableUnits < context2.oldMasterableUnits ? "complete" : "revoke";
|
|
82704
82716
|
const failed = [];
|
|
82705
82717
|
let processed = 0;
|
|
82706
|
-
await
|
|
82718
|
+
await runWithConcurrency(context2.affectedStudentIds, 8, async (studentId) => {
|
|
82707
82719
|
try {
|
|
82708
82720
|
await upsertMasteryCompletionEntry({
|
|
82709
82721
|
client,
|
|
@@ -82924,28 +82936,9 @@ class TimebackAdminService {
|
|
|
82924
82936
|
}
|
|
82925
82937
|
async getCompletionStatusByCourse(client, courseIds, studentId) {
|
|
82926
82938
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
82927
|
-
const results = await
|
|
82939
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (courseId) => [courseId, await this.getCompletionStatus(client, courseId, studentId)]);
|
|
82928
82940
|
return new Map(results);
|
|
82929
82941
|
}
|
|
82930
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
82931
|
-
if (items.length === 0) {
|
|
82932
|
-
return [];
|
|
82933
|
-
}
|
|
82934
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
82935
|
-
const results = Array.from({ length: items.length });
|
|
82936
|
-
let nextIndex = 0;
|
|
82937
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
82938
|
-
while (true) {
|
|
82939
|
-
const currentIndex = nextIndex;
|
|
82940
|
-
nextIndex++;
|
|
82941
|
-
if (currentIndex >= items.length) {
|
|
82942
|
-
return;
|
|
82943
|
-
}
|
|
82944
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
82945
|
-
}
|
|
82946
|
-
}));
|
|
82947
|
-
return results;
|
|
82948
|
-
}
|
|
82949
82942
|
}
|
|
82950
82943
|
var init_timeback_admin_service = __esm(async () => {
|
|
82951
82944
|
init_drizzle_orm();
|
|
@@ -82972,8 +82965,877 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
82972
82965
|
]);
|
|
82973
82966
|
});
|
|
82974
82967
|
|
|
82968
|
+
// ../api-core/src/utils/timeback-assessment-rules.util.ts
|
|
82969
|
+
function validateAssessmentStatusTransition(current, next) {
|
|
82970
|
+
if (current === next) {
|
|
82971
|
+
return;
|
|
82972
|
+
}
|
|
82973
|
+
const allowed = current === "draft" && next === "live" || current === "live" && next === "archived" || current === "archived" && next === "live";
|
|
82974
|
+
if (!allowed) {
|
|
82975
|
+
throw new ValidationError(`Assessment status cannot change from ${current} to ${next}`);
|
|
82976
|
+
}
|
|
82977
|
+
}
|
|
82978
|
+
function isAssessmentPublicationTransition(current, next) {
|
|
82979
|
+
return current !== "live" && next === "live";
|
|
82980
|
+
}
|
|
82981
|
+
function assertDraftAssessment(row) {
|
|
82982
|
+
if (row.status !== "draft") {
|
|
82983
|
+
throw new ValidationError("Only draft assessments can change QTI content or question membership");
|
|
82984
|
+
}
|
|
82985
|
+
}
|
|
82986
|
+
function assertAllAssessmentAssociationsDraft(rows) {
|
|
82987
|
+
if (rows.some((row) => row.status !== "draft")) {
|
|
82988
|
+
throw new ValidationError("QTI content cannot change while any associated assessment is live or archived");
|
|
82989
|
+
}
|
|
82990
|
+
}
|
|
82991
|
+
function assertAssessmentHasQuestions(questions) {
|
|
82992
|
+
if (questions.length === 0) {
|
|
82993
|
+
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
82994
|
+
}
|
|
82995
|
+
}
|
|
82996
|
+
function planAssessmentRemoval(status) {
|
|
82997
|
+
if (status === "draft") {
|
|
82998
|
+
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
82999
|
+
}
|
|
83000
|
+
if (status === "live") {
|
|
83001
|
+
return { kind: "archive", action: "archived", operation: "archive" };
|
|
83002
|
+
}
|
|
83003
|
+
return { kind: "none", action: "archived" };
|
|
83004
|
+
}
|
|
83005
|
+
function buildAssessmentAssociationUpdates(row, input) {
|
|
83006
|
+
const updates = {};
|
|
83007
|
+
if (input.purpose !== undefined) {
|
|
83008
|
+
updates.purpose = input.purpose;
|
|
83009
|
+
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
83010
|
+
updates.sortOrder = null;
|
|
83011
|
+
}
|
|
83012
|
+
}
|
|
83013
|
+
if (input.status !== undefined) {
|
|
83014
|
+
updates.status = input.status;
|
|
83015
|
+
if (input.status === "archived") {
|
|
83016
|
+
updates.sortOrder = null;
|
|
83017
|
+
}
|
|
83018
|
+
}
|
|
83019
|
+
return updates;
|
|
83020
|
+
}
|
|
83021
|
+
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
83022
|
+
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
83023
|
+
throw new ValidationError("Assessment order must contain unique identifiers");
|
|
83024
|
+
}
|
|
83025
|
+
}
|
|
83026
|
+
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
83027
|
+
if (!updatedRow) {
|
|
83028
|
+
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
83029
|
+
}
|
|
83030
|
+
return updatedRow;
|
|
83031
|
+
}
|
|
83032
|
+
function lockOrderAssessmentRows(rows) {
|
|
83033
|
+
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
83034
|
+
}
|
|
83035
|
+
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
83036
|
+
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
83037
|
+
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
83038
|
+
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
83039
|
+
}
|
|
83040
|
+
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
83041
|
+
}
|
|
83042
|
+
var init_timeback_assessment_rules_util = __esm(() => {
|
|
83043
|
+
init_errors();
|
|
83044
|
+
});
|
|
83045
|
+
|
|
83046
|
+
// ../api-core/src/utils/timeback-qti-authoring.util.ts
|
|
83047
|
+
function recordValue(value) {
|
|
83048
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
83049
|
+
}
|
|
83050
|
+
function qtiAuthoringSnapshot(input) {
|
|
83051
|
+
if (!recordValue(input.interaction)) {
|
|
83052
|
+
return;
|
|
83053
|
+
}
|
|
83054
|
+
return Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => input[field] === undefined ? [] : [[field, input[field]]]));
|
|
83055
|
+
}
|
|
83056
|
+
function buildOwnedQtiQuestionMetadata(input, ownership, existingMetadata = {}) {
|
|
83057
|
+
const inputMetadata = recordValue(input.metadata) ?? {};
|
|
83058
|
+
const metadata2 = {
|
|
83059
|
+
...existingMetadata,
|
|
83060
|
+
...inputMetadata
|
|
83061
|
+
};
|
|
83062
|
+
const existingAuthoring = recordValue(existingMetadata[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
83063
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
83064
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_EDITOR_MODE_KEY);
|
|
83065
|
+
const authoring = qtiAuthoringSnapshot(input);
|
|
83066
|
+
const shouldReplaceAuthoring = "interaction" in input || "numericTextEntry" in input;
|
|
83067
|
+
const persistedAuthoring = authoring ?? (shouldReplaceAuthoring ? undefined : existingAuthoring);
|
|
83068
|
+
return {
|
|
83069
|
+
...metadata2,
|
|
83070
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
83071
|
+
ownerGameSlug: ownership.gameSlug,
|
|
83072
|
+
[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]: ownership.testIdentifier,
|
|
83073
|
+
...persistedAuthoring ? { [PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]: persistedAuthoring } : {}
|
|
83074
|
+
};
|
|
83075
|
+
}
|
|
83076
|
+
function restoreQtiQuestionAuthoringData(item) {
|
|
83077
|
+
const authoring = recordValue(item.metadata?.[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
83078
|
+
if (!authoring) {
|
|
83079
|
+
return item;
|
|
83080
|
+
}
|
|
83081
|
+
const restored = Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => authoring[field] === undefined ? [] : [[field, authoring[field]]]));
|
|
83082
|
+
return { ...item, ...restored };
|
|
83083
|
+
}
|
|
83084
|
+
function mergeQtiQuestionItem(item, fallbackItem, itemIdentifier, metadata2) {
|
|
83085
|
+
return restoreQtiQuestionAuthoringData({
|
|
83086
|
+
...fallbackItem,
|
|
83087
|
+
...item,
|
|
83088
|
+
identifier: itemIdentifier,
|
|
83089
|
+
title: item.title || fallbackItem?.title || itemIdentifier,
|
|
83090
|
+
type: item.type ?? fallbackItem?.type,
|
|
83091
|
+
rawXml: item.rawXml ?? fallbackItem?.rawXml,
|
|
83092
|
+
interaction: item.interaction ?? fallbackItem?.interaction,
|
|
83093
|
+
responseDeclarations: item.responseDeclarations ?? fallbackItem?.responseDeclarations,
|
|
83094
|
+
metadata: {
|
|
83095
|
+
...fallbackItem?.metadata,
|
|
83096
|
+
...item.metadata,
|
|
83097
|
+
...metadata2
|
|
83098
|
+
}
|
|
83099
|
+
});
|
|
83100
|
+
}
|
|
83101
|
+
function newPlaycademyQuestionIdentifier(testIdentifier) {
|
|
83102
|
+
return `${testIdentifier}-q${crypto.randomUUID().slice(0, 8)}`;
|
|
83103
|
+
}
|
|
83104
|
+
function parseQtiQuestionCreationInput(input) {
|
|
83105
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
83106
|
+
throw new ValidationError("Question creation input must be an object");
|
|
83107
|
+
}
|
|
83108
|
+
const rawInput = input;
|
|
83109
|
+
if (rawInput.mode === undefined) {
|
|
83110
|
+
return { kind: "authoring", input: rawInput };
|
|
83111
|
+
}
|
|
83112
|
+
if (rawInput.mode !== "copy") {
|
|
83113
|
+
throw new ValidationError("Question creation mode is invalid");
|
|
83114
|
+
}
|
|
83115
|
+
const sourceItemIdentifier = typeof rawInput.sourceItemIdentifier === "string" ? rawInput.sourceItemIdentifier.trim() : "";
|
|
83116
|
+
if (!sourceItemIdentifier) {
|
|
83117
|
+
throw new ValidationError("A source question identifier is required to create a copy");
|
|
83118
|
+
}
|
|
83119
|
+
const unexpectedField = Object.keys(rawInput).find((field) => field !== "mode" && field !== "sourceItemIdentifier");
|
|
83120
|
+
if (unexpectedField) {
|
|
83121
|
+
throw new ValidationError(`Question copy input contains an unexpected “${unexpectedField}” field`);
|
|
83122
|
+
}
|
|
83123
|
+
return { kind: "copy", sourceItemIdentifier };
|
|
83124
|
+
}
|
|
83125
|
+
function nonEmptyMetadataString(value) {
|
|
83126
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
83127
|
+
}
|
|
83128
|
+
function qtiQuestionOwnerTestIdentifier(item) {
|
|
83129
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83130
|
+
return;
|
|
83131
|
+
}
|
|
83132
|
+
return nonEmptyMetadataString(item.metadata[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]);
|
|
83133
|
+
}
|
|
83134
|
+
function qtiTestReferencesItem(test, itemIdentifier) {
|
|
83135
|
+
return (test["qti-test-part"] ?? []).some((part) => (part["qti-assessment-section"] ?? []).some((section) => (section["qti-assessment-item-ref"] ?? []).some((reference) => reference.identifier === itemIdentifier)));
|
|
83136
|
+
}
|
|
83137
|
+
function resolveQtiQuestionOwnerGameSlug(item, ownerTest) {
|
|
83138
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83139
|
+
return;
|
|
83140
|
+
}
|
|
83141
|
+
if ("ownerGameSlug" in item.metadata) {
|
|
83142
|
+
return nonEmptyMetadataString(item.metadata.ownerGameSlug);
|
|
83143
|
+
}
|
|
83144
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
83145
|
+
if (!ownerTestIdentifier || !ownerTest || ownerTest.identifier !== ownerTestIdentifier || !qtiTestReferencesItem(ownerTest, item.identifier) || ownerTest.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83146
|
+
return;
|
|
83147
|
+
}
|
|
83148
|
+
return nonEmptyMetadataString(ownerTest.metadata.ownerGameSlug);
|
|
83149
|
+
}
|
|
83150
|
+
function isQtiQuestionOwnedByGame(item, gameSlug, ownerTest) {
|
|
83151
|
+
return resolveQtiQuestionOwnerGameSlug(item, ownerTest) === gameSlug;
|
|
83152
|
+
}
|
|
83153
|
+
function isQtiQuestionOwnedByContext(item, ownership, ownerTest) {
|
|
83154
|
+
const declaredOwnerTest = qtiQuestionOwnerTestIdentifier(item);
|
|
83155
|
+
if (declaredOwnerTest) {
|
|
83156
|
+
return declaredOwnerTest === ownership.testIdentifier && isQtiQuestionOwnedByGame(item, ownership.gameSlug, ownerTest);
|
|
83157
|
+
}
|
|
83158
|
+
return isQtiItemOwnedByTest(item, ownership.testIdentifier);
|
|
83159
|
+
}
|
|
83160
|
+
function normalizedQtiInteractionType(value) {
|
|
83161
|
+
if (typeof value !== "string") {
|
|
83162
|
+
return;
|
|
83163
|
+
}
|
|
83164
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
83165
|
+
return normalized || undefined;
|
|
83166
|
+
}
|
|
83167
|
+
function qtiXmlInteractionTypes(xml) {
|
|
83168
|
+
const markup = xml.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
83169
|
+
const interactionPattern = /<(?!\/)(?:[A-Za-z_][\w.-]*:)?qti-([a-z][\w-]*)-interaction\b[^>]*>/gi;
|
|
83170
|
+
return [...markup.matchAll(interactionPattern)].map((match) => match[1].toLowerCase());
|
|
83171
|
+
}
|
|
83172
|
+
function supportedQtiQuestionInteractionType(question) {
|
|
83173
|
+
const interaction = recordValue(question.interaction);
|
|
83174
|
+
const structuredTypes = [
|
|
83175
|
+
normalizedQtiInteractionType(question.type),
|
|
83176
|
+
normalizedQtiInteractionType(interaction?.type)
|
|
83177
|
+
].filter((type) => Boolean(type));
|
|
83178
|
+
const distinctStructuredTypes = [...new Set(structuredTypes)];
|
|
83179
|
+
if (distinctStructuredTypes.length > 1) {
|
|
83180
|
+
return;
|
|
83181
|
+
}
|
|
83182
|
+
const rawXml = typeof question.rawXml === "string" ? question.rawXml : undefined;
|
|
83183
|
+
if (rawXml) {
|
|
83184
|
+
const xmlTypes = qtiXmlInteractionTypes(rawXml);
|
|
83185
|
+
if (xmlTypes.length !== 1) {
|
|
83186
|
+
return;
|
|
83187
|
+
}
|
|
83188
|
+
const [xmlType] = xmlTypes;
|
|
83189
|
+
const [structuredType2] = distinctStructuredTypes;
|
|
83190
|
+
if (structuredType2 && structuredType2 !== xmlType) {
|
|
83191
|
+
return;
|
|
83192
|
+
}
|
|
83193
|
+
return playcademySupportedQtiInteractionType(xmlType);
|
|
83194
|
+
}
|
|
83195
|
+
const [structuredType] = distinctStructuredTypes;
|
|
83196
|
+
return playcademySupportedQtiInteractionType(structuredType);
|
|
83197
|
+
}
|
|
83198
|
+
function assertSupportedQtiQuestionInteraction(question) {
|
|
83199
|
+
if (!supportedQtiQuestionInteractionType(question)) {
|
|
83200
|
+
throw new ValidationError("This QTI interaction type is not supported by the Playcademy question editor.");
|
|
83201
|
+
}
|
|
83202
|
+
}
|
|
83203
|
+
function invalidQtiCopyXml() {
|
|
83204
|
+
throw new ValidationError("The source question does not contain valid QTI item XML");
|
|
83205
|
+
}
|
|
83206
|
+
function markupEnd(xml, start2, allowInternalSubset) {
|
|
83207
|
+
let quote = null;
|
|
83208
|
+
let subsetDepth = 0;
|
|
83209
|
+
for (let index2 = start2;index2 < xml.length; index2 += 1) {
|
|
83210
|
+
const character = xml[index2];
|
|
83211
|
+
if (quote) {
|
|
83212
|
+
if (character === quote) {
|
|
83213
|
+
quote = null;
|
|
83214
|
+
}
|
|
83215
|
+
} else if (character === '"' || character === "'") {
|
|
83216
|
+
quote = character;
|
|
83217
|
+
} else if (allowInternalSubset && character === "[") {
|
|
83218
|
+
subsetDepth += 1;
|
|
83219
|
+
} else if (allowInternalSubset && character === "]") {
|
|
83220
|
+
subsetDepth = Math.max(0, subsetDepth - 1);
|
|
83221
|
+
} else if (character === ">" && subsetDepth === 0) {
|
|
83222
|
+
return index2;
|
|
83223
|
+
}
|
|
83224
|
+
}
|
|
83225
|
+
return invalidQtiCopyXml();
|
|
83226
|
+
}
|
|
83227
|
+
function skipXmlWhitespace(xml, start2, end = xml.length) {
|
|
83228
|
+
let cursor2 = start2;
|
|
83229
|
+
while (cursor2 < end && /\s/.test(xml[cursor2] ?? "")) {
|
|
83230
|
+
cursor2 += 1;
|
|
83231
|
+
}
|
|
83232
|
+
return cursor2;
|
|
83233
|
+
}
|
|
83234
|
+
function xmlPreambleEnd(xml, cursor2) {
|
|
83235
|
+
if (xml.startsWith("<?", cursor2)) {
|
|
83236
|
+
const end = xml.indexOf("?>", cursor2 + 2);
|
|
83237
|
+
if (end === -1) {
|
|
83238
|
+
return invalidQtiCopyXml();
|
|
83239
|
+
}
|
|
83240
|
+
return end + 2;
|
|
83241
|
+
}
|
|
83242
|
+
if (xml.startsWith("<!--", cursor2)) {
|
|
83243
|
+
const end = xml.indexOf("-->", cursor2 + 4);
|
|
83244
|
+
if (end === -1) {
|
|
83245
|
+
return invalidQtiCopyXml();
|
|
83246
|
+
}
|
|
83247
|
+
return end + 3;
|
|
83248
|
+
}
|
|
83249
|
+
if (/^<!DOCTYPE\b/i.test(xml.slice(cursor2))) {
|
|
83250
|
+
return markupEnd(xml, cursor2 + 2, true) + 1;
|
|
83251
|
+
}
|
|
83252
|
+
return null;
|
|
83253
|
+
}
|
|
83254
|
+
function qtiRootAttributes(xml, start2, end) {
|
|
83255
|
+
const attributes = [];
|
|
83256
|
+
let cursor2 = start2;
|
|
83257
|
+
while (cursor2 < end) {
|
|
83258
|
+
cursor2 = skipXmlWhitespace(xml, cursor2, end);
|
|
83259
|
+
if (xml[cursor2] === "/") {
|
|
83260
|
+
cursor2 += 1;
|
|
83261
|
+
} else if (cursor2 < end) {
|
|
83262
|
+
const nameMatch = /^[A-Za-z_][\w.:-]*/.exec(xml.slice(cursor2, end));
|
|
83263
|
+
if (!nameMatch) {
|
|
83264
|
+
return invalidQtiCopyXml();
|
|
83265
|
+
}
|
|
83266
|
+
const name3 = nameMatch[0];
|
|
83267
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + name3.length, end);
|
|
83268
|
+
if (xml[cursor2] !== "=") {
|
|
83269
|
+
return invalidQtiCopyXml();
|
|
83270
|
+
}
|
|
83271
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + 1, end);
|
|
83272
|
+
const quote = xml[cursor2];
|
|
83273
|
+
if (quote !== '"' && quote !== "'") {
|
|
83274
|
+
return invalidQtiCopyXml();
|
|
83275
|
+
}
|
|
83276
|
+
const valueStart = cursor2 + 1;
|
|
83277
|
+
const valueEnd = xml.indexOf(quote, valueStart);
|
|
83278
|
+
if (valueEnd === -1 || valueEnd > end) {
|
|
83279
|
+
return invalidQtiCopyXml();
|
|
83280
|
+
}
|
|
83281
|
+
attributes.push({ name: name3, quote, valueStart, valueEnd });
|
|
83282
|
+
cursor2 = valueEnd + 1;
|
|
83283
|
+
}
|
|
83284
|
+
}
|
|
83285
|
+
return attributes;
|
|
83286
|
+
}
|
|
83287
|
+
function qtiItemStartTag(xml) {
|
|
83288
|
+
let cursor2 = xml.charCodeAt(0) === 65279 ? 1 : 0;
|
|
83289
|
+
while (cursor2 < xml.length) {
|
|
83290
|
+
cursor2 = skipXmlWhitespace(xml, cursor2);
|
|
83291
|
+
const preambleEnd = xmlPreambleEnd(xml, cursor2);
|
|
83292
|
+
if (preambleEnd !== null) {
|
|
83293
|
+
cursor2 = preambleEnd;
|
|
83294
|
+
} else {
|
|
83295
|
+
const root = /^<([A-Za-z_][\w.:-]*)/.exec(xml.slice(cursor2));
|
|
83296
|
+
if (!root || root[1].split(":").at(-1) !== "qti-assessment-item") {
|
|
83297
|
+
return invalidQtiCopyXml();
|
|
83298
|
+
}
|
|
83299
|
+
const attributesStart = cursor2 + root[0].length;
|
|
83300
|
+
const end = markupEnd(xml, attributesStart, false);
|
|
83301
|
+
const attributes = qtiRootAttributes(xml, attributesStart, end);
|
|
83302
|
+
let insertionPoint = skipXmlWhitespaceBackward(xml, end);
|
|
83303
|
+
if (xml[insertionPoint - 1] === "/") {
|
|
83304
|
+
insertionPoint -= 1;
|
|
83305
|
+
}
|
|
83306
|
+
return { attributes, insertionPoint };
|
|
83307
|
+
}
|
|
83308
|
+
}
|
|
83309
|
+
return invalidQtiCopyXml();
|
|
83310
|
+
}
|
|
83311
|
+
function skipXmlWhitespaceBackward(xml, start2) {
|
|
83312
|
+
let cursor2 = start2;
|
|
83313
|
+
while (/\s/.test(xml[cursor2 - 1] ?? "")) {
|
|
83314
|
+
cursor2 -= 1;
|
|
83315
|
+
}
|
|
83316
|
+
return cursor2;
|
|
83317
|
+
}
|
|
83318
|
+
function escapeXmlAttribute(value, quote) {
|
|
83319
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(quote, quote === '"' ? """ : "'");
|
|
83320
|
+
}
|
|
83321
|
+
function rewriteQtiItemIdentity(xml, identifier, title) {
|
|
83322
|
+
if (!xml.trim() || !identifier.trim() || !title.trim()) {
|
|
83323
|
+
return invalidQtiCopyXml();
|
|
83324
|
+
}
|
|
83325
|
+
const root = qtiItemStartTag(xml);
|
|
83326
|
+
const replacements = [];
|
|
83327
|
+
const targetAttributes = new Map([
|
|
83328
|
+
["identifier", identifier],
|
|
83329
|
+
["title", title]
|
|
83330
|
+
]);
|
|
83331
|
+
for (const [name3, value] of targetAttributes) {
|
|
83332
|
+
const matches = root.attributes.filter((attribute2) => attribute2.name === name3);
|
|
83333
|
+
if (matches.length > 1) {
|
|
83334
|
+
return invalidQtiCopyXml();
|
|
83335
|
+
}
|
|
83336
|
+
const attribute = matches[0];
|
|
83337
|
+
if (attribute) {
|
|
83338
|
+
replacements.push({
|
|
83339
|
+
start: attribute.valueStart,
|
|
83340
|
+
end: attribute.valueEnd,
|
|
83341
|
+
value: escapeXmlAttribute(value, attribute.quote)
|
|
83342
|
+
});
|
|
83343
|
+
} else {
|
|
83344
|
+
replacements.push({
|
|
83345
|
+
start: root.insertionPoint,
|
|
83346
|
+
end: root.insertionPoint,
|
|
83347
|
+
value: ` ${name3}="${escapeXmlAttribute(value, '"')}"`
|
|
83348
|
+
});
|
|
83349
|
+
}
|
|
83350
|
+
}
|
|
83351
|
+
return replacements.toSorted((left, right) => right.start - left.start).reduce((rewritten, replacement) => `${rewritten.slice(0, replacement.start)}${replacement.value}${rewritten.slice(replacement.end)}`, xml);
|
|
83352
|
+
}
|
|
83353
|
+
function buildQtiQuestionCopyInput(input) {
|
|
83354
|
+
assertSupportedQtiQuestionInteraction(input.source);
|
|
83355
|
+
const title = input.source.title?.trim() || input.source.identifier;
|
|
83356
|
+
if (!input.source.rawXml) {
|
|
83357
|
+
return invalidQtiCopyXml();
|
|
83358
|
+
}
|
|
83359
|
+
const sourceMetadata = { ...input.source.metadata };
|
|
83360
|
+
if (sourceMetadata.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83361
|
+
Reflect.deleteProperty(sourceMetadata, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
83362
|
+
}
|
|
83363
|
+
const metadata2 = buildOwnedQtiQuestionMetadata({
|
|
83364
|
+
metadata: {
|
|
83365
|
+
copiedFromItemIdentifier: input.source.identifier,
|
|
83366
|
+
integrationId: input.integrationId
|
|
83367
|
+
}
|
|
83368
|
+
}, {
|
|
83369
|
+
gameSlug: input.gameSlug,
|
|
83370
|
+
testIdentifier: input.targetTestIdentifier
|
|
83371
|
+
}, sourceMetadata);
|
|
83372
|
+
return {
|
|
83373
|
+
identifier: input.targetIdentifier,
|
|
83374
|
+
title,
|
|
83375
|
+
xml: rewriteQtiItemIdentity(input.source.rawXml, input.targetIdentifier, title),
|
|
83376
|
+
metadata: metadata2
|
|
83377
|
+
};
|
|
83378
|
+
}
|
|
83379
|
+
function assertQtiQuestionEditable(item, ownership, ownerTest) {
|
|
83380
|
+
if (!isQtiQuestionOwnedByContext(item, ownership, ownerTest)) {
|
|
83381
|
+
throw new ValidationError("Shared question references are read-only. Copy the question to create an editable independent item.");
|
|
83382
|
+
}
|
|
83383
|
+
}
|
|
83384
|
+
function buildNumericQuestionXml(input, identifier, title) {
|
|
83385
|
+
if (input.format === "xml") {
|
|
83386
|
+
throw new ValidationError("Raw question XML is not accepted at this API boundary");
|
|
83387
|
+
}
|
|
83388
|
+
const candidate = input.numericTextEntry;
|
|
83389
|
+
if (candidate === undefined) {
|
|
83390
|
+
return null;
|
|
83391
|
+
}
|
|
83392
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
83393
|
+
throw new ValidationError("Numeric text-entry data is invalid");
|
|
83394
|
+
}
|
|
83395
|
+
const prompt = "prompt" in candidate ? candidate.prompt : undefined;
|
|
83396
|
+
const numeric3 = parseNumericTextEntry({
|
|
83397
|
+
baseType: "baseType" in candidate ? candidate.baseType : undefined,
|
|
83398
|
+
answer: "answer" in candidate ? candidate.answer : undefined,
|
|
83399
|
+
comparison: "comparison" in candidate ? candidate.comparison : undefined
|
|
83400
|
+
});
|
|
83401
|
+
if (!numeric3.success) {
|
|
83402
|
+
throw new ValidationError(numeric3.message);
|
|
83403
|
+
}
|
|
83404
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83405
|
+
throw new ValidationError("Numeric questions require a title and prompt");
|
|
83406
|
+
}
|
|
83407
|
+
return numericTextEntryXml({ identifier, title, prompt: prompt.trim(), numeric: numeric3.value });
|
|
83408
|
+
}
|
|
83409
|
+
function buildExactMatchQuestionXml(input) {
|
|
83410
|
+
const responseIdentifier = escapeXml(input.responseIdentifier);
|
|
83411
|
+
const correctValues = input.correctIdentifiers.map((identifier) => ` <qti-value>${escapeXml(identifier)}</qti-value>`).join(`
|
|
83412
|
+
`);
|
|
83413
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
83414
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
83415
|
+
<qti-response-declaration identifier="${responseIdentifier}" cardinality="${input.cardinality}" base-type="identifier">
|
|
83416
|
+
<qti-correct-response>
|
|
83417
|
+
${correctValues}
|
|
83418
|
+
</qti-correct-response>
|
|
83419
|
+
</qti-response-declaration>
|
|
83420
|
+
<qti-outcome-declaration identifier="FEEDBACK" cardinality="single" base-type="identifier" />
|
|
83421
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
83422
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
83423
|
+
</qti-outcome-declaration>
|
|
83424
|
+
<qti-item-body>
|
|
83425
|
+
${input.itemBody}
|
|
83426
|
+
</qti-item-body>
|
|
83427
|
+
<qti-response-processing>
|
|
83428
|
+
<qti-response-condition>
|
|
83429
|
+
<qti-response-if>
|
|
83430
|
+
<qti-match><qti-variable identifier="${responseIdentifier}" /><qti-correct identifier="${responseIdentifier}" /></qti-match>
|
|
83431
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">CORRECT</qti-base-value></qti-set-outcome-value>
|
|
83432
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
83433
|
+
</qti-response-if>
|
|
83434
|
+
<qti-response-else>
|
|
83435
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">INCORRECT</qti-base-value></qti-set-outcome-value>
|
|
83436
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">0</qti-base-value></qti-set-outcome-value>
|
|
83437
|
+
</qti-response-else>
|
|
83438
|
+
</qti-response-condition>
|
|
83439
|
+
</qti-response-processing>
|
|
83440
|
+
</qti-assessment-item>`;
|
|
83441
|
+
}
|
|
83442
|
+
function parseInlineChoices(value) {
|
|
83443
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
83444
|
+
throw new ValidationError("Inline-choice questions require at least two choices");
|
|
83445
|
+
}
|
|
83446
|
+
const choices = value.map((choice) => {
|
|
83447
|
+
const record3 = recordValue(choice);
|
|
83448
|
+
const identifier = record3?.identifier;
|
|
83449
|
+
const content = record3?.content;
|
|
83450
|
+
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
|
|
83451
|
+
throw new ValidationError("Inline-choice options are invalid");
|
|
83452
|
+
}
|
|
83453
|
+
return { identifier: identifier.trim(), content: content.trim() };
|
|
83454
|
+
});
|
|
83455
|
+
if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
|
|
83456
|
+
throw new ValidationError("Inline-choice option identifiers must be unique");
|
|
83457
|
+
}
|
|
83458
|
+
return choices;
|
|
83459
|
+
}
|
|
83460
|
+
function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
|
|
83461
|
+
const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
|
|
83462
|
+
const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
|
|
83463
|
+
const correctResponse = recordValue(declaration?.correctResponse);
|
|
83464
|
+
const rawValues = correctResponse?.value;
|
|
83465
|
+
const values = Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : [];
|
|
83466
|
+
if (declaration?.cardinality !== "single" || declaration.baseType !== "identifier") {
|
|
83467
|
+
throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
|
|
83468
|
+
}
|
|
83469
|
+
if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
|
|
83470
|
+
throw new ValidationError("Inline-choice questions require one valid correct option");
|
|
83471
|
+
}
|
|
83472
|
+
return values[0];
|
|
83473
|
+
}
|
|
83474
|
+
function buildInlineChoiceQuestionXml(input, identifier, title) {
|
|
83475
|
+
const interaction = recordValue(input.interaction);
|
|
83476
|
+
const interactionType = normalizedQtiInteractionType(interaction?.type ?? input.type);
|
|
83477
|
+
if (interactionType !== "inline-choice") {
|
|
83478
|
+
return null;
|
|
83479
|
+
}
|
|
83480
|
+
const responseIdentifier = interaction?.responseIdentifier;
|
|
83481
|
+
const structure = recordValue(interaction?.questionStructure);
|
|
83482
|
+
const prompt = structure?.prompt;
|
|
83483
|
+
if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
83484
|
+
throw new ValidationError("Inline-choice question data is invalid");
|
|
83485
|
+
}
|
|
83486
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83487
|
+
throw new ValidationError("Inline-choice questions require a title and prompt");
|
|
83488
|
+
}
|
|
83489
|
+
const normalizedPrompt = prompt.trim();
|
|
83490
|
+
const promptParts = normalizedPrompt.split(INLINE_CHOICE_BLANK);
|
|
83491
|
+
if (promptParts.length !== 2) {
|
|
83492
|
+
throw new ValidationError("Inline-choice questions require exactly one blank");
|
|
83493
|
+
}
|
|
83494
|
+
const choices = parseInlineChoices(structure.inlineChoices);
|
|
83495
|
+
const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
|
|
83496
|
+
const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
|
|
83497
|
+
const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
|
|
83498
|
+
`);
|
|
83499
|
+
const [before = "", after = ""] = promptParts;
|
|
83500
|
+
const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
|
|
83501
|
+
${choicesXml}
|
|
83502
|
+
</qti-inline-choice-interaction>${escapeXml(after)}</p>`;
|
|
83503
|
+
return buildExactMatchQuestionXml({
|
|
83504
|
+
identifier,
|
|
83505
|
+
title,
|
|
83506
|
+
responseIdentifier,
|
|
83507
|
+
cardinality: "single",
|
|
83508
|
+
correctIdentifiers: [correctIdentifier],
|
|
83509
|
+
itemBody
|
|
83510
|
+
});
|
|
83511
|
+
}
|
|
83512
|
+
function parseHottextSegments(value) {
|
|
83513
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
83514
|
+
throw new ValidationError("Hottext questions require passage segments");
|
|
83515
|
+
}
|
|
83516
|
+
const segments = value.map((segment) => {
|
|
83517
|
+
const record3 = recordValue(segment);
|
|
83518
|
+
const identifier = record3?.identifier;
|
|
83519
|
+
const content = record3?.content;
|
|
83520
|
+
const mode = record3?.mode;
|
|
83521
|
+
if (typeof identifier !== "string" || typeof content !== "string" || !content.trim() || !["plain", "option", "correct"].includes(String(mode))) {
|
|
83522
|
+
throw new ValidationError("Hottext passage segments are invalid");
|
|
83523
|
+
}
|
|
83524
|
+
return {
|
|
83525
|
+
identifier: identifier.trim(),
|
|
83526
|
+
content: content.trim(),
|
|
83527
|
+
mode
|
|
83528
|
+
};
|
|
83529
|
+
});
|
|
83530
|
+
const selectable = segments.filter((segment) => segment.mode !== "plain");
|
|
83531
|
+
const correct = selectable.filter((segment) => segment.mode === "correct");
|
|
83532
|
+
const identifiers = selectable.map((segment) => segment.identifier);
|
|
83533
|
+
if (selectable.length < 2) {
|
|
83534
|
+
throw new ValidationError("Hottext questions require at least two selectable phrases");
|
|
83535
|
+
}
|
|
83536
|
+
if (identifiers.some((identifier) => !identifier)) {
|
|
83537
|
+
throw new ValidationError("Hottext selectable phrases require identifiers");
|
|
83538
|
+
}
|
|
83539
|
+
if (new Set(identifiers).size !== identifiers.length) {
|
|
83540
|
+
throw new ValidationError("Hottext selectable phrase identifiers must be unique");
|
|
83541
|
+
}
|
|
83542
|
+
if (correct.length === 0) {
|
|
83543
|
+
throw new ValidationError("Hottext questions require a correct phrase");
|
|
83544
|
+
}
|
|
83545
|
+
return { segments, selectable, correct };
|
|
83546
|
+
}
|
|
83547
|
+
function buildHottextQuestionXml(input, identifier, title) {
|
|
83548
|
+
const candidate = input.hottext;
|
|
83549
|
+
if (candidate === undefined) {
|
|
83550
|
+
return null;
|
|
83551
|
+
}
|
|
83552
|
+
const hottext = recordValue(candidate);
|
|
83553
|
+
if (!hottext) {
|
|
83554
|
+
throw new ValidationError("Hottext question data is invalid");
|
|
83555
|
+
}
|
|
83556
|
+
const prompt = hottext.prompt;
|
|
83557
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83558
|
+
throw new ValidationError("Hottext questions require a title and prompt");
|
|
83559
|
+
}
|
|
83560
|
+
const { segments, selectable, correct } = parseHottextSegments(hottext.segments);
|
|
83561
|
+
const multiple = correct.length > 1;
|
|
83562
|
+
const maxChoices = hottextMaxChoices(selectable.length, multiple);
|
|
83563
|
+
const passage = segments.map((segment) => segment.mode === "plain" ? escapeXml(segment.content) : `<qti-hottext identifier="${escapeXml(segment.identifier)}">${escapeXml(segment.content)}</qti-hottext>`).join(" ");
|
|
83564
|
+
const itemBody = ` <qti-hottext-interaction response-identifier="RESPONSE" max-choices="${maxChoices}">
|
|
83565
|
+
<qti-prompt>${escapeXml(prompt.trim())}</qti-prompt>
|
|
83566
|
+
<p>${passage}</p>
|
|
83567
|
+
</qti-hottext-interaction>`;
|
|
83568
|
+
return buildExactMatchQuestionXml({
|
|
83569
|
+
identifier,
|
|
83570
|
+
title,
|
|
83571
|
+
responseIdentifier: "RESPONSE",
|
|
83572
|
+
cardinality: multiple ? "multiple" : "single",
|
|
83573
|
+
correctIdentifiers: correct.map((segment) => segment.identifier),
|
|
83574
|
+
itemBody
|
|
83575
|
+
});
|
|
83576
|
+
}
|
|
83577
|
+
function buildQuestionXml(input, identifier, title) {
|
|
83578
|
+
for (const build2 of QUESTION_XML_BUILDERS) {
|
|
83579
|
+
const xml = build2(input, identifier, title);
|
|
83580
|
+
if (xml !== null) {
|
|
83581
|
+
return xml;
|
|
83582
|
+
}
|
|
83583
|
+
}
|
|
83584
|
+
return null;
|
|
83585
|
+
}
|
|
83586
|
+
function buildCreatedQtiQuestionReference(input) {
|
|
83587
|
+
return {
|
|
83588
|
+
ownership: "owned",
|
|
83589
|
+
reference: {
|
|
83590
|
+
identifier: input.itemIdentifier,
|
|
83591
|
+
href: input.href,
|
|
83592
|
+
testPart: input.partIdentifier,
|
|
83593
|
+
section: input.sectionIdentifier
|
|
83594
|
+
},
|
|
83595
|
+
question: mergeQtiQuestionItem(input.item, input.fallbackItem, input.itemIdentifier, input.metadata)
|
|
83596
|
+
};
|
|
83597
|
+
}
|
|
83598
|
+
function buildHydratedQtiQuestionReference(input) {
|
|
83599
|
+
const identifier = input.reference.reference.identifier;
|
|
83600
|
+
const ownerGameSlug = resolveQtiQuestionOwnerGameSlug(input.item, input.ownerTest);
|
|
83601
|
+
const authoritativeItem = ownerGameSlug ? {
|
|
83602
|
+
...input.item,
|
|
83603
|
+
metadata: { ...input.item.metadata, ownerGameSlug }
|
|
83604
|
+
} : input.item;
|
|
83605
|
+
const question = mergeQtiQuestionItem(authoritativeItem, input.reference.question, identifier);
|
|
83606
|
+
return {
|
|
83607
|
+
...input.reference,
|
|
83608
|
+
ownership: isQtiQuestionOwnedByContext(question, { gameSlug: input.gameSlug, testIdentifier: input.testIdentifier }, input.ownerTest) ? "owned" : "shared",
|
|
83609
|
+
question
|
|
83610
|
+
};
|
|
83611
|
+
}
|
|
83612
|
+
function isQtiTestOwnedByGame(test, gameSlug) {
|
|
83613
|
+
return test.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && test.metadata.ownerGameSlug === gameSlug;
|
|
83614
|
+
}
|
|
83615
|
+
function assertQtiTestOwnedByGame(test, gameSlug) {
|
|
83616
|
+
if (!isQtiTestOwnedByGame(test, gameSlug)) {
|
|
83617
|
+
throw new ValidationError("Shared assessment references are read-only. Copy the assessment to create an editable independent test.");
|
|
83618
|
+
}
|
|
83619
|
+
}
|
|
83620
|
+
function qtiTestParts(test) {
|
|
83621
|
+
const parts2 = test["qti-test-part"];
|
|
83622
|
+
if (!Array.isArray(parts2) || parts2.length === 0) {
|
|
83623
|
+
throw new ValidationError(`Assessment ${test.identifier} has no test parts`);
|
|
83624
|
+
}
|
|
83625
|
+
for (const [partIndex, part] of parts2.entries()) {
|
|
83626
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
|
83627
|
+
throw new ValidationError(`Assessment ${test.identifier} contains an invalid test part at position ${partIndex + 1}`);
|
|
83628
|
+
}
|
|
83629
|
+
const sections = part["qti-assessment-section"];
|
|
83630
|
+
if (!Array.isArray(sections)) {
|
|
83631
|
+
throw new ValidationError(`Assessment ${test.identifier} test part ${partIndex + 1} has no section list`);
|
|
83632
|
+
}
|
|
83633
|
+
}
|
|
83634
|
+
return parts2;
|
|
83635
|
+
}
|
|
83636
|
+
function qtiTestOptionalAttributes(test) {
|
|
83637
|
+
return {
|
|
83638
|
+
...test.qtiVersion ? { qtiVersion: test.qtiVersion } : {},
|
|
83639
|
+
...test.timeLimit !== undefined ? { timeLimit: test.timeLimit } : {},
|
|
83640
|
+
...test.maxAttempts !== undefined ? { maxAttempts: test.maxAttempts } : {},
|
|
83641
|
+
...test.toolsEnabled !== undefined ? { toolsEnabled: test.toolsEnabled } : {}
|
|
83642
|
+
};
|
|
83643
|
+
}
|
|
83644
|
+
function buildQtiAssessmentItemCopyPlan(test, targetTestIdentifier, hrefForIdentifier, createIdentifier = newPlaycademyQuestionIdentifier) {
|
|
83645
|
+
const sourceIdentifiers = [];
|
|
83646
|
+
const seen = new Set;
|
|
83647
|
+
for (const part of qtiTestParts(test)) {
|
|
83648
|
+
for (const section of part["qti-assessment-section"]) {
|
|
83649
|
+
for (const reference of section["qti-assessment-item-ref"] ?? []) {
|
|
83650
|
+
if (!seen.has(reference.identifier)) {
|
|
83651
|
+
seen.add(reference.identifier);
|
|
83652
|
+
sourceIdentifiers.push(reference.identifier);
|
|
83653
|
+
}
|
|
83654
|
+
}
|
|
83655
|
+
}
|
|
83656
|
+
}
|
|
83657
|
+
return sourceIdentifiers.map((sourceIdentifier) => {
|
|
83658
|
+
const targetIdentifier = createIdentifier(targetTestIdentifier);
|
|
83659
|
+
return {
|
|
83660
|
+
sourceIdentifier,
|
|
83661
|
+
targetIdentifier,
|
|
83662
|
+
href: hrefForIdentifier(targetIdentifier)
|
|
83663
|
+
};
|
|
83664
|
+
});
|
|
83665
|
+
}
|
|
83666
|
+
function buildQtiTestStructureInput(test, targetTestIdentifier, itemCopies) {
|
|
83667
|
+
const outcomeDeclarations = test["qti-outcome-declaration"];
|
|
83668
|
+
if (outcomeDeclarations !== undefined && !Array.isArray(outcomeDeclarations)) {
|
|
83669
|
+
throw new ValidationError(`Assessment ${test.identifier} has an invalid outcome declaration list`);
|
|
83670
|
+
}
|
|
83671
|
+
return {
|
|
83672
|
+
"qti-test-part": qtiTestParts(test).map((part, partIndex) => ({
|
|
83673
|
+
identifier: targetTestIdentifier ? `${targetTestIdentifier}-part${partIndex + 1}` : part.identifier,
|
|
83674
|
+
navigationMode: part.navigationMode,
|
|
83675
|
+
submissionMode: part.submissionMode,
|
|
83676
|
+
"qti-assessment-section": part["qti-assessment-section"].map((section, sectionIndex) => {
|
|
83677
|
+
let sectionIdentifier = section.identifier;
|
|
83678
|
+
if (targetTestIdentifier) {
|
|
83679
|
+
sectionIdentifier = partIndex === 0 && sectionIndex === 0 ? `${targetTestIdentifier}-section1` : `${targetTestIdentifier}-part${partIndex + 1}-section${sectionIndex + 1}`;
|
|
83680
|
+
}
|
|
83681
|
+
return {
|
|
83682
|
+
identifier: sectionIdentifier,
|
|
83683
|
+
title: section.title,
|
|
83684
|
+
visible: section.visible ?? true,
|
|
83685
|
+
...section.required !== undefined ? { required: section.required } : {},
|
|
83686
|
+
...section.fixed !== undefined ? { fixed: section.fixed } : {},
|
|
83687
|
+
sequence: section.sequence ?? sectionIndex + 1,
|
|
83688
|
+
...section["qti-assessment-item-ref"] ? {
|
|
83689
|
+
"qti-assessment-item-ref": section["qti-assessment-item-ref"].map((item, itemIndex) => {
|
|
83690
|
+
const copy = itemCopies?.get(item.identifier);
|
|
83691
|
+
if (itemCopies && !copy) {
|
|
83692
|
+
throw new ValidationError(`Assessment copy is missing question ${item.identifier}`);
|
|
83693
|
+
}
|
|
83694
|
+
return {
|
|
83695
|
+
identifier: copy?.targetIdentifier ?? item.identifier,
|
|
83696
|
+
href: copy?.href ?? item.href,
|
|
83697
|
+
sequence: item.sequence ?? itemIndex + 1
|
|
83698
|
+
};
|
|
83699
|
+
})
|
|
83700
|
+
} : {}
|
|
83701
|
+
};
|
|
83702
|
+
})
|
|
83703
|
+
})),
|
|
83704
|
+
...outcomeDeclarations ? {
|
|
83705
|
+
"qti-outcome-declaration": outcomeDeclarations.map((declaration) => ({
|
|
83706
|
+
identifier: declaration.identifier,
|
|
83707
|
+
...declaration.cardinality !== undefined ? { cardinality: declaration.cardinality } : {},
|
|
83708
|
+
baseType: declaration.baseType,
|
|
83709
|
+
...declaration.normalMaximum !== undefined ? { normalMaximum: declaration.normalMaximum } : {},
|
|
83710
|
+
...declaration.normalMinimum !== undefined ? { normalMinimum: declaration.normalMinimum } : {},
|
|
83711
|
+
...declaration.defaultValue ? {
|
|
83712
|
+
defaultValue: declaration.defaultValue.value !== undefined ? { value: declaration.defaultValue.value } : {}
|
|
83713
|
+
} : {}
|
|
83714
|
+
}))
|
|
83715
|
+
} : {}
|
|
83716
|
+
};
|
|
83717
|
+
}
|
|
83718
|
+
function buildQtiTestUpdateInput(test, title) {
|
|
83719
|
+
return {
|
|
83720
|
+
title,
|
|
83721
|
+
...qtiTestOptionalAttributes(test),
|
|
83722
|
+
...test.metadata ? { metadata: test.metadata } : {},
|
|
83723
|
+
...buildQtiTestStructureInput(test)
|
|
83724
|
+
};
|
|
83725
|
+
}
|
|
83726
|
+
function buildQtiTestCopyInput(source, targetTestIdentifier, metadata2, itemCopies) {
|
|
83727
|
+
const copiesBySourceIdentifier = new Map(itemCopies.map((copy) => [copy.sourceIdentifier, copy]));
|
|
83728
|
+
return {
|
|
83729
|
+
identifier: targetTestIdentifier,
|
|
83730
|
+
title: `${source.title} (copy)`,
|
|
83731
|
+
...qtiTestOptionalAttributes(source),
|
|
83732
|
+
metadata: metadata2,
|
|
83733
|
+
...buildQtiTestStructureInput(source, targetTestIdentifier, copiesBySourceIdentifier)
|
|
83734
|
+
};
|
|
83735
|
+
}
|
|
83736
|
+
function validateUniqueQuestionIdentifiers(itemIdentifiers) {
|
|
83737
|
+
if (itemIdentifiers.length === 0 || new Set(itemIdentifiers).size !== itemIdentifiers.length) {
|
|
83738
|
+
throw new ValidationError("Question order must contain unique question identifiers");
|
|
83739
|
+
}
|
|
83740
|
+
}
|
|
83741
|
+
function resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers) {
|
|
83742
|
+
let selectedPart;
|
|
83743
|
+
let selectedSection;
|
|
83744
|
+
for (const part of qtiTestParts(test)) {
|
|
83745
|
+
for (const section of part["qti-assessment-section"]) {
|
|
83746
|
+
if (!itemIdentifier || section["qti-assessment-item-ref"]?.some((item) => item.identifier === itemIdentifier)) {
|
|
83747
|
+
selectedPart = part;
|
|
83748
|
+
selectedSection = section;
|
|
83749
|
+
break;
|
|
83750
|
+
}
|
|
83751
|
+
}
|
|
83752
|
+
if (selectedSection) {
|
|
83753
|
+
break;
|
|
83754
|
+
}
|
|
83755
|
+
}
|
|
83756
|
+
if (!selectedPart || !selectedSection) {
|
|
83757
|
+
throw new ValidationError(itemIdentifier ? `Question ${itemIdentifier} is not referenced by assessment ${qtiTestIdentifier}` : `Assessment ${qtiTestIdentifier} has no section for questions`);
|
|
83758
|
+
}
|
|
83759
|
+
if (expectedItemIdentifiers) {
|
|
83760
|
+
const sectionIdentifiers = selectedSection["qti-assessment-item-ref"]?.map((item) => item.identifier) ?? [];
|
|
83761
|
+
const expectedIdentifiers = new Set(expectedItemIdentifiers);
|
|
83762
|
+
if (expectedIdentifiers.size !== sectionIdentifiers.length || sectionIdentifiers.some((identifier) => !expectedIdentifiers.has(identifier))) {
|
|
83763
|
+
throw new ValidationError("Questions can only be reordered within one complete assessment section");
|
|
83764
|
+
}
|
|
83765
|
+
}
|
|
83766
|
+
return {
|
|
83767
|
+
partIdentifier: selectedPart.identifier,
|
|
83768
|
+
sectionIdentifier: selectedSection.identifier
|
|
83769
|
+
};
|
|
83770
|
+
}
|
|
83771
|
+
var PLAYCADEMY_QTI_AUTHORING_METADATA_KEY = "playcademyAuthoring", QTI_AUTHORING_FIELDS, QUESTION_XML_BUILDERS;
|
|
83772
|
+
var init_timeback_qti_authoring_util = __esm(() => {
|
|
83773
|
+
init_timeback3();
|
|
83774
|
+
init_errors();
|
|
83775
|
+
QTI_AUTHORING_FIELDS = [
|
|
83776
|
+
"type",
|
|
83777
|
+
"qtiVersion",
|
|
83778
|
+
"timeDependent",
|
|
83779
|
+
"adaptive",
|
|
83780
|
+
"preInteraction",
|
|
83781
|
+
"interaction",
|
|
83782
|
+
"postInteraction",
|
|
83783
|
+
"responseDeclarations",
|
|
83784
|
+
"outcomeDeclarations",
|
|
83785
|
+
"responseProcessing",
|
|
83786
|
+
"modalFeedback",
|
|
83787
|
+
"feedbackInline",
|
|
83788
|
+
"feedbackBlock",
|
|
83789
|
+
"rubrics",
|
|
83790
|
+
"stimulus",
|
|
83791
|
+
"content"
|
|
83792
|
+
];
|
|
83793
|
+
QUESTION_XML_BUILDERS = [
|
|
83794
|
+
buildNumericQuestionXml,
|
|
83795
|
+
buildHottextQuestionXml,
|
|
83796
|
+
buildInlineChoiceQuestionXml
|
|
83797
|
+
];
|
|
83798
|
+
});
|
|
83799
|
+
|
|
83800
|
+
// ../api-core/src/utils/timeback-qti-library.util.ts
|
|
83801
|
+
function newPlaycademyTestIdentifier() {
|
|
83802
|
+
return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
|
|
83803
|
+
}
|
|
83804
|
+
function playcademySourceWhere() {
|
|
83805
|
+
return {
|
|
83806
|
+
identifier: {
|
|
83807
|
+
gte: PLAYCADEMY_QTI_SOURCE_PREFIX,
|
|
83808
|
+
lt: PLAYCADEMY_QTI_SOURCE_UPPER_BOUND
|
|
83809
|
+
}
|
|
83810
|
+
};
|
|
83811
|
+
}
|
|
83812
|
+
function buildQtiLibraryListPlan(params) {
|
|
83813
|
+
const query = params.query?.trim() || undefined;
|
|
83814
|
+
if (!params.source && !query) {
|
|
83815
|
+
throw new Error("An exact QTI source identifier is required for a global lookup");
|
|
83816
|
+
}
|
|
83817
|
+
let where;
|
|
83818
|
+
if (params.source) {
|
|
83819
|
+
where = playcademySourceWhere();
|
|
83820
|
+
} else if (query) {
|
|
83821
|
+
where = { identifier: query };
|
|
83822
|
+
}
|
|
83823
|
+
return {
|
|
83824
|
+
kind: "direct",
|
|
83825
|
+
params: {
|
|
83826
|
+
...where ? { where } : {},
|
|
83827
|
+
...params.source && query ? { query } : {},
|
|
83828
|
+
page: params.page,
|
|
83829
|
+
limit: params.limit,
|
|
83830
|
+
...params.source ? { sort: "identifier", order: "asc" } : {}
|
|
83831
|
+
}
|
|
83832
|
+
};
|
|
83833
|
+
}
|
|
83834
|
+
var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-", PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
|
|
83835
|
+
|
|
82975
83836
|
// ../api-core/src/services/timeback-assessments.service.ts
|
|
82976
83837
|
class TimebackAssessmentsService {
|
|
83838
|
+
static QTI_HYDRATION_CONCURRENCY = 8;
|
|
82977
83839
|
deps;
|
|
82978
83840
|
constructor(deps) {
|
|
82979
83841
|
this.deps = deps;
|
|
@@ -82990,33 +83852,19 @@ class TimebackAssessmentsService {
|
|
|
82990
83852
|
}
|
|
82991
83853
|
async listAssessments(integrationId) {
|
|
82992
83854
|
const client = this.requireClient();
|
|
82993
|
-
await this.
|
|
83855
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
82994
83856
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
82995
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
82996
|
-
orderBy: asc(gameTimebackAssessmentTests.sortOrder)
|
|
83857
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
82997
83858
|
});
|
|
82998
|
-
|
|
82999
|
-
return [];
|
|
83000
|
-
}
|
|
83001
|
-
const assessments = await Promise.all(rows.map(async (row) => {
|
|
83859
|
+
const assessments = await runWithConcurrency(rows, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
83002
83860
|
try {
|
|
83003
83861
|
const test = await client.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
83004
|
-
let itemCount = 0;
|
|
83005
|
-
for (const part of test["qti-test-part"] ?? []) {
|
|
83006
|
-
for (const section of part["qti-assessment-section"] ?? []) {
|
|
83007
|
-
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
83008
|
-
}
|
|
83009
|
-
}
|
|
83010
83862
|
return {
|
|
83011
|
-
|
|
83012
|
-
integrationId: row.integrationId,
|
|
83013
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
83014
|
-
bankResourceId: row.bankResourceId,
|
|
83015
|
-
bankActive: row.bankActive,
|
|
83016
|
-
sortOrder: row.sortOrder,
|
|
83863
|
+
...this.associationSummary(row),
|
|
83017
83864
|
title: test.title,
|
|
83018
|
-
questionCount:
|
|
83019
|
-
|
|
83865
|
+
questionCount: countQtiTestItems(test),
|
|
83866
|
+
available: true,
|
|
83867
|
+
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
83020
83868
|
};
|
|
83021
83869
|
} catch (error88) {
|
|
83022
83870
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -83025,319 +83873,420 @@ class TimebackAssessmentsService {
|
|
|
83025
83873
|
"app.error.message": errorMessage(error88)
|
|
83026
83874
|
});
|
|
83027
83875
|
return {
|
|
83028
|
-
|
|
83029
|
-
integrationId: row.integrationId,
|
|
83030
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
83031
|
-
bankResourceId: row.bankResourceId,
|
|
83032
|
-
bankActive: row.bankActive,
|
|
83033
|
-
sortOrder: row.sortOrder,
|
|
83876
|
+
...this.associationSummary(row),
|
|
83034
83877
|
title: row.qtiTestIdentifier,
|
|
83035
83878
|
questionCount: 0,
|
|
83036
|
-
|
|
83879
|
+
available: false,
|
|
83880
|
+
editable: false
|
|
83037
83881
|
};
|
|
83038
83882
|
}
|
|
83039
|
-
})
|
|
83040
|
-
return assessments;
|
|
83883
|
+
});
|
|
83884
|
+
return assessments.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
83041
83885
|
}
|
|
83042
83886
|
async createAssessment(integrationId, input) {
|
|
83043
83887
|
const client = this.requireClient();
|
|
83044
|
-
const
|
|
83045
|
-
|
|
83046
|
-
courseId: integration.courseId,
|
|
83047
|
-
subject: integration.subject,
|
|
83048
|
-
grade: integration.grade
|
|
83049
|
-
});
|
|
83888
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
83889
|
+
const { integration } = ownership;
|
|
83050
83890
|
await client.course.createAssessmentTest({
|
|
83051
83891
|
identifier: input.qtiTestIdentifier,
|
|
83052
83892
|
title: input.title,
|
|
83053
83893
|
metadata: {
|
|
83894
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
83895
|
+
ownerGameSlug: ownership.gameSlug,
|
|
83054
83896
|
integrationId,
|
|
83055
83897
|
subject: integration.subject,
|
|
83056
83898
|
grade: String(integration.grade)
|
|
83057
83899
|
}
|
|
83058
83900
|
});
|
|
83059
|
-
const maxSortOrder = await this.getMaxSortOrder(integrationId);
|
|
83060
|
-
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
83061
|
-
integrationId,
|
|
83062
|
-
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
83063
|
-
sortOrder: maxSortOrder + 1
|
|
83064
|
-
}).returning();
|
|
83065
|
-
setAttribute("app.assessment.operation", "create");
|
|
83066
|
-
return row;
|
|
83067
|
-
}
|
|
83068
|
-
async deleteAssessment(integrationId, qtiTestIdentifier) {
|
|
83069
|
-
const client = this.requireClient();
|
|
83070
|
-
const row = await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
83071
|
-
if (row.bankActive) {
|
|
83072
|
-
await this.deactivateAssessment(integrationId, qtiTestIdentifier);
|
|
83073
|
-
}
|
|
83074
83901
|
try {
|
|
83075
|
-
await
|
|
83902
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
83903
|
+
integrationId,
|
|
83904
|
+
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
83905
|
+
purpose: input.purpose,
|
|
83906
|
+
status: "draft"
|
|
83907
|
+
}).returning();
|
|
83908
|
+
setAttribute("app.assessment.operation", "create");
|
|
83909
|
+
return row;
|
|
83076
83910
|
} catch (error88) {
|
|
83077
|
-
|
|
83078
|
-
|
|
83079
|
-
|
|
83080
|
-
|
|
83081
|
-
|
|
83911
|
+
let committedRow;
|
|
83912
|
+
try {
|
|
83913
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
83914
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, input.qtiTestIdentifier))
|
|
83915
|
+
});
|
|
83916
|
+
} catch (verificationError) {
|
|
83917
|
+
addEvent("assessment.qti_test_create_commit_verification_failed", {
|
|
83918
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
83919
|
+
"exception.type": errorType(verificationError),
|
|
83920
|
+
"app.error.message": errorMessage(verificationError)
|
|
83921
|
+
});
|
|
83922
|
+
throw error88;
|
|
83923
|
+
}
|
|
83924
|
+
if (committedRow) {
|
|
83925
|
+
setAttribute("app.assessment.operation", "create");
|
|
83926
|
+
return committedRow;
|
|
83927
|
+
}
|
|
83928
|
+
try {
|
|
83929
|
+
await client.qtiApi.assessmentTests.delete(input.qtiTestIdentifier);
|
|
83930
|
+
} catch (cleanupError) {
|
|
83931
|
+
if (!isApiError(cleanupError) || cleanupError.statusCode !== 404) {
|
|
83932
|
+
addEvent("assessment.qti_test_create_cleanup_failed", {
|
|
83933
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
83934
|
+
"exception.type": errorType(cleanupError),
|
|
83935
|
+
"app.error.message": errorMessage(cleanupError)
|
|
83936
|
+
});
|
|
83937
|
+
}
|
|
83938
|
+
}
|
|
83939
|
+
throw error88;
|
|
83082
83940
|
}
|
|
83083
|
-
await this.deps.db.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83084
|
-
setAttribute("app.assessment.operation", "delete");
|
|
83085
|
-
}
|
|
83086
|
-
async reorderAssessments(integrationId, identifiers) {
|
|
83087
|
-
await this.requireIntegration(integrationId);
|
|
83088
|
-
await Promise.all(identifiers.map((identifier, i2) => this.deps.db.update(gameTimebackAssessmentTests).set({ sortOrder: i2 }).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, identifier)))));
|
|
83089
83941
|
}
|
|
83090
|
-
async
|
|
83091
|
-
|
|
83092
|
-
|
|
83093
|
-
|
|
83094
|
-
|
|
83095
|
-
|
|
83096
|
-
|
|
83097
|
-
|
|
83098
|
-
|
|
83099
|
-
|
|
83100
|
-
|
|
83101
|
-
|
|
83102
|
-
|
|
83103
|
-
|
|
83104
|
-
|
|
83105
|
-
|
|
83942
|
+
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
83943
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
83944
|
+
const updates = buildAssessmentAssociationUpdates(row, input);
|
|
83945
|
+
if (input.status !== undefined) {
|
|
83946
|
+
validateAssessmentStatusTransition(row.status, input.status);
|
|
83947
|
+
if (isAssessmentPublicationTransition(row.status, input.status)) {
|
|
83948
|
+
await this.validateAssessmentHasQuestions(qtiTestIdentifier);
|
|
83949
|
+
}
|
|
83950
|
+
}
|
|
83951
|
+
if (input.title !== undefined) {
|
|
83952
|
+
assertDraftAssessment(row);
|
|
83953
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
83954
|
+
const client = this.requireClient();
|
|
83955
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
83956
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
83957
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
83958
|
+
await client.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
83959
|
+
}
|
|
83960
|
+
let updated = row;
|
|
83961
|
+
if (Object.keys(updates).length > 0) {
|
|
83962
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ ...updates, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
|
|
83963
|
+
updated = updatedRow ?? row;
|
|
83964
|
+
}
|
|
83965
|
+
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
83966
|
+
return updated;
|
|
83106
83967
|
});
|
|
83107
|
-
return item;
|
|
83108
|
-
}
|
|
83109
|
-
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
83110
|
-
const client = this.requireClient();
|
|
83111
|
-
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
83112
|
-
return client.qtiApi.assessmentItems.update(itemIdentifier, input);
|
|
83113
83968
|
}
|
|
83114
|
-
async
|
|
83115
|
-
|
|
83116
|
-
|
|
83117
|
-
|
|
83118
|
-
|
|
83969
|
+
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
83970
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
83971
|
+
const plan = planAssessmentRemoval(row.status);
|
|
83972
|
+
if (plan.kind === "delete") {
|
|
83973
|
+
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83974
|
+
} else if (plan.kind === "archive") {
|
|
83975
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", sortOrder: null, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83976
|
+
}
|
|
83977
|
+
if (plan.kind !== "none") {
|
|
83978
|
+
setAttribute("app.assessment.operation", plan.operation);
|
|
83979
|
+
}
|
|
83980
|
+
return { action: plan.action };
|
|
83981
|
+
});
|
|
83119
83982
|
}
|
|
83120
|
-
async
|
|
83121
|
-
|
|
83122
|
-
|
|
83123
|
-
const
|
|
83124
|
-
|
|
83125
|
-
|
|
83126
|
-
|
|
83127
|
-
|
|
83128
|
-
|
|
83129
|
-
|
|
83983
|
+
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
83984
|
+
await this.requireIntegration(integrationId);
|
|
83985
|
+
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
83986
|
+
const updatedAt = new Date;
|
|
83987
|
+
await this.deps.db.transaction(async (tx) => {
|
|
83988
|
+
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
83989
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
83990
|
+
});
|
|
83991
|
+
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
83992
|
+
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
83993
|
+
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
83994
|
+
const testIdentifier = row.qtiTestIdentifier;
|
|
83995
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ sortOrder: positionByRowId.get(row.id), updatedAt }).where(and(eq(gameTimebackAssessmentTests.id, row.id), eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, testIdentifier), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))).returning({ id: gameTimebackAssessmentTests.id });
|
|
83996
|
+
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
83997
|
+
}
|
|
83130
83998
|
});
|
|
83999
|
+
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
83131
84000
|
}
|
|
83132
|
-
async
|
|
84001
|
+
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
83133
84002
|
const client = this.requireClient();
|
|
83134
|
-
|
|
83135
|
-
const
|
|
83136
|
-
|
|
83137
|
-
|
|
83138
|
-
|
|
83139
|
-
|
|
83140
|
-
const qtiTestUrl = `${client.getQtiBaseUrl()}/assessment-tests/${qtiTestIdentifier}`;
|
|
83141
|
-
await client.course.ensureAssessmentBank({
|
|
83142
|
-
courseId: integration.courseId,
|
|
83143
|
-
subject: integration.subject,
|
|
83144
|
-
grade: integration.grade
|
|
83145
|
-
});
|
|
83146
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
83147
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
83148
|
-
let childResourceId = row.bankResourceId;
|
|
83149
|
-
if (childResourceId) {
|
|
84003
|
+
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
84004
|
+
const { gameSlug } = await this.requireQtiTestOwnershipContext(integrationId);
|
|
84005
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84006
|
+
const ownerTests = new Map;
|
|
84007
|
+
const questions = await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
84008
|
+
let item;
|
|
83150
84009
|
try {
|
|
83151
|
-
await client.
|
|
83152
|
-
status: "active",
|
|
83153
|
-
title: `Assessment Bank Test: ${qtiTestIdentifier}`,
|
|
83154
|
-
vendorResourceId: "",
|
|
83155
|
-
metadata: {
|
|
83156
|
-
type: "qti",
|
|
83157
|
-
subType: "qti-test",
|
|
83158
|
-
url: qtiTestUrl
|
|
83159
|
-
}
|
|
83160
|
-
});
|
|
83161
|
-
if (!currentResources.includes(childResourceId)) {
|
|
83162
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83163
|
-
status: "active",
|
|
83164
|
-
title: parentResource.title,
|
|
83165
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83166
|
-
vendorId: parentResource.vendorId,
|
|
83167
|
-
metadata: {
|
|
83168
|
-
...parentResource.metadata,
|
|
83169
|
-
resources: [...currentResources, childResourceId]
|
|
83170
|
-
}
|
|
83171
|
-
});
|
|
83172
|
-
}
|
|
84010
|
+
item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
83173
84011
|
} catch (error88) {
|
|
83174
|
-
addEvent("assessment.
|
|
83175
|
-
"app.assessment.
|
|
84012
|
+
addEvent("assessment.qti_question_hydration_failed", {
|
|
84013
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84014
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
83176
84015
|
"exception.type": errorType(error88),
|
|
83177
84016
|
"app.error.message": errorMessage(error88)
|
|
83178
84017
|
});
|
|
83179
|
-
|
|
84018
|
+
return reference;
|
|
83180
84019
|
}
|
|
83181
|
-
|
|
83182
|
-
|
|
83183
|
-
|
|
83184
|
-
|
|
83185
|
-
|
|
83186
|
-
|
|
83187
|
-
|
|
83188
|
-
|
|
83189
|
-
|
|
83190
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
83191
|
-
"app.assessment.child_resource_id": resourceId
|
|
83192
|
-
});
|
|
83193
|
-
break;
|
|
83194
|
-
}
|
|
83195
|
-
} catch {}
|
|
83196
|
-
}
|
|
83197
|
-
if (!childResourceId) {
|
|
83198
|
-
const childResult = await client.api.oneroster.resources.create({
|
|
83199
|
-
status: "active",
|
|
83200
|
-
title: `Assessment Bank Test: ${qtiTestIdentifier}`,
|
|
83201
|
-
vendorResourceId: "",
|
|
83202
|
-
vendorId: undefined,
|
|
83203
|
-
metadata: {
|
|
83204
|
-
type: "qti",
|
|
83205
|
-
subType: "qti-test",
|
|
83206
|
-
url: qtiTestUrl
|
|
83207
|
-
}
|
|
83208
|
-
});
|
|
83209
|
-
childResourceId = childResult.sourcedIdPairs.allocatedSourcedId;
|
|
83210
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83211
|
-
status: "active",
|
|
83212
|
-
title: parentResource.title,
|
|
83213
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83214
|
-
vendorId: parentResource.vendorId,
|
|
83215
|
-
metadata: {
|
|
83216
|
-
...parentResource.metadata,
|
|
83217
|
-
resources: [...currentResources, childResourceId]
|
|
83218
|
-
}
|
|
84020
|
+
let ownerTest;
|
|
84021
|
+
try {
|
|
84022
|
+
ownerTest = await this.qtiQuestionOwnerTest(client, item, ownerTests);
|
|
84023
|
+
} catch (error88) {
|
|
84024
|
+
addEvent("assessment.qti_question_owner_test_fetch_failed", {
|
|
84025
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84026
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
84027
|
+
"exception.type": errorType(error88),
|
|
84028
|
+
"app.error.message": errorMessage(error88)
|
|
83219
84029
|
});
|
|
83220
84030
|
}
|
|
83221
|
-
|
|
83222
|
-
|
|
83223
|
-
|
|
83224
|
-
|
|
83225
|
-
|
|
84031
|
+
return buildHydratedQtiQuestionReference({
|
|
84032
|
+
reference,
|
|
84033
|
+
item,
|
|
84034
|
+
gameSlug,
|
|
84035
|
+
ownerTest,
|
|
84036
|
+
testIdentifier: qtiTestIdentifier
|
|
84037
|
+
});
|
|
83226
84038
|
});
|
|
84039
|
+
return { ...result, questions };
|
|
83227
84040
|
}
|
|
83228
|
-
async
|
|
84041
|
+
async listQuestionLibrary(integrationId, params) {
|
|
83229
84042
|
const client = this.requireClient();
|
|
83230
|
-
|
|
83231
|
-
|
|
83232
|
-
if (!row.bankActive) {
|
|
83233
|
-
throw new ValidationError("Assessment is already in draft");
|
|
83234
|
-
}
|
|
83235
|
-
if (!row.bankResourceId) {
|
|
83236
|
-
throw new ValidationError("Assessment has no bank resource — activate it first");
|
|
83237
|
-
}
|
|
83238
|
-
const childResourceId = row.bankResourceId;
|
|
83239
|
-
const bankIds = deriveAssessmentBankIds2(integration.courseId);
|
|
83240
|
-
try {
|
|
83241
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
83242
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
83243
|
-
const filtered = currentResources.filter((id) => id !== childResourceId);
|
|
83244
|
-
if (filtered.length !== currentResources.length) {
|
|
83245
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83246
|
-
status: "active",
|
|
83247
|
-
title: parentResource.title,
|
|
83248
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83249
|
-
vendorId: parentResource.vendorId,
|
|
83250
|
-
metadata: {
|
|
83251
|
-
...parentResource.metadata,
|
|
83252
|
-
resources: filtered
|
|
83253
|
-
}
|
|
83254
|
-
});
|
|
83255
|
-
}
|
|
83256
|
-
} catch (error88) {
|
|
83257
|
-
addEvent("assessment.parent_resource_update_failed", {
|
|
83258
|
-
"app.assessment.bank_resource_id": bankIds.resource,
|
|
83259
|
-
"exception.type": errorType(error88),
|
|
83260
|
-
"app.error.message": errorMessage(error88)
|
|
83261
|
-
});
|
|
83262
|
-
}
|
|
83263
|
-
try {
|
|
83264
|
-
await client.api.oneroster.resources.delete(childResourceId);
|
|
83265
|
-
} catch (error88) {
|
|
83266
|
-
addEvent("assessment.child_resource_delete_failed", {
|
|
83267
|
-
"app.assessment.child_resource_id": childResourceId,
|
|
83268
|
-
"exception.type": errorType(error88),
|
|
83269
|
-
"app.error.message": errorMessage(error88)
|
|
83270
|
-
});
|
|
83271
|
-
}
|
|
83272
|
-
await this.deps.db.update(gameTimebackAssessmentTests).set({ bankActive: false }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83273
|
-
setAttribute("app.assessment.operation", "deactivate");
|
|
83274
|
-
}
|
|
83275
|
-
isAssessmentActive(row) {
|
|
83276
|
-
return row.bankActive;
|
|
84043
|
+
await this.requireIntegration(integrationId);
|
|
84044
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentItems.list(listParams));
|
|
83277
84045
|
}
|
|
83278
|
-
async
|
|
83279
|
-
const
|
|
83280
|
-
|
|
83281
|
-
|
|
83282
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
83283
|
-
});
|
|
83284
|
-
const activeCount = rows.filter((r) => r.bankActive).length;
|
|
83285
|
-
return {
|
|
83286
|
-
bankIds,
|
|
83287
|
-
totalAssessments: rows.length,
|
|
83288
|
-
activeAssessments: activeCount,
|
|
83289
|
-
draftAssessments: rows.length - activeCount
|
|
83290
|
-
};
|
|
84046
|
+
async listTestLibrary(integrationId, params) {
|
|
84047
|
+
const client = this.requireClient();
|
|
84048
|
+
await this.requireIntegration(integrationId);
|
|
84049
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentTests.list(listParams));
|
|
83291
84050
|
}
|
|
83292
|
-
async
|
|
84051
|
+
async copyAssessment(integrationId, sourceTestIdentifier, targetTestIdentifier, purpose) {
|
|
83293
84052
|
const client = this.requireClient();
|
|
83294
|
-
const
|
|
83295
|
-
const
|
|
83296
|
-
const
|
|
83297
|
-
|
|
84053
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
84054
|
+
const { integration } = ownership;
|
|
84055
|
+
const source = await client.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
84056
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => this.qtiItemHref(client, identifier));
|
|
84057
|
+
const itemCopies = await runWithConcurrency(itemPlan, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
84058
|
+
const sourceItem = await client.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
84059
|
+
return {
|
|
84060
|
+
plan,
|
|
84061
|
+
input: buildQtiQuestionCopyInput({
|
|
84062
|
+
source: sourceItem,
|
|
84063
|
+
targetIdentifier: plan.targetIdentifier,
|
|
84064
|
+
targetTestIdentifier,
|
|
84065
|
+
gameSlug: ownership.gameSlug,
|
|
84066
|
+
integrationId
|
|
84067
|
+
})
|
|
84068
|
+
};
|
|
83298
84069
|
});
|
|
84070
|
+
const testInput = buildQtiTestCopyInput(source, targetTestIdentifier, {
|
|
84071
|
+
...source.metadata,
|
|
84072
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
84073
|
+
ownerGameSlug: ownership.gameSlug,
|
|
84074
|
+
integrationId,
|
|
84075
|
+
subject: integration.subject,
|
|
84076
|
+
grade: String(integration.grade),
|
|
84077
|
+
copiedFromTestIdentifier: sourceTestIdentifier
|
|
84078
|
+
}, itemPlan);
|
|
84079
|
+
const attemptedItemIdentifiers = [];
|
|
84080
|
+
let testCreationAttempted = false;
|
|
84081
|
+
let associationCreationAttempted = false;
|
|
83299
84082
|
try {
|
|
83300
|
-
|
|
84083
|
+
for (const copy of itemCopies) {
|
|
84084
|
+
attemptedItemIdentifiers.push(copy.plan.targetIdentifier);
|
|
84085
|
+
await client.course.createAssessmentItemXml({
|
|
84086
|
+
xml: copy.input.xml,
|
|
84087
|
+
metadata: copy.input.metadata
|
|
84088
|
+
});
|
|
84089
|
+
}
|
|
84090
|
+
testCreationAttempted = true;
|
|
84091
|
+
await client.qtiApi.assessmentTests.create(testInput);
|
|
84092
|
+
associationCreationAttempted = true;
|
|
84093
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
84094
|
+
integrationId,
|
|
84095
|
+
qtiTestIdentifier: targetTestIdentifier,
|
|
84096
|
+
purpose,
|
|
84097
|
+
status: "draft"
|
|
84098
|
+
}).returning();
|
|
84099
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
84100
|
+
return row;
|
|
83301
84101
|
} catch (error88) {
|
|
83302
|
-
|
|
83303
|
-
|
|
83304
|
-
"exception.type": errorType(error88),
|
|
83305
|
-
"app.error.message": errorMessage(error88)
|
|
83306
|
-
});
|
|
83307
|
-
}
|
|
83308
|
-
for (const row of activeRows) {
|
|
83309
|
-
if (row.bankResourceId) {
|
|
84102
|
+
if (associationCreationAttempted) {
|
|
84103
|
+
let committedRow;
|
|
83310
84104
|
try {
|
|
83311
|
-
await
|
|
83312
|
-
|
|
83313
|
-
addEvent("assessment.child_resource_delete_failed", {
|
|
83314
|
-
"app.assessment.child_resource_id": row.bankResourceId,
|
|
83315
|
-
"exception.type": errorType(error88),
|
|
83316
|
-
"app.error.message": errorMessage(error88)
|
|
84105
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
84106
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, targetTestIdentifier))
|
|
83317
84107
|
});
|
|
84108
|
+
} catch (verificationError) {
|
|
84109
|
+
addEvent("assessment.qti_test_copy_commit_verification_failed", {
|
|
84110
|
+
"app.assessment.qti_test_identifier": targetTestIdentifier,
|
|
84111
|
+
"exception.type": errorType(verificationError),
|
|
84112
|
+
"app.error.message": errorMessage(verificationError)
|
|
84113
|
+
});
|
|
84114
|
+
throw error88;
|
|
84115
|
+
}
|
|
84116
|
+
if (committedRow) {
|
|
84117
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
84118
|
+
return committedRow;
|
|
83318
84119
|
}
|
|
83319
84120
|
}
|
|
84121
|
+
await this.cleanupQtiAssessmentCopy(client, testCreationAttempted ? targetTestIdentifier : undefined, attemptedItemIdentifiers);
|
|
84122
|
+
throw error88;
|
|
83320
84123
|
}
|
|
83321
|
-
|
|
83322
|
-
|
|
83323
|
-
|
|
83324
|
-
|
|
83325
|
-
|
|
83326
|
-
|
|
83327
|
-
|
|
84124
|
+
}
|
|
84125
|
+
async createQuestion(integrationId, qtiTestIdentifier, input) {
|
|
84126
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84127
|
+
const client = this.requireClient();
|
|
84128
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84129
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84130
|
+
const creation = parseQtiQuestionCreationInput(input);
|
|
84131
|
+
const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
|
|
84132
|
+
let title;
|
|
84133
|
+
let metadata2;
|
|
84134
|
+
let xml;
|
|
84135
|
+
let structuredInput;
|
|
84136
|
+
let fallbackItem;
|
|
84137
|
+
if (creation.kind === "copy") {
|
|
84138
|
+
const source = await client.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
|
|
84139
|
+
const copy = buildQtiQuestionCopyInput({
|
|
84140
|
+
source,
|
|
84141
|
+
targetIdentifier: itemIdentifier,
|
|
84142
|
+
targetTestIdentifier: qtiTestIdentifier,
|
|
84143
|
+
gameSlug: ownership.gameSlug,
|
|
84144
|
+
integrationId
|
|
84145
|
+
});
|
|
84146
|
+
title = copy.title;
|
|
84147
|
+
metadata2 = copy.metadata;
|
|
84148
|
+
xml = copy.xml;
|
|
84149
|
+
fallbackItem = {
|
|
84150
|
+
...source,
|
|
84151
|
+
identifier: itemIdentifier,
|
|
84152
|
+
title,
|
|
84153
|
+
rawXml: xml,
|
|
84154
|
+
metadata: metadata2
|
|
84155
|
+
};
|
|
84156
|
+
} else {
|
|
84157
|
+
structuredInput = creation.input;
|
|
84158
|
+
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
84159
|
+
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
84160
|
+
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
84161
|
+
gameSlug: ownership.gameSlug,
|
|
84162
|
+
testIdentifier: qtiTestIdentifier
|
|
84163
|
+
});
|
|
84164
|
+
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
84165
|
+
fallbackItem = {
|
|
84166
|
+
...structuredInput,
|
|
84167
|
+
identifier: itemIdentifier,
|
|
84168
|
+
title,
|
|
84169
|
+
...xml ? { rawXml: xml } : {}
|
|
84170
|
+
};
|
|
84171
|
+
}
|
|
84172
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
84173
|
+
const href = this.qtiItemHref(client, itemIdentifier);
|
|
84174
|
+
let item;
|
|
84175
|
+
let itemCreationAttempted = false;
|
|
84176
|
+
let referenceCreationAttempted = false;
|
|
84177
|
+
try {
|
|
84178
|
+
itemCreationAttempted = true;
|
|
84179
|
+
item = xml ? await client.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.create({
|
|
84180
|
+
...structuredInput,
|
|
84181
|
+
identifier: itemIdentifier,
|
|
84182
|
+
metadata: metadata2
|
|
84183
|
+
});
|
|
84184
|
+
referenceCreationAttempted = true;
|
|
84185
|
+
await section.items.add({
|
|
84186
|
+
identifier: itemIdentifier,
|
|
84187
|
+
href
|
|
84188
|
+
});
|
|
84189
|
+
} catch (error88) {
|
|
84190
|
+
let referenceRemoved = !referenceCreationAttempted;
|
|
84191
|
+
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
84192
|
+
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
84193
|
+
if (referenceCreationAttempted) {
|
|
84194
|
+
try {
|
|
84195
|
+
await section.items.remove(itemIdentifier);
|
|
84196
|
+
referenceRemoved = true;
|
|
84197
|
+
} catch (cleanupError) {
|
|
84198
|
+
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
84199
|
+
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
84200
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84201
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
84202
|
+
"exception.type": errorType(cleanupError),
|
|
84203
|
+
"app.error.message": errorMessage(cleanupError)
|
|
84204
|
+
});
|
|
84205
|
+
}
|
|
84206
|
+
}
|
|
84207
|
+
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
84208
|
+
try {
|
|
84209
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
84210
|
+
} catch (cleanupError) {
|
|
84211
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
84212
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84213
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
84214
|
+
"exception.type": errorType(cleanupError),
|
|
84215
|
+
"app.error.message": errorMessage(cleanupError)
|
|
84216
|
+
});
|
|
84217
|
+
}
|
|
84218
|
+
}
|
|
84219
|
+
throw error88;
|
|
84220
|
+
}
|
|
84221
|
+
setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
|
|
84222
|
+
return buildCreatedQtiQuestionReference({
|
|
84223
|
+
item,
|
|
84224
|
+
fallbackItem,
|
|
84225
|
+
itemIdentifier,
|
|
84226
|
+
href,
|
|
84227
|
+
partIdentifier: section.partIdentifier,
|
|
84228
|
+
sectionIdentifier: section.sectionIdentifier,
|
|
84229
|
+
metadata: metadata2
|
|
83328
84230
|
});
|
|
83329
|
-
}
|
|
83330
|
-
|
|
83331
|
-
|
|
83332
|
-
|
|
83333
|
-
|
|
83334
|
-
|
|
83335
|
-
|
|
83336
|
-
|
|
84231
|
+
});
|
|
84232
|
+
}
|
|
84233
|
+
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
84234
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84235
|
+
const client = this.requireClient();
|
|
84236
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84237
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84238
|
+
const [, item] = await Promise.all([
|
|
84239
|
+
this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test),
|
|
84240
|
+
client.qtiApi.assessmentItems.get(itemIdentifier)
|
|
84241
|
+
]);
|
|
84242
|
+
const ownerTest = await this.qtiQuestionOwnerTest(client, item, undefined, ownership.test);
|
|
84243
|
+
const replacesInteraction = "type" in input || "interaction" in input || "numericTextEntry" in input;
|
|
84244
|
+
assertQtiQuestionEditable(item, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, ownerTest);
|
|
84245
|
+
assertSupportedQtiQuestionInteraction(replacesInteraction ? input : item);
|
|
84246
|
+
let title = typeof input.title === "string" ? input.title.trim() : "";
|
|
84247
|
+
if (!title) {
|
|
84248
|
+
title = item.title;
|
|
84249
|
+
}
|
|
84250
|
+
const metadata2 = buildOwnedQtiQuestionMetadata(input, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, item.metadata);
|
|
84251
|
+
const xml = buildQuestionXml(input, itemIdentifier, title);
|
|
84252
|
+
const updated = xml ? await client.course.updateAssessmentItemXml(itemIdentifier, { xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.update(itemIdentifier, {
|
|
84253
|
+
...input,
|
|
84254
|
+
title,
|
|
84255
|
+
metadata: metadata2
|
|
84256
|
+
});
|
|
84257
|
+
setAttribute("app.assessment.operation", "update_question");
|
|
84258
|
+
return updated;
|
|
84259
|
+
});
|
|
84260
|
+
}
|
|
84261
|
+
async removeQuestion(integrationId, qtiTestIdentifier, itemIdentifier) {
|
|
84262
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84263
|
+
const client = this.requireClient();
|
|
84264
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84265
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84266
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test);
|
|
84267
|
+
await section.items.remove(itemIdentifier);
|
|
84268
|
+
setAttribute("app.assessment.operation", "remove_question");
|
|
84269
|
+
});
|
|
84270
|
+
}
|
|
84271
|
+
async reorderQuestions(integrationId, qtiTestIdentifier, items) {
|
|
84272
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84273
|
+
const client = this.requireClient();
|
|
84274
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84275
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84276
|
+
const identifiers = items.map((item) => item.identifier);
|
|
84277
|
+
validateUniqueQuestionIdentifiers(identifiers);
|
|
84278
|
+
const firstIdentifier = identifiers[0];
|
|
84279
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, firstIdentifier, identifiers, ownership.test);
|
|
84280
|
+
const result = await section.items.reorder({
|
|
84281
|
+
items: items.map((item) => ({
|
|
84282
|
+
identifier: item.identifier,
|
|
84283
|
+
href: item.href ?? this.qtiItemHref(client, item.identifier),
|
|
84284
|
+
sequence: item.sequence
|
|
84285
|
+
}))
|
|
83337
84286
|
});
|
|
83338
|
-
|
|
83339
|
-
|
|
83340
|
-
|
|
84287
|
+
setAttribute("app.assessment.operation", "reorder_questions");
|
|
84288
|
+
return result;
|
|
84289
|
+
});
|
|
83341
84290
|
}
|
|
83342
84291
|
requireClient() {
|
|
83343
84292
|
if (!this.deps.timeback) {
|
|
@@ -83345,8 +84294,18 @@ class TimebackAssessmentsService {
|
|
|
83345
84294
|
}
|
|
83346
84295
|
return this.deps.timeback;
|
|
83347
84296
|
}
|
|
83348
|
-
async
|
|
83349
|
-
|
|
84297
|
+
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
84298
|
+
return this.deps.db.transaction(async (tx) => {
|
|
84299
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
84300
|
+
const row = rows.find((candidate) => candidate.integrationId === integrationId);
|
|
84301
|
+
if (!row) {
|
|
84302
|
+
throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
|
|
84303
|
+
}
|
|
84304
|
+
return action(row, tx, rows);
|
|
84305
|
+
});
|
|
84306
|
+
}
|
|
84307
|
+
async requireIntegration(integrationId, db2 = this.deps.db) {
|
|
84308
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
83350
84309
|
where: and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())
|
|
83351
84310
|
});
|
|
83352
84311
|
if (!integration) {
|
|
@@ -83354,6 +84313,17 @@ class TimebackAssessmentsService {
|
|
|
83354
84313
|
}
|
|
83355
84314
|
return integration;
|
|
83356
84315
|
}
|
|
84316
|
+
async requireQtiTestOwnershipContext(integrationId, db2 = this.deps.db) {
|
|
84317
|
+
const integration = await this.requireIntegration(integrationId, db2);
|
|
84318
|
+
const game2 = await db2.query.games.findFirst({
|
|
84319
|
+
where: eq(games.id, integration.gameId),
|
|
84320
|
+
columns: { slug: true }
|
|
84321
|
+
});
|
|
84322
|
+
if (!game2) {
|
|
84323
|
+
throw new NotFoundError(`Game not found for integration: ${integrationId}`);
|
|
84324
|
+
}
|
|
84325
|
+
return { integration, gameSlug: game2.slug };
|
|
84326
|
+
}
|
|
83357
84327
|
async requireAssessmentRow(integrationId, qtiTestIdentifier) {
|
|
83358
84328
|
const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
83359
84329
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
|
|
@@ -83363,27 +84333,125 @@ class TimebackAssessmentsService {
|
|
|
83363
84333
|
}
|
|
83364
84334
|
return row;
|
|
83365
84335
|
}
|
|
83366
|
-
|
|
83367
|
-
|
|
84336
|
+
async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
|
|
84337
|
+
const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
84338
|
+
assertDraftAssessment(row);
|
|
84339
|
+
return row;
|
|
83368
84340
|
}
|
|
83369
|
-
async
|
|
83370
|
-
const
|
|
83371
|
-
|
|
83372
|
-
|
|
84341
|
+
async requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, lockedRow, db2 = this.deps.db) {
|
|
84342
|
+
const client = this.requireClient();
|
|
84343
|
+
await this.requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow);
|
|
84344
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
84345
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, db2);
|
|
84346
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
84347
|
+
return { ...ownership, test };
|
|
84348
|
+
}
|
|
84349
|
+
async qtiQuestionOwnerTest(client, item, cache, knownTest) {
|
|
84350
|
+
if (item.metadata && "ownerGameSlug" in item.metadata) {
|
|
84351
|
+
return;
|
|
84352
|
+
}
|
|
84353
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
84354
|
+
if (!ownerTestIdentifier) {
|
|
84355
|
+
return;
|
|
84356
|
+
}
|
|
84357
|
+
if (knownTest?.identifier === ownerTestIdentifier) {
|
|
84358
|
+
return knownTest;
|
|
84359
|
+
}
|
|
84360
|
+
function loadOwnerTest(identifier) {
|
|
84361
|
+
return client.qtiApi.assessmentTests.get(identifier);
|
|
84362
|
+
}
|
|
84363
|
+
if (!cache) {
|
|
84364
|
+
return loadOwnerTest(ownerTestIdentifier);
|
|
84365
|
+
}
|
|
84366
|
+
let ownerTest = cache.get(ownerTestIdentifier);
|
|
84367
|
+
if (!ownerTest) {
|
|
84368
|
+
ownerTest = loadOwnerTest(ownerTestIdentifier);
|
|
84369
|
+
cache.set(ownerTestIdentifier, ownerTest);
|
|
84370
|
+
}
|
|
84371
|
+
return ownerTest;
|
|
84372
|
+
}
|
|
84373
|
+
async assertQtiTestQuestionsSupported(client, qtiTestIdentifier, questions) {
|
|
84374
|
+
const result = questions ?? await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84375
|
+
await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
84376
|
+
const item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
84377
|
+
assertSupportedQtiQuestionInteraction(item);
|
|
83373
84378
|
});
|
|
83374
|
-
|
|
83375
|
-
|
|
84379
|
+
}
|
|
84380
|
+
async qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers, knownTest) {
|
|
84381
|
+
const test = knownTest ?? await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
84382
|
+
const section = resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers);
|
|
84383
|
+
return {
|
|
84384
|
+
...section,
|
|
84385
|
+
items: client.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
84386
|
+
};
|
|
84387
|
+
}
|
|
84388
|
+
qtiItemHref(client, itemIdentifier) {
|
|
84389
|
+
return `${client.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
84390
|
+
}
|
|
84391
|
+
async cleanupQtiAssessmentCopy(client, testIdentifier, itemIdentifiers) {
|
|
84392
|
+
let testDeleted = !testIdentifier;
|
|
84393
|
+
if (testIdentifier) {
|
|
84394
|
+
try {
|
|
84395
|
+
await client.qtiApi.assessmentTests.delete(testIdentifier);
|
|
84396
|
+
testDeleted = true;
|
|
84397
|
+
} catch (error88) {
|
|
84398
|
+
testDeleted = isApiError(error88) && error88.statusCode === 404;
|
|
84399
|
+
addEvent("assessment.qti_test_copy_cleanup_failed", {
|
|
84400
|
+
"app.assessment.qti_test_identifier": testIdentifier,
|
|
84401
|
+
"exception.type": errorType(error88),
|
|
84402
|
+
"app.error.message": errorMessage(error88)
|
|
84403
|
+
});
|
|
84404
|
+
}
|
|
84405
|
+
}
|
|
84406
|
+
if (!testDeleted) {
|
|
84407
|
+
return;
|
|
83376
84408
|
}
|
|
83377
|
-
|
|
84409
|
+
for (const itemIdentifier of itemIdentifiers.toReversed()) {
|
|
84410
|
+
try {
|
|
84411
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
84412
|
+
} catch (error88) {
|
|
84413
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
84414
|
+
...testIdentifier ? { "app.assessment.qti_test_identifier": testIdentifier } : {},
|
|
84415
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
84416
|
+
"exception.type": errorType(error88),
|
|
84417
|
+
"app.error.message": errorMessage(error88)
|
|
84418
|
+
});
|
|
84419
|
+
}
|
|
84420
|
+
}
|
|
84421
|
+
}
|
|
84422
|
+
associationSummary(row) {
|
|
84423
|
+
return {
|
|
84424
|
+
id: row.id,
|
|
84425
|
+
integrationId: row.integrationId,
|
|
84426
|
+
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
84427
|
+
purpose: row.purpose,
|
|
84428
|
+
status: row.status,
|
|
84429
|
+
sortOrder: row.sortOrder,
|
|
84430
|
+
createdAt: row.createdAt,
|
|
84431
|
+
updatedAt: row.updatedAt
|
|
84432
|
+
};
|
|
84433
|
+
}
|
|
84434
|
+
async listQtiLibrary(params, list) {
|
|
84435
|
+
const plan = buildQtiLibraryListPlan(params);
|
|
84436
|
+
return list(plan.params);
|
|
84437
|
+
}
|
|
84438
|
+
async validateAssessmentHasQuestions(qtiTestIdentifier) {
|
|
84439
|
+
const client = this.requireClient();
|
|
84440
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84441
|
+
assertAssessmentHasQuestions(result.questions);
|
|
84442
|
+
await this.assertQtiTestQuestionsSupported(client, qtiTestIdentifier, result);
|
|
83378
84443
|
}
|
|
83379
84444
|
}
|
|
83380
|
-
var init_timeback_assessments_service = __esm(() => {
|
|
84445
|
+
var init_timeback_assessments_service = __esm(async () => {
|
|
83381
84446
|
init_drizzle_orm();
|
|
83382
84447
|
init_helpers_index();
|
|
83383
84448
|
init_tables_index();
|
|
83384
84449
|
init_spans();
|
|
83385
|
-
|
|
84450
|
+
init_timeback3();
|
|
83386
84451
|
init_errors();
|
|
84452
|
+
init_timeback_assessment_rules_util();
|
|
84453
|
+
init_timeback_qti_authoring_util();
|
|
84454
|
+
await init_errors8();
|
|
83387
84455
|
});
|
|
83388
84456
|
|
|
83389
84457
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
@@ -85476,10 +86544,10 @@ var init_platform2 = __esm(async () => {
|
|
|
85476
86544
|
init_kv_service();
|
|
85477
86545
|
init_secrets_service();
|
|
85478
86546
|
init_seed_service();
|
|
85479
|
-
init_timeback_assessments_service();
|
|
85480
86547
|
init_upload_service();
|
|
85481
86548
|
await __promiseAll([
|
|
85482
86549
|
init_timeback_admin_service(),
|
|
86550
|
+
init_timeback_assessments_service(),
|
|
85483
86551
|
init_timeback_service()
|
|
85484
86552
|
]);
|
|
85485
86553
|
});
|
|
@@ -142098,16 +143166,6 @@ function requireAnonymous(handler) {
|
|
|
142098
143166
|
return handler(ctx);
|
|
142099
143167
|
};
|
|
142100
143168
|
}
|
|
142101
|
-
function requireAdmin(handler) {
|
|
142102
|
-
return async (ctx) => {
|
|
142103
|
-
assertAuthenticatedRequest(ctx);
|
|
142104
|
-
rejectDashboardWorkerKey(ctx);
|
|
142105
|
-
if (ctx.user.role !== "admin") {
|
|
142106
|
-
throw ApiError.forbidden("Admin access required");
|
|
142107
|
-
}
|
|
142108
|
-
return handler(ctx);
|
|
142109
|
-
};
|
|
142110
|
-
}
|
|
142111
143169
|
function requireDeveloper(handler) {
|
|
142112
143170
|
return async (ctx) => {
|
|
142113
143171
|
assertAuthenticatedRequest(ctx);
|
|
@@ -143271,7 +144329,27 @@ var init_session_controller = __esm(() => {
|
|
|
143271
144329
|
});
|
|
143272
144330
|
|
|
143273
144331
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
143274
|
-
|
|
144332
|
+
function parseQtiLibraryParams(searchParams) {
|
|
144333
|
+
const requestedPage = Number(searchParams.get("page"));
|
|
144334
|
+
const requestedLimit = Number(searchParams.get("limit"));
|
|
144335
|
+
const page = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
|
144336
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 50) : 20;
|
|
144337
|
+
const query = searchParams.get("q")?.trim() || undefined;
|
|
144338
|
+
const requestedSource = searchParams.get("source");
|
|
144339
|
+
if (requestedSource && requestedSource !== "playcademy") {
|
|
144340
|
+
throw ApiError.badRequest("Unsupported QTI library source");
|
|
144341
|
+
}
|
|
144342
|
+
if (requestedSource !== "playcademy" && !query) {
|
|
144343
|
+
throw ApiError.badRequest("An exact QTI source identifier is required for a global lookup");
|
|
144344
|
+
}
|
|
144345
|
+
return {
|
|
144346
|
+
query,
|
|
144347
|
+
source: requestedSource === "playcademy" ? "playcademy" : undefined,
|
|
144348
|
+
page,
|
|
144349
|
+
limit
|
|
144350
|
+
};
|
|
144351
|
+
}
|
|
144352
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, updateAssessment, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
143275
144353
|
var init_timeback_controller = __esm(() => {
|
|
143276
144354
|
init_esm();
|
|
143277
144355
|
init_schemas_index();
|
|
@@ -143786,7 +144864,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
143786
144864
|
const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
|
|
143787
144865
|
return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
|
|
143788
144866
|
});
|
|
143789
|
-
listAssessments =
|
|
144867
|
+
listAssessments = requireDeveloper(async (ctx) => {
|
|
143790
144868
|
const { gameId, courseId } = ctx.params;
|
|
143791
144869
|
if (!gameId || !courseId) {
|
|
143792
144870
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
@@ -143794,40 +144872,40 @@ var init_timeback_controller = __esm(() => {
|
|
|
143794
144872
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143795
144873
|
return ctx.services.timebackAssessments.listAssessments(integrationId);
|
|
143796
144874
|
});
|
|
143797
|
-
createAssessment =
|
|
144875
|
+
createAssessment = requireDeveloper(async (ctx) => {
|
|
143798
144876
|
const { gameId, courseId } = ctx.params;
|
|
143799
144877
|
if (!gameId || !courseId) {
|
|
143800
144878
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143801
144879
|
}
|
|
143802
144880
|
const body2 = await parseRequestBody(ctx.request, CreateAssessmentRequestSchema);
|
|
143803
144881
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143804
|
-
const
|
|
143805
|
-
const qtiTestIdentifier = `assessment-${shortId}`;
|
|
144882
|
+
const qtiTestIdentifier = newPlaycademyTestIdentifier();
|
|
143806
144883
|
return ctx.services.timebackAssessments.createAssessment(integrationId, {
|
|
143807
144884
|
title: body2.title,
|
|
144885
|
+
purpose: body2.purpose,
|
|
143808
144886
|
qtiTestIdentifier
|
|
143809
144887
|
});
|
|
143810
144888
|
});
|
|
143811
|
-
|
|
144889
|
+
updateAssessment = requireDeveloper(async (ctx) => {
|
|
143812
144890
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143813
144891
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143814
144892
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143815
144893
|
}
|
|
143816
144894
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143817
|
-
await ctx.
|
|
143818
|
-
return
|
|
144895
|
+
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
144896
|
+
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
143819
144897
|
});
|
|
143820
|
-
reorderAssessments =
|
|
144898
|
+
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
143821
144899
|
const { gameId, courseId } = ctx.params;
|
|
143822
144900
|
if (!gameId || !courseId) {
|
|
143823
144901
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143824
144902
|
}
|
|
143825
144903
|
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
143826
144904
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143827
|
-
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.
|
|
144905
|
+
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.purpose, body2.testIdentifiers);
|
|
143828
144906
|
return { success: true };
|
|
143829
144907
|
});
|
|
143830
|
-
reorderQuestions =
|
|
144908
|
+
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
143831
144909
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143832
144910
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143833
144911
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -143837,33 +144915,51 @@ var init_timeback_controller = __esm(() => {
|
|
|
143837
144915
|
await ctx.services.timebackAssessments.reorderQuestions(integrationId, testIdentifier, body2.items);
|
|
143838
144916
|
return { success: true };
|
|
143839
144917
|
});
|
|
143840
|
-
|
|
144918
|
+
removeAssessment = requireDeveloper(async (ctx) => {
|
|
143841
144919
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143842
144920
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143843
144921
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143844
144922
|
}
|
|
143845
144923
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143846
|
-
|
|
143847
|
-
return { success: true };
|
|
144924
|
+
return ctx.services.timebackAssessments.removeAssessment(integrationId, testIdentifier);
|
|
143848
144925
|
});
|
|
143849
|
-
|
|
144926
|
+
listQuestions = requireDeveloper(async (ctx) => {
|
|
143850
144927
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143851
144928
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143852
144929
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143853
144930
|
}
|
|
143854
144931
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143855
|
-
|
|
143856
|
-
return { success: true };
|
|
144932
|
+
return ctx.services.timebackAssessments.listQuestions(integrationId, testIdentifier);
|
|
143857
144933
|
});
|
|
143858
|
-
|
|
143859
|
-
const { gameId, courseId
|
|
143860
|
-
if (!gameId || !courseId
|
|
143861
|
-
throw ApiError.badRequest("Missing gameId
|
|
144934
|
+
listQuestionLibrary = requireDeveloper(async (ctx) => {
|
|
144935
|
+
const { gameId, courseId } = ctx.params;
|
|
144936
|
+
if (!gameId || !courseId) {
|
|
144937
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143862
144938
|
}
|
|
144939
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
143863
144940
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143864
|
-
return ctx.services.timebackAssessments.
|
|
144941
|
+
return ctx.services.timebackAssessments.listQuestionLibrary(integrationId, params);
|
|
144942
|
+
});
|
|
144943
|
+
listTestLibrary = requireDeveloper(async (ctx) => {
|
|
144944
|
+
const { gameId, courseId } = ctx.params;
|
|
144945
|
+
if (!gameId || !courseId) {
|
|
144946
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
144947
|
+
}
|
|
144948
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
144949
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
144950
|
+
return ctx.services.timebackAssessments.listTestLibrary(integrationId, params);
|
|
143865
144951
|
});
|
|
143866
|
-
|
|
144952
|
+
copyAssessment = requireDeveloper(async (ctx) => {
|
|
144953
|
+
const { gameId, courseId } = ctx.params;
|
|
144954
|
+
if (!gameId || !courseId) {
|
|
144955
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
144956
|
+
}
|
|
144957
|
+
const body2 = await parseRequestBody(ctx.request, CopyAssessmentRequestSchema);
|
|
144958
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
144959
|
+
const targetTestIdentifier = newPlaycademyTestIdentifier();
|
|
144960
|
+
return ctx.services.timebackAssessments.copyAssessment(integrationId, body2.testIdentifier, targetTestIdentifier, body2.purpose);
|
|
144961
|
+
});
|
|
144962
|
+
createQuestion = requireDeveloper(async (ctx) => {
|
|
143867
144963
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143868
144964
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143869
144965
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -143872,7 +144968,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
143872
144968
|
const body2 = await ctx.request.json();
|
|
143873
144969
|
return ctx.services.timebackAssessments.createQuestion(integrationId, testIdentifier, body2);
|
|
143874
144970
|
});
|
|
143875
|
-
updateQuestion =
|
|
144971
|
+
updateQuestion = requireDeveloper(async (ctx) => {
|
|
143876
144972
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
143877
144973
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
143878
144974
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
@@ -143881,30 +144977,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
143881
144977
|
const body2 = await ctx.request.json();
|
|
143882
144978
|
return ctx.services.timebackAssessments.updateQuestion(integrationId, testIdentifier, itemIdentifier, body2);
|
|
143883
144979
|
});
|
|
143884
|
-
|
|
144980
|
+
removeQuestion = requireDeveloper(async (ctx) => {
|
|
143885
144981
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
143886
144982
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
143887
144983
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
143888
144984
|
}
|
|
143889
144985
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143890
|
-
await ctx.services.timebackAssessments.
|
|
143891
|
-
return { success: true };
|
|
143892
|
-
});
|
|
143893
|
-
getAssessmentBankStatus = requireGameManagementAccess(async (ctx) => {
|
|
143894
|
-
const { gameId, courseId } = ctx.params;
|
|
143895
|
-
if (!gameId || !courseId) {
|
|
143896
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143897
|
-
}
|
|
143898
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143899
|
-
return ctx.services.timebackAssessments.getBankStatus(integrationId);
|
|
143900
|
-
});
|
|
143901
|
-
destroyAssessmentBank = requireAdmin(async (ctx) => {
|
|
143902
|
-
const { gameId, courseId } = ctx.params;
|
|
143903
|
-
if (!gameId || !courseId) {
|
|
143904
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143905
|
-
}
|
|
143906
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143907
|
-
await ctx.services.timebackAssessments.destroyBank(integrationId);
|
|
144986
|
+
await ctx.services.timebackAssessments.removeQuestion(integrationId, testIdentifier, itemIdentifier);
|
|
143908
144987
|
return { success: true };
|
|
143909
144988
|
});
|
|
143910
144989
|
timeback2 = defineControllerNames("timeback", {
|
|
@@ -143949,17 +145028,17 @@ var init_timeback_controller = __esm(() => {
|
|
|
143949
145028
|
reactivateEnrollment,
|
|
143950
145029
|
listAssessments,
|
|
143951
145030
|
createAssessment,
|
|
143952
|
-
|
|
145031
|
+
updateAssessment,
|
|
143953
145032
|
reorderAssessments,
|
|
145033
|
+
removeAssessment,
|
|
143954
145034
|
reorderQuestions,
|
|
143955
|
-
activateAssessment,
|
|
143956
|
-
deactivateAssessment,
|
|
143957
145035
|
listQuestions,
|
|
145036
|
+
listTestLibrary,
|
|
145037
|
+
copyAssessment,
|
|
145038
|
+
listQuestionLibrary,
|
|
143958
145039
|
createQuestion,
|
|
143959
145040
|
updateQuestion,
|
|
143960
|
-
|
|
143961
|
-
getAssessmentBankStatus,
|
|
143962
|
-
destroyAssessmentBank
|
|
145041
|
+
removeQuestion
|
|
143963
145042
|
});
|
|
143964
145043
|
});
|
|
143965
145044
|
|