@playcademy/sandbox 0.6.1-beta.7 → 0.6.1-beta.9
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 +1781 -626
- package/dist/server.js +1781 -626
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1084,7 +1084,7 @@ var package_default;
|
|
|
1084
1084
|
var init_package = __esm(() => {
|
|
1085
1085
|
package_default = {
|
|
1086
1086
|
name: "@playcademy/sandbox",
|
|
1087
|
-
version: "0.6.1-beta.
|
|
1087
|
+
version: "0.6.1-beta.9",
|
|
1088
1088
|
description: "Local development server for Playcademy game development",
|
|
1089
1089
|
type: "module",
|
|
1090
1090
|
exports: {
|
|
@@ -11587,7 +11587,7 @@ var init_table6 = __esm(() => {
|
|
|
11587
11587
|
});
|
|
11588
11588
|
|
|
11589
11589
|
// ../data/src/domains/timeback/table.ts
|
|
11590
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11590
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11591
11591
|
var init_table7 = __esm(() => {
|
|
11592
11592
|
init_drizzle_orm();
|
|
11593
11593
|
init_pg_core();
|
|
@@ -11597,6 +11597,15 @@ var init_table7 = __esm(() => {
|
|
|
11597
11597
|
"active",
|
|
11598
11598
|
"deactivated"
|
|
11599
11599
|
]);
|
|
11600
|
+
gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", [
|
|
11601
|
+
"end_of_course",
|
|
11602
|
+
"diagnostic"
|
|
11603
|
+
]);
|
|
11604
|
+
gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
|
|
11605
|
+
"draft",
|
|
11606
|
+
"live",
|
|
11607
|
+
"archived"
|
|
11608
|
+
]);
|
|
11600
11609
|
gameTimebackIntegrations = pgTable("game_timeback_integrations", {
|
|
11601
11610
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11602
11611
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
@@ -11617,10 +11626,11 @@ var init_table7 = __esm(() => {
|
|
|
11617
11626
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11618
11627
|
integrationId: uuid("integration_id").notNull().references(() => gameTimebackIntegrations.id, { onDelete: "cascade" }),
|
|
11619
11628
|
qtiTestIdentifier: text("qti_test_identifier").notNull(),
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
sortOrder: integer("sort_order")
|
|
11623
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
11629
|
+
purpose: gameTimebackAssessmentPurposeEnum("purpose").notNull().default("end_of_course"),
|
|
11630
|
+
status: gameTimebackAssessmentStatusEnum("status").notNull().default("draft"),
|
|
11631
|
+
sortOrder: integer("sort_order"),
|
|
11632
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
11633
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
11624
11634
|
}, (table3) => [
|
|
11625
11635
|
uniqueIndex("game_timeback_assessment_tests_integration_qti_idx").on(table3.integrationId, table3.qtiTestIdentifier)
|
|
11626
11636
|
]);
|
|
@@ -11672,6 +11682,8 @@ __export(exports_tables_index, {
|
|
|
11672
11682
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11673
11683
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
11674
11684
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11685
|
+
gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
|
|
11686
|
+
gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
|
|
11675
11687
|
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
11676
11688
|
gameScoresRelations: () => gameScoresRelations,
|
|
11677
11689
|
gameScores: () => gameScores,
|
|
@@ -28293,14 +28305,16 @@ function assertBaselineClaimValid(args2) {
|
|
|
28293
28305
|
function validateBaselineClaim(args2) {
|
|
28294
28306
|
const { claimedTag, evidence, tables, indexes: indexes2, views, lastDeployAt } = args2;
|
|
28295
28307
|
const claimedIndex = evidence.findIndex((entry) => entry.tag === claimedTag);
|
|
28308
|
+
const expected = replayEvidence(evidence, claimedIndex);
|
|
28309
|
+
const live = { tables, indexes: indexes2, views };
|
|
28296
28310
|
const verdicts = [];
|
|
28297
28311
|
const unverified = [];
|
|
28298
28312
|
evidence.forEach((entry, index2) => {
|
|
28299
28313
|
if (claimedIndex === -1 || index2 > claimedIndex) {
|
|
28300
|
-
verdicts.push(judgeBeyond(entry,
|
|
28314
|
+
verdicts.push(judgeBeyond(entry, expected, live));
|
|
28301
28315
|
return;
|
|
28302
28316
|
}
|
|
28303
|
-
const judged = judgeClaimed(entry,
|
|
28317
|
+
const judged = judgeClaimed(entry, expected, live, lastDeployAt);
|
|
28304
28318
|
verdicts.push(judged.verdict);
|
|
28305
28319
|
if (judged.unverifiable) {
|
|
28306
28320
|
unverified.push(judged.verdict);
|
|
@@ -28313,8 +28327,99 @@ function validateBaselineClaim(args2) {
|
|
|
28313
28327
|
suggestedTag: suggestTag(evidence, tables)
|
|
28314
28328
|
};
|
|
28315
28329
|
}
|
|
28316
|
-
function
|
|
28317
|
-
|
|
28330
|
+
function replayEvidence(evidence, claimedIndex) {
|
|
28331
|
+
const state = { tables: new Map, indexes: new Map, views: new Map };
|
|
28332
|
+
for (let index2 = 0;index2 <= claimedIndex; index2++) {
|
|
28333
|
+
applyEvidenceEntry(state, evidence[index2]);
|
|
28334
|
+
}
|
|
28335
|
+
return state;
|
|
28336
|
+
}
|
|
28337
|
+
function applyEvidenceEntry(state, entry) {
|
|
28338
|
+
for (const table8 of entry.dropsTables) {
|
|
28339
|
+
state.tables.delete(table8);
|
|
28340
|
+
}
|
|
28341
|
+
for (const dropped of entry.dropsColumns) {
|
|
28342
|
+
state.tables.get(dropped.table)?.columns.delete(dropped.column);
|
|
28343
|
+
}
|
|
28344
|
+
for (const droppedIndex of entry.dropsIndexes) {
|
|
28345
|
+
state.indexes.delete(droppedIndex);
|
|
28346
|
+
}
|
|
28347
|
+
for (const view2 of entry.dropsViews) {
|
|
28348
|
+
state.views.delete(view2);
|
|
28349
|
+
}
|
|
28350
|
+
for (const table8 of entry.createsTables) {
|
|
28351
|
+
state.tables.set(table8.name, {
|
|
28352
|
+
creator: entry.tag,
|
|
28353
|
+
columns: new Map(table8.columns.map((column2) => [column2, entry.tag]))
|
|
28354
|
+
});
|
|
28355
|
+
}
|
|
28356
|
+
for (const added of entry.addsColumns) {
|
|
28357
|
+
state.tables.get(added.table)?.columns.set(added.column, entry.tag);
|
|
28358
|
+
}
|
|
28359
|
+
for (const createdIndex of entry.createsIndexes) {
|
|
28360
|
+
state.indexes.set(createdIndex, entry.tag);
|
|
28361
|
+
}
|
|
28362
|
+
for (const view2 of entry.createsViews) {
|
|
28363
|
+
state.views.set(view2, entry.tag);
|
|
28364
|
+
}
|
|
28365
|
+
}
|
|
28366
|
+
function judgeClaimed(entry, expected, live, lastDeployAt) {
|
|
28367
|
+
let surviving = 0;
|
|
28368
|
+
for (const table8 of entry.createsTables) {
|
|
28369
|
+
const expectation = expected.tables.get(table8.name);
|
|
28370
|
+
if (expectation?.creator === entry.tag) {
|
|
28371
|
+
surviving++;
|
|
28372
|
+
const columns2 = live.tables.get(table8.name);
|
|
28373
|
+
if (!columns2) {
|
|
28374
|
+
return {
|
|
28375
|
+
verdict: {
|
|
28376
|
+
tag: entry.tag,
|
|
28377
|
+
verdict: "contradicted",
|
|
28378
|
+
detail: `creates table \`${table8.name}\`, which is not in the live database`
|
|
28379
|
+
},
|
|
28380
|
+
unverifiable: false
|
|
28381
|
+
};
|
|
28382
|
+
}
|
|
28383
|
+
const missing = [...expectation.columns.entries()].filter(([, creator]) => creator === entry.tag).map(([column2]) => column2).filter((column2) => !columns2.includes(column2));
|
|
28384
|
+
if (missing.length > 0) {
|
|
28385
|
+
return {
|
|
28386
|
+
verdict: {
|
|
28387
|
+
tag: entry.tag,
|
|
28388
|
+
verdict: "contradicted",
|
|
28389
|
+
detail: `table \`${table8.name}\` exists but is missing column(s) ${missing.map((column2) => `\`${column2}\``).join(", ")} this migration defines`
|
|
28390
|
+
},
|
|
28391
|
+
unverifiable: false
|
|
28392
|
+
};
|
|
28393
|
+
}
|
|
28394
|
+
}
|
|
28395
|
+
}
|
|
28396
|
+
for (const added of entry.addsColumns) {
|
|
28397
|
+
if (expected.tables.get(added.table)?.columns.get(added.column) === entry.tag) {
|
|
28398
|
+
surviving++;
|
|
28399
|
+
const columns2 = live.tables.get(added.table);
|
|
28400
|
+
if (columns2 && !columns2.includes(added.column)) {
|
|
28401
|
+
return {
|
|
28402
|
+
verdict: {
|
|
28403
|
+
tag: entry.tag,
|
|
28404
|
+
verdict: "contradicted",
|
|
28405
|
+
detail: `adds column \`${added.column}\` to \`${added.table}\`, which the live table does not have`
|
|
28406
|
+
},
|
|
28407
|
+
unverifiable: false
|
|
28408
|
+
};
|
|
28409
|
+
}
|
|
28410
|
+
}
|
|
28411
|
+
}
|
|
28412
|
+
for (const name2 of entry.createsIndexes) {
|
|
28413
|
+
if (expected.indexes.get(name2) === entry.tag && live.indexes.has(name2)) {
|
|
28414
|
+
surviving++;
|
|
28415
|
+
}
|
|
28416
|
+
}
|
|
28417
|
+
for (const name2 of entry.createsViews) {
|
|
28418
|
+
if (expected.views.get(name2) === entry.tag && live.views.has(name2)) {
|
|
28419
|
+
surviving++;
|
|
28420
|
+
}
|
|
28421
|
+
}
|
|
28422
|
+
if (surviving === 0) {
|
|
28318
28423
|
const generated = new Date(entry.generatedAt);
|
|
28319
28424
|
if (lastDeployAt && generated > lastDeployAt) {
|
|
28320
28425
|
return {
|
|
@@ -28328,48 +28433,11 @@ function judgeClaimed(entry, tables, lastDeployAt) {
|
|
|
28328
28433
|
}
|
|
28329
28434
|
return { verdict: { tag: entry.tag, verdict: "no-signal" }, unverifiable: false };
|
|
28330
28435
|
}
|
|
28331
|
-
for (const table8 of entry.createsTables) {
|
|
28332
|
-
const columns2 = tables.get(table8.name);
|
|
28333
|
-
if (!columns2) {
|
|
28334
|
-
return {
|
|
28335
|
-
verdict: {
|
|
28336
|
-
tag: entry.tag,
|
|
28337
|
-
verdict: "contradicted",
|
|
28338
|
-
detail: `creates table \`${table8.name}\`, which is not in the live database`
|
|
28339
|
-
},
|
|
28340
|
-
unverifiable: false
|
|
28341
|
-
};
|
|
28342
|
-
}
|
|
28343
|
-
const missing = table8.columns.filter((column2) => !columns2.includes(column2));
|
|
28344
|
-
if (missing.length > 0) {
|
|
28345
|
-
return {
|
|
28346
|
-
verdict: {
|
|
28347
|
-
tag: entry.tag,
|
|
28348
|
-
verdict: "contradicted",
|
|
28349
|
-
detail: `table \`${table8.name}\` exists but is missing column(s) ${missing.map((column2) => `\`${column2}\``).join(", ")} this migration defines`
|
|
28350
|
-
},
|
|
28351
|
-
unverifiable: false
|
|
28352
|
-
};
|
|
28353
|
-
}
|
|
28354
|
-
}
|
|
28355
|
-
for (const added of entry.addsColumns) {
|
|
28356
|
-
const columns2 = tables.get(added.table);
|
|
28357
|
-
if (columns2 && !columns2.includes(added.column)) {
|
|
28358
|
-
return {
|
|
28359
|
-
verdict: {
|
|
28360
|
-
tag: entry.tag,
|
|
28361
|
-
verdict: "contradicted",
|
|
28362
|
-
detail: `adds column \`${added.column}\` to \`${added.table}\`, which the live table does not have`
|
|
28363
|
-
},
|
|
28364
|
-
unverifiable: false
|
|
28365
|
-
};
|
|
28366
|
-
}
|
|
28367
|
-
}
|
|
28368
28436
|
return { verdict: { tag: entry.tag, verdict: "verified" }, unverifiable: false };
|
|
28369
28437
|
}
|
|
28370
|
-
function judgeBeyond(entry,
|
|
28438
|
+
function judgeBeyond(entry, expected, live) {
|
|
28371
28439
|
for (const table8 of entry.createsTables) {
|
|
28372
|
-
if (tables.has(table8.name)) {
|
|
28440
|
+
if (live.tables.has(table8.name) && !expected.tables.has(table8.name)) {
|
|
28373
28441
|
return {
|
|
28374
28442
|
tag: entry.tag,
|
|
28375
28443
|
verdict: "contradicted",
|
|
@@ -28378,8 +28446,9 @@ function judgeBeyond(entry, tables, indexes2, views) {
|
|
|
28378
28446
|
}
|
|
28379
28447
|
}
|
|
28380
28448
|
for (const added of entry.addsColumns) {
|
|
28381
|
-
const columns2 = tables.get(added.table);
|
|
28382
|
-
|
|
28449
|
+
const columns2 = live.tables.get(added.table);
|
|
28450
|
+
const explained = expected.tables.get(added.table)?.columns.has(added.column);
|
|
28451
|
+
if (columns2?.includes(added.column) && !explained) {
|
|
28383
28452
|
return {
|
|
28384
28453
|
tag: entry.tag,
|
|
28385
28454
|
verdict: "contradicted",
|
|
@@ -28388,7 +28457,7 @@ function judgeBeyond(entry, tables, indexes2, views) {
|
|
|
28388
28457
|
}
|
|
28389
28458
|
}
|
|
28390
28459
|
for (const index2 of entry.createsIndexes) {
|
|
28391
|
-
if (
|
|
28460
|
+
if (live.indexes.has(index2) && !expected.indexes.has(index2)) {
|
|
28392
28461
|
return {
|
|
28393
28462
|
tag: entry.tag,
|
|
28394
28463
|
verdict: "contradicted",
|
|
@@ -28397,7 +28466,7 @@ function judgeBeyond(entry, tables, indexes2, views) {
|
|
|
28397
28466
|
}
|
|
28398
28467
|
}
|
|
28399
28468
|
for (const view2 of entry.createsViews) {
|
|
28400
|
-
if (views.has(view2)) {
|
|
28469
|
+
if (live.views.has(view2) && !expected.views.has(view2)) {
|
|
28401
28470
|
return {
|
|
28402
28471
|
tag: entry.tag,
|
|
28403
28472
|
verdict: "contradicted",
|
|
@@ -28408,19 +28477,20 @@ function judgeBeyond(entry, tables, indexes2, views) {
|
|
|
28408
28477
|
return { tag: entry.tag, verdict: "verified" };
|
|
28409
28478
|
}
|
|
28410
28479
|
function suggestTag(evidence, tables) {
|
|
28411
|
-
|
|
28412
|
-
|
|
28413
|
-
|
|
28414
|
-
}
|
|
28415
|
-
let
|
|
28416
|
-
|
|
28417
|
-
|
|
28480
|
+
const liveCreated = [
|
|
28481
|
+
...new Set(evidence.flatMap((entry) => entry.createsTables.map((table8) => table8.name)))
|
|
28482
|
+
].filter((name2) => tables.has(name2));
|
|
28483
|
+
const state = { tables: new Map, indexes: new Map, views: new Map };
|
|
28484
|
+
let best = null;
|
|
28485
|
+
for (const entry of evidence) {
|
|
28486
|
+
applyEvidenceEntry(state, entry);
|
|
28487
|
+
const allPresent = [...state.tables.keys()].every((name2) => tables.has(name2));
|
|
28488
|
+
const noStray = liveCreated.every((name2) => state.tables.has(name2));
|
|
28489
|
+
if (allPresent && noStray) {
|
|
28490
|
+
best = entry.tag;
|
|
28491
|
+
}
|
|
28418
28492
|
}
|
|
28419
|
-
|
|
28420
|
-
return candidate >= 0 && candidate >= absentFrom - 1 ? evidence[candidate].tag : null;
|
|
28421
|
-
}
|
|
28422
|
-
function hasSignal(entry) {
|
|
28423
|
-
return entry.createsTables.length > 0 || entry.addsColumns.length > 0 || entry.createsIndexes.length > 0 || entry.createsViews.length > 0;
|
|
28493
|
+
return best;
|
|
28424
28494
|
}
|
|
28425
28495
|
var init_baseline_validation_util = __esm(() => {
|
|
28426
28496
|
init_spans();
|
|
@@ -30043,6 +30113,25 @@ function sleep(ms) {
|
|
|
30043
30113
|
}
|
|
30044
30114
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30045
30115
|
}
|
|
30116
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
30117
|
+
if (items.length === 0) {
|
|
30118
|
+
return [];
|
|
30119
|
+
}
|
|
30120
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
30121
|
+
const results = Array.from({ length: items.length });
|
|
30122
|
+
let nextIndex = 0;
|
|
30123
|
+
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
30124
|
+
while (true) {
|
|
30125
|
+
const currentIndex = nextIndex;
|
|
30126
|
+
nextIndex++;
|
|
30127
|
+
if (currentIndex >= items.length) {
|
|
30128
|
+
return;
|
|
30129
|
+
}
|
|
30130
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
30131
|
+
}
|
|
30132
|
+
}));
|
|
30133
|
+
return results;
|
|
30134
|
+
}
|
|
30046
30135
|
|
|
30047
30136
|
// ../timeback/dist/types.js
|
|
30048
30137
|
function isObject(value) {
|
|
@@ -31577,7 +31666,7 @@ class KVBackupService {
|
|
|
31577
31666
|
"app.kv_backup.dry_run": dryRun,
|
|
31578
31667
|
"app.kv_backup.game_slug": options.gameSlug
|
|
31579
31668
|
});
|
|
31580
|
-
const results = await
|
|
31669
|
+
const results = await runWithConcurrency(targets, namespaceConcurrency, (target) => this.backupNamespace(target, {
|
|
31581
31670
|
bucketName,
|
|
31582
31671
|
stage,
|
|
31583
31672
|
runId,
|
|
@@ -31643,7 +31732,7 @@ class KVBackupService {
|
|
|
31643
31732
|
}
|
|
31644
31733
|
static async fetchNamespaceEntries(cloudflare2, namespaceId, keyConcurrency = DEFAULT_KEY_CONCURRENCY) {
|
|
31645
31734
|
const keys = await KVBackupService.withRetries(`List KV keys for namespace ${namespaceId}`, () => cloudflare2.kv.listKeys(namespaceId));
|
|
31646
|
-
return
|
|
31735
|
+
return runWithConcurrency(keys, keyConcurrency, async (key) => {
|
|
31647
31736
|
const safeLabel = KVBackupService.redactKeyForLog(key.name);
|
|
31648
31737
|
const value = await KVBackupService.withRetries(`Fetch KV value ${safeLabel}`, () => cloudflare2.kv.getValue(namespaceId, key.name));
|
|
31649
31738
|
const metadata2 = KVBackupService.parseMetadata(key.metadata);
|
|
@@ -31820,25 +31909,6 @@ class KVBackupService {
|
|
|
31820
31909
|
}
|
|
31821
31910
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
31822
31911
|
}
|
|
31823
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
31824
|
-
if (items.length === 0) {
|
|
31825
|
-
return [];
|
|
31826
|
-
}
|
|
31827
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
31828
|
-
const results = Array.from({ length: items.length });
|
|
31829
|
-
let nextIndex = 0;
|
|
31830
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
31831
|
-
while (true) {
|
|
31832
|
-
const currentIndex = nextIndex;
|
|
31833
|
-
nextIndex++;
|
|
31834
|
-
if (currentIndex >= items.length) {
|
|
31835
|
-
return;
|
|
31836
|
-
}
|
|
31837
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
31838
|
-
}
|
|
31839
|
-
}));
|
|
31840
|
-
return results;
|
|
31841
|
-
}
|
|
31842
31912
|
}
|
|
31843
31913
|
var BACKUP_SCHEMA_VERSION = 1, DEFAULT_NAMESPACE_CONCURRENCY = 2, DEFAULT_KEY_CONCURRENCY = 10, DEFAULT_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 500;
|
|
31844
31914
|
var init_kv_backup_service = __esm(() => {
|
|
@@ -33980,7 +34050,14 @@ var init_schemas2 = __esm(() => {
|
|
|
33980
34050
|
column: exports_external.string().min(1)
|
|
33981
34051
|
})),
|
|
33982
34052
|
createsIndexes: exports_external.array(exports_external.string()),
|
|
33983
|
-
createsViews: exports_external.array(exports_external.string())
|
|
34053
|
+
createsViews: exports_external.array(exports_external.string()),
|
|
34054
|
+
dropsTables: exports_external.array(exports_external.string()),
|
|
34055
|
+
dropsColumns: exports_external.array(exports_external.object({
|
|
34056
|
+
table: exports_external.string().min(1),
|
|
34057
|
+
column: exports_external.string().min(1)
|
|
34058
|
+
})),
|
|
34059
|
+
dropsIndexes: exports_external.array(exports_external.string()),
|
|
34060
|
+
dropsViews: exports_external.array(exports_external.string())
|
|
33984
34061
|
})).optional();
|
|
33985
34062
|
DeployBaselineSchema = exports_external.object({
|
|
33986
34063
|
lastAppliedMigrationTag: exports_external.string().min(1).optional(),
|
|
@@ -34138,9 +34215,8 @@ function isValidAdminAttributionDate(value) {
|
|
|
34138
34215
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
34139
34216
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
34140
34217
|
}
|
|
34141
|
-
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,
|
|
34218
|
+
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;
|
|
34142
34219
|
var init_schemas4 = __esm(() => {
|
|
34143
|
-
init_drizzle_zod();
|
|
34144
34220
|
init_esm();
|
|
34145
34221
|
init_table7();
|
|
34146
34222
|
TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
@@ -34393,15 +34469,26 @@ var init_schemas4 = __esm(() => {
|
|
|
34393
34469
|
runId: exports_external.string().uuid(),
|
|
34394
34470
|
activityId: exports_external.string().min(1).optional()
|
|
34395
34471
|
});
|
|
34396
|
-
|
|
34397
|
-
|
|
34398
|
-
createdAt: true
|
|
34399
|
-
});
|
|
34472
|
+
AssessmentPurposeSchema = exports_external.enum(gameTimebackAssessmentPurposeEnum.enumValues);
|
|
34473
|
+
AssessmentStatusSchema = exports_external.enum(gameTimebackAssessmentStatusEnum.enumValues);
|
|
34400
34474
|
CreateAssessmentRequestSchema = exports_external.object({
|
|
34401
|
-
title: exports_external.string().min(1, "Assessment title is required")
|
|
34475
|
+
title: exports_external.string().min(1, "Assessment title is required"),
|
|
34476
|
+
purpose: AssessmentPurposeSchema
|
|
34477
|
+
});
|
|
34478
|
+
UpdateAssessmentRequestSchema = exports_external.object({
|
|
34479
|
+
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
34480
|
+
purpose: AssessmentPurposeSchema.optional(),
|
|
34481
|
+
status: AssessmentStatusSchema.optional()
|
|
34482
|
+
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.status !== undefined, {
|
|
34483
|
+
message: "Title, purpose, or status is required"
|
|
34484
|
+
});
|
|
34485
|
+
CopyAssessmentRequestSchema = exports_external.object({
|
|
34486
|
+
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
34487
|
+
purpose: AssessmentPurposeSchema
|
|
34402
34488
|
});
|
|
34403
34489
|
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
34404
|
-
|
|
34490
|
+
purpose: AssessmentPurposeSchema,
|
|
34491
|
+
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
34405
34492
|
});
|
|
34406
34493
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
34407
34494
|
items: exports_external.array(exports_external.object({
|
|
@@ -53900,10 +53987,10 @@ var init_dist2 = __esm(async () => {
|
|
|
53900
53987
|
};
|
|
53901
53988
|
});
|
|
53902
53989
|
|
|
53903
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
53990
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-6jf1natv.js
|
|
53904
53991
|
var init_chunk_6jf1natv = () => {};
|
|
53905
53992
|
|
|
53906
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
53993
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-dd0pbbq3.js
|
|
53907
53994
|
var exports_chunk_dd0pbbq3 = {};
|
|
53908
53995
|
__export(exports_chunk_dd0pbbq3, {
|
|
53909
53996
|
types: () => types6,
|
|
@@ -54314,7 +54401,7 @@ var init_chunk_dd0pbbq3 = __esm(() => {
|
|
|
54314
54401
|
util_default2 = { TextEncoder: TextEncoder3, TextDecoder: TextDecoder3, promisify: promisify2, log: log9, inherits: inherits2, _extend: _extend2, callbackifyOnRejected: callbackifyOnRejected2, callbackify: callbackify2 };
|
|
54315
54402
|
});
|
|
54316
54403
|
|
|
54317
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
54404
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/chunk-pt7w63g2.js
|
|
54318
54405
|
function createInputValidationError2(message, issues) {
|
|
54319
54406
|
return new InputValidationError2(message, issues);
|
|
54320
54407
|
}
|
|
@@ -55365,7 +55452,7 @@ var ApiError3, UnauthorizedError3, ForbiddenError2, NotFoundError3, ValidationEr
|
|
|
55365
55452
|
console[method](label, style);
|
|
55366
55453
|
}
|
|
55367
55454
|
}, 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;
|
|
55368
|
-
var
|
|
55455
|
+
var init_chunk_pt7w63g2 = __esm(async () => {
|
|
55369
55456
|
init_chunk_6jf1natv();
|
|
55370
55457
|
ApiError3 = class ApiError3 extends Error {
|
|
55371
55458
|
statusCode;
|
|
@@ -70511,15 +70598,14 @@ var init_v42 = __esm(() => {
|
|
|
70511
70598
|
init_classic2();
|
|
70512
70599
|
});
|
|
70513
70600
|
|
|
70514
|
-
// ../../node_modules/.bun/@timeback+qti@0.
|
|
70601
|
+
// ../../node_modules/.bun/@timeback+qti@0.4.1-beta.20260717234944+1fb4c65d43e298b9/node_modules/@timeback/qti/dist/index.js
|
|
70515
70602
|
function resolveToProvider23(config4, registry3 = DEFAULT_PROVIDER_REGISTRY2) {
|
|
70516
70603
|
return resolveToProvider10(config4, QTI_ENV_VARS2, registry3);
|
|
70517
70604
|
}
|
|
70518
70605
|
function translateParams2(params) {
|
|
70519
70606
|
const {
|
|
70520
70607
|
orderBy,
|
|
70521
|
-
|
|
70522
|
-
fields: __,
|
|
70608
|
+
fields: _,
|
|
70523
70609
|
...rest
|
|
70524
70610
|
} = params;
|
|
70525
70611
|
return {
|
|
@@ -70527,6 +70613,23 @@ function translateParams2(params) {
|
|
|
70527
70613
|
order: orderBy
|
|
70528
70614
|
};
|
|
70529
70615
|
}
|
|
70616
|
+
function buildListQueryParams(params) {
|
|
70617
|
+
const queryParams = {};
|
|
70618
|
+
const filter = params.where ? whereToFilter2(params.where) : undefined;
|
|
70619
|
+
if (filter !== undefined)
|
|
70620
|
+
queryParams.filter = filter;
|
|
70621
|
+
if (params.query !== undefined)
|
|
70622
|
+
queryParams.query = params.query;
|
|
70623
|
+
if (params.page !== undefined)
|
|
70624
|
+
queryParams.page = params.page;
|
|
70625
|
+
if (params.limit !== undefined)
|
|
70626
|
+
queryParams.limit = params.limit;
|
|
70627
|
+
if (params.sort)
|
|
70628
|
+
queryParams.sort = params.sort;
|
|
70629
|
+
if (params.order)
|
|
70630
|
+
queryParams.order = params.order;
|
|
70631
|
+
return queryParams;
|
|
70632
|
+
}
|
|
70530
70633
|
|
|
70531
70634
|
class AssessmentItemsResource2 {
|
|
70532
70635
|
transport;
|
|
@@ -70535,19 +70638,8 @@ class AssessmentItemsResource2 {
|
|
|
70535
70638
|
}
|
|
70536
70639
|
list(params = {}) {
|
|
70537
70640
|
validatePageListParams2(params);
|
|
70538
|
-
const queryParams = {};
|
|
70539
|
-
if (params.query !== undefined)
|
|
70540
|
-
queryParams.query = params.query;
|
|
70541
|
-
if (params.page !== undefined)
|
|
70542
|
-
queryParams.page = params.page;
|
|
70543
|
-
if (params.limit !== undefined)
|
|
70544
|
-
queryParams.limit = params.limit;
|
|
70545
|
-
if (params.sort)
|
|
70546
|
-
queryParams.sort = params.sort;
|
|
70547
|
-
if (params.order)
|
|
70548
|
-
queryParams.order = params.order;
|
|
70549
70641
|
return this.transport.request("/assessment-items", {
|
|
70550
|
-
params:
|
|
70642
|
+
params: buildListQueryParams(params)
|
|
70551
70643
|
});
|
|
70552
70644
|
}
|
|
70553
70645
|
stream(params = {}) {
|
|
@@ -70670,20 +70762,9 @@ class TestPartSectionsHelper2 {
|
|
|
70670
70762
|
}
|
|
70671
70763
|
list(params = {}) {
|
|
70672
70764
|
validatePageListParams2(params);
|
|
70673
|
-
const queryParams = {};
|
|
70674
|
-
if (params.query !== undefined)
|
|
70675
|
-
queryParams.query = params.query;
|
|
70676
|
-
if (params.page !== undefined)
|
|
70677
|
-
queryParams.page = params.page;
|
|
70678
|
-
if (params.limit !== undefined)
|
|
70679
|
-
queryParams.limit = params.limit;
|
|
70680
|
-
if (params.sort)
|
|
70681
|
-
queryParams.sort = params.sort;
|
|
70682
|
-
if (params.order)
|
|
70683
|
-
queryParams.order = params.order;
|
|
70684
70765
|
const path = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts/${encodeURIComponent(this.testPartId)}/sections`;
|
|
70685
70766
|
return this.transport.request(path, {
|
|
70686
|
-
params:
|
|
70767
|
+
params: buildListQueryParams(params)
|
|
70687
70768
|
});
|
|
70688
70769
|
}
|
|
70689
70770
|
get(identifier) {
|
|
@@ -70730,19 +70811,10 @@ class AssessmentTestPartsHelper2 {
|
|
|
70730
70811
|
}
|
|
70731
70812
|
list(params = {}) {
|
|
70732
70813
|
validatePageListParams2(params);
|
|
70733
|
-
const queryParams = {};
|
|
70734
|
-
if (params.query !== undefined)
|
|
70735
|
-
queryParams.query = params.query;
|
|
70736
|
-
if (params.page !== undefined)
|
|
70737
|
-
queryParams.page = params.page;
|
|
70738
|
-
if (params.limit !== undefined)
|
|
70739
|
-
queryParams.limit = params.limit;
|
|
70740
|
-
if (params.sort)
|
|
70741
|
-
queryParams.sort = params.sort;
|
|
70742
|
-
if (params.order)
|
|
70743
|
-
queryParams.order = params.order;
|
|
70744
70814
|
const path = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts`;
|
|
70745
|
-
return this.transport.request(path, {
|
|
70815
|
+
return this.transport.request(path, {
|
|
70816
|
+
params: buildListQueryParams(params)
|
|
70817
|
+
});
|
|
70746
70818
|
}
|
|
70747
70819
|
get(identifier) {
|
|
70748
70820
|
validateNonEmptyString2(identifier, "testPartId");
|
|
@@ -70785,19 +70857,8 @@ class AssessmentTestsResource2 {
|
|
|
70785
70857
|
}
|
|
70786
70858
|
list(params = {}) {
|
|
70787
70859
|
validatePageListParams2(params);
|
|
70788
|
-
const queryParams = {};
|
|
70789
|
-
if (params.query !== undefined)
|
|
70790
|
-
queryParams.query = params.query;
|
|
70791
|
-
if (params.page !== undefined)
|
|
70792
|
-
queryParams.page = params.page;
|
|
70793
|
-
if (params.limit !== undefined)
|
|
70794
|
-
queryParams.limit = params.limit;
|
|
70795
|
-
if (params.sort)
|
|
70796
|
-
queryParams.sort = params.sort;
|
|
70797
|
-
if (params.order)
|
|
70798
|
-
queryParams.order = params.order;
|
|
70799
70860
|
return this.transport.request("/assessment-tests", {
|
|
70800
|
-
params:
|
|
70861
|
+
params: buildListQueryParams(params)
|
|
70801
70862
|
});
|
|
70802
70863
|
}
|
|
70803
70864
|
stream(params = {}) {
|
|
@@ -70903,19 +70964,8 @@ class StimuliResource2 {
|
|
|
70903
70964
|
}
|
|
70904
70965
|
list(params = {}) {
|
|
70905
70966
|
validatePageListParams2(params);
|
|
70906
|
-
const queryParams = {};
|
|
70907
|
-
if (params.query !== undefined)
|
|
70908
|
-
queryParams.query = params.query;
|
|
70909
|
-
if (params.page !== undefined)
|
|
70910
|
-
queryParams.page = params.page;
|
|
70911
|
-
if (params.limit !== undefined)
|
|
70912
|
-
queryParams.limit = params.limit;
|
|
70913
|
-
if (params.sort)
|
|
70914
|
-
queryParams.sort = params.sort;
|
|
70915
|
-
if (params.order)
|
|
70916
|
-
queryParams.order = params.order;
|
|
70917
70967
|
return this.transport.request("/stimuli", {
|
|
70918
|
-
params:
|
|
70968
|
+
params: buildListQueryParams(params)
|
|
70919
70969
|
});
|
|
70920
70970
|
}
|
|
70921
70971
|
stream(params = {}) {
|
|
@@ -71047,7 +71097,7 @@ var init_dist3 = __esm(async () => {
|
|
|
71047
71097
|
init_v42();
|
|
71048
71098
|
init_v42();
|
|
71049
71099
|
init_v42();
|
|
71050
|
-
await
|
|
71100
|
+
await init_chunk_pt7w63g2();
|
|
71051
71101
|
QTI_ENV_VARS2 = {
|
|
71052
71102
|
baseUrl: ["TIMEBACK_API_BASE_URL", "TIMEBACK_BASE_URL", "QTI_BASE_URL"],
|
|
71053
71103
|
clientId: ["TIMEBACK_API_CLIENT_ID", "TIMEBACK_CLIENT_ID", "QTI_CLIENT_ID"],
|
|
@@ -77888,13 +77938,6 @@ function deriveSourcedIds(courseId) {
|
|
|
77888
77938
|
componentResource: `${courseId}-cr`
|
|
77889
77939
|
};
|
|
77890
77940
|
}
|
|
77891
|
-
function deriveAssessmentBankIds(courseId) {
|
|
77892
|
-
return {
|
|
77893
|
-
component: `${courseId}-assessment-bank-component`,
|
|
77894
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
77895
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
77896
|
-
};
|
|
77897
|
-
}
|
|
77898
77941
|
function validateProgressData(progressData) {
|
|
77899
77942
|
if (!progressData.subject) {
|
|
77900
77943
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -79183,59 +79226,22 @@ class CourseAssessments {
|
|
|
79183
79226
|
constructor(core3) {
|
|
79184
79227
|
this.core = core3;
|
|
79185
79228
|
}
|
|
79186
|
-
|
|
79187
|
-
|
|
79188
|
-
|
|
79189
|
-
|
|
79190
|
-
|
|
79191
|
-
|
|
79192
|
-
|
|
79193
|
-
|
|
79194
|
-
|
|
79195
|
-
|
|
79196
|
-
|
|
79197
|
-
|
|
79198
|
-
|
|
79199
|
-
|
|
79200
|
-
throw error88;
|
|
79201
|
-
}
|
|
79202
|
-
}
|
|
79203
|
-
try {
|
|
79204
|
-
await oneroster.resources.create({
|
|
79205
|
-
sourcedId: bankIds.resource,
|
|
79206
|
-
status: "active",
|
|
79207
|
-
title: "Assessment Bank",
|
|
79208
|
-
vendorResourceId: "",
|
|
79209
|
-
vendorId: undefined,
|
|
79210
|
-
metadata: {
|
|
79211
|
-
type: "assessment-bank",
|
|
79212
|
-
resources: [],
|
|
79213
|
-
lessonType: "test-out",
|
|
79214
|
-
subject: input.subject,
|
|
79215
|
-
grade: String(input.grade)
|
|
79216
|
-
}
|
|
79217
|
-
});
|
|
79218
|
-
} catch (error88) {
|
|
79219
|
-
if (!isAlreadyExists(error88)) {
|
|
79220
|
-
throw error88;
|
|
79221
|
-
}
|
|
79222
|
-
}
|
|
79223
|
-
try {
|
|
79224
|
-
await oneroster.courses.createComponentResource({
|
|
79225
|
-
sourcedId: bankIds.componentResource,
|
|
79226
|
-
status: "active",
|
|
79227
|
-
title: "Test Out",
|
|
79228
|
-
resource: { sourcedId: bankIds.resource },
|
|
79229
|
-
courseComponent: { sourcedId: bankIds.component },
|
|
79230
|
-
sortOrder: 9999,
|
|
79231
|
-
metadata: { lessonType: "test-out" },
|
|
79232
|
-
lessonType: "test-out"
|
|
79233
|
-
});
|
|
79234
|
-
} catch (error88) {
|
|
79235
|
-
if (!isAlreadyExists(error88)) {
|
|
79236
|
-
throw error88;
|
|
79229
|
+
createItemFromXml(input) {
|
|
79230
|
+
return this.core.qti.assessmentItems.createFromXml({
|
|
79231
|
+
format: "xml",
|
|
79232
|
+
xml: input.xml,
|
|
79233
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
79234
|
+
});
|
|
79235
|
+
}
|
|
79236
|
+
updateItemFromXml(identifier, input) {
|
|
79237
|
+
return this.core.qti.getTransport().request(`/assessment-items/${encodeURIComponent(identifier)}`, {
|
|
79238
|
+
method: "PUT",
|
|
79239
|
+
body: {
|
|
79240
|
+
format: "xml",
|
|
79241
|
+
xml: input.xml,
|
|
79242
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
79237
79243
|
}
|
|
79238
|
-
}
|
|
79244
|
+
});
|
|
79239
79245
|
}
|
|
79240
79246
|
async createTestScaffold(input) {
|
|
79241
79247
|
const partId = `${input.identifier}-part1`;
|
|
@@ -79270,18 +79276,6 @@ class CourseAssessments {
|
|
|
79270
79276
|
}
|
|
79271
79277
|
}
|
|
79272
79278
|
}
|
|
79273
|
-
async teardownTest(qtiTestIdentifier) {
|
|
79274
|
-
const qti = this.core.qti;
|
|
79275
|
-
const partId = `${qtiTestIdentifier}-part1`;
|
|
79276
|
-
const sectionId = `${qtiTestIdentifier}-section1`;
|
|
79277
|
-
const questions = await qti.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
79278
|
-
const sectionItems = qti.assessmentTests.testParts(qtiTestIdentifier).sections(partId).items(sectionId);
|
|
79279
|
-
for (const q of questions.questions ?? []) {
|
|
79280
|
-
await sectionItems.remove(q.reference.identifier);
|
|
79281
|
-
await qti.assessmentItems.delete(q.reference.identifier);
|
|
79282
|
-
}
|
|
79283
|
-
await qti.assessmentTests.delete(qtiTestIdentifier);
|
|
79284
|
-
}
|
|
79285
79279
|
}
|
|
79286
79280
|
function buildCoursePayload(config4) {
|
|
79287
79281
|
return {
|
|
@@ -79511,9 +79505,9 @@ function createCourseNamespace(core3) {
|
|
|
79511
79505
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
79512
79506
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
79513
79507
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
79514
|
-
ensureAssessmentBank: (input) => assessments.ensureBank(input),
|
|
79515
79508
|
createAssessmentTest: (input) => assessments.createTestScaffold(input),
|
|
79516
|
-
|
|
79509
|
+
createAssessmentItemXml: (input) => assessments.createItemFromXml(input),
|
|
79510
|
+
updateAssessmentItemXml: (identifier, input) => assessments.updateItemFromXml(identifier, input)
|
|
79517
79511
|
};
|
|
79518
79512
|
}
|
|
79519
79513
|
function recordTimebackClientSummary(outcome) {
|
|
@@ -79996,13 +79990,6 @@ function deriveSourcedIds2(courseId) {
|
|
|
79996
79990
|
componentResource: `${courseId}-cr`
|
|
79997
79991
|
};
|
|
79998
79992
|
}
|
|
79999
|
-
function deriveAssessmentBankIds2(courseId) {
|
|
80000
|
-
return {
|
|
80001
|
-
component: `${courseId}-assessment-bank-component`,
|
|
80002
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
80003
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
80004
|
-
};
|
|
80005
|
-
}
|
|
80006
79993
|
var CACHE_DEFAULTS4, RESOURCE_DEFAULTS4;
|
|
80007
79994
|
var init_utils6 = __esm(() => {
|
|
80008
79995
|
init_src();
|
|
@@ -80037,6 +80024,96 @@ var init_utils6 = __esm(() => {
|
|
|
80037
80024
|
});
|
|
80038
80025
|
|
|
80039
80026
|
// ../utils/src/timeback.ts
|
|
80027
|
+
function playcademySupportedQtiInteractionType(value) {
|
|
80028
|
+
if (typeof value !== "string") {
|
|
80029
|
+
return;
|
|
80030
|
+
}
|
|
80031
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
80032
|
+
return PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET.has(normalized) ? normalized : undefined;
|
|
80033
|
+
}
|
|
80034
|
+
function parseNumericTextEntry(input) {
|
|
80035
|
+
const answer = typeof input.answer === "string" ? input.answer.trim() : "";
|
|
80036
|
+
const comparison = input.comparison;
|
|
80037
|
+
if (input.baseType !== "integer" && input.baseType !== "float") {
|
|
80038
|
+
return { success: false, message: "Numeric answers must be integer or float responses" };
|
|
80039
|
+
}
|
|
80040
|
+
if (!answer || (input.baseType === "integer" ? !INTEGER_ANSWER_PATTERN.test(answer) : !FLOAT_ANSWER_PATTERN.test(answer) || !Number.isFinite(Number(answer)))) {
|
|
80041
|
+
return { success: false, message: `The correct answer is not a valid ${input.baseType}` };
|
|
80042
|
+
}
|
|
80043
|
+
if (!comparison || typeof comparison !== "object" || !("kind" in comparison)) {
|
|
80044
|
+
return { success: false, message: "Numeric comparison is required" };
|
|
80045
|
+
}
|
|
80046
|
+
if (comparison.kind === "equal") {
|
|
80047
|
+
return {
|
|
80048
|
+
success: true,
|
|
80049
|
+
value: { baseType: input.baseType, answer, comparison: { kind: "equal" } }
|
|
80050
|
+
};
|
|
80051
|
+
}
|
|
80052
|
+
if (input.baseType === "integer") {
|
|
80053
|
+
return { success: false, message: "Integer answers cannot use decimal-place rounding" };
|
|
80054
|
+
}
|
|
80055
|
+
const figures = "figures" in comparison ? comparison.figures : undefined;
|
|
80056
|
+
const roundingMode = "roundingMode" in comparison ? comparison.roundingMode : undefined;
|
|
80057
|
+
if (comparison.kind !== "equal-rounded" || roundingMode !== "decimalPlaces" || !Number.isInteger(figures) || figures < 0 || figures > 15) {
|
|
80058
|
+
return { success: false, message: "Decimal places must be a whole number from 0 to 15" };
|
|
80059
|
+
}
|
|
80060
|
+
return {
|
|
80061
|
+
success: true,
|
|
80062
|
+
value: {
|
|
80063
|
+
baseType: "float",
|
|
80064
|
+
answer,
|
|
80065
|
+
comparison: { kind: "equal-rounded", roundingMode, figures }
|
|
80066
|
+
}
|
|
80067
|
+
};
|
|
80068
|
+
}
|
|
80069
|
+
function escapeXml(value) {
|
|
80070
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
80071
|
+
}
|
|
80072
|
+
function hottextMaxChoices(selectableCount, multiple) {
|
|
80073
|
+
return multiple ? selectableCount : 1;
|
|
80074
|
+
}
|
|
80075
|
+
function numericTextEntryXml(input) {
|
|
80076
|
+
const comparison = input.numeric.comparison.kind === "equal-rounded" ? `<qti-equal-rounded rounding-mode="decimalPlaces" figures="${input.numeric.comparison.figures}">` : "<qti-equal>";
|
|
80077
|
+
const closeComparison = input.numeric.comparison.kind === "equal-rounded" ? "</qti-equal-rounded>" : "</qti-equal>";
|
|
80078
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
80079
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
80080
|
+
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="${input.numeric.baseType}">
|
|
80081
|
+
<qti-correct-response><qti-value>${escapeXml(input.numeric.answer)}</qti-value></qti-correct-response>
|
|
80082
|
+
</qti-response-declaration>
|
|
80083
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
80084
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
80085
|
+
</qti-outcome-declaration>
|
|
80086
|
+
<qti-item-body>
|
|
80087
|
+
<p>${escapeXml(input.prompt)} <qti-text-entry-interaction response-identifier="RESPONSE" expected-length="15" /></p>
|
|
80088
|
+
</qti-item-body>
|
|
80089
|
+
<qti-response-processing>
|
|
80090
|
+
<qti-response-condition>
|
|
80091
|
+
<qti-response-if>
|
|
80092
|
+
${comparison}<qti-variable identifier="RESPONSE" /><qti-correct identifier="RESPONSE" />${closeComparison}
|
|
80093
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
80094
|
+
</qti-response-if>
|
|
80095
|
+
</qti-response-condition>
|
|
80096
|
+
</qti-response-processing>
|
|
80097
|
+
</qti-assessment-item>`;
|
|
80098
|
+
}
|
|
80099
|
+
function countQtiTestItems(test) {
|
|
80100
|
+
let itemCount = 0;
|
|
80101
|
+
for (const part of test["qti-test-part"] ?? []) {
|
|
80102
|
+
for (const section of part["qti-assessment-section"] ?? []) {
|
|
80103
|
+
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
80104
|
+
}
|
|
80105
|
+
}
|
|
80106
|
+
return itemCount;
|
|
80107
|
+
}
|
|
80108
|
+
function isQtiItemOwnedByTest(item, testIdentifier) {
|
|
80109
|
+
const hasOwnershipMetadata = Boolean(item.metadata && (("ownerSystem" in item.metadata) || (PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY in item.metadata)));
|
|
80110
|
+
if (hasOwnershipMetadata) {
|
|
80111
|
+
return item.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && item.metadata?.[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY] === testIdentifier;
|
|
80112
|
+
}
|
|
80113
|
+
const identifierPrefix = `${testIdentifier}-q`;
|
|
80114
|
+
const identifierSuffix = item.identifier?.startsWith(identifierPrefix) ? item.identifier.slice(identifierPrefix.length) : undefined;
|
|
80115
|
+
return identifierSuffix !== undefined && /^[0-9a-f]{8}$/i.test(identifierSuffix);
|
|
80116
|
+
}
|
|
80040
80117
|
function formatGradeLabel(grade) {
|
|
80041
80118
|
if (grade === null || grade === undefined) {
|
|
80042
80119
|
return "N/A";
|
|
@@ -80078,7 +80155,7 @@ function parseTimebackDiscrepancyQueueMetrics(values) {
|
|
|
80078
80155
|
}
|
|
80079
80156
|
return metrics;
|
|
80080
80157
|
}
|
|
80081
|
-
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;
|
|
80158
|
+
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;
|
|
80082
80159
|
var init_timeback3 = __esm(() => {
|
|
80083
80160
|
TIMEBACK_DISCREPANCY_QUEUE_WINDOWS = [
|
|
80084
80161
|
"today",
|
|
@@ -80089,6 +80166,17 @@ var init_timeback3 = __esm(() => {
|
|
|
80089
80166
|
"custom"
|
|
80090
80167
|
];
|
|
80091
80168
|
TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
|
|
80169
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES = [
|
|
80170
|
+
"choice",
|
|
80171
|
+
"inline-choice",
|
|
80172
|
+
"text-entry",
|
|
80173
|
+
"order",
|
|
80174
|
+
"match",
|
|
80175
|
+
"hottext"
|
|
80176
|
+
];
|
|
80177
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET = new Set(PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES);
|
|
80178
|
+
INTEGER_ANSWER_PATTERN = /^[+-]?\d+$/;
|
|
80179
|
+
FLOAT_ANSWER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
80092
80180
|
TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
|
|
80093
80181
|
TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
|
|
80094
80182
|
});
|
|
@@ -81331,7 +81419,7 @@ class TimebackAdminService {
|
|
|
81331
81419
|
}
|
|
81332
81420
|
async getMasterableUnitsByCourse(courseIds) {
|
|
81333
81421
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
81334
|
-
const results = await
|
|
81422
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.MASTERABLE_UNITS_CONCURRENCY, async (courseId) => [courseId, await this.getMasterableUnits(courseId)]);
|
|
81335
81423
|
return new Map(results);
|
|
81336
81424
|
}
|
|
81337
81425
|
deriveGameSensorUrl(game2) {
|
|
@@ -81512,7 +81600,7 @@ class TimebackAdminService {
|
|
|
81512
81600
|
async loadEnrollmentAnalyticsSummaries(enrollmentIds) {
|
|
81513
81601
|
const client = this.requireClient();
|
|
81514
81602
|
const uniqueEnrollmentIds = [...new Set(enrollmentIds)];
|
|
81515
|
-
const results = await
|
|
81603
|
+
const results = await runWithConcurrency(uniqueEnrollmentIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (enrollmentId) => {
|
|
81516
81604
|
try {
|
|
81517
81605
|
const analytics = await client.api.edubridge.analytics.getEnrollmentFacts({
|
|
81518
81606
|
enrollmentId,
|
|
@@ -81606,7 +81694,7 @@ class TimebackAdminService {
|
|
|
81606
81694
|
if (options.runOwnersById.size === 0) {
|
|
81607
81695
|
return { events: [], runIds: new Set };
|
|
81608
81696
|
}
|
|
81609
|
-
const hydratedRuns = await
|
|
81697
|
+
const hydratedRuns = await runWithConcurrency([...options.runOwnersById.entries()], TimebackAdminService.DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY, async ([runId, studentId]) => {
|
|
81610
81698
|
try {
|
|
81611
81699
|
return {
|
|
81612
81700
|
runId,
|
|
@@ -81639,7 +81727,7 @@ class TimebackAdminService {
|
|
|
81639
81727
|
};
|
|
81640
81728
|
}
|
|
81641
81729
|
async buildMetricDiscrepancyQueueCandidates(user, options) {
|
|
81642
|
-
const comparisonResults = await
|
|
81730
|
+
const comparisonResults = await runWithConcurrency(options.activityGroups, TimebackAdminService.DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY, async (group) => {
|
|
81643
81731
|
try {
|
|
81644
81732
|
return {
|
|
81645
81733
|
group,
|
|
@@ -82639,7 +82727,7 @@ class TimebackAdminService {
|
|
|
82639
82727
|
const action = context2.newMasterableUnits < context2.oldMasterableUnits ? "complete" : "revoke";
|
|
82640
82728
|
const failed = [];
|
|
82641
82729
|
let processed = 0;
|
|
82642
|
-
await
|
|
82730
|
+
await runWithConcurrency(context2.affectedStudentIds, 8, async (studentId) => {
|
|
82643
82731
|
try {
|
|
82644
82732
|
await upsertMasteryCompletionEntry({
|
|
82645
82733
|
client,
|
|
@@ -82860,28 +82948,9 @@ class TimebackAdminService {
|
|
|
82860
82948
|
}
|
|
82861
82949
|
async getCompletionStatusByCourse(client, courseIds, studentId) {
|
|
82862
82950
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
82863
|
-
const results = await
|
|
82951
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (courseId) => [courseId, await this.getCompletionStatus(client, courseId, studentId)]);
|
|
82864
82952
|
return new Map(results);
|
|
82865
82953
|
}
|
|
82866
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
82867
|
-
if (items.length === 0) {
|
|
82868
|
-
return [];
|
|
82869
|
-
}
|
|
82870
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
82871
|
-
const results = Array.from({ length: items.length });
|
|
82872
|
-
let nextIndex = 0;
|
|
82873
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
82874
|
-
while (true) {
|
|
82875
|
-
const currentIndex = nextIndex;
|
|
82876
|
-
nextIndex++;
|
|
82877
|
-
if (currentIndex >= items.length) {
|
|
82878
|
-
return;
|
|
82879
|
-
}
|
|
82880
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
82881
|
-
}
|
|
82882
|
-
}));
|
|
82883
|
-
return results;
|
|
82884
|
-
}
|
|
82885
82954
|
}
|
|
82886
82955
|
var init_timeback_admin_service = __esm(async () => {
|
|
82887
82956
|
init_drizzle_orm();
|
|
@@ -82908,8 +82977,877 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
82908
82977
|
]);
|
|
82909
82978
|
});
|
|
82910
82979
|
|
|
82980
|
+
// ../api-core/src/utils/timeback-assessment-rules.util.ts
|
|
82981
|
+
function validateAssessmentStatusTransition(current, next) {
|
|
82982
|
+
if (current === next) {
|
|
82983
|
+
return;
|
|
82984
|
+
}
|
|
82985
|
+
const allowed = current === "draft" && next === "live" || current === "live" && next === "archived" || current === "archived" && next === "live";
|
|
82986
|
+
if (!allowed) {
|
|
82987
|
+
throw new ValidationError(`Assessment status cannot change from ${current} to ${next}`);
|
|
82988
|
+
}
|
|
82989
|
+
}
|
|
82990
|
+
function isAssessmentPublicationTransition(current, next) {
|
|
82991
|
+
return current !== "live" && next === "live";
|
|
82992
|
+
}
|
|
82993
|
+
function assertDraftAssessment(row) {
|
|
82994
|
+
if (row.status !== "draft") {
|
|
82995
|
+
throw new ValidationError("Only draft assessments can change QTI content or question membership");
|
|
82996
|
+
}
|
|
82997
|
+
}
|
|
82998
|
+
function assertAllAssessmentAssociationsDraft(rows) {
|
|
82999
|
+
if (rows.some((row) => row.status !== "draft")) {
|
|
83000
|
+
throw new ValidationError("QTI content cannot change while any associated assessment is live or archived");
|
|
83001
|
+
}
|
|
83002
|
+
}
|
|
83003
|
+
function assertAssessmentHasQuestions(questions) {
|
|
83004
|
+
if (questions.length === 0) {
|
|
83005
|
+
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
83006
|
+
}
|
|
83007
|
+
}
|
|
83008
|
+
function planAssessmentRemoval(status) {
|
|
83009
|
+
if (status === "draft") {
|
|
83010
|
+
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
83011
|
+
}
|
|
83012
|
+
if (status === "live") {
|
|
83013
|
+
return { kind: "archive", action: "archived", operation: "archive" };
|
|
83014
|
+
}
|
|
83015
|
+
return { kind: "none", action: "archived" };
|
|
83016
|
+
}
|
|
83017
|
+
function buildAssessmentAssociationUpdates(row, input) {
|
|
83018
|
+
const updates = {};
|
|
83019
|
+
if (input.purpose !== undefined) {
|
|
83020
|
+
updates.purpose = input.purpose;
|
|
83021
|
+
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
83022
|
+
updates.sortOrder = null;
|
|
83023
|
+
}
|
|
83024
|
+
}
|
|
83025
|
+
if (input.status !== undefined) {
|
|
83026
|
+
updates.status = input.status;
|
|
83027
|
+
if (input.status === "archived") {
|
|
83028
|
+
updates.sortOrder = null;
|
|
83029
|
+
}
|
|
83030
|
+
}
|
|
83031
|
+
return updates;
|
|
83032
|
+
}
|
|
83033
|
+
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
83034
|
+
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
83035
|
+
throw new ValidationError("Assessment order must contain unique identifiers");
|
|
83036
|
+
}
|
|
83037
|
+
}
|
|
83038
|
+
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
83039
|
+
if (!updatedRow) {
|
|
83040
|
+
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
83041
|
+
}
|
|
83042
|
+
return updatedRow;
|
|
83043
|
+
}
|
|
83044
|
+
function lockOrderAssessmentRows(rows) {
|
|
83045
|
+
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
83046
|
+
}
|
|
83047
|
+
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
83048
|
+
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
83049
|
+
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
83050
|
+
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
83051
|
+
}
|
|
83052
|
+
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
83053
|
+
}
|
|
83054
|
+
var init_timeback_assessment_rules_util = __esm(() => {
|
|
83055
|
+
init_errors();
|
|
83056
|
+
});
|
|
83057
|
+
|
|
83058
|
+
// ../api-core/src/utils/timeback-qti-authoring.util.ts
|
|
83059
|
+
function recordValue(value) {
|
|
83060
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
83061
|
+
}
|
|
83062
|
+
function qtiAuthoringSnapshot(input) {
|
|
83063
|
+
if (!recordValue(input.interaction)) {
|
|
83064
|
+
return;
|
|
83065
|
+
}
|
|
83066
|
+
return Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => input[field] === undefined ? [] : [[field, input[field]]]));
|
|
83067
|
+
}
|
|
83068
|
+
function buildOwnedQtiQuestionMetadata(input, ownership, existingMetadata = {}) {
|
|
83069
|
+
const inputMetadata = recordValue(input.metadata) ?? {};
|
|
83070
|
+
const metadata2 = {
|
|
83071
|
+
...existingMetadata,
|
|
83072
|
+
...inputMetadata
|
|
83073
|
+
};
|
|
83074
|
+
const existingAuthoring = recordValue(existingMetadata[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
83075
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
83076
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_EDITOR_MODE_KEY);
|
|
83077
|
+
const authoring = qtiAuthoringSnapshot(input);
|
|
83078
|
+
const shouldReplaceAuthoring = "interaction" in input || "numericTextEntry" in input;
|
|
83079
|
+
const persistedAuthoring = authoring ?? (shouldReplaceAuthoring ? undefined : existingAuthoring);
|
|
83080
|
+
return {
|
|
83081
|
+
...metadata2,
|
|
83082
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
83083
|
+
ownerGameSlug: ownership.gameSlug,
|
|
83084
|
+
[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]: ownership.testIdentifier,
|
|
83085
|
+
...persistedAuthoring ? { [PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]: persistedAuthoring } : {}
|
|
83086
|
+
};
|
|
83087
|
+
}
|
|
83088
|
+
function restoreQtiQuestionAuthoringData(item) {
|
|
83089
|
+
const authoring = recordValue(item.metadata?.[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
83090
|
+
if (!authoring) {
|
|
83091
|
+
return item;
|
|
83092
|
+
}
|
|
83093
|
+
const restored = Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => authoring[field] === undefined ? [] : [[field, authoring[field]]]));
|
|
83094
|
+
return { ...item, ...restored };
|
|
83095
|
+
}
|
|
83096
|
+
function mergeQtiQuestionItem(item, fallbackItem, itemIdentifier, metadata2) {
|
|
83097
|
+
return restoreQtiQuestionAuthoringData({
|
|
83098
|
+
...fallbackItem,
|
|
83099
|
+
...item,
|
|
83100
|
+
identifier: itemIdentifier,
|
|
83101
|
+
title: item.title || fallbackItem?.title || itemIdentifier,
|
|
83102
|
+
type: item.type ?? fallbackItem?.type,
|
|
83103
|
+
rawXml: item.rawXml ?? fallbackItem?.rawXml,
|
|
83104
|
+
interaction: item.interaction ?? fallbackItem?.interaction,
|
|
83105
|
+
responseDeclarations: item.responseDeclarations ?? fallbackItem?.responseDeclarations,
|
|
83106
|
+
metadata: {
|
|
83107
|
+
...fallbackItem?.metadata,
|
|
83108
|
+
...item.metadata,
|
|
83109
|
+
...metadata2
|
|
83110
|
+
}
|
|
83111
|
+
});
|
|
83112
|
+
}
|
|
83113
|
+
function newPlaycademyQuestionIdentifier(testIdentifier) {
|
|
83114
|
+
return `${testIdentifier}-q${crypto.randomUUID().slice(0, 8)}`;
|
|
83115
|
+
}
|
|
83116
|
+
function parseQtiQuestionCreationInput(input) {
|
|
83117
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
83118
|
+
throw new ValidationError("Question creation input must be an object");
|
|
83119
|
+
}
|
|
83120
|
+
const rawInput = input;
|
|
83121
|
+
if (rawInput.mode === undefined) {
|
|
83122
|
+
return { kind: "authoring", input: rawInput };
|
|
83123
|
+
}
|
|
83124
|
+
if (rawInput.mode !== "copy") {
|
|
83125
|
+
throw new ValidationError("Question creation mode is invalid");
|
|
83126
|
+
}
|
|
83127
|
+
const sourceItemIdentifier = typeof rawInput.sourceItemIdentifier === "string" ? rawInput.sourceItemIdentifier.trim() : "";
|
|
83128
|
+
if (!sourceItemIdentifier) {
|
|
83129
|
+
throw new ValidationError("A source question identifier is required to create a copy");
|
|
83130
|
+
}
|
|
83131
|
+
const unexpectedField = Object.keys(rawInput).find((field) => field !== "mode" && field !== "sourceItemIdentifier");
|
|
83132
|
+
if (unexpectedField) {
|
|
83133
|
+
throw new ValidationError(`Question copy input contains an unexpected “${unexpectedField}” field`);
|
|
83134
|
+
}
|
|
83135
|
+
return { kind: "copy", sourceItemIdentifier };
|
|
83136
|
+
}
|
|
83137
|
+
function nonEmptyMetadataString(value) {
|
|
83138
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
83139
|
+
}
|
|
83140
|
+
function qtiQuestionOwnerTestIdentifier(item) {
|
|
83141
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83142
|
+
return;
|
|
83143
|
+
}
|
|
83144
|
+
return nonEmptyMetadataString(item.metadata[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]);
|
|
83145
|
+
}
|
|
83146
|
+
function qtiTestReferencesItem(test, itemIdentifier) {
|
|
83147
|
+
return (test["qti-test-part"] ?? []).some((part) => (part["qti-assessment-section"] ?? []).some((section) => (section["qti-assessment-item-ref"] ?? []).some((reference) => reference.identifier === itemIdentifier)));
|
|
83148
|
+
}
|
|
83149
|
+
function resolveQtiQuestionOwnerGameSlug(item, ownerTest) {
|
|
83150
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83151
|
+
return;
|
|
83152
|
+
}
|
|
83153
|
+
if ("ownerGameSlug" in item.metadata) {
|
|
83154
|
+
return nonEmptyMetadataString(item.metadata.ownerGameSlug);
|
|
83155
|
+
}
|
|
83156
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
83157
|
+
if (!ownerTestIdentifier || !ownerTest || ownerTest.identifier !== ownerTestIdentifier || !qtiTestReferencesItem(ownerTest, item.identifier) || ownerTest.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83158
|
+
return;
|
|
83159
|
+
}
|
|
83160
|
+
return nonEmptyMetadataString(ownerTest.metadata.ownerGameSlug);
|
|
83161
|
+
}
|
|
83162
|
+
function isQtiQuestionOwnedByGame(item, gameSlug, ownerTest) {
|
|
83163
|
+
return resolveQtiQuestionOwnerGameSlug(item, ownerTest) === gameSlug;
|
|
83164
|
+
}
|
|
83165
|
+
function isQtiQuestionOwnedByContext(item, ownership, ownerTest) {
|
|
83166
|
+
const declaredOwnerTest = qtiQuestionOwnerTestIdentifier(item);
|
|
83167
|
+
if (declaredOwnerTest) {
|
|
83168
|
+
return declaredOwnerTest === ownership.testIdentifier && isQtiQuestionOwnedByGame(item, ownership.gameSlug, ownerTest);
|
|
83169
|
+
}
|
|
83170
|
+
return isQtiItemOwnedByTest(item, ownership.testIdentifier);
|
|
83171
|
+
}
|
|
83172
|
+
function normalizedQtiInteractionType(value) {
|
|
83173
|
+
if (typeof value !== "string") {
|
|
83174
|
+
return;
|
|
83175
|
+
}
|
|
83176
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
83177
|
+
return normalized || undefined;
|
|
83178
|
+
}
|
|
83179
|
+
function qtiXmlInteractionTypes(xml) {
|
|
83180
|
+
const markup = xml.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
83181
|
+
const interactionPattern = /<(?!\/)(?:[A-Za-z_][\w.-]*:)?qti-([a-z][\w-]*)-interaction\b[^>]*>/gi;
|
|
83182
|
+
return [...markup.matchAll(interactionPattern)].map((match) => match[1].toLowerCase());
|
|
83183
|
+
}
|
|
83184
|
+
function supportedQtiQuestionInteractionType(question) {
|
|
83185
|
+
const interaction = recordValue(question.interaction);
|
|
83186
|
+
const structuredTypes = [
|
|
83187
|
+
normalizedQtiInteractionType(question.type),
|
|
83188
|
+
normalizedQtiInteractionType(interaction?.type)
|
|
83189
|
+
].filter((type) => Boolean(type));
|
|
83190
|
+
const distinctStructuredTypes = [...new Set(structuredTypes)];
|
|
83191
|
+
if (distinctStructuredTypes.length > 1) {
|
|
83192
|
+
return;
|
|
83193
|
+
}
|
|
83194
|
+
const rawXml = typeof question.rawXml === "string" ? question.rawXml : undefined;
|
|
83195
|
+
if (rawXml) {
|
|
83196
|
+
const xmlTypes = qtiXmlInteractionTypes(rawXml);
|
|
83197
|
+
if (xmlTypes.length !== 1) {
|
|
83198
|
+
return;
|
|
83199
|
+
}
|
|
83200
|
+
const [xmlType] = xmlTypes;
|
|
83201
|
+
const [structuredType2] = distinctStructuredTypes;
|
|
83202
|
+
if (structuredType2 && structuredType2 !== xmlType) {
|
|
83203
|
+
return;
|
|
83204
|
+
}
|
|
83205
|
+
return playcademySupportedQtiInteractionType(xmlType);
|
|
83206
|
+
}
|
|
83207
|
+
const [structuredType] = distinctStructuredTypes;
|
|
83208
|
+
return playcademySupportedQtiInteractionType(structuredType);
|
|
83209
|
+
}
|
|
83210
|
+
function assertSupportedQtiQuestionInteraction(question) {
|
|
83211
|
+
if (!supportedQtiQuestionInteractionType(question)) {
|
|
83212
|
+
throw new ValidationError("This QTI interaction type is not supported by the Playcademy question editor.");
|
|
83213
|
+
}
|
|
83214
|
+
}
|
|
83215
|
+
function invalidQtiCopyXml() {
|
|
83216
|
+
throw new ValidationError("The source question does not contain valid QTI item XML");
|
|
83217
|
+
}
|
|
83218
|
+
function markupEnd(xml, start2, allowInternalSubset) {
|
|
83219
|
+
let quote = null;
|
|
83220
|
+
let subsetDepth = 0;
|
|
83221
|
+
for (let index2 = start2;index2 < xml.length; index2 += 1) {
|
|
83222
|
+
const character = xml[index2];
|
|
83223
|
+
if (quote) {
|
|
83224
|
+
if (character === quote) {
|
|
83225
|
+
quote = null;
|
|
83226
|
+
}
|
|
83227
|
+
} else if (character === '"' || character === "'") {
|
|
83228
|
+
quote = character;
|
|
83229
|
+
} else if (allowInternalSubset && character === "[") {
|
|
83230
|
+
subsetDepth += 1;
|
|
83231
|
+
} else if (allowInternalSubset && character === "]") {
|
|
83232
|
+
subsetDepth = Math.max(0, subsetDepth - 1);
|
|
83233
|
+
} else if (character === ">" && subsetDepth === 0) {
|
|
83234
|
+
return index2;
|
|
83235
|
+
}
|
|
83236
|
+
}
|
|
83237
|
+
return invalidQtiCopyXml();
|
|
83238
|
+
}
|
|
83239
|
+
function skipXmlWhitespace(xml, start2, end = xml.length) {
|
|
83240
|
+
let cursor2 = start2;
|
|
83241
|
+
while (cursor2 < end && /\s/.test(xml[cursor2] ?? "")) {
|
|
83242
|
+
cursor2 += 1;
|
|
83243
|
+
}
|
|
83244
|
+
return cursor2;
|
|
83245
|
+
}
|
|
83246
|
+
function xmlPreambleEnd(xml, cursor2) {
|
|
83247
|
+
if (xml.startsWith("<?", cursor2)) {
|
|
83248
|
+
const end = xml.indexOf("?>", cursor2 + 2);
|
|
83249
|
+
if (end === -1) {
|
|
83250
|
+
return invalidQtiCopyXml();
|
|
83251
|
+
}
|
|
83252
|
+
return end + 2;
|
|
83253
|
+
}
|
|
83254
|
+
if (xml.startsWith("<!--", cursor2)) {
|
|
83255
|
+
const end = xml.indexOf("-->", cursor2 + 4);
|
|
83256
|
+
if (end === -1) {
|
|
83257
|
+
return invalidQtiCopyXml();
|
|
83258
|
+
}
|
|
83259
|
+
return end + 3;
|
|
83260
|
+
}
|
|
83261
|
+
if (/^<!DOCTYPE\b/i.test(xml.slice(cursor2))) {
|
|
83262
|
+
return markupEnd(xml, cursor2 + 2, true) + 1;
|
|
83263
|
+
}
|
|
83264
|
+
return null;
|
|
83265
|
+
}
|
|
83266
|
+
function qtiRootAttributes(xml, start2, end) {
|
|
83267
|
+
const attributes = [];
|
|
83268
|
+
let cursor2 = start2;
|
|
83269
|
+
while (cursor2 < end) {
|
|
83270
|
+
cursor2 = skipXmlWhitespace(xml, cursor2, end);
|
|
83271
|
+
if (xml[cursor2] === "/") {
|
|
83272
|
+
cursor2 += 1;
|
|
83273
|
+
} else if (cursor2 < end) {
|
|
83274
|
+
const nameMatch = /^[A-Za-z_][\w.:-]*/.exec(xml.slice(cursor2, end));
|
|
83275
|
+
if (!nameMatch) {
|
|
83276
|
+
return invalidQtiCopyXml();
|
|
83277
|
+
}
|
|
83278
|
+
const name3 = nameMatch[0];
|
|
83279
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + name3.length, end);
|
|
83280
|
+
if (xml[cursor2] !== "=") {
|
|
83281
|
+
return invalidQtiCopyXml();
|
|
83282
|
+
}
|
|
83283
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + 1, end);
|
|
83284
|
+
const quote = xml[cursor2];
|
|
83285
|
+
if (quote !== '"' && quote !== "'") {
|
|
83286
|
+
return invalidQtiCopyXml();
|
|
83287
|
+
}
|
|
83288
|
+
const valueStart = cursor2 + 1;
|
|
83289
|
+
const valueEnd = xml.indexOf(quote, valueStart);
|
|
83290
|
+
if (valueEnd === -1 || valueEnd > end) {
|
|
83291
|
+
return invalidQtiCopyXml();
|
|
83292
|
+
}
|
|
83293
|
+
attributes.push({ name: name3, quote, valueStart, valueEnd });
|
|
83294
|
+
cursor2 = valueEnd + 1;
|
|
83295
|
+
}
|
|
83296
|
+
}
|
|
83297
|
+
return attributes;
|
|
83298
|
+
}
|
|
83299
|
+
function qtiItemStartTag(xml) {
|
|
83300
|
+
let cursor2 = xml.charCodeAt(0) === 65279 ? 1 : 0;
|
|
83301
|
+
while (cursor2 < xml.length) {
|
|
83302
|
+
cursor2 = skipXmlWhitespace(xml, cursor2);
|
|
83303
|
+
const preambleEnd = xmlPreambleEnd(xml, cursor2);
|
|
83304
|
+
if (preambleEnd !== null) {
|
|
83305
|
+
cursor2 = preambleEnd;
|
|
83306
|
+
} else {
|
|
83307
|
+
const root = /^<([A-Za-z_][\w.:-]*)/.exec(xml.slice(cursor2));
|
|
83308
|
+
if (!root || root[1].split(":").at(-1) !== "qti-assessment-item") {
|
|
83309
|
+
return invalidQtiCopyXml();
|
|
83310
|
+
}
|
|
83311
|
+
const attributesStart = cursor2 + root[0].length;
|
|
83312
|
+
const end = markupEnd(xml, attributesStart, false);
|
|
83313
|
+
const attributes = qtiRootAttributes(xml, attributesStart, end);
|
|
83314
|
+
let insertionPoint = skipXmlWhitespaceBackward(xml, end);
|
|
83315
|
+
if (xml[insertionPoint - 1] === "/") {
|
|
83316
|
+
insertionPoint -= 1;
|
|
83317
|
+
}
|
|
83318
|
+
return { attributes, insertionPoint };
|
|
83319
|
+
}
|
|
83320
|
+
}
|
|
83321
|
+
return invalidQtiCopyXml();
|
|
83322
|
+
}
|
|
83323
|
+
function skipXmlWhitespaceBackward(xml, start2) {
|
|
83324
|
+
let cursor2 = start2;
|
|
83325
|
+
while (/\s/.test(xml[cursor2 - 1] ?? "")) {
|
|
83326
|
+
cursor2 -= 1;
|
|
83327
|
+
}
|
|
83328
|
+
return cursor2;
|
|
83329
|
+
}
|
|
83330
|
+
function escapeXmlAttribute(value, quote) {
|
|
83331
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(quote, quote === '"' ? """ : "'");
|
|
83332
|
+
}
|
|
83333
|
+
function rewriteQtiItemIdentity(xml, identifier, title) {
|
|
83334
|
+
if (!xml.trim() || !identifier.trim() || !title.trim()) {
|
|
83335
|
+
return invalidQtiCopyXml();
|
|
83336
|
+
}
|
|
83337
|
+
const root = qtiItemStartTag(xml);
|
|
83338
|
+
const replacements = [];
|
|
83339
|
+
const targetAttributes = new Map([
|
|
83340
|
+
["identifier", identifier],
|
|
83341
|
+
["title", title]
|
|
83342
|
+
]);
|
|
83343
|
+
for (const [name3, value] of targetAttributes) {
|
|
83344
|
+
const matches = root.attributes.filter((attribute2) => attribute2.name === name3);
|
|
83345
|
+
if (matches.length > 1) {
|
|
83346
|
+
return invalidQtiCopyXml();
|
|
83347
|
+
}
|
|
83348
|
+
const attribute = matches[0];
|
|
83349
|
+
if (attribute) {
|
|
83350
|
+
replacements.push({
|
|
83351
|
+
start: attribute.valueStart,
|
|
83352
|
+
end: attribute.valueEnd,
|
|
83353
|
+
value: escapeXmlAttribute(value, attribute.quote)
|
|
83354
|
+
});
|
|
83355
|
+
} else {
|
|
83356
|
+
replacements.push({
|
|
83357
|
+
start: root.insertionPoint,
|
|
83358
|
+
end: root.insertionPoint,
|
|
83359
|
+
value: ` ${name3}="${escapeXmlAttribute(value, '"')}"`
|
|
83360
|
+
});
|
|
83361
|
+
}
|
|
83362
|
+
}
|
|
83363
|
+
return replacements.toSorted((left, right) => right.start - left.start).reduce((rewritten, replacement) => `${rewritten.slice(0, replacement.start)}${replacement.value}${rewritten.slice(replacement.end)}`, xml);
|
|
83364
|
+
}
|
|
83365
|
+
function buildQtiQuestionCopyInput(input) {
|
|
83366
|
+
assertSupportedQtiQuestionInteraction(input.source);
|
|
83367
|
+
const title = input.source.title?.trim() || input.source.identifier;
|
|
83368
|
+
if (!input.source.rawXml) {
|
|
83369
|
+
return invalidQtiCopyXml();
|
|
83370
|
+
}
|
|
83371
|
+
const sourceMetadata = { ...input.source.metadata };
|
|
83372
|
+
if (sourceMetadata.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
83373
|
+
Reflect.deleteProperty(sourceMetadata, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
83374
|
+
}
|
|
83375
|
+
const metadata2 = buildOwnedQtiQuestionMetadata({
|
|
83376
|
+
metadata: {
|
|
83377
|
+
copiedFromItemIdentifier: input.source.identifier,
|
|
83378
|
+
integrationId: input.integrationId
|
|
83379
|
+
}
|
|
83380
|
+
}, {
|
|
83381
|
+
gameSlug: input.gameSlug,
|
|
83382
|
+
testIdentifier: input.targetTestIdentifier
|
|
83383
|
+
}, sourceMetadata);
|
|
83384
|
+
return {
|
|
83385
|
+
identifier: input.targetIdentifier,
|
|
83386
|
+
title,
|
|
83387
|
+
xml: rewriteQtiItemIdentity(input.source.rawXml, input.targetIdentifier, title),
|
|
83388
|
+
metadata: metadata2
|
|
83389
|
+
};
|
|
83390
|
+
}
|
|
83391
|
+
function assertQtiQuestionEditable(item, ownership, ownerTest) {
|
|
83392
|
+
if (!isQtiQuestionOwnedByContext(item, ownership, ownerTest)) {
|
|
83393
|
+
throw new ValidationError("Shared question references are read-only. Copy the question to create an editable independent item.");
|
|
83394
|
+
}
|
|
83395
|
+
}
|
|
83396
|
+
function buildNumericQuestionXml(input, identifier, title) {
|
|
83397
|
+
if (input.format === "xml") {
|
|
83398
|
+
throw new ValidationError("Raw question XML is not accepted at this API boundary");
|
|
83399
|
+
}
|
|
83400
|
+
const candidate = input.numericTextEntry;
|
|
83401
|
+
if (candidate === undefined) {
|
|
83402
|
+
return null;
|
|
83403
|
+
}
|
|
83404
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
83405
|
+
throw new ValidationError("Numeric text-entry data is invalid");
|
|
83406
|
+
}
|
|
83407
|
+
const prompt = "prompt" in candidate ? candidate.prompt : undefined;
|
|
83408
|
+
const numeric3 = parseNumericTextEntry({
|
|
83409
|
+
baseType: "baseType" in candidate ? candidate.baseType : undefined,
|
|
83410
|
+
answer: "answer" in candidate ? candidate.answer : undefined,
|
|
83411
|
+
comparison: "comparison" in candidate ? candidate.comparison : undefined
|
|
83412
|
+
});
|
|
83413
|
+
if (!numeric3.success) {
|
|
83414
|
+
throw new ValidationError(numeric3.message);
|
|
83415
|
+
}
|
|
83416
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83417
|
+
throw new ValidationError("Numeric questions require a title and prompt");
|
|
83418
|
+
}
|
|
83419
|
+
return numericTextEntryXml({ identifier, title, prompt: prompt.trim(), numeric: numeric3.value });
|
|
83420
|
+
}
|
|
83421
|
+
function buildExactMatchQuestionXml(input) {
|
|
83422
|
+
const responseIdentifier = escapeXml(input.responseIdentifier);
|
|
83423
|
+
const correctValues = input.correctIdentifiers.map((identifier) => ` <qti-value>${escapeXml(identifier)}</qti-value>`).join(`
|
|
83424
|
+
`);
|
|
83425
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
83426
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
83427
|
+
<qti-response-declaration identifier="${responseIdentifier}" cardinality="${input.cardinality}" base-type="identifier">
|
|
83428
|
+
<qti-correct-response>
|
|
83429
|
+
${correctValues}
|
|
83430
|
+
</qti-correct-response>
|
|
83431
|
+
</qti-response-declaration>
|
|
83432
|
+
<qti-outcome-declaration identifier="FEEDBACK" cardinality="single" base-type="identifier" />
|
|
83433
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
83434
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
83435
|
+
</qti-outcome-declaration>
|
|
83436
|
+
<qti-item-body>
|
|
83437
|
+
${input.itemBody}
|
|
83438
|
+
</qti-item-body>
|
|
83439
|
+
<qti-response-processing>
|
|
83440
|
+
<qti-response-condition>
|
|
83441
|
+
<qti-response-if>
|
|
83442
|
+
<qti-match><qti-variable identifier="${responseIdentifier}" /><qti-correct identifier="${responseIdentifier}" /></qti-match>
|
|
83443
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">CORRECT</qti-base-value></qti-set-outcome-value>
|
|
83444
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
83445
|
+
</qti-response-if>
|
|
83446
|
+
<qti-response-else>
|
|
83447
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">INCORRECT</qti-base-value></qti-set-outcome-value>
|
|
83448
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">0</qti-base-value></qti-set-outcome-value>
|
|
83449
|
+
</qti-response-else>
|
|
83450
|
+
</qti-response-condition>
|
|
83451
|
+
</qti-response-processing>
|
|
83452
|
+
</qti-assessment-item>`;
|
|
83453
|
+
}
|
|
83454
|
+
function parseInlineChoices(value) {
|
|
83455
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
83456
|
+
throw new ValidationError("Inline-choice questions require at least two choices");
|
|
83457
|
+
}
|
|
83458
|
+
const choices = value.map((choice) => {
|
|
83459
|
+
const record3 = recordValue(choice);
|
|
83460
|
+
const identifier = record3?.identifier;
|
|
83461
|
+
const content = record3?.content;
|
|
83462
|
+
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
|
|
83463
|
+
throw new ValidationError("Inline-choice options are invalid");
|
|
83464
|
+
}
|
|
83465
|
+
return { identifier: identifier.trim(), content: content.trim() };
|
|
83466
|
+
});
|
|
83467
|
+
if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
|
|
83468
|
+
throw new ValidationError("Inline-choice option identifiers must be unique");
|
|
83469
|
+
}
|
|
83470
|
+
return choices;
|
|
83471
|
+
}
|
|
83472
|
+
function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
|
|
83473
|
+
const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
|
|
83474
|
+
const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
|
|
83475
|
+
const correctResponse = recordValue(declaration?.correctResponse);
|
|
83476
|
+
const rawValues = correctResponse?.value;
|
|
83477
|
+
const values = Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : [];
|
|
83478
|
+
if (declaration?.cardinality !== "single" || declaration.baseType !== "identifier") {
|
|
83479
|
+
throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
|
|
83480
|
+
}
|
|
83481
|
+
if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
|
|
83482
|
+
throw new ValidationError("Inline-choice questions require one valid correct option");
|
|
83483
|
+
}
|
|
83484
|
+
return values[0];
|
|
83485
|
+
}
|
|
83486
|
+
function buildInlineChoiceQuestionXml(input, identifier, title) {
|
|
83487
|
+
const interaction = recordValue(input.interaction);
|
|
83488
|
+
const interactionType = normalizedQtiInteractionType(interaction?.type ?? input.type);
|
|
83489
|
+
if (interactionType !== "inline-choice") {
|
|
83490
|
+
return null;
|
|
83491
|
+
}
|
|
83492
|
+
const responseIdentifier = interaction?.responseIdentifier;
|
|
83493
|
+
const structure = recordValue(interaction?.questionStructure);
|
|
83494
|
+
const prompt = structure?.prompt;
|
|
83495
|
+
if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
83496
|
+
throw new ValidationError("Inline-choice question data is invalid");
|
|
83497
|
+
}
|
|
83498
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83499
|
+
throw new ValidationError("Inline-choice questions require a title and prompt");
|
|
83500
|
+
}
|
|
83501
|
+
const normalizedPrompt = prompt.trim();
|
|
83502
|
+
const promptParts = normalizedPrompt.split(INLINE_CHOICE_BLANK);
|
|
83503
|
+
if (promptParts.length !== 2) {
|
|
83504
|
+
throw new ValidationError("Inline-choice questions require exactly one blank");
|
|
83505
|
+
}
|
|
83506
|
+
const choices = parseInlineChoices(structure.inlineChoices);
|
|
83507
|
+
const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
|
|
83508
|
+
const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
|
|
83509
|
+
const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
|
|
83510
|
+
`);
|
|
83511
|
+
const [before = "", after = ""] = promptParts;
|
|
83512
|
+
const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
|
|
83513
|
+
${choicesXml}
|
|
83514
|
+
</qti-inline-choice-interaction>${escapeXml(after)}</p>`;
|
|
83515
|
+
return buildExactMatchQuestionXml({
|
|
83516
|
+
identifier,
|
|
83517
|
+
title,
|
|
83518
|
+
responseIdentifier,
|
|
83519
|
+
cardinality: "single",
|
|
83520
|
+
correctIdentifiers: [correctIdentifier],
|
|
83521
|
+
itemBody
|
|
83522
|
+
});
|
|
83523
|
+
}
|
|
83524
|
+
function parseHottextSegments(value) {
|
|
83525
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
83526
|
+
throw new ValidationError("Hottext questions require passage segments");
|
|
83527
|
+
}
|
|
83528
|
+
const segments = value.map((segment) => {
|
|
83529
|
+
const record3 = recordValue(segment);
|
|
83530
|
+
const identifier = record3?.identifier;
|
|
83531
|
+
const content = record3?.content;
|
|
83532
|
+
const mode = record3?.mode;
|
|
83533
|
+
if (typeof identifier !== "string" || typeof content !== "string" || !content.trim() || !["plain", "option", "correct"].includes(String(mode))) {
|
|
83534
|
+
throw new ValidationError("Hottext passage segments are invalid");
|
|
83535
|
+
}
|
|
83536
|
+
return {
|
|
83537
|
+
identifier: identifier.trim(),
|
|
83538
|
+
content: content.trim(),
|
|
83539
|
+
mode
|
|
83540
|
+
};
|
|
83541
|
+
});
|
|
83542
|
+
const selectable = segments.filter((segment) => segment.mode !== "plain");
|
|
83543
|
+
const correct = selectable.filter((segment) => segment.mode === "correct");
|
|
83544
|
+
const identifiers = selectable.map((segment) => segment.identifier);
|
|
83545
|
+
if (selectable.length < 2) {
|
|
83546
|
+
throw new ValidationError("Hottext questions require at least two selectable phrases");
|
|
83547
|
+
}
|
|
83548
|
+
if (identifiers.some((identifier) => !identifier)) {
|
|
83549
|
+
throw new ValidationError("Hottext selectable phrases require identifiers");
|
|
83550
|
+
}
|
|
83551
|
+
if (new Set(identifiers).size !== identifiers.length) {
|
|
83552
|
+
throw new ValidationError("Hottext selectable phrase identifiers must be unique");
|
|
83553
|
+
}
|
|
83554
|
+
if (correct.length === 0) {
|
|
83555
|
+
throw new ValidationError("Hottext questions require a correct phrase");
|
|
83556
|
+
}
|
|
83557
|
+
return { segments, selectable, correct };
|
|
83558
|
+
}
|
|
83559
|
+
function buildHottextQuestionXml(input, identifier, title) {
|
|
83560
|
+
const candidate = input.hottext;
|
|
83561
|
+
if (candidate === undefined) {
|
|
83562
|
+
return null;
|
|
83563
|
+
}
|
|
83564
|
+
const hottext = recordValue(candidate);
|
|
83565
|
+
if (!hottext) {
|
|
83566
|
+
throw new ValidationError("Hottext question data is invalid");
|
|
83567
|
+
}
|
|
83568
|
+
const prompt = hottext.prompt;
|
|
83569
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
83570
|
+
throw new ValidationError("Hottext questions require a title and prompt");
|
|
83571
|
+
}
|
|
83572
|
+
const { segments, selectable, correct } = parseHottextSegments(hottext.segments);
|
|
83573
|
+
const multiple = correct.length > 1;
|
|
83574
|
+
const maxChoices = hottextMaxChoices(selectable.length, multiple);
|
|
83575
|
+
const passage = segments.map((segment) => segment.mode === "plain" ? escapeXml(segment.content) : `<qti-hottext identifier="${escapeXml(segment.identifier)}">${escapeXml(segment.content)}</qti-hottext>`).join(" ");
|
|
83576
|
+
const itemBody = ` <qti-hottext-interaction response-identifier="RESPONSE" max-choices="${maxChoices}">
|
|
83577
|
+
<qti-prompt>${escapeXml(prompt.trim())}</qti-prompt>
|
|
83578
|
+
<p>${passage}</p>
|
|
83579
|
+
</qti-hottext-interaction>`;
|
|
83580
|
+
return buildExactMatchQuestionXml({
|
|
83581
|
+
identifier,
|
|
83582
|
+
title,
|
|
83583
|
+
responseIdentifier: "RESPONSE",
|
|
83584
|
+
cardinality: multiple ? "multiple" : "single",
|
|
83585
|
+
correctIdentifiers: correct.map((segment) => segment.identifier),
|
|
83586
|
+
itemBody
|
|
83587
|
+
});
|
|
83588
|
+
}
|
|
83589
|
+
function buildQuestionXml(input, identifier, title) {
|
|
83590
|
+
for (const build2 of QUESTION_XML_BUILDERS) {
|
|
83591
|
+
const xml = build2(input, identifier, title);
|
|
83592
|
+
if (xml !== null) {
|
|
83593
|
+
return xml;
|
|
83594
|
+
}
|
|
83595
|
+
}
|
|
83596
|
+
return null;
|
|
83597
|
+
}
|
|
83598
|
+
function buildCreatedQtiQuestionReference(input) {
|
|
83599
|
+
return {
|
|
83600
|
+
ownership: "owned",
|
|
83601
|
+
reference: {
|
|
83602
|
+
identifier: input.itemIdentifier,
|
|
83603
|
+
href: input.href,
|
|
83604
|
+
testPart: input.partIdentifier,
|
|
83605
|
+
section: input.sectionIdentifier
|
|
83606
|
+
},
|
|
83607
|
+
question: mergeQtiQuestionItem(input.item, input.fallbackItem, input.itemIdentifier, input.metadata)
|
|
83608
|
+
};
|
|
83609
|
+
}
|
|
83610
|
+
function buildHydratedQtiQuestionReference(input) {
|
|
83611
|
+
const identifier = input.reference.reference.identifier;
|
|
83612
|
+
const ownerGameSlug = resolveQtiQuestionOwnerGameSlug(input.item, input.ownerTest);
|
|
83613
|
+
const authoritativeItem = ownerGameSlug ? {
|
|
83614
|
+
...input.item,
|
|
83615
|
+
metadata: { ...input.item.metadata, ownerGameSlug }
|
|
83616
|
+
} : input.item;
|
|
83617
|
+
const question = mergeQtiQuestionItem(authoritativeItem, input.reference.question, identifier);
|
|
83618
|
+
return {
|
|
83619
|
+
...input.reference,
|
|
83620
|
+
ownership: isQtiQuestionOwnedByContext(question, { gameSlug: input.gameSlug, testIdentifier: input.testIdentifier }, input.ownerTest) ? "owned" : "shared",
|
|
83621
|
+
question
|
|
83622
|
+
};
|
|
83623
|
+
}
|
|
83624
|
+
function isQtiTestOwnedByGame(test, gameSlug) {
|
|
83625
|
+
return test.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && test.metadata.ownerGameSlug === gameSlug;
|
|
83626
|
+
}
|
|
83627
|
+
function assertQtiTestOwnedByGame(test, gameSlug) {
|
|
83628
|
+
if (!isQtiTestOwnedByGame(test, gameSlug)) {
|
|
83629
|
+
throw new ValidationError("Shared assessment references are read-only. Copy the assessment to create an editable independent test.");
|
|
83630
|
+
}
|
|
83631
|
+
}
|
|
83632
|
+
function qtiTestParts(test) {
|
|
83633
|
+
const parts2 = test["qti-test-part"];
|
|
83634
|
+
if (!Array.isArray(parts2) || parts2.length === 0) {
|
|
83635
|
+
throw new ValidationError(`Assessment ${test.identifier} has no test parts`);
|
|
83636
|
+
}
|
|
83637
|
+
for (const [partIndex, part] of parts2.entries()) {
|
|
83638
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
|
83639
|
+
throw new ValidationError(`Assessment ${test.identifier} contains an invalid test part at position ${partIndex + 1}`);
|
|
83640
|
+
}
|
|
83641
|
+
const sections = part["qti-assessment-section"];
|
|
83642
|
+
if (!Array.isArray(sections)) {
|
|
83643
|
+
throw new ValidationError(`Assessment ${test.identifier} test part ${partIndex + 1} has no section list`);
|
|
83644
|
+
}
|
|
83645
|
+
}
|
|
83646
|
+
return parts2;
|
|
83647
|
+
}
|
|
83648
|
+
function qtiTestOptionalAttributes(test) {
|
|
83649
|
+
return {
|
|
83650
|
+
...test.qtiVersion ? { qtiVersion: test.qtiVersion } : {},
|
|
83651
|
+
...test.timeLimit !== undefined ? { timeLimit: test.timeLimit } : {},
|
|
83652
|
+
...test.maxAttempts !== undefined ? { maxAttempts: test.maxAttempts } : {},
|
|
83653
|
+
...test.toolsEnabled !== undefined ? { toolsEnabled: test.toolsEnabled } : {}
|
|
83654
|
+
};
|
|
83655
|
+
}
|
|
83656
|
+
function buildQtiAssessmentItemCopyPlan(test, targetTestIdentifier, hrefForIdentifier, createIdentifier = newPlaycademyQuestionIdentifier) {
|
|
83657
|
+
const sourceIdentifiers = [];
|
|
83658
|
+
const seen = new Set;
|
|
83659
|
+
for (const part of qtiTestParts(test)) {
|
|
83660
|
+
for (const section of part["qti-assessment-section"]) {
|
|
83661
|
+
for (const reference of section["qti-assessment-item-ref"] ?? []) {
|
|
83662
|
+
if (!seen.has(reference.identifier)) {
|
|
83663
|
+
seen.add(reference.identifier);
|
|
83664
|
+
sourceIdentifiers.push(reference.identifier);
|
|
83665
|
+
}
|
|
83666
|
+
}
|
|
83667
|
+
}
|
|
83668
|
+
}
|
|
83669
|
+
return sourceIdentifiers.map((sourceIdentifier) => {
|
|
83670
|
+
const targetIdentifier = createIdentifier(targetTestIdentifier);
|
|
83671
|
+
return {
|
|
83672
|
+
sourceIdentifier,
|
|
83673
|
+
targetIdentifier,
|
|
83674
|
+
href: hrefForIdentifier(targetIdentifier)
|
|
83675
|
+
};
|
|
83676
|
+
});
|
|
83677
|
+
}
|
|
83678
|
+
function buildQtiTestStructureInput(test, targetTestIdentifier, itemCopies) {
|
|
83679
|
+
const outcomeDeclarations = test["qti-outcome-declaration"];
|
|
83680
|
+
if (outcomeDeclarations !== undefined && !Array.isArray(outcomeDeclarations)) {
|
|
83681
|
+
throw new ValidationError(`Assessment ${test.identifier} has an invalid outcome declaration list`);
|
|
83682
|
+
}
|
|
83683
|
+
return {
|
|
83684
|
+
"qti-test-part": qtiTestParts(test).map((part, partIndex) => ({
|
|
83685
|
+
identifier: targetTestIdentifier ? `${targetTestIdentifier}-part${partIndex + 1}` : part.identifier,
|
|
83686
|
+
navigationMode: part.navigationMode,
|
|
83687
|
+
submissionMode: part.submissionMode,
|
|
83688
|
+
"qti-assessment-section": part["qti-assessment-section"].map((section, sectionIndex) => {
|
|
83689
|
+
let sectionIdentifier = section.identifier;
|
|
83690
|
+
if (targetTestIdentifier) {
|
|
83691
|
+
sectionIdentifier = partIndex === 0 && sectionIndex === 0 ? `${targetTestIdentifier}-section1` : `${targetTestIdentifier}-part${partIndex + 1}-section${sectionIndex + 1}`;
|
|
83692
|
+
}
|
|
83693
|
+
return {
|
|
83694
|
+
identifier: sectionIdentifier,
|
|
83695
|
+
title: section.title,
|
|
83696
|
+
visible: section.visible ?? true,
|
|
83697
|
+
...section.required !== undefined ? { required: section.required } : {},
|
|
83698
|
+
...section.fixed !== undefined ? { fixed: section.fixed } : {},
|
|
83699
|
+
sequence: section.sequence ?? sectionIndex + 1,
|
|
83700
|
+
...section["qti-assessment-item-ref"] ? {
|
|
83701
|
+
"qti-assessment-item-ref": section["qti-assessment-item-ref"].map((item, itemIndex) => {
|
|
83702
|
+
const copy = itemCopies?.get(item.identifier);
|
|
83703
|
+
if (itemCopies && !copy) {
|
|
83704
|
+
throw new ValidationError(`Assessment copy is missing question ${item.identifier}`);
|
|
83705
|
+
}
|
|
83706
|
+
return {
|
|
83707
|
+
identifier: copy?.targetIdentifier ?? item.identifier,
|
|
83708
|
+
href: copy?.href ?? item.href,
|
|
83709
|
+
sequence: item.sequence ?? itemIndex + 1
|
|
83710
|
+
};
|
|
83711
|
+
})
|
|
83712
|
+
} : {}
|
|
83713
|
+
};
|
|
83714
|
+
})
|
|
83715
|
+
})),
|
|
83716
|
+
...outcomeDeclarations ? {
|
|
83717
|
+
"qti-outcome-declaration": outcomeDeclarations.map((declaration) => ({
|
|
83718
|
+
identifier: declaration.identifier,
|
|
83719
|
+
...declaration.cardinality !== undefined ? { cardinality: declaration.cardinality } : {},
|
|
83720
|
+
baseType: declaration.baseType,
|
|
83721
|
+
...declaration.normalMaximum !== undefined ? { normalMaximum: declaration.normalMaximum } : {},
|
|
83722
|
+
...declaration.normalMinimum !== undefined ? { normalMinimum: declaration.normalMinimum } : {},
|
|
83723
|
+
...declaration.defaultValue ? {
|
|
83724
|
+
defaultValue: declaration.defaultValue.value !== undefined ? { value: declaration.defaultValue.value } : {}
|
|
83725
|
+
} : {}
|
|
83726
|
+
}))
|
|
83727
|
+
} : {}
|
|
83728
|
+
};
|
|
83729
|
+
}
|
|
83730
|
+
function buildQtiTestUpdateInput(test, title) {
|
|
83731
|
+
return {
|
|
83732
|
+
title,
|
|
83733
|
+
...qtiTestOptionalAttributes(test),
|
|
83734
|
+
...test.metadata ? { metadata: test.metadata } : {},
|
|
83735
|
+
...buildQtiTestStructureInput(test)
|
|
83736
|
+
};
|
|
83737
|
+
}
|
|
83738
|
+
function buildQtiTestCopyInput(source, targetTestIdentifier, metadata2, itemCopies) {
|
|
83739
|
+
const copiesBySourceIdentifier = new Map(itemCopies.map((copy) => [copy.sourceIdentifier, copy]));
|
|
83740
|
+
return {
|
|
83741
|
+
identifier: targetTestIdentifier,
|
|
83742
|
+
title: `${source.title} (copy)`,
|
|
83743
|
+
...qtiTestOptionalAttributes(source),
|
|
83744
|
+
metadata: metadata2,
|
|
83745
|
+
...buildQtiTestStructureInput(source, targetTestIdentifier, copiesBySourceIdentifier)
|
|
83746
|
+
};
|
|
83747
|
+
}
|
|
83748
|
+
function validateUniqueQuestionIdentifiers(itemIdentifiers) {
|
|
83749
|
+
if (itemIdentifiers.length === 0 || new Set(itemIdentifiers).size !== itemIdentifiers.length) {
|
|
83750
|
+
throw new ValidationError("Question order must contain unique question identifiers");
|
|
83751
|
+
}
|
|
83752
|
+
}
|
|
83753
|
+
function resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers) {
|
|
83754
|
+
let selectedPart;
|
|
83755
|
+
let selectedSection;
|
|
83756
|
+
for (const part of qtiTestParts(test)) {
|
|
83757
|
+
for (const section of part["qti-assessment-section"]) {
|
|
83758
|
+
if (!itemIdentifier || section["qti-assessment-item-ref"]?.some((item) => item.identifier === itemIdentifier)) {
|
|
83759
|
+
selectedPart = part;
|
|
83760
|
+
selectedSection = section;
|
|
83761
|
+
break;
|
|
83762
|
+
}
|
|
83763
|
+
}
|
|
83764
|
+
if (selectedSection) {
|
|
83765
|
+
break;
|
|
83766
|
+
}
|
|
83767
|
+
}
|
|
83768
|
+
if (!selectedPart || !selectedSection) {
|
|
83769
|
+
throw new ValidationError(itemIdentifier ? `Question ${itemIdentifier} is not referenced by assessment ${qtiTestIdentifier}` : `Assessment ${qtiTestIdentifier} has no section for questions`);
|
|
83770
|
+
}
|
|
83771
|
+
if (expectedItemIdentifiers) {
|
|
83772
|
+
const sectionIdentifiers = selectedSection["qti-assessment-item-ref"]?.map((item) => item.identifier) ?? [];
|
|
83773
|
+
const expectedIdentifiers = new Set(expectedItemIdentifiers);
|
|
83774
|
+
if (expectedIdentifiers.size !== sectionIdentifiers.length || sectionIdentifiers.some((identifier) => !expectedIdentifiers.has(identifier))) {
|
|
83775
|
+
throw new ValidationError("Questions can only be reordered within one complete assessment section");
|
|
83776
|
+
}
|
|
83777
|
+
}
|
|
83778
|
+
return {
|
|
83779
|
+
partIdentifier: selectedPart.identifier,
|
|
83780
|
+
sectionIdentifier: selectedSection.identifier
|
|
83781
|
+
};
|
|
83782
|
+
}
|
|
83783
|
+
var PLAYCADEMY_QTI_AUTHORING_METADATA_KEY = "playcademyAuthoring", QTI_AUTHORING_FIELDS, QUESTION_XML_BUILDERS;
|
|
83784
|
+
var init_timeback_qti_authoring_util = __esm(() => {
|
|
83785
|
+
init_timeback3();
|
|
83786
|
+
init_errors();
|
|
83787
|
+
QTI_AUTHORING_FIELDS = [
|
|
83788
|
+
"type",
|
|
83789
|
+
"qtiVersion",
|
|
83790
|
+
"timeDependent",
|
|
83791
|
+
"adaptive",
|
|
83792
|
+
"preInteraction",
|
|
83793
|
+
"interaction",
|
|
83794
|
+
"postInteraction",
|
|
83795
|
+
"responseDeclarations",
|
|
83796
|
+
"outcomeDeclarations",
|
|
83797
|
+
"responseProcessing",
|
|
83798
|
+
"modalFeedback",
|
|
83799
|
+
"feedbackInline",
|
|
83800
|
+
"feedbackBlock",
|
|
83801
|
+
"rubrics",
|
|
83802
|
+
"stimulus",
|
|
83803
|
+
"content"
|
|
83804
|
+
];
|
|
83805
|
+
QUESTION_XML_BUILDERS = [
|
|
83806
|
+
buildNumericQuestionXml,
|
|
83807
|
+
buildHottextQuestionXml,
|
|
83808
|
+
buildInlineChoiceQuestionXml
|
|
83809
|
+
];
|
|
83810
|
+
});
|
|
83811
|
+
|
|
83812
|
+
// ../api-core/src/utils/timeback-qti-library.util.ts
|
|
83813
|
+
function newPlaycademyTestIdentifier() {
|
|
83814
|
+
return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
|
|
83815
|
+
}
|
|
83816
|
+
function playcademySourceWhere() {
|
|
83817
|
+
return {
|
|
83818
|
+
identifier: {
|
|
83819
|
+
gte: PLAYCADEMY_QTI_SOURCE_PREFIX,
|
|
83820
|
+
lt: PLAYCADEMY_QTI_SOURCE_UPPER_BOUND
|
|
83821
|
+
}
|
|
83822
|
+
};
|
|
83823
|
+
}
|
|
83824
|
+
function buildQtiLibraryListPlan(params) {
|
|
83825
|
+
const query = params.query?.trim() || undefined;
|
|
83826
|
+
if (!params.source && !query) {
|
|
83827
|
+
throw new Error("An exact QTI source identifier is required for a global lookup");
|
|
83828
|
+
}
|
|
83829
|
+
let where;
|
|
83830
|
+
if (params.source) {
|
|
83831
|
+
where = playcademySourceWhere();
|
|
83832
|
+
} else if (query) {
|
|
83833
|
+
where = { identifier: query };
|
|
83834
|
+
}
|
|
83835
|
+
return {
|
|
83836
|
+
kind: "direct",
|
|
83837
|
+
params: {
|
|
83838
|
+
...where ? { where } : {},
|
|
83839
|
+
...params.source && query ? { query } : {},
|
|
83840
|
+
page: params.page,
|
|
83841
|
+
limit: params.limit,
|
|
83842
|
+
...params.source ? { sort: "identifier", order: "asc" } : {}
|
|
83843
|
+
}
|
|
83844
|
+
};
|
|
83845
|
+
}
|
|
83846
|
+
var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-", PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
|
|
83847
|
+
|
|
82911
83848
|
// ../api-core/src/services/timeback-assessments.service.ts
|
|
82912
83849
|
class TimebackAssessmentsService {
|
|
83850
|
+
static QTI_HYDRATION_CONCURRENCY = 8;
|
|
82913
83851
|
deps;
|
|
82914
83852
|
constructor(deps) {
|
|
82915
83853
|
this.deps = deps;
|
|
@@ -82926,33 +83864,19 @@ class TimebackAssessmentsService {
|
|
|
82926
83864
|
}
|
|
82927
83865
|
async listAssessments(integrationId) {
|
|
82928
83866
|
const client = this.requireClient();
|
|
82929
|
-
await this.
|
|
83867
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
82930
83868
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
82931
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
82932
|
-
orderBy: asc(gameTimebackAssessmentTests.sortOrder)
|
|
83869
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
82933
83870
|
});
|
|
82934
|
-
|
|
82935
|
-
return [];
|
|
82936
|
-
}
|
|
82937
|
-
const assessments = await Promise.all(rows.map(async (row) => {
|
|
83871
|
+
const assessments = await runWithConcurrency(rows, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
82938
83872
|
try {
|
|
82939
83873
|
const test = await client.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
82940
|
-
let itemCount = 0;
|
|
82941
|
-
for (const part of test["qti-test-part"] ?? []) {
|
|
82942
|
-
for (const section of part["qti-assessment-section"] ?? []) {
|
|
82943
|
-
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
82944
|
-
}
|
|
82945
|
-
}
|
|
82946
83874
|
return {
|
|
82947
|
-
|
|
82948
|
-
integrationId: row.integrationId,
|
|
82949
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
82950
|
-
bankResourceId: row.bankResourceId,
|
|
82951
|
-
bankActive: row.bankActive,
|
|
82952
|
-
sortOrder: row.sortOrder,
|
|
83875
|
+
...this.associationSummary(row),
|
|
82953
83876
|
title: test.title,
|
|
82954
|
-
questionCount:
|
|
82955
|
-
|
|
83877
|
+
questionCount: countQtiTestItems(test),
|
|
83878
|
+
available: true,
|
|
83879
|
+
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
82956
83880
|
};
|
|
82957
83881
|
} catch (error88) {
|
|
82958
83882
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -82961,319 +83885,420 @@ class TimebackAssessmentsService {
|
|
|
82961
83885
|
"app.error.message": errorMessage(error88)
|
|
82962
83886
|
});
|
|
82963
83887
|
return {
|
|
82964
|
-
|
|
82965
|
-
integrationId: row.integrationId,
|
|
82966
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
82967
|
-
bankResourceId: row.bankResourceId,
|
|
82968
|
-
bankActive: row.bankActive,
|
|
82969
|
-
sortOrder: row.sortOrder,
|
|
83888
|
+
...this.associationSummary(row),
|
|
82970
83889
|
title: row.qtiTestIdentifier,
|
|
82971
83890
|
questionCount: 0,
|
|
82972
|
-
|
|
83891
|
+
available: false,
|
|
83892
|
+
editable: false
|
|
82973
83893
|
};
|
|
82974
83894
|
}
|
|
82975
|
-
})
|
|
82976
|
-
return assessments;
|
|
83895
|
+
});
|
|
83896
|
+
return assessments.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
82977
83897
|
}
|
|
82978
83898
|
async createAssessment(integrationId, input) {
|
|
82979
83899
|
const client = this.requireClient();
|
|
82980
|
-
const
|
|
82981
|
-
|
|
82982
|
-
courseId: integration.courseId,
|
|
82983
|
-
subject: integration.subject,
|
|
82984
|
-
grade: integration.grade
|
|
82985
|
-
});
|
|
83900
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
83901
|
+
const { integration } = ownership;
|
|
82986
83902
|
await client.course.createAssessmentTest({
|
|
82987
83903
|
identifier: input.qtiTestIdentifier,
|
|
82988
83904
|
title: input.title,
|
|
82989
83905
|
metadata: {
|
|
83906
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
83907
|
+
ownerGameSlug: ownership.gameSlug,
|
|
82990
83908
|
integrationId,
|
|
82991
83909
|
subject: integration.subject,
|
|
82992
83910
|
grade: String(integration.grade)
|
|
82993
83911
|
}
|
|
82994
83912
|
});
|
|
82995
|
-
const maxSortOrder = await this.getMaxSortOrder(integrationId);
|
|
82996
|
-
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
82997
|
-
integrationId,
|
|
82998
|
-
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
82999
|
-
sortOrder: maxSortOrder + 1
|
|
83000
|
-
}).returning();
|
|
83001
|
-
setAttribute("app.assessment.operation", "create");
|
|
83002
|
-
return row;
|
|
83003
|
-
}
|
|
83004
|
-
async deleteAssessment(integrationId, qtiTestIdentifier) {
|
|
83005
|
-
const client = this.requireClient();
|
|
83006
|
-
const row = await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
83007
|
-
if (row.bankActive) {
|
|
83008
|
-
await this.deactivateAssessment(integrationId, qtiTestIdentifier);
|
|
83009
|
-
}
|
|
83010
83913
|
try {
|
|
83011
|
-
await
|
|
83914
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
83915
|
+
integrationId,
|
|
83916
|
+
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
83917
|
+
purpose: input.purpose,
|
|
83918
|
+
status: "draft"
|
|
83919
|
+
}).returning();
|
|
83920
|
+
setAttribute("app.assessment.operation", "create");
|
|
83921
|
+
return row;
|
|
83012
83922
|
} catch (error88) {
|
|
83013
|
-
|
|
83014
|
-
|
|
83015
|
-
|
|
83016
|
-
|
|
83017
|
-
|
|
83923
|
+
let committedRow;
|
|
83924
|
+
try {
|
|
83925
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
83926
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, input.qtiTestIdentifier))
|
|
83927
|
+
});
|
|
83928
|
+
} catch (verificationError) {
|
|
83929
|
+
addEvent("assessment.qti_test_create_commit_verification_failed", {
|
|
83930
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
83931
|
+
"exception.type": errorType(verificationError),
|
|
83932
|
+
"app.error.message": errorMessage(verificationError)
|
|
83933
|
+
});
|
|
83934
|
+
throw error88;
|
|
83935
|
+
}
|
|
83936
|
+
if (committedRow) {
|
|
83937
|
+
setAttribute("app.assessment.operation", "create");
|
|
83938
|
+
return committedRow;
|
|
83939
|
+
}
|
|
83940
|
+
try {
|
|
83941
|
+
await client.qtiApi.assessmentTests.delete(input.qtiTestIdentifier);
|
|
83942
|
+
} catch (cleanupError) {
|
|
83943
|
+
if (!isApiError(cleanupError) || cleanupError.statusCode !== 404) {
|
|
83944
|
+
addEvent("assessment.qti_test_create_cleanup_failed", {
|
|
83945
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
83946
|
+
"exception.type": errorType(cleanupError),
|
|
83947
|
+
"app.error.message": errorMessage(cleanupError)
|
|
83948
|
+
});
|
|
83949
|
+
}
|
|
83950
|
+
}
|
|
83951
|
+
throw error88;
|
|
83018
83952
|
}
|
|
83019
|
-
await this.deps.db.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83020
|
-
setAttribute("app.assessment.operation", "delete");
|
|
83021
|
-
}
|
|
83022
|
-
async reorderAssessments(integrationId, identifiers) {
|
|
83023
|
-
await this.requireIntegration(integrationId);
|
|
83024
|
-
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)))));
|
|
83025
|
-
}
|
|
83026
|
-
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
83027
|
-
const client = this.requireClient();
|
|
83028
|
-
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
83029
|
-
return await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
83030
83953
|
}
|
|
83031
|
-
async
|
|
83032
|
-
|
|
83033
|
-
|
|
83034
|
-
|
|
83035
|
-
|
|
83036
|
-
|
|
83037
|
-
|
|
83038
|
-
|
|
83039
|
-
|
|
83040
|
-
|
|
83041
|
-
|
|
83954
|
+
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
83955
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
83956
|
+
const updates = buildAssessmentAssociationUpdates(row, input);
|
|
83957
|
+
if (input.status !== undefined) {
|
|
83958
|
+
validateAssessmentStatusTransition(row.status, input.status);
|
|
83959
|
+
if (isAssessmentPublicationTransition(row.status, input.status)) {
|
|
83960
|
+
await this.validateAssessmentHasQuestions(qtiTestIdentifier);
|
|
83961
|
+
}
|
|
83962
|
+
}
|
|
83963
|
+
if (input.title !== undefined) {
|
|
83964
|
+
assertDraftAssessment(row);
|
|
83965
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
83966
|
+
const client = this.requireClient();
|
|
83967
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
83968
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
83969
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
83970
|
+
await client.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
83971
|
+
}
|
|
83972
|
+
let updated = row;
|
|
83973
|
+
if (Object.keys(updates).length > 0) {
|
|
83974
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ ...updates, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
|
|
83975
|
+
updated = updatedRow ?? row;
|
|
83976
|
+
}
|
|
83977
|
+
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
83978
|
+
return updated;
|
|
83042
83979
|
});
|
|
83043
|
-
return item;
|
|
83044
83980
|
}
|
|
83045
|
-
async
|
|
83046
|
-
|
|
83047
|
-
|
|
83048
|
-
|
|
83049
|
-
|
|
83050
|
-
|
|
83051
|
-
|
|
83052
|
-
|
|
83053
|
-
|
|
83054
|
-
|
|
83981
|
+
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
83982
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
83983
|
+
const plan = planAssessmentRemoval(row.status);
|
|
83984
|
+
if (plan.kind === "delete") {
|
|
83985
|
+
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83986
|
+
} else if (plan.kind === "archive") {
|
|
83987
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", sortOrder: null, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83988
|
+
}
|
|
83989
|
+
if (plan.kind !== "none") {
|
|
83990
|
+
setAttribute("app.assessment.operation", plan.operation);
|
|
83991
|
+
}
|
|
83992
|
+
return { action: plan.action };
|
|
83993
|
+
});
|
|
83055
83994
|
}
|
|
83056
|
-
async
|
|
83057
|
-
|
|
83058
|
-
|
|
83059
|
-
const
|
|
83060
|
-
|
|
83061
|
-
|
|
83062
|
-
|
|
83063
|
-
|
|
83064
|
-
|
|
83065
|
-
|
|
83995
|
+
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
83996
|
+
await this.requireIntegration(integrationId);
|
|
83997
|
+
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
83998
|
+
const updatedAt = new Date;
|
|
83999
|
+
await this.deps.db.transaction(async (tx) => {
|
|
84000
|
+
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
84001
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
84002
|
+
});
|
|
84003
|
+
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
84004
|
+
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
84005
|
+
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
84006
|
+
const testIdentifier = row.qtiTestIdentifier;
|
|
84007
|
+
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 });
|
|
84008
|
+
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
84009
|
+
}
|
|
83066
84010
|
});
|
|
84011
|
+
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
83067
84012
|
}
|
|
83068
|
-
async
|
|
84013
|
+
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
83069
84014
|
const client = this.requireClient();
|
|
83070
|
-
|
|
83071
|
-
const
|
|
83072
|
-
|
|
83073
|
-
|
|
83074
|
-
|
|
83075
|
-
|
|
83076
|
-
const qtiTestUrl = `${client.getQtiBaseUrl()}/assessment-tests/${qtiTestIdentifier}`;
|
|
83077
|
-
await client.course.ensureAssessmentBank({
|
|
83078
|
-
courseId: integration.courseId,
|
|
83079
|
-
subject: integration.subject,
|
|
83080
|
-
grade: integration.grade
|
|
83081
|
-
});
|
|
83082
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
83083
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
83084
|
-
let childResourceId = row.bankResourceId;
|
|
83085
|
-
if (childResourceId) {
|
|
84015
|
+
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
84016
|
+
const { gameSlug } = await this.requireQtiTestOwnershipContext(integrationId);
|
|
84017
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84018
|
+
const ownerTests = new Map;
|
|
84019
|
+
const questions = await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
84020
|
+
let item;
|
|
83086
84021
|
try {
|
|
83087
|
-
await client.
|
|
83088
|
-
status: "active",
|
|
83089
|
-
title: `Assessment Bank Test: ${qtiTestIdentifier}`,
|
|
83090
|
-
vendorResourceId: "",
|
|
83091
|
-
metadata: {
|
|
83092
|
-
type: "qti",
|
|
83093
|
-
subType: "qti-test",
|
|
83094
|
-
url: qtiTestUrl
|
|
83095
|
-
}
|
|
83096
|
-
});
|
|
83097
|
-
if (!currentResources.includes(childResourceId)) {
|
|
83098
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83099
|
-
status: "active",
|
|
83100
|
-
title: parentResource.title,
|
|
83101
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83102
|
-
vendorId: parentResource.vendorId,
|
|
83103
|
-
metadata: {
|
|
83104
|
-
...parentResource.metadata,
|
|
83105
|
-
resources: [...currentResources, childResourceId]
|
|
83106
|
-
}
|
|
83107
|
-
});
|
|
83108
|
-
}
|
|
84022
|
+
item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
83109
84023
|
} catch (error88) {
|
|
83110
|
-
addEvent("assessment.
|
|
83111
|
-
"app.assessment.
|
|
84024
|
+
addEvent("assessment.qti_question_hydration_failed", {
|
|
84025
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84026
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
83112
84027
|
"exception.type": errorType(error88),
|
|
83113
84028
|
"app.error.message": errorMessage(error88)
|
|
83114
84029
|
});
|
|
83115
|
-
|
|
83116
|
-
}
|
|
83117
|
-
}
|
|
83118
|
-
if (!childResourceId) {
|
|
83119
|
-
for (const resourceId of currentResources) {
|
|
83120
|
-
try {
|
|
83121
|
-
const resource = await client.api.oneroster.resources.get(resourceId);
|
|
83122
|
-
const resourceUrl = resource.metadata?.url;
|
|
83123
|
-
if (resourceUrl === qtiTestUrl) {
|
|
83124
|
-
childResourceId = resourceId;
|
|
83125
|
-
addEvent("assessment.child_resource_reused", {
|
|
83126
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
83127
|
-
"app.assessment.child_resource_id": resourceId
|
|
83128
|
-
});
|
|
83129
|
-
break;
|
|
83130
|
-
}
|
|
83131
|
-
} catch {}
|
|
84030
|
+
return reference;
|
|
83132
84031
|
}
|
|
83133
|
-
|
|
83134
|
-
|
|
83135
|
-
|
|
83136
|
-
|
|
83137
|
-
|
|
83138
|
-
|
|
83139
|
-
|
|
83140
|
-
|
|
83141
|
-
|
|
83142
|
-
url: qtiTestUrl
|
|
83143
|
-
}
|
|
83144
|
-
});
|
|
83145
|
-
childResourceId = childResult.sourcedIdPairs.allocatedSourcedId;
|
|
83146
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83147
|
-
status: "active",
|
|
83148
|
-
title: parentResource.title,
|
|
83149
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83150
|
-
vendorId: parentResource.vendorId,
|
|
83151
|
-
metadata: {
|
|
83152
|
-
...parentResource.metadata,
|
|
83153
|
-
resources: [...currentResources, childResourceId]
|
|
83154
|
-
}
|
|
84032
|
+
let ownerTest;
|
|
84033
|
+
try {
|
|
84034
|
+
ownerTest = await this.qtiQuestionOwnerTest(client, item, ownerTests);
|
|
84035
|
+
} catch (error88) {
|
|
84036
|
+
addEvent("assessment.qti_question_owner_test_fetch_failed", {
|
|
84037
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84038
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
84039
|
+
"exception.type": errorType(error88),
|
|
84040
|
+
"app.error.message": errorMessage(error88)
|
|
83155
84041
|
});
|
|
83156
84042
|
}
|
|
83157
|
-
|
|
83158
|
-
|
|
83159
|
-
|
|
83160
|
-
|
|
83161
|
-
|
|
84043
|
+
return buildHydratedQtiQuestionReference({
|
|
84044
|
+
reference,
|
|
84045
|
+
item,
|
|
84046
|
+
gameSlug,
|
|
84047
|
+
ownerTest,
|
|
84048
|
+
testIdentifier: qtiTestIdentifier
|
|
84049
|
+
});
|
|
83162
84050
|
});
|
|
84051
|
+
return { ...result, questions };
|
|
83163
84052
|
}
|
|
83164
|
-
async
|
|
84053
|
+
async listQuestionLibrary(integrationId, params) {
|
|
83165
84054
|
const client = this.requireClient();
|
|
83166
|
-
|
|
83167
|
-
|
|
83168
|
-
if (!row.bankActive) {
|
|
83169
|
-
throw new ValidationError("Assessment is already in draft");
|
|
83170
|
-
}
|
|
83171
|
-
if (!row.bankResourceId) {
|
|
83172
|
-
throw new ValidationError("Assessment has no bank resource — activate it first");
|
|
83173
|
-
}
|
|
83174
|
-
const childResourceId = row.bankResourceId;
|
|
83175
|
-
const bankIds = deriveAssessmentBankIds2(integration.courseId);
|
|
83176
|
-
try {
|
|
83177
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
83178
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
83179
|
-
const filtered = currentResources.filter((id) => id !== childResourceId);
|
|
83180
|
-
if (filtered.length !== currentResources.length) {
|
|
83181
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
83182
|
-
status: "active",
|
|
83183
|
-
title: parentResource.title,
|
|
83184
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
83185
|
-
vendorId: parentResource.vendorId,
|
|
83186
|
-
metadata: {
|
|
83187
|
-
...parentResource.metadata,
|
|
83188
|
-
resources: filtered
|
|
83189
|
-
}
|
|
83190
|
-
});
|
|
83191
|
-
}
|
|
83192
|
-
} catch (error88) {
|
|
83193
|
-
addEvent("assessment.parent_resource_update_failed", {
|
|
83194
|
-
"app.assessment.bank_resource_id": bankIds.resource,
|
|
83195
|
-
"exception.type": errorType(error88),
|
|
83196
|
-
"app.error.message": errorMessage(error88)
|
|
83197
|
-
});
|
|
83198
|
-
}
|
|
83199
|
-
try {
|
|
83200
|
-
await client.api.oneroster.resources.delete(childResourceId);
|
|
83201
|
-
} catch (error88) {
|
|
83202
|
-
addEvent("assessment.child_resource_delete_failed", {
|
|
83203
|
-
"app.assessment.child_resource_id": childResourceId,
|
|
83204
|
-
"exception.type": errorType(error88),
|
|
83205
|
-
"app.error.message": errorMessage(error88)
|
|
83206
|
-
});
|
|
83207
|
-
}
|
|
83208
|
-
await this.deps.db.update(gameTimebackAssessmentTests).set({ bankActive: false }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
83209
|
-
setAttribute("app.assessment.operation", "deactivate");
|
|
83210
|
-
}
|
|
83211
|
-
isAssessmentActive(row) {
|
|
83212
|
-
return row.bankActive;
|
|
84055
|
+
await this.requireIntegration(integrationId);
|
|
84056
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentItems.list(listParams));
|
|
83213
84057
|
}
|
|
83214
|
-
async
|
|
83215
|
-
const
|
|
83216
|
-
|
|
83217
|
-
|
|
83218
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
83219
|
-
});
|
|
83220
|
-
const activeCount = rows.filter((r) => r.bankActive).length;
|
|
83221
|
-
return {
|
|
83222
|
-
bankIds,
|
|
83223
|
-
totalAssessments: rows.length,
|
|
83224
|
-
activeAssessments: activeCount,
|
|
83225
|
-
draftAssessments: rows.length - activeCount
|
|
83226
|
-
};
|
|
84058
|
+
async listTestLibrary(integrationId, params) {
|
|
84059
|
+
const client = this.requireClient();
|
|
84060
|
+
await this.requireIntegration(integrationId);
|
|
84061
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentTests.list(listParams));
|
|
83227
84062
|
}
|
|
83228
|
-
async
|
|
84063
|
+
async copyAssessment(integrationId, sourceTestIdentifier, targetTestIdentifier, purpose) {
|
|
83229
84064
|
const client = this.requireClient();
|
|
83230
|
-
const
|
|
83231
|
-
const
|
|
83232
|
-
const
|
|
83233
|
-
|
|
84065
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
84066
|
+
const { integration } = ownership;
|
|
84067
|
+
const source = await client.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
84068
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => this.qtiItemHref(client, identifier));
|
|
84069
|
+
const itemCopies = await runWithConcurrency(itemPlan, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
84070
|
+
const sourceItem = await client.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
84071
|
+
return {
|
|
84072
|
+
plan,
|
|
84073
|
+
input: buildQtiQuestionCopyInput({
|
|
84074
|
+
source: sourceItem,
|
|
84075
|
+
targetIdentifier: plan.targetIdentifier,
|
|
84076
|
+
targetTestIdentifier,
|
|
84077
|
+
gameSlug: ownership.gameSlug,
|
|
84078
|
+
integrationId
|
|
84079
|
+
})
|
|
84080
|
+
};
|
|
83234
84081
|
});
|
|
84082
|
+
const testInput = buildQtiTestCopyInput(source, targetTestIdentifier, {
|
|
84083
|
+
...source.metadata,
|
|
84084
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
84085
|
+
ownerGameSlug: ownership.gameSlug,
|
|
84086
|
+
integrationId,
|
|
84087
|
+
subject: integration.subject,
|
|
84088
|
+
grade: String(integration.grade),
|
|
84089
|
+
copiedFromTestIdentifier: sourceTestIdentifier
|
|
84090
|
+
}, itemPlan);
|
|
84091
|
+
const attemptedItemIdentifiers = [];
|
|
84092
|
+
let testCreationAttempted = false;
|
|
84093
|
+
let associationCreationAttempted = false;
|
|
83235
84094
|
try {
|
|
83236
|
-
|
|
84095
|
+
for (const copy of itemCopies) {
|
|
84096
|
+
attemptedItemIdentifiers.push(copy.plan.targetIdentifier);
|
|
84097
|
+
await client.course.createAssessmentItemXml({
|
|
84098
|
+
xml: copy.input.xml,
|
|
84099
|
+
metadata: copy.input.metadata
|
|
84100
|
+
});
|
|
84101
|
+
}
|
|
84102
|
+
testCreationAttempted = true;
|
|
84103
|
+
await client.qtiApi.assessmentTests.create(testInput);
|
|
84104
|
+
associationCreationAttempted = true;
|
|
84105
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
84106
|
+
integrationId,
|
|
84107
|
+
qtiTestIdentifier: targetTestIdentifier,
|
|
84108
|
+
purpose,
|
|
84109
|
+
status: "draft"
|
|
84110
|
+
}).returning();
|
|
84111
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
84112
|
+
return row;
|
|
83237
84113
|
} catch (error88) {
|
|
83238
|
-
|
|
83239
|
-
|
|
83240
|
-
"exception.type": errorType(error88),
|
|
83241
|
-
"app.error.message": errorMessage(error88)
|
|
83242
|
-
});
|
|
83243
|
-
}
|
|
83244
|
-
for (const row of activeRows) {
|
|
83245
|
-
if (row.bankResourceId) {
|
|
84114
|
+
if (associationCreationAttempted) {
|
|
84115
|
+
let committedRow;
|
|
83246
84116
|
try {
|
|
83247
|
-
await
|
|
83248
|
-
|
|
83249
|
-
addEvent("assessment.child_resource_delete_failed", {
|
|
83250
|
-
"app.assessment.child_resource_id": row.bankResourceId,
|
|
83251
|
-
"exception.type": errorType(error88),
|
|
83252
|
-
"app.error.message": errorMessage(error88)
|
|
84117
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
84118
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, targetTestIdentifier))
|
|
83253
84119
|
});
|
|
84120
|
+
} catch (verificationError) {
|
|
84121
|
+
addEvent("assessment.qti_test_copy_commit_verification_failed", {
|
|
84122
|
+
"app.assessment.qti_test_identifier": targetTestIdentifier,
|
|
84123
|
+
"exception.type": errorType(verificationError),
|
|
84124
|
+
"app.error.message": errorMessage(verificationError)
|
|
84125
|
+
});
|
|
84126
|
+
throw error88;
|
|
84127
|
+
}
|
|
84128
|
+
if (committedRow) {
|
|
84129
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
84130
|
+
return committedRow;
|
|
83254
84131
|
}
|
|
83255
84132
|
}
|
|
84133
|
+
await this.cleanupQtiAssessmentCopy(client, testCreationAttempted ? targetTestIdentifier : undefined, attemptedItemIdentifiers);
|
|
84134
|
+
throw error88;
|
|
83256
84135
|
}
|
|
83257
|
-
|
|
83258
|
-
|
|
83259
|
-
|
|
83260
|
-
|
|
83261
|
-
|
|
83262
|
-
|
|
83263
|
-
|
|
84136
|
+
}
|
|
84137
|
+
async createQuestion(integrationId, qtiTestIdentifier, input) {
|
|
84138
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84139
|
+
const client = this.requireClient();
|
|
84140
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84141
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84142
|
+
const creation = parseQtiQuestionCreationInput(input);
|
|
84143
|
+
const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
|
|
84144
|
+
let title;
|
|
84145
|
+
let metadata2;
|
|
84146
|
+
let xml;
|
|
84147
|
+
let structuredInput;
|
|
84148
|
+
let fallbackItem;
|
|
84149
|
+
if (creation.kind === "copy") {
|
|
84150
|
+
const source = await client.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
|
|
84151
|
+
const copy = buildQtiQuestionCopyInput({
|
|
84152
|
+
source,
|
|
84153
|
+
targetIdentifier: itemIdentifier,
|
|
84154
|
+
targetTestIdentifier: qtiTestIdentifier,
|
|
84155
|
+
gameSlug: ownership.gameSlug,
|
|
84156
|
+
integrationId
|
|
84157
|
+
});
|
|
84158
|
+
title = copy.title;
|
|
84159
|
+
metadata2 = copy.metadata;
|
|
84160
|
+
xml = copy.xml;
|
|
84161
|
+
fallbackItem = {
|
|
84162
|
+
...source,
|
|
84163
|
+
identifier: itemIdentifier,
|
|
84164
|
+
title,
|
|
84165
|
+
rawXml: xml,
|
|
84166
|
+
metadata: metadata2
|
|
84167
|
+
};
|
|
84168
|
+
} else {
|
|
84169
|
+
structuredInput = creation.input;
|
|
84170
|
+
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
84171
|
+
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
84172
|
+
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
84173
|
+
gameSlug: ownership.gameSlug,
|
|
84174
|
+
testIdentifier: qtiTestIdentifier
|
|
84175
|
+
});
|
|
84176
|
+
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
84177
|
+
fallbackItem = {
|
|
84178
|
+
...structuredInput,
|
|
84179
|
+
identifier: itemIdentifier,
|
|
84180
|
+
title,
|
|
84181
|
+
...xml ? { rawXml: xml } : {}
|
|
84182
|
+
};
|
|
84183
|
+
}
|
|
84184
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
84185
|
+
const href = this.qtiItemHref(client, itemIdentifier);
|
|
84186
|
+
let item;
|
|
84187
|
+
let itemCreationAttempted = false;
|
|
84188
|
+
let referenceCreationAttempted = false;
|
|
84189
|
+
try {
|
|
84190
|
+
itemCreationAttempted = true;
|
|
84191
|
+
item = xml ? await client.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.create({
|
|
84192
|
+
...structuredInput,
|
|
84193
|
+
identifier: itemIdentifier,
|
|
84194
|
+
metadata: metadata2
|
|
84195
|
+
});
|
|
84196
|
+
referenceCreationAttempted = true;
|
|
84197
|
+
await section.items.add({
|
|
84198
|
+
identifier: itemIdentifier,
|
|
84199
|
+
href
|
|
84200
|
+
});
|
|
84201
|
+
} catch (error88) {
|
|
84202
|
+
let referenceRemoved = !referenceCreationAttempted;
|
|
84203
|
+
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
84204
|
+
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
84205
|
+
if (referenceCreationAttempted) {
|
|
84206
|
+
try {
|
|
84207
|
+
await section.items.remove(itemIdentifier);
|
|
84208
|
+
referenceRemoved = true;
|
|
84209
|
+
} catch (cleanupError) {
|
|
84210
|
+
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
84211
|
+
addEvent("assessment.qti_question_reference_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
|
+
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
84220
|
+
try {
|
|
84221
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
84222
|
+
} catch (cleanupError) {
|
|
84223
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
84224
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
84225
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
84226
|
+
"exception.type": errorType(cleanupError),
|
|
84227
|
+
"app.error.message": errorMessage(cleanupError)
|
|
84228
|
+
});
|
|
84229
|
+
}
|
|
84230
|
+
}
|
|
84231
|
+
throw error88;
|
|
84232
|
+
}
|
|
84233
|
+
setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
|
|
84234
|
+
return buildCreatedQtiQuestionReference({
|
|
84235
|
+
item,
|
|
84236
|
+
fallbackItem,
|
|
84237
|
+
itemIdentifier,
|
|
84238
|
+
href,
|
|
84239
|
+
partIdentifier: section.partIdentifier,
|
|
84240
|
+
sectionIdentifier: section.sectionIdentifier,
|
|
84241
|
+
metadata: metadata2
|
|
83264
84242
|
});
|
|
83265
|
-
}
|
|
83266
|
-
|
|
83267
|
-
|
|
83268
|
-
|
|
83269
|
-
|
|
83270
|
-
|
|
83271
|
-
|
|
83272
|
-
|
|
84243
|
+
});
|
|
84244
|
+
}
|
|
84245
|
+
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
84246
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84247
|
+
const client = this.requireClient();
|
|
84248
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84249
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84250
|
+
const [, item] = await Promise.all([
|
|
84251
|
+
this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test),
|
|
84252
|
+
client.qtiApi.assessmentItems.get(itemIdentifier)
|
|
84253
|
+
]);
|
|
84254
|
+
const ownerTest = await this.qtiQuestionOwnerTest(client, item, undefined, ownership.test);
|
|
84255
|
+
const replacesInteraction = "type" in input || "interaction" in input || "numericTextEntry" in input;
|
|
84256
|
+
assertQtiQuestionEditable(item, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, ownerTest);
|
|
84257
|
+
assertSupportedQtiQuestionInteraction(replacesInteraction ? input : item);
|
|
84258
|
+
let title = typeof input.title === "string" ? input.title.trim() : "";
|
|
84259
|
+
if (!title) {
|
|
84260
|
+
title = item.title;
|
|
84261
|
+
}
|
|
84262
|
+
const metadata2 = buildOwnedQtiQuestionMetadata(input, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, item.metadata);
|
|
84263
|
+
const xml = buildQuestionXml(input, itemIdentifier, title);
|
|
84264
|
+
const updated = xml ? await client.course.updateAssessmentItemXml(itemIdentifier, { xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.update(itemIdentifier, {
|
|
84265
|
+
...input,
|
|
84266
|
+
title,
|
|
84267
|
+
metadata: metadata2
|
|
84268
|
+
});
|
|
84269
|
+
setAttribute("app.assessment.operation", "update_question");
|
|
84270
|
+
return updated;
|
|
84271
|
+
});
|
|
84272
|
+
}
|
|
84273
|
+
async removeQuestion(integrationId, qtiTestIdentifier, itemIdentifier) {
|
|
84274
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84275
|
+
const client = this.requireClient();
|
|
84276
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84277
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84278
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test);
|
|
84279
|
+
await section.items.remove(itemIdentifier);
|
|
84280
|
+
setAttribute("app.assessment.operation", "remove_question");
|
|
84281
|
+
});
|
|
84282
|
+
}
|
|
84283
|
+
async reorderQuestions(integrationId, qtiTestIdentifier, items) {
|
|
84284
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
84285
|
+
const client = this.requireClient();
|
|
84286
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
84287
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
84288
|
+
const identifiers = items.map((item) => item.identifier);
|
|
84289
|
+
validateUniqueQuestionIdentifiers(identifiers);
|
|
84290
|
+
const firstIdentifier = identifiers[0];
|
|
84291
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, firstIdentifier, identifiers, ownership.test);
|
|
84292
|
+
const result = await section.items.reorder({
|
|
84293
|
+
items: items.map((item) => ({
|
|
84294
|
+
identifier: item.identifier,
|
|
84295
|
+
href: item.href ?? this.qtiItemHref(client, item.identifier),
|
|
84296
|
+
sequence: item.sequence
|
|
84297
|
+
}))
|
|
83273
84298
|
});
|
|
83274
|
-
|
|
83275
|
-
|
|
83276
|
-
|
|
84299
|
+
setAttribute("app.assessment.operation", "reorder_questions");
|
|
84300
|
+
return result;
|
|
84301
|
+
});
|
|
83277
84302
|
}
|
|
83278
84303
|
requireClient() {
|
|
83279
84304
|
if (!this.deps.timeback) {
|
|
@@ -83281,8 +84306,18 @@ class TimebackAssessmentsService {
|
|
|
83281
84306
|
}
|
|
83282
84307
|
return this.deps.timeback;
|
|
83283
84308
|
}
|
|
83284
|
-
async
|
|
83285
|
-
|
|
84309
|
+
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
84310
|
+
return this.deps.db.transaction(async (tx) => {
|
|
84311
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
84312
|
+
const row = rows.find((candidate) => candidate.integrationId === integrationId);
|
|
84313
|
+
if (!row) {
|
|
84314
|
+
throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
|
|
84315
|
+
}
|
|
84316
|
+
return action(row, tx, rows);
|
|
84317
|
+
});
|
|
84318
|
+
}
|
|
84319
|
+
async requireIntegration(integrationId, db2 = this.deps.db) {
|
|
84320
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
83286
84321
|
where: and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())
|
|
83287
84322
|
});
|
|
83288
84323
|
if (!integration) {
|
|
@@ -83290,6 +84325,17 @@ class TimebackAssessmentsService {
|
|
|
83290
84325
|
}
|
|
83291
84326
|
return integration;
|
|
83292
84327
|
}
|
|
84328
|
+
async requireQtiTestOwnershipContext(integrationId, db2 = this.deps.db) {
|
|
84329
|
+
const integration = await this.requireIntegration(integrationId, db2);
|
|
84330
|
+
const game2 = await db2.query.games.findFirst({
|
|
84331
|
+
where: eq(games.id, integration.gameId),
|
|
84332
|
+
columns: { slug: true }
|
|
84333
|
+
});
|
|
84334
|
+
if (!game2) {
|
|
84335
|
+
throw new NotFoundError(`Game not found for integration: ${integrationId}`);
|
|
84336
|
+
}
|
|
84337
|
+
return { integration, gameSlug: game2.slug };
|
|
84338
|
+
}
|
|
83293
84339
|
async requireAssessmentRow(integrationId, qtiTestIdentifier) {
|
|
83294
84340
|
const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
83295
84341
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
|
|
@@ -83299,27 +84345,125 @@ class TimebackAssessmentsService {
|
|
|
83299
84345
|
}
|
|
83300
84346
|
return row;
|
|
83301
84347
|
}
|
|
83302
|
-
|
|
83303
|
-
|
|
84348
|
+
async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
|
|
84349
|
+
const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
84350
|
+
assertDraftAssessment(row);
|
|
84351
|
+
return row;
|
|
83304
84352
|
}
|
|
83305
|
-
async
|
|
83306
|
-
const
|
|
83307
|
-
|
|
83308
|
-
|
|
84353
|
+
async requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, lockedRow, db2 = this.deps.db) {
|
|
84354
|
+
const client = this.requireClient();
|
|
84355
|
+
await this.requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow);
|
|
84356
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
84357
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, db2);
|
|
84358
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
84359
|
+
return { ...ownership, test };
|
|
84360
|
+
}
|
|
84361
|
+
async qtiQuestionOwnerTest(client, item, cache, knownTest) {
|
|
84362
|
+
if (item.metadata && "ownerGameSlug" in item.metadata) {
|
|
84363
|
+
return;
|
|
84364
|
+
}
|
|
84365
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
84366
|
+
if (!ownerTestIdentifier) {
|
|
84367
|
+
return;
|
|
84368
|
+
}
|
|
84369
|
+
if (knownTest?.identifier === ownerTestIdentifier) {
|
|
84370
|
+
return knownTest;
|
|
84371
|
+
}
|
|
84372
|
+
function loadOwnerTest(identifier) {
|
|
84373
|
+
return client.qtiApi.assessmentTests.get(identifier);
|
|
84374
|
+
}
|
|
84375
|
+
if (!cache) {
|
|
84376
|
+
return loadOwnerTest(ownerTestIdentifier);
|
|
84377
|
+
}
|
|
84378
|
+
let ownerTest = cache.get(ownerTestIdentifier);
|
|
84379
|
+
if (!ownerTest) {
|
|
84380
|
+
ownerTest = loadOwnerTest(ownerTestIdentifier);
|
|
84381
|
+
cache.set(ownerTestIdentifier, ownerTest);
|
|
84382
|
+
}
|
|
84383
|
+
return ownerTest;
|
|
84384
|
+
}
|
|
84385
|
+
async assertQtiTestQuestionsSupported(client, qtiTestIdentifier, questions) {
|
|
84386
|
+
const result = questions ?? await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84387
|
+
await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
84388
|
+
const item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
84389
|
+
assertSupportedQtiQuestionInteraction(item);
|
|
83309
84390
|
});
|
|
83310
|
-
|
|
83311
|
-
|
|
84391
|
+
}
|
|
84392
|
+
async qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers, knownTest) {
|
|
84393
|
+
const test = knownTest ?? await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
84394
|
+
const section = resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers);
|
|
84395
|
+
return {
|
|
84396
|
+
...section,
|
|
84397
|
+
items: client.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
84398
|
+
};
|
|
84399
|
+
}
|
|
84400
|
+
qtiItemHref(client, itemIdentifier) {
|
|
84401
|
+
return `${client.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
84402
|
+
}
|
|
84403
|
+
async cleanupQtiAssessmentCopy(client, testIdentifier, itemIdentifiers) {
|
|
84404
|
+
let testDeleted = !testIdentifier;
|
|
84405
|
+
if (testIdentifier) {
|
|
84406
|
+
try {
|
|
84407
|
+
await client.qtiApi.assessmentTests.delete(testIdentifier);
|
|
84408
|
+
testDeleted = true;
|
|
84409
|
+
} catch (error88) {
|
|
84410
|
+
testDeleted = isApiError(error88) && error88.statusCode === 404;
|
|
84411
|
+
addEvent("assessment.qti_test_copy_cleanup_failed", {
|
|
84412
|
+
"app.assessment.qti_test_identifier": testIdentifier,
|
|
84413
|
+
"exception.type": errorType(error88),
|
|
84414
|
+
"app.error.message": errorMessage(error88)
|
|
84415
|
+
});
|
|
84416
|
+
}
|
|
84417
|
+
}
|
|
84418
|
+
if (!testDeleted) {
|
|
84419
|
+
return;
|
|
83312
84420
|
}
|
|
83313
|
-
|
|
84421
|
+
for (const itemIdentifier of itemIdentifiers.toReversed()) {
|
|
84422
|
+
try {
|
|
84423
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
84424
|
+
} catch (error88) {
|
|
84425
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
84426
|
+
...testIdentifier ? { "app.assessment.qti_test_identifier": testIdentifier } : {},
|
|
84427
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
84428
|
+
"exception.type": errorType(error88),
|
|
84429
|
+
"app.error.message": errorMessage(error88)
|
|
84430
|
+
});
|
|
84431
|
+
}
|
|
84432
|
+
}
|
|
84433
|
+
}
|
|
84434
|
+
associationSummary(row) {
|
|
84435
|
+
return {
|
|
84436
|
+
id: row.id,
|
|
84437
|
+
integrationId: row.integrationId,
|
|
84438
|
+
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
84439
|
+
purpose: row.purpose,
|
|
84440
|
+
status: row.status,
|
|
84441
|
+
sortOrder: row.sortOrder,
|
|
84442
|
+
createdAt: row.createdAt,
|
|
84443
|
+
updatedAt: row.updatedAt
|
|
84444
|
+
};
|
|
84445
|
+
}
|
|
84446
|
+
async listQtiLibrary(params, list) {
|
|
84447
|
+
const plan = buildQtiLibraryListPlan(params);
|
|
84448
|
+
return list(plan.params);
|
|
84449
|
+
}
|
|
84450
|
+
async validateAssessmentHasQuestions(qtiTestIdentifier) {
|
|
84451
|
+
const client = this.requireClient();
|
|
84452
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
84453
|
+
assertAssessmentHasQuestions(result.questions);
|
|
84454
|
+
await this.assertQtiTestQuestionsSupported(client, qtiTestIdentifier, result);
|
|
83314
84455
|
}
|
|
83315
84456
|
}
|
|
83316
|
-
var init_timeback_assessments_service = __esm(() => {
|
|
84457
|
+
var init_timeback_assessments_service = __esm(async () => {
|
|
83317
84458
|
init_drizzle_orm();
|
|
83318
84459
|
init_helpers_index();
|
|
83319
84460
|
init_tables_index();
|
|
83320
84461
|
init_spans();
|
|
83321
|
-
|
|
84462
|
+
init_timeback3();
|
|
83322
84463
|
init_errors();
|
|
84464
|
+
init_timeback_assessment_rules_util();
|
|
84465
|
+
init_timeback_qti_authoring_util();
|
|
84466
|
+
await init_errors8();
|
|
83323
84467
|
});
|
|
83324
84468
|
|
|
83325
84469
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
@@ -85412,10 +86556,10 @@ var init_platform2 = __esm(async () => {
|
|
|
85412
86556
|
init_kv_service();
|
|
85413
86557
|
init_secrets_service();
|
|
85414
86558
|
init_seed_service();
|
|
85415
|
-
init_timeback_assessments_service();
|
|
85416
86559
|
init_upload_service();
|
|
85417
86560
|
await __promiseAll([
|
|
85418
86561
|
init_timeback_admin_service(),
|
|
86562
|
+
init_timeback_assessments_service(),
|
|
85419
86563
|
init_timeback_service()
|
|
85420
86564
|
]);
|
|
85421
86565
|
});
|
|
@@ -142034,16 +143178,6 @@ function requireAnonymous(handler) {
|
|
|
142034
143178
|
return handler(ctx);
|
|
142035
143179
|
};
|
|
142036
143180
|
}
|
|
142037
|
-
function requireAdmin(handler) {
|
|
142038
|
-
return async (ctx) => {
|
|
142039
|
-
assertAuthenticatedRequest(ctx);
|
|
142040
|
-
rejectDashboardWorkerKey(ctx);
|
|
142041
|
-
if (ctx.user.role !== "admin") {
|
|
142042
|
-
throw ApiError.forbidden("Admin access required");
|
|
142043
|
-
}
|
|
142044
|
-
return handler(ctx);
|
|
142045
|
-
};
|
|
142046
|
-
}
|
|
142047
143181
|
function requireDeveloper(handler) {
|
|
142048
143182
|
return async (ctx) => {
|
|
142049
143183
|
assertAuthenticatedRequest(ctx);
|
|
@@ -143207,7 +144341,27 @@ var init_session_controller = __esm(() => {
|
|
|
143207
144341
|
});
|
|
143208
144342
|
|
|
143209
144343
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
143210
|
-
|
|
144344
|
+
function parseQtiLibraryParams(searchParams) {
|
|
144345
|
+
const requestedPage = Number(searchParams.get("page"));
|
|
144346
|
+
const requestedLimit = Number(searchParams.get("limit"));
|
|
144347
|
+
const page = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
|
144348
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 50) : 20;
|
|
144349
|
+
const query = searchParams.get("q")?.trim() || undefined;
|
|
144350
|
+
const requestedSource = searchParams.get("source");
|
|
144351
|
+
if (requestedSource && requestedSource !== "playcademy") {
|
|
144352
|
+
throw ApiError.badRequest("Unsupported QTI library source");
|
|
144353
|
+
}
|
|
144354
|
+
if (requestedSource !== "playcademy" && !query) {
|
|
144355
|
+
throw ApiError.badRequest("An exact QTI source identifier is required for a global lookup");
|
|
144356
|
+
}
|
|
144357
|
+
return {
|
|
144358
|
+
query,
|
|
144359
|
+
source: requestedSource === "playcademy" ? "playcademy" : undefined,
|
|
144360
|
+
page,
|
|
144361
|
+
limit
|
|
144362
|
+
};
|
|
144363
|
+
}
|
|
144364
|
+
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;
|
|
143211
144365
|
var init_timeback_controller = __esm(() => {
|
|
143212
144366
|
init_esm();
|
|
143213
144367
|
init_schemas_index();
|
|
@@ -143722,7 +144876,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
143722
144876
|
const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
|
|
143723
144877
|
return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
|
|
143724
144878
|
});
|
|
143725
|
-
listAssessments =
|
|
144879
|
+
listAssessments = requireDeveloper(async (ctx) => {
|
|
143726
144880
|
const { gameId, courseId } = ctx.params;
|
|
143727
144881
|
if (!gameId || !courseId) {
|
|
143728
144882
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
@@ -143730,40 +144884,40 @@ var init_timeback_controller = __esm(() => {
|
|
|
143730
144884
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143731
144885
|
return ctx.services.timebackAssessments.listAssessments(integrationId);
|
|
143732
144886
|
});
|
|
143733
|
-
createAssessment =
|
|
144887
|
+
createAssessment = requireDeveloper(async (ctx) => {
|
|
143734
144888
|
const { gameId, courseId } = ctx.params;
|
|
143735
144889
|
if (!gameId || !courseId) {
|
|
143736
144890
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143737
144891
|
}
|
|
143738
144892
|
const body2 = await parseRequestBody(ctx.request, CreateAssessmentRequestSchema);
|
|
143739
144893
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143740
|
-
const
|
|
143741
|
-
const qtiTestIdentifier = `assessment-${shortId}`;
|
|
144894
|
+
const qtiTestIdentifier = newPlaycademyTestIdentifier();
|
|
143742
144895
|
return ctx.services.timebackAssessments.createAssessment(integrationId, {
|
|
143743
144896
|
title: body2.title,
|
|
144897
|
+
purpose: body2.purpose,
|
|
143744
144898
|
qtiTestIdentifier
|
|
143745
144899
|
});
|
|
143746
144900
|
});
|
|
143747
|
-
|
|
144901
|
+
updateAssessment = requireDeveloper(async (ctx) => {
|
|
143748
144902
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143749
144903
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143750
144904
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143751
144905
|
}
|
|
143752
144906
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143753
|
-
await ctx.
|
|
143754
|
-
return
|
|
144907
|
+
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
144908
|
+
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
143755
144909
|
});
|
|
143756
|
-
reorderAssessments =
|
|
144910
|
+
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
143757
144911
|
const { gameId, courseId } = ctx.params;
|
|
143758
144912
|
if (!gameId || !courseId) {
|
|
143759
144913
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143760
144914
|
}
|
|
143761
144915
|
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
143762
144916
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143763
|
-
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.
|
|
144917
|
+
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.purpose, body2.testIdentifiers);
|
|
143764
144918
|
return { success: true };
|
|
143765
144919
|
});
|
|
143766
|
-
reorderQuestions =
|
|
144920
|
+
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
143767
144921
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143768
144922
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143769
144923
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -143773,33 +144927,51 @@ var init_timeback_controller = __esm(() => {
|
|
|
143773
144927
|
await ctx.services.timebackAssessments.reorderQuestions(integrationId, testIdentifier, body2.items);
|
|
143774
144928
|
return { success: true };
|
|
143775
144929
|
});
|
|
143776
|
-
|
|
144930
|
+
removeAssessment = requireDeveloper(async (ctx) => {
|
|
143777
144931
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143778
144932
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143779
144933
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143780
144934
|
}
|
|
143781
144935
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143782
|
-
|
|
143783
|
-
return { success: true };
|
|
144936
|
+
return ctx.services.timebackAssessments.removeAssessment(integrationId, testIdentifier);
|
|
143784
144937
|
});
|
|
143785
|
-
|
|
144938
|
+
listQuestions = requireDeveloper(async (ctx) => {
|
|
143786
144939
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143787
144940
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143788
144941
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
143789
144942
|
}
|
|
143790
144943
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143791
|
-
|
|
143792
|
-
return { success: true };
|
|
144944
|
+
return ctx.services.timebackAssessments.listQuestions(integrationId, testIdentifier);
|
|
143793
144945
|
});
|
|
143794
|
-
|
|
143795
|
-
const { gameId, courseId
|
|
143796
|
-
if (!gameId || !courseId
|
|
143797
|
-
throw ApiError.badRequest("Missing gameId
|
|
144946
|
+
listQuestionLibrary = requireDeveloper(async (ctx) => {
|
|
144947
|
+
const { gameId, courseId } = ctx.params;
|
|
144948
|
+
if (!gameId || !courseId) {
|
|
144949
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143798
144950
|
}
|
|
144951
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
143799
144952
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143800
|
-
return ctx.services.timebackAssessments.
|
|
144953
|
+
return ctx.services.timebackAssessments.listQuestionLibrary(integrationId, params);
|
|
144954
|
+
});
|
|
144955
|
+
listTestLibrary = requireDeveloper(async (ctx) => {
|
|
144956
|
+
const { gameId, courseId } = ctx.params;
|
|
144957
|
+
if (!gameId || !courseId) {
|
|
144958
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
144959
|
+
}
|
|
144960
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
144961
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
144962
|
+
return ctx.services.timebackAssessments.listTestLibrary(integrationId, params);
|
|
144963
|
+
});
|
|
144964
|
+
copyAssessment = requireDeveloper(async (ctx) => {
|
|
144965
|
+
const { gameId, courseId } = ctx.params;
|
|
144966
|
+
if (!gameId || !courseId) {
|
|
144967
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
144968
|
+
}
|
|
144969
|
+
const body2 = await parseRequestBody(ctx.request, CopyAssessmentRequestSchema);
|
|
144970
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
144971
|
+
const targetTestIdentifier = newPlaycademyTestIdentifier();
|
|
144972
|
+
return ctx.services.timebackAssessments.copyAssessment(integrationId, body2.testIdentifier, targetTestIdentifier, body2.purpose);
|
|
143801
144973
|
});
|
|
143802
|
-
createQuestion =
|
|
144974
|
+
createQuestion = requireDeveloper(async (ctx) => {
|
|
143803
144975
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
143804
144976
|
if (!gameId || !courseId || !testIdentifier) {
|
|
143805
144977
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -143808,7 +144980,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
143808
144980
|
const body2 = await ctx.request.json();
|
|
143809
144981
|
return ctx.services.timebackAssessments.createQuestion(integrationId, testIdentifier, body2);
|
|
143810
144982
|
});
|
|
143811
|
-
updateQuestion =
|
|
144983
|
+
updateQuestion = requireDeveloper(async (ctx) => {
|
|
143812
144984
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
143813
144985
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
143814
144986
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
@@ -143817,30 +144989,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
143817
144989
|
const body2 = await ctx.request.json();
|
|
143818
144990
|
return ctx.services.timebackAssessments.updateQuestion(integrationId, testIdentifier, itemIdentifier, body2);
|
|
143819
144991
|
});
|
|
143820
|
-
|
|
144992
|
+
removeQuestion = requireDeveloper(async (ctx) => {
|
|
143821
144993
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
143822
144994
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
143823
144995
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
143824
144996
|
}
|
|
143825
144997
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143826
|
-
await ctx.services.timebackAssessments.
|
|
143827
|
-
return { success: true };
|
|
143828
|
-
});
|
|
143829
|
-
getAssessmentBankStatus = requireGameManagementAccess(async (ctx) => {
|
|
143830
|
-
const { gameId, courseId } = ctx.params;
|
|
143831
|
-
if (!gameId || !courseId) {
|
|
143832
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143833
|
-
}
|
|
143834
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143835
|
-
return ctx.services.timebackAssessments.getBankStatus(integrationId);
|
|
143836
|
-
});
|
|
143837
|
-
destroyAssessmentBank = requireAdmin(async (ctx) => {
|
|
143838
|
-
const { gameId, courseId } = ctx.params;
|
|
143839
|
-
if (!gameId || !courseId) {
|
|
143840
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
143841
|
-
}
|
|
143842
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
143843
|
-
await ctx.services.timebackAssessments.destroyBank(integrationId);
|
|
144998
|
+
await ctx.services.timebackAssessments.removeQuestion(integrationId, testIdentifier, itemIdentifier);
|
|
143844
144999
|
return { success: true };
|
|
143845
145000
|
});
|
|
143846
145001
|
timeback2 = defineControllerNames("timeback", {
|
|
@@ -143885,17 +145040,17 @@ var init_timeback_controller = __esm(() => {
|
|
|
143885
145040
|
reactivateEnrollment,
|
|
143886
145041
|
listAssessments,
|
|
143887
145042
|
createAssessment,
|
|
143888
|
-
|
|
145043
|
+
updateAssessment,
|
|
143889
145044
|
reorderAssessments,
|
|
145045
|
+
removeAssessment,
|
|
143890
145046
|
reorderQuestions,
|
|
143891
|
-
activateAssessment,
|
|
143892
|
-
deactivateAssessment,
|
|
143893
145047
|
listQuestions,
|
|
145048
|
+
listTestLibrary,
|
|
145049
|
+
copyAssessment,
|
|
145050
|
+
listQuestionLibrary,
|
|
143894
145051
|
createQuestion,
|
|
143895
145052
|
updateQuestion,
|
|
143896
|
-
|
|
143897
|
-
getAssessmentBankStatus,
|
|
143898
|
-
destroyAssessmentBank
|
|
145053
|
+
removeQuestion
|
|
143899
145054
|
});
|
|
143900
145055
|
});
|
|
143901
145056
|
|