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