@playcademy/vite-plugin 1.1.3-beta.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1669 -581
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24314,7 +24314,7 @@ import path2 from "node:path";
|
|
|
24314
24314
|
// package.json
|
|
24315
24315
|
var package_default = {
|
|
24316
24316
|
name: "@playcademy/vite-plugin",
|
|
24317
|
-
version: "1.
|
|
24317
|
+
version: "1.2.0",
|
|
24318
24318
|
type: "module",
|
|
24319
24319
|
exports: {
|
|
24320
24320
|
".": {
|
|
@@ -25846,7 +25846,7 @@ var package_default2;
|
|
|
25846
25846
|
var init_package = __esm(() => {
|
|
25847
25847
|
package_default2 = {
|
|
25848
25848
|
name: "@playcademy/sandbox",
|
|
25849
|
-
version: "0.
|
|
25849
|
+
version: "0.7.0",
|
|
25850
25850
|
description: "Local development server for Playcademy game development",
|
|
25851
25851
|
type: "module",
|
|
25852
25852
|
exports: {
|
|
@@ -36036,7 +36036,6 @@ var users;
|
|
|
36036
36036
|
var accounts;
|
|
36037
36037
|
var sessions;
|
|
36038
36038
|
var verification;
|
|
36039
|
-
var ssoProvider;
|
|
36040
36039
|
var init_table3 = __esm(() => {
|
|
36041
36040
|
init_pg_core();
|
|
36042
36041
|
userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
|
|
@@ -36129,16 +36128,6 @@ var init_table3 = __esm(() => {
|
|
|
36129
36128
|
withTimezone: true
|
|
36130
36129
|
}).notNull()
|
|
36131
36130
|
});
|
|
36132
|
-
ssoProvider = pgTable("sso_provider", {
|
|
36133
|
-
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
36134
|
-
issuer: text("issuer").notNull(),
|
|
36135
|
-
oidcConfig: text("oidc_config"),
|
|
36136
|
-
samlConfig: text("saml_config"),
|
|
36137
|
-
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
36138
|
-
providerId: text("provider_id").notNull().unique(),
|
|
36139
|
-
organizationId: text("organization_id"),
|
|
36140
|
-
domain: text("domain").notNull()
|
|
36141
|
-
});
|
|
36142
36131
|
});
|
|
36143
36132
|
var apikey;
|
|
36144
36133
|
var init_table4 = __esm(() => {
|
|
@@ -36392,6 +36381,8 @@ var init_table6 = __esm(() => {
|
|
|
36392
36381
|
}));
|
|
36393
36382
|
});
|
|
36394
36383
|
var gameTimebackIntegrationStatusEnum;
|
|
36384
|
+
var gameTimebackAssessmentPurposeEnum;
|
|
36385
|
+
var gameTimebackAssessmentStatusEnum;
|
|
36395
36386
|
var gameTimebackIntegrations;
|
|
36396
36387
|
var gameTimebackAssessmentTests;
|
|
36397
36388
|
var gameTimebackMetricDiscrepancyVerifications;
|
|
@@ -36405,6 +36396,15 @@ var init_table7 = __esm(() => {
|
|
|
36405
36396
|
"active",
|
|
36406
36397
|
"deactivated"
|
|
36407
36398
|
]);
|
|
36399
|
+
gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", [
|
|
36400
|
+
"end_of_course",
|
|
36401
|
+
"diagnostic"
|
|
36402
|
+
]);
|
|
36403
|
+
gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
|
|
36404
|
+
"draft",
|
|
36405
|
+
"live",
|
|
36406
|
+
"archived"
|
|
36407
|
+
]);
|
|
36408
36408
|
gameTimebackIntegrations = pgTable("game_timeback_integrations", {
|
|
36409
36409
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
36410
36410
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
@@ -36425,10 +36425,11 @@ var init_table7 = __esm(() => {
|
|
|
36425
36425
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
36426
36426
|
integrationId: uuid("integration_id").notNull().references(() => gameTimebackIntegrations.id, { onDelete: "cascade" }),
|
|
36427
36427
|
qtiTestIdentifier: text("qti_test_identifier").notNull(),
|
|
36428
|
-
|
|
36429
|
-
|
|
36430
|
-
sortOrder: integer("sort_order")
|
|
36431
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
36428
|
+
purpose: gameTimebackAssessmentPurposeEnum("purpose").notNull().default("end_of_course"),
|
|
36429
|
+
status: gameTimebackAssessmentStatusEnum("status").notNull().default("draft"),
|
|
36430
|
+
sortOrder: integer("sort_order"),
|
|
36431
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
36432
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
36432
36433
|
}, (table3) => [
|
|
36433
36434
|
uniqueIndex("game_timeback_assessment_tests_integration_qti_idx").on(table3.integrationId, table3.qtiTestIdentifier)
|
|
36434
36435
|
]);
|
|
@@ -36469,7 +36470,6 @@ __export(exports_tables_index, {
|
|
|
36469
36470
|
verification: () => verification,
|
|
36470
36471
|
users: () => users,
|
|
36471
36472
|
userRoleEnum: () => userRoleEnum,
|
|
36472
|
-
ssoProvider: () => ssoProvider,
|
|
36473
36473
|
sessions: () => sessions,
|
|
36474
36474
|
games: () => games,
|
|
36475
36475
|
gameVisibilityEnum: () => gameVisibilityEnum,
|
|
@@ -36478,6 +36478,8 @@ __export(exports_tables_index, {
|
|
|
36478
36478
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
36479
36479
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
36480
36480
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
36481
|
+
gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
|
|
36482
|
+
gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
|
|
36481
36483
|
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
36482
36484
|
gameScoresRelations: () => gameScoresRelations,
|
|
36483
36485
|
gameScores: () => gameScores,
|
|
@@ -52987,7 +52989,7 @@ function evaluateBaselineGuardrails(input) {
|
|
|
52987
52989
|
}
|
|
52988
52990
|
return "ok";
|
|
52989
52991
|
}
|
|
52990
|
-
function
|
|
52992
|
+
function planMigrations2(journal, ledger) {
|
|
52991
52993
|
const ledgerByTag = new Map(ledger.map((row) => [row.tag, row]));
|
|
52992
52994
|
const journalTags = new Set(journal.map((entry2) => entry2.tag));
|
|
52993
52995
|
const pendingTags = [];
|
|
@@ -53566,7 +53568,7 @@ class DeployService {
|
|
|
53566
53568
|
await this.assertNoSchemaDrift(databaseId, state);
|
|
53567
53569
|
await cf.d1.ensureMigrationLedger(databaseId);
|
|
53568
53570
|
const ledger = await cf.d1.readMigrationLedger(databaseId);
|
|
53569
|
-
const plan =
|
|
53571
|
+
const plan = planMigrations2(migrations.map((migration) => ({ tag: migration.tag, checksum: migration.checksum })), ledger.map((row) => ({
|
|
53570
53572
|
tag: row.tag,
|
|
53571
53573
|
checksum: row.checksum,
|
|
53572
53574
|
checksumAlgo: row.checksum_algo
|
|
@@ -53919,7 +53921,7 @@ class DeployService {
|
|
|
53919
53921
|
}
|
|
53920
53922
|
await cf.d1.ensureMigrationLedger(databaseId);
|
|
53921
53923
|
const ledger = await cf.d1.readMigrationLedger(databaseId);
|
|
53922
|
-
const plan =
|
|
53924
|
+
const plan = planMigrations2(slice, ledger.map((row) => ({
|
|
53923
53925
|
tag: row.tag,
|
|
53924
53926
|
checksum: row.checksum,
|
|
53925
53927
|
checksumAlgo: row.checksum_algo
|
|
@@ -54578,6 +54580,25 @@ function sleep(ms) {
|
|
|
54578
54580
|
}
|
|
54579
54581
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
54580
54582
|
}
|
|
54583
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
54584
|
+
if (items.length === 0) {
|
|
54585
|
+
return [];
|
|
54586
|
+
}
|
|
54587
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
54588
|
+
const results = Array.from({ length: items.length });
|
|
54589
|
+
let nextIndex = 0;
|
|
54590
|
+
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
54591
|
+
while (true) {
|
|
54592
|
+
const currentIndex = nextIndex;
|
|
54593
|
+
nextIndex++;
|
|
54594
|
+
if (currentIndex >= items.length) {
|
|
54595
|
+
return;
|
|
54596
|
+
}
|
|
54597
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
54598
|
+
}
|
|
54599
|
+
}));
|
|
54600
|
+
return results;
|
|
54601
|
+
}
|
|
54581
54602
|
function isObject(value) {
|
|
54582
54603
|
return typeof value === "object" && value !== null;
|
|
54583
54604
|
}
|
|
@@ -56094,7 +56115,7 @@ class KVBackupService {
|
|
|
56094
56115
|
"app.kv_backup.dry_run": dryRun,
|
|
56095
56116
|
"app.kv_backup.game_slug": options.gameSlug
|
|
56096
56117
|
});
|
|
56097
|
-
const results = await
|
|
56118
|
+
const results = await runWithConcurrency(targets, namespaceConcurrency, (target) => this.backupNamespace(target, {
|
|
56098
56119
|
bucketName,
|
|
56099
56120
|
stage,
|
|
56100
56121
|
runId,
|
|
@@ -56160,7 +56181,7 @@ class KVBackupService {
|
|
|
56160
56181
|
}
|
|
56161
56182
|
static async fetchNamespaceEntries(cloudflare2, namespaceId, keyConcurrency = DEFAULT_KEY_CONCURRENCY) {
|
|
56162
56183
|
const keys = await KVBackupService.withRetries(`List KV keys for namespace ${namespaceId}`, () => cloudflare2.kv.listKeys(namespaceId));
|
|
56163
|
-
return
|
|
56184
|
+
return runWithConcurrency(keys, keyConcurrency, async (key) => {
|
|
56164
56185
|
const safeLabel = KVBackupService.redactKeyForLog(key.name);
|
|
56165
56186
|
const value = await KVBackupService.withRetries(`Fetch KV value ${safeLabel}`, () => cloudflare2.kv.getValue(namespaceId, key.name));
|
|
56166
56187
|
const metadata2 = KVBackupService.parseMetadata(key.metadata);
|
|
@@ -56337,25 +56358,6 @@ class KVBackupService {
|
|
|
56337
56358
|
}
|
|
56338
56359
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
56339
56360
|
}
|
|
56340
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
56341
|
-
if (items.length === 0) {
|
|
56342
|
-
return [];
|
|
56343
|
-
}
|
|
56344
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
56345
|
-
const results = Array.from({ length: items.length });
|
|
56346
|
-
let nextIndex = 0;
|
|
56347
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
56348
|
-
while (true) {
|
|
56349
|
-
const currentIndex = nextIndex;
|
|
56350
|
-
nextIndex++;
|
|
56351
|
-
if (currentIndex >= items.length) {
|
|
56352
|
-
return;
|
|
56353
|
-
}
|
|
56354
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
56355
|
-
}
|
|
56356
|
-
}));
|
|
56357
|
-
return results;
|
|
56358
|
-
}
|
|
56359
56361
|
}
|
|
56360
56362
|
var BACKUP_SCHEMA_VERSION = 1;
|
|
56361
56363
|
var DEFAULT_NAMESPACE_CONCURRENCY = 2;
|
|
@@ -58718,12 +58720,14 @@ var EnrollStudentRequestSchema;
|
|
|
58718
58720
|
var UnenrollStudentRequestSchema;
|
|
58719
58721
|
var ReactivateEnrollmentRequestSchema;
|
|
58720
58722
|
var VerifyTimebackMetricDiscrepancyRequestSchema;
|
|
58721
|
-
var
|
|
58723
|
+
var AssessmentPurposeSchema;
|
|
58724
|
+
var AssessmentStatusSchema;
|
|
58722
58725
|
var CreateAssessmentRequestSchema;
|
|
58726
|
+
var UpdateAssessmentRequestSchema;
|
|
58727
|
+
var CopyAssessmentRequestSchema;
|
|
58723
58728
|
var ReorderAssessmentsRequestSchema;
|
|
58724
58729
|
var ReorderQuestionsRequestSchema;
|
|
58725
58730
|
var init_schemas4 = __esm(() => {
|
|
58726
|
-
init_drizzle_zod();
|
|
58727
58731
|
init_esm();
|
|
58728
58732
|
init_table7();
|
|
58729
58733
|
TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
@@ -58976,15 +58980,26 @@ var init_schemas4 = __esm(() => {
|
|
|
58976
58980
|
runId: exports_external.string().uuid(),
|
|
58977
58981
|
activityId: exports_external.string().min(1).optional()
|
|
58978
58982
|
});
|
|
58979
|
-
|
|
58980
|
-
|
|
58981
|
-
createdAt: true
|
|
58982
|
-
});
|
|
58983
|
+
AssessmentPurposeSchema = exports_external.enum(gameTimebackAssessmentPurposeEnum.enumValues);
|
|
58984
|
+
AssessmentStatusSchema = exports_external.enum(gameTimebackAssessmentStatusEnum.enumValues);
|
|
58983
58985
|
CreateAssessmentRequestSchema = exports_external.object({
|
|
58984
|
-
title: exports_external.string().min(1, "Assessment title is required")
|
|
58986
|
+
title: exports_external.string().min(1, "Assessment title is required"),
|
|
58987
|
+
purpose: AssessmentPurposeSchema
|
|
58988
|
+
});
|
|
58989
|
+
UpdateAssessmentRequestSchema = exports_external.object({
|
|
58990
|
+
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
58991
|
+
purpose: AssessmentPurposeSchema.optional(),
|
|
58992
|
+
status: AssessmentStatusSchema.optional()
|
|
58993
|
+
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.status !== undefined, {
|
|
58994
|
+
message: "Title, purpose, or status is required"
|
|
58995
|
+
});
|
|
58996
|
+
CopyAssessmentRequestSchema = exports_external.object({
|
|
58997
|
+
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
58998
|
+
purpose: AssessmentPurposeSchema
|
|
58985
58999
|
});
|
|
58986
59000
|
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
58987
|
-
|
|
59001
|
+
purpose: AssessmentPurposeSchema,
|
|
59002
|
+
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
58988
59003
|
});
|
|
58989
59004
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
58990
59005
|
items: exports_external.array(exports_external.object({
|
|
@@ -80449,7 +80464,7 @@ var INITIAL_RETRY_DELAY_MS2 = 1000;
|
|
|
80449
80464
|
var DEFAULT_LIMIT2 = 100;
|
|
80450
80465
|
var DEFAULT_MAX_ITEMS2 = 1e4;
|
|
80451
80466
|
var Paginator6;
|
|
80452
|
-
var
|
|
80467
|
+
var init_chunk_pt7w63g2 = __esm(async () => {
|
|
80453
80468
|
init_chunk_6jf1natv();
|
|
80454
80469
|
ApiError3 = class ApiError32 extends Error {
|
|
80455
80470
|
statusCode;
|
|
@@ -95770,8 +95785,7 @@ function resolveToProvider23(config4, registry3 = DEFAULT_PROVIDER_REGISTRY2) {
|
|
|
95770
95785
|
function translateParams2(params) {
|
|
95771
95786
|
const {
|
|
95772
95787
|
orderBy,
|
|
95773
|
-
|
|
95774
|
-
fields: __,
|
|
95788
|
+
fields: _2,
|
|
95775
95789
|
...rest
|
|
95776
95790
|
} = params;
|
|
95777
95791
|
return {
|
|
@@ -95779,6 +95793,23 @@ function translateParams2(params) {
|
|
|
95779
95793
|
order: orderBy
|
|
95780
95794
|
};
|
|
95781
95795
|
}
|
|
95796
|
+
function buildListQueryParams(params) {
|
|
95797
|
+
const queryParams = {};
|
|
95798
|
+
const filter = params.where ? whereToFilter2(params.where) : undefined;
|
|
95799
|
+
if (filter !== undefined)
|
|
95800
|
+
queryParams.filter = filter;
|
|
95801
|
+
if (params.query !== undefined)
|
|
95802
|
+
queryParams.query = params.query;
|
|
95803
|
+
if (params.page !== undefined)
|
|
95804
|
+
queryParams.page = params.page;
|
|
95805
|
+
if (params.limit !== undefined)
|
|
95806
|
+
queryParams.limit = params.limit;
|
|
95807
|
+
if (params.sort)
|
|
95808
|
+
queryParams.sort = params.sort;
|
|
95809
|
+
if (params.order)
|
|
95810
|
+
queryParams.order = params.order;
|
|
95811
|
+
return queryParams;
|
|
95812
|
+
}
|
|
95782
95813
|
|
|
95783
95814
|
class AssessmentItemsResource2 {
|
|
95784
95815
|
transport;
|
|
@@ -95787,19 +95818,8 @@ class AssessmentItemsResource2 {
|
|
|
95787
95818
|
}
|
|
95788
95819
|
list(params = {}) {
|
|
95789
95820
|
validatePageListParams2(params);
|
|
95790
|
-
const queryParams = {};
|
|
95791
|
-
if (params.query !== undefined)
|
|
95792
|
-
queryParams.query = params.query;
|
|
95793
|
-
if (params.page !== undefined)
|
|
95794
|
-
queryParams.page = params.page;
|
|
95795
|
-
if (params.limit !== undefined)
|
|
95796
|
-
queryParams.limit = params.limit;
|
|
95797
|
-
if (params.sort)
|
|
95798
|
-
queryParams.sort = params.sort;
|
|
95799
|
-
if (params.order)
|
|
95800
|
-
queryParams.order = params.order;
|
|
95801
95821
|
return this.transport.request("/assessment-items", {
|
|
95802
|
-
params:
|
|
95822
|
+
params: buildListQueryParams(params)
|
|
95803
95823
|
});
|
|
95804
95824
|
}
|
|
95805
95825
|
stream(params = {}) {
|
|
@@ -95922,20 +95942,9 @@ class TestPartSectionsHelper2 {
|
|
|
95922
95942
|
}
|
|
95923
95943
|
list(params = {}) {
|
|
95924
95944
|
validatePageListParams2(params);
|
|
95925
|
-
const queryParams = {};
|
|
95926
|
-
if (params.query !== undefined)
|
|
95927
|
-
queryParams.query = params.query;
|
|
95928
|
-
if (params.page !== undefined)
|
|
95929
|
-
queryParams.page = params.page;
|
|
95930
|
-
if (params.limit !== undefined)
|
|
95931
|
-
queryParams.limit = params.limit;
|
|
95932
|
-
if (params.sort)
|
|
95933
|
-
queryParams.sort = params.sort;
|
|
95934
|
-
if (params.order)
|
|
95935
|
-
queryParams.order = params.order;
|
|
95936
95945
|
const path3 = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts/${encodeURIComponent(this.testPartId)}/sections`;
|
|
95937
95946
|
return this.transport.request(path3, {
|
|
95938
|
-
params:
|
|
95947
|
+
params: buildListQueryParams(params)
|
|
95939
95948
|
});
|
|
95940
95949
|
}
|
|
95941
95950
|
get(identifier) {
|
|
@@ -95982,19 +95991,10 @@ class AssessmentTestPartsHelper2 {
|
|
|
95982
95991
|
}
|
|
95983
95992
|
list(params = {}) {
|
|
95984
95993
|
validatePageListParams2(params);
|
|
95985
|
-
const queryParams = {};
|
|
95986
|
-
if (params.query !== undefined)
|
|
95987
|
-
queryParams.query = params.query;
|
|
95988
|
-
if (params.page !== undefined)
|
|
95989
|
-
queryParams.page = params.page;
|
|
95990
|
-
if (params.limit !== undefined)
|
|
95991
|
-
queryParams.limit = params.limit;
|
|
95992
|
-
if (params.sort)
|
|
95993
|
-
queryParams.sort = params.sort;
|
|
95994
|
-
if (params.order)
|
|
95995
|
-
queryParams.order = params.order;
|
|
95996
95994
|
const path3 = `/assessment-tests/${encodeURIComponent(this.testId)}/test-parts`;
|
|
95997
|
-
return this.transport.request(path3, {
|
|
95995
|
+
return this.transport.request(path3, {
|
|
95996
|
+
params: buildListQueryParams(params)
|
|
95997
|
+
});
|
|
95998
95998
|
}
|
|
95999
95999
|
get(identifier) {
|
|
96000
96000
|
validateNonEmptyString2(identifier, "testPartId");
|
|
@@ -96037,19 +96037,8 @@ class AssessmentTestsResource2 {
|
|
|
96037
96037
|
}
|
|
96038
96038
|
list(params = {}) {
|
|
96039
96039
|
validatePageListParams2(params);
|
|
96040
|
-
const queryParams = {};
|
|
96041
|
-
if (params.query !== undefined)
|
|
96042
|
-
queryParams.query = params.query;
|
|
96043
|
-
if (params.page !== undefined)
|
|
96044
|
-
queryParams.page = params.page;
|
|
96045
|
-
if (params.limit !== undefined)
|
|
96046
|
-
queryParams.limit = params.limit;
|
|
96047
|
-
if (params.sort)
|
|
96048
|
-
queryParams.sort = params.sort;
|
|
96049
|
-
if (params.order)
|
|
96050
|
-
queryParams.order = params.order;
|
|
96051
96040
|
return this.transport.request("/assessment-tests", {
|
|
96052
|
-
params:
|
|
96041
|
+
params: buildListQueryParams(params)
|
|
96053
96042
|
});
|
|
96054
96043
|
}
|
|
96055
96044
|
stream(params = {}) {
|
|
@@ -96155,19 +96144,8 @@ class StimuliResource2 {
|
|
|
96155
96144
|
}
|
|
96156
96145
|
list(params = {}) {
|
|
96157
96146
|
validatePageListParams2(params);
|
|
96158
|
-
const queryParams = {};
|
|
96159
|
-
if (params.query !== undefined)
|
|
96160
|
-
queryParams.query = params.query;
|
|
96161
|
-
if (params.page !== undefined)
|
|
96162
|
-
queryParams.page = params.page;
|
|
96163
|
-
if (params.limit !== undefined)
|
|
96164
|
-
queryParams.limit = params.limit;
|
|
96165
|
-
if (params.sort)
|
|
96166
|
-
queryParams.sort = params.sort;
|
|
96167
|
-
if (params.order)
|
|
96168
|
-
queryParams.order = params.order;
|
|
96169
96147
|
return this.transport.request("/stimuli", {
|
|
96170
|
-
params:
|
|
96148
|
+
params: buildListQueryParams(params)
|
|
96171
96149
|
});
|
|
96172
96150
|
}
|
|
96173
96151
|
stream(params = {}) {
|
|
@@ -96540,7 +96518,7 @@ var init_dist3 = __esm(async () => {
|
|
|
96540
96518
|
init_v42();
|
|
96541
96519
|
init_v42();
|
|
96542
96520
|
init_v42();
|
|
96543
|
-
await
|
|
96521
|
+
await init_chunk_pt7w63g2();
|
|
96544
96522
|
QTI_ENV_VARS2 = {
|
|
96545
96523
|
baseUrl: ["TIMEBACK_API_BASE_URL", "TIMEBACK_BASE_URL", "QTI_BASE_URL"],
|
|
96546
96524
|
clientId: ["TIMEBACK_API_CLIENT_ID", "TIMEBACK_CLIENT_ID", "QTI_CLIENT_ID"],
|
|
@@ -103667,13 +103645,6 @@ function deriveSourcedIds(courseId) {
|
|
|
103667
103645
|
componentResource: `${courseId}-cr`
|
|
103668
103646
|
};
|
|
103669
103647
|
}
|
|
103670
|
-
function deriveAssessmentBankIds(courseId) {
|
|
103671
|
-
return {
|
|
103672
|
-
component: `${courseId}-assessment-bank-component`,
|
|
103673
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
103674
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
103675
|
-
};
|
|
103676
|
-
}
|
|
103677
103648
|
function validateProgressData(progressData) {
|
|
103678
103649
|
if (!progressData.subject) {
|
|
103679
103650
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -104962,59 +104933,22 @@ class CourseAssessments {
|
|
|
104962
104933
|
constructor(core3) {
|
|
104963
104934
|
this.core = core3;
|
|
104964
104935
|
}
|
|
104965
|
-
|
|
104966
|
-
|
|
104967
|
-
|
|
104968
|
-
|
|
104969
|
-
|
|
104970
|
-
|
|
104971
|
-
|
|
104972
|
-
|
|
104973
|
-
|
|
104974
|
-
|
|
104975
|
-
|
|
104976
|
-
|
|
104977
|
-
|
|
104978
|
-
|
|
104979
|
-
throw error88;
|
|
104980
|
-
}
|
|
104981
|
-
}
|
|
104982
|
-
try {
|
|
104983
|
-
await oneroster.resources.create({
|
|
104984
|
-
sourcedId: bankIds.resource,
|
|
104985
|
-
status: "active",
|
|
104986
|
-
title: "Assessment Bank",
|
|
104987
|
-
vendorResourceId: "",
|
|
104988
|
-
vendorId: undefined,
|
|
104989
|
-
metadata: {
|
|
104990
|
-
type: "assessment-bank",
|
|
104991
|
-
resources: [],
|
|
104992
|
-
lessonType: "test-out",
|
|
104993
|
-
subject: input.subject,
|
|
104994
|
-
grade: String(input.grade)
|
|
104995
|
-
}
|
|
104996
|
-
});
|
|
104997
|
-
} catch (error88) {
|
|
104998
|
-
if (!isAlreadyExists(error88)) {
|
|
104999
|
-
throw error88;
|
|
105000
|
-
}
|
|
105001
|
-
}
|
|
105002
|
-
try {
|
|
105003
|
-
await oneroster.courses.createComponentResource({
|
|
105004
|
-
sourcedId: bankIds.componentResource,
|
|
105005
|
-
status: "active",
|
|
105006
|
-
title: "Test Out",
|
|
105007
|
-
resource: { sourcedId: bankIds.resource },
|
|
105008
|
-
courseComponent: { sourcedId: bankIds.component },
|
|
105009
|
-
sortOrder: 9999,
|
|
105010
|
-
metadata: { lessonType: "test-out" },
|
|
105011
|
-
lessonType: "test-out"
|
|
105012
|
-
});
|
|
105013
|
-
} catch (error88) {
|
|
105014
|
-
if (!isAlreadyExists(error88)) {
|
|
105015
|
-
throw error88;
|
|
104936
|
+
createItemFromXml(input) {
|
|
104937
|
+
return this.core.qti.assessmentItems.createFromXml({
|
|
104938
|
+
format: "xml",
|
|
104939
|
+
xml: input.xml,
|
|
104940
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
104941
|
+
});
|
|
104942
|
+
}
|
|
104943
|
+
updateItemFromXml(identifier, input) {
|
|
104944
|
+
return this.core.qti.getTransport().request(`/assessment-items/${encodeURIComponent(identifier)}`, {
|
|
104945
|
+
method: "PUT",
|
|
104946
|
+
body: {
|
|
104947
|
+
format: "xml",
|
|
104948
|
+
xml: input.xml,
|
|
104949
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
105016
104950
|
}
|
|
105017
|
-
}
|
|
104951
|
+
});
|
|
105018
104952
|
}
|
|
105019
104953
|
async createTestScaffold(input) {
|
|
105020
104954
|
const partId = `${input.identifier}-part1`;
|
|
@@ -105049,18 +104983,6 @@ class CourseAssessments {
|
|
|
105049
104983
|
}
|
|
105050
104984
|
}
|
|
105051
104985
|
}
|
|
105052
|
-
async teardownTest(qtiTestIdentifier) {
|
|
105053
|
-
const qti = this.core.qti;
|
|
105054
|
-
const partId = `${qtiTestIdentifier}-part1`;
|
|
105055
|
-
const sectionId = `${qtiTestIdentifier}-section1`;
|
|
105056
|
-
const questions = await qti.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
105057
|
-
const sectionItems = qti.assessmentTests.testParts(qtiTestIdentifier).sections(partId).items(sectionId);
|
|
105058
|
-
for (const q of questions.questions ?? []) {
|
|
105059
|
-
await sectionItems.remove(q.reference.identifier);
|
|
105060
|
-
await qti.assessmentItems.delete(q.reference.identifier);
|
|
105061
|
-
}
|
|
105062
|
-
await qti.assessmentTests.delete(qtiTestIdentifier);
|
|
105063
|
-
}
|
|
105064
104986
|
}
|
|
105065
104987
|
function buildCoursePayload(config4) {
|
|
105066
104988
|
return {
|
|
@@ -105290,9 +105212,9 @@ function createCourseNamespace(core3) {
|
|
|
105290
105212
|
cleanup: (courseId) => integration.cleanup(courseId),
|
|
105291
105213
|
deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
|
|
105292
105214
|
reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
|
|
105293
|
-
ensureAssessmentBank: (input) => assessments.ensureBank(input),
|
|
105294
105215
|
createAssessmentTest: (input) => assessments.createTestScaffold(input),
|
|
105295
|
-
|
|
105216
|
+
createAssessmentItemXml: (input) => assessments.createItemFromXml(input),
|
|
105217
|
+
updateAssessmentItemXml: (identifier, input) => assessments.updateItemFromXml(identifier, input)
|
|
105296
105218
|
};
|
|
105297
105219
|
}
|
|
105298
105220
|
function recordTimebackClientSummary(outcome) {
|
|
@@ -105796,13 +105718,6 @@ function deriveSourcedIds2(courseId) {
|
|
|
105796
105718
|
componentResource: `${courseId}-cr`
|
|
105797
105719
|
};
|
|
105798
105720
|
}
|
|
105799
|
-
function deriveAssessmentBankIds2(courseId) {
|
|
105800
|
-
return {
|
|
105801
|
-
component: `${courseId}-assessment-bank-component`,
|
|
105802
|
-
resource: `${courseId}-assessment-bank-resource`,
|
|
105803
|
-
componentResource: `${courseId}-assessment-bank-cr`
|
|
105804
|
-
};
|
|
105805
|
-
}
|
|
105806
105721
|
var CACHE_DEFAULTS4;
|
|
105807
105722
|
var RESOURCE_DEFAULTS4;
|
|
105808
105723
|
var init_utils6 = __esm(() => {
|
|
@@ -105836,6 +105751,96 @@ var init_utils6 = __esm(() => {
|
|
|
105836
105751
|
componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
|
|
105837
105752
|
};
|
|
105838
105753
|
});
|
|
105754
|
+
function playcademySupportedQtiInteractionType(value) {
|
|
105755
|
+
if (typeof value !== "string") {
|
|
105756
|
+
return;
|
|
105757
|
+
}
|
|
105758
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
105759
|
+
return PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET.has(normalized) ? normalized : undefined;
|
|
105760
|
+
}
|
|
105761
|
+
function parseNumericTextEntry(input) {
|
|
105762
|
+
const answer = typeof input.answer === "string" ? input.answer.trim() : "";
|
|
105763
|
+
const comparison = input.comparison;
|
|
105764
|
+
if (input.baseType !== "integer" && input.baseType !== "float") {
|
|
105765
|
+
return { success: false, message: "Numeric answers must be integer or float responses" };
|
|
105766
|
+
}
|
|
105767
|
+
if (!answer || (input.baseType === "integer" ? !INTEGER_ANSWER_PATTERN.test(answer) : !FLOAT_ANSWER_PATTERN.test(answer) || !Number.isFinite(Number(answer)))) {
|
|
105768
|
+
return { success: false, message: `The correct answer is not a valid ${input.baseType}` };
|
|
105769
|
+
}
|
|
105770
|
+
if (!comparison || typeof comparison !== "object" || !("kind" in comparison)) {
|
|
105771
|
+
return { success: false, message: "Numeric comparison is required" };
|
|
105772
|
+
}
|
|
105773
|
+
if (comparison.kind === "equal") {
|
|
105774
|
+
return {
|
|
105775
|
+
success: true,
|
|
105776
|
+
value: { baseType: input.baseType, answer, comparison: { kind: "equal" } }
|
|
105777
|
+
};
|
|
105778
|
+
}
|
|
105779
|
+
if (input.baseType === "integer") {
|
|
105780
|
+
return { success: false, message: "Integer answers cannot use decimal-place rounding" };
|
|
105781
|
+
}
|
|
105782
|
+
const figures = "figures" in comparison ? comparison.figures : undefined;
|
|
105783
|
+
const roundingMode = "roundingMode" in comparison ? comparison.roundingMode : undefined;
|
|
105784
|
+
if (comparison.kind !== "equal-rounded" || roundingMode !== "decimalPlaces" || !Number.isInteger(figures) || figures < 0 || figures > 15) {
|
|
105785
|
+
return { success: false, message: "Decimal places must be a whole number from 0 to 15" };
|
|
105786
|
+
}
|
|
105787
|
+
return {
|
|
105788
|
+
success: true,
|
|
105789
|
+
value: {
|
|
105790
|
+
baseType: "float",
|
|
105791
|
+
answer,
|
|
105792
|
+
comparison: { kind: "equal-rounded", roundingMode, figures }
|
|
105793
|
+
}
|
|
105794
|
+
};
|
|
105795
|
+
}
|
|
105796
|
+
function escapeXml(value) {
|
|
105797
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
105798
|
+
}
|
|
105799
|
+
function hottextMaxChoices(selectableCount, multiple) {
|
|
105800
|
+
return multiple ? selectableCount : 1;
|
|
105801
|
+
}
|
|
105802
|
+
function numericTextEntryXml(input) {
|
|
105803
|
+
const comparison = input.numeric.comparison.kind === "equal-rounded" ? `<qti-equal-rounded rounding-mode="decimalPlaces" figures="${input.numeric.comparison.figures}">` : "<qti-equal>";
|
|
105804
|
+
const closeComparison = input.numeric.comparison.kind === "equal-rounded" ? "</qti-equal-rounded>" : "</qti-equal>";
|
|
105805
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
105806
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
105807
|
+
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="${input.numeric.baseType}">
|
|
105808
|
+
<qti-correct-response><qti-value>${escapeXml(input.numeric.answer)}</qti-value></qti-correct-response>
|
|
105809
|
+
</qti-response-declaration>
|
|
105810
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
105811
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
105812
|
+
</qti-outcome-declaration>
|
|
105813
|
+
<qti-item-body>
|
|
105814
|
+
<p>${escapeXml(input.prompt)} <qti-text-entry-interaction response-identifier="RESPONSE" expected-length="15" /></p>
|
|
105815
|
+
</qti-item-body>
|
|
105816
|
+
<qti-response-processing>
|
|
105817
|
+
<qti-response-condition>
|
|
105818
|
+
<qti-response-if>
|
|
105819
|
+
${comparison}<qti-variable identifier="RESPONSE" /><qti-correct identifier="RESPONSE" />${closeComparison}
|
|
105820
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
105821
|
+
</qti-response-if>
|
|
105822
|
+
</qti-response-condition>
|
|
105823
|
+
</qti-response-processing>
|
|
105824
|
+
</qti-assessment-item>`;
|
|
105825
|
+
}
|
|
105826
|
+
function countQtiTestItems(test) {
|
|
105827
|
+
let itemCount = 0;
|
|
105828
|
+
for (const part of test["qti-test-part"] ?? []) {
|
|
105829
|
+
for (const section of part["qti-assessment-section"] ?? []) {
|
|
105830
|
+
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
105831
|
+
}
|
|
105832
|
+
}
|
|
105833
|
+
return itemCount;
|
|
105834
|
+
}
|
|
105835
|
+
function isQtiItemOwnedByTest(item, testIdentifier) {
|
|
105836
|
+
const hasOwnershipMetadata = Boolean(item.metadata && (("ownerSystem" in item.metadata) || (PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY in item.metadata)));
|
|
105837
|
+
if (hasOwnershipMetadata) {
|
|
105838
|
+
return item.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && item.metadata?.[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY] === testIdentifier;
|
|
105839
|
+
}
|
|
105840
|
+
const identifierPrefix = `${testIdentifier}-q`;
|
|
105841
|
+
const identifierSuffix = item.identifier?.startsWith(identifierPrefix) ? item.identifier.slice(identifierPrefix.length) : undefined;
|
|
105842
|
+
return identifierSuffix !== undefined && /^[0-9a-f]{8}$/i.test(identifierSuffix);
|
|
105843
|
+
}
|
|
105839
105844
|
function formatGradeLabel(grade) {
|
|
105840
105845
|
if (grade === null || grade === undefined) {
|
|
105841
105846
|
return "N/A";
|
|
@@ -105879,6 +105884,14 @@ function parseTimebackDiscrepancyQueueMetrics(values) {
|
|
|
105879
105884
|
}
|
|
105880
105885
|
var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS;
|
|
105881
105886
|
var TIMEBACK_DISCREPANCY_QUEUE_METRICS;
|
|
105887
|
+
var PLAYCADEMY_QTI_OWNER_SYSTEM = "playcademy";
|
|
105888
|
+
var PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY = "ownerTestIdentifier";
|
|
105889
|
+
var PLAYCADEMY_QTI_EDITOR_MODE_KEY = "playcademyEditorMode";
|
|
105890
|
+
var PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES;
|
|
105891
|
+
var PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET;
|
|
105892
|
+
var INTEGER_ANSWER_PATTERN;
|
|
105893
|
+
var FLOAT_ANSWER_PATTERN;
|
|
105894
|
+
var INLINE_CHOICE_BLANK = "_____";
|
|
105882
105895
|
var DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW = "this-week";
|
|
105883
105896
|
var TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES;
|
|
105884
105897
|
var TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES;
|
|
@@ -105892,6 +105905,17 @@ var init_timeback3 = __esm(() => {
|
|
|
105892
105905
|
"custom"
|
|
105893
105906
|
];
|
|
105894
105907
|
TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
|
|
105908
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES = [
|
|
105909
|
+
"choice",
|
|
105910
|
+
"inline-choice",
|
|
105911
|
+
"text-entry",
|
|
105912
|
+
"order",
|
|
105913
|
+
"match",
|
|
105914
|
+
"hottext"
|
|
105915
|
+
];
|
|
105916
|
+
PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPE_SET = new Set(PLAYCADEMY_SUPPORTED_QTI_INTERACTION_TYPES);
|
|
105917
|
+
INTEGER_ANSWER_PATTERN = /^[+-]?\d+$/;
|
|
105918
|
+
FLOAT_ANSWER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
105895
105919
|
TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
|
|
105896
105920
|
TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
|
|
105897
105921
|
});
|
|
@@ -107122,7 +107146,7 @@ class TimebackAdminService {
|
|
|
107122
107146
|
}
|
|
107123
107147
|
async getMasterableUnitsByCourse(courseIds) {
|
|
107124
107148
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
107125
|
-
const results = await
|
|
107149
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.MASTERABLE_UNITS_CONCURRENCY, async (courseId) => [courseId, await this.getMasterableUnits(courseId)]);
|
|
107126
107150
|
return new Map(results);
|
|
107127
107151
|
}
|
|
107128
107152
|
deriveGameSensorUrl(game2) {
|
|
@@ -107303,7 +107327,7 @@ class TimebackAdminService {
|
|
|
107303
107327
|
async loadEnrollmentAnalyticsSummaries(enrollmentIds) {
|
|
107304
107328
|
const client = this.requireClient();
|
|
107305
107329
|
const uniqueEnrollmentIds = [...new Set(enrollmentIds)];
|
|
107306
|
-
const results = await
|
|
107330
|
+
const results = await runWithConcurrency(uniqueEnrollmentIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (enrollmentId) => {
|
|
107307
107331
|
try {
|
|
107308
107332
|
const analytics = await client.api.edubridge.analytics.getEnrollmentFacts({
|
|
107309
107333
|
enrollmentId,
|
|
@@ -107397,7 +107421,7 @@ class TimebackAdminService {
|
|
|
107397
107421
|
if (options.runOwnersById.size === 0) {
|
|
107398
107422
|
return { events: [], runIds: new Set };
|
|
107399
107423
|
}
|
|
107400
|
-
const hydratedRuns = await
|
|
107424
|
+
const hydratedRuns = await runWithConcurrency([...options.runOwnersById.entries()], TimebackAdminService.DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY, async ([runId, studentId]) => {
|
|
107401
107425
|
try {
|
|
107402
107426
|
return {
|
|
107403
107427
|
runId,
|
|
@@ -107430,7 +107454,7 @@ class TimebackAdminService {
|
|
|
107430
107454
|
};
|
|
107431
107455
|
}
|
|
107432
107456
|
async buildMetricDiscrepancyQueueCandidates(user, options) {
|
|
107433
|
-
const comparisonResults = await
|
|
107457
|
+
const comparisonResults = await runWithConcurrency(options.activityGroups, TimebackAdminService.DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY, async (group) => {
|
|
107434
107458
|
try {
|
|
107435
107459
|
return {
|
|
107436
107460
|
group,
|
|
@@ -108430,7 +108454,7 @@ class TimebackAdminService {
|
|
|
108430
108454
|
const action = context2.newMasterableUnits < context2.oldMasterableUnits ? "complete" : "revoke";
|
|
108431
108455
|
const failed = [];
|
|
108432
108456
|
let processed = 0;
|
|
108433
|
-
await
|
|
108457
|
+
await runWithConcurrency(context2.affectedStudentIds, 8, async (studentId) => {
|
|
108434
108458
|
try {
|
|
108435
108459
|
await upsertMasteryCompletionEntry({
|
|
108436
108460
|
client,
|
|
@@ -108651,28 +108675,9 @@ class TimebackAdminService {
|
|
|
108651
108675
|
}
|
|
108652
108676
|
async getCompletionStatusByCourse(client, courseIds, studentId) {
|
|
108653
108677
|
const uniqueCourseIds = [...new Set(courseIds)];
|
|
108654
|
-
const results = await
|
|
108678
|
+
const results = await runWithConcurrency(uniqueCourseIds, TimebackAdminService.ANALYTICS_CONCURRENCY, async (courseId) => [courseId, await this.getCompletionStatus(client, courseId, studentId)]);
|
|
108655
108679
|
return new Map(results);
|
|
108656
108680
|
}
|
|
108657
|
-
static async runWithConcurrency(items, concurrency, worker) {
|
|
108658
|
-
if (items.length === 0) {
|
|
108659
|
-
return [];
|
|
108660
|
-
}
|
|
108661
|
-
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
108662
|
-
const results = Array.from({ length: items.length });
|
|
108663
|
-
let nextIndex = 0;
|
|
108664
|
-
await Promise.all(Array.from({ length: effectiveConcurrency }, async () => {
|
|
108665
|
-
while (true) {
|
|
108666
|
-
const currentIndex = nextIndex;
|
|
108667
|
-
nextIndex++;
|
|
108668
|
-
if (currentIndex >= items.length) {
|
|
108669
|
-
return;
|
|
108670
|
-
}
|
|
108671
|
-
results[currentIndex] = await worker(items[currentIndex]);
|
|
108672
|
-
}
|
|
108673
|
-
}));
|
|
108674
|
-
return results;
|
|
108675
|
-
}
|
|
108676
108681
|
}
|
|
108677
108682
|
var init_timeback_admin_service = __esm(async () => {
|
|
108678
108683
|
init_drizzle_orm();
|
|
@@ -108698,8 +108703,874 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
108698
108703
|
init_timeback_mastery_completion_util()
|
|
108699
108704
|
]);
|
|
108700
108705
|
});
|
|
108706
|
+
function validateAssessmentStatusTransition(current, next) {
|
|
108707
|
+
if (current === next) {
|
|
108708
|
+
return;
|
|
108709
|
+
}
|
|
108710
|
+
const allowed = current === "draft" && next === "live" || current === "live" && next === "archived" || current === "archived" && next === "live";
|
|
108711
|
+
if (!allowed) {
|
|
108712
|
+
throw new ValidationError(`Assessment status cannot change from ${current} to ${next}`);
|
|
108713
|
+
}
|
|
108714
|
+
}
|
|
108715
|
+
function isAssessmentPublicationTransition(current, next) {
|
|
108716
|
+
return current !== "live" && next === "live";
|
|
108717
|
+
}
|
|
108718
|
+
function assertDraftAssessment(row) {
|
|
108719
|
+
if (row.status !== "draft") {
|
|
108720
|
+
throw new ValidationError("Only draft assessments can change QTI content or question membership");
|
|
108721
|
+
}
|
|
108722
|
+
}
|
|
108723
|
+
function assertAllAssessmentAssociationsDraft(rows) {
|
|
108724
|
+
if (rows.some((row) => row.status !== "draft")) {
|
|
108725
|
+
throw new ValidationError("QTI content cannot change while any associated assessment is live or archived");
|
|
108726
|
+
}
|
|
108727
|
+
}
|
|
108728
|
+
function assertAssessmentHasQuestions(questions) {
|
|
108729
|
+
if (questions.length === 0) {
|
|
108730
|
+
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
108731
|
+
}
|
|
108732
|
+
}
|
|
108733
|
+
function planAssessmentRemoval(status) {
|
|
108734
|
+
if (status === "draft") {
|
|
108735
|
+
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
108736
|
+
}
|
|
108737
|
+
if (status === "live") {
|
|
108738
|
+
return { kind: "archive", action: "archived", operation: "archive" };
|
|
108739
|
+
}
|
|
108740
|
+
return { kind: "none", action: "archived" };
|
|
108741
|
+
}
|
|
108742
|
+
function buildAssessmentAssociationUpdates(row, input) {
|
|
108743
|
+
const updates = {};
|
|
108744
|
+
if (input.purpose !== undefined) {
|
|
108745
|
+
updates.purpose = input.purpose;
|
|
108746
|
+
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
108747
|
+
updates.sortOrder = null;
|
|
108748
|
+
}
|
|
108749
|
+
}
|
|
108750
|
+
if (input.status !== undefined) {
|
|
108751
|
+
updates.status = input.status;
|
|
108752
|
+
if (input.status === "archived") {
|
|
108753
|
+
updates.sortOrder = null;
|
|
108754
|
+
}
|
|
108755
|
+
}
|
|
108756
|
+
return updates;
|
|
108757
|
+
}
|
|
108758
|
+
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
108759
|
+
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
108760
|
+
throw new ValidationError("Assessment order must contain unique identifiers");
|
|
108761
|
+
}
|
|
108762
|
+
}
|
|
108763
|
+
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
108764
|
+
if (!updatedRow) {
|
|
108765
|
+
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
108766
|
+
}
|
|
108767
|
+
return updatedRow;
|
|
108768
|
+
}
|
|
108769
|
+
function lockOrderAssessmentRows(rows) {
|
|
108770
|
+
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
108771
|
+
}
|
|
108772
|
+
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
108773
|
+
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
108774
|
+
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
108775
|
+
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
108776
|
+
}
|
|
108777
|
+
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
108778
|
+
}
|
|
108779
|
+
var init_timeback_assessment_rules_util = __esm(() => {
|
|
108780
|
+
init_errors();
|
|
108781
|
+
});
|
|
108782
|
+
function recordValue(value) {
|
|
108783
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
108784
|
+
}
|
|
108785
|
+
function qtiAuthoringSnapshot(input) {
|
|
108786
|
+
if (!recordValue(input.interaction)) {
|
|
108787
|
+
return;
|
|
108788
|
+
}
|
|
108789
|
+
return Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => input[field] === undefined ? [] : [[field, input[field]]]));
|
|
108790
|
+
}
|
|
108791
|
+
function buildOwnedQtiQuestionMetadata(input, ownership, existingMetadata = {}) {
|
|
108792
|
+
const inputMetadata = recordValue(input.metadata) ?? {};
|
|
108793
|
+
const metadata2 = {
|
|
108794
|
+
...existingMetadata,
|
|
108795
|
+
...inputMetadata
|
|
108796
|
+
};
|
|
108797
|
+
const existingAuthoring = recordValue(existingMetadata[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
108798
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
108799
|
+
Reflect.deleteProperty(metadata2, PLAYCADEMY_QTI_EDITOR_MODE_KEY);
|
|
108800
|
+
const authoring = qtiAuthoringSnapshot(input);
|
|
108801
|
+
const shouldReplaceAuthoring = "interaction" in input || "numericTextEntry" in input;
|
|
108802
|
+
const persistedAuthoring = authoring ?? (shouldReplaceAuthoring ? undefined : existingAuthoring);
|
|
108803
|
+
return {
|
|
108804
|
+
...metadata2,
|
|
108805
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
108806
|
+
ownerGameSlug: ownership.gameSlug,
|
|
108807
|
+
[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]: ownership.testIdentifier,
|
|
108808
|
+
...persistedAuthoring ? { [PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]: persistedAuthoring } : {}
|
|
108809
|
+
};
|
|
108810
|
+
}
|
|
108811
|
+
function restoreQtiQuestionAuthoringData(item) {
|
|
108812
|
+
const authoring = recordValue(item.metadata?.[PLAYCADEMY_QTI_AUTHORING_METADATA_KEY]);
|
|
108813
|
+
if (!authoring) {
|
|
108814
|
+
return item;
|
|
108815
|
+
}
|
|
108816
|
+
const restored = Object.fromEntries(QTI_AUTHORING_FIELDS.flatMap((field) => authoring[field] === undefined ? [] : [[field, authoring[field]]]));
|
|
108817
|
+
return { ...item, ...restored };
|
|
108818
|
+
}
|
|
108819
|
+
function mergeQtiQuestionItem(item, fallbackItem, itemIdentifier, metadata2) {
|
|
108820
|
+
return restoreQtiQuestionAuthoringData({
|
|
108821
|
+
...fallbackItem,
|
|
108822
|
+
...item,
|
|
108823
|
+
identifier: itemIdentifier,
|
|
108824
|
+
title: item.title || fallbackItem?.title || itemIdentifier,
|
|
108825
|
+
type: item.type ?? fallbackItem?.type,
|
|
108826
|
+
rawXml: item.rawXml ?? fallbackItem?.rawXml,
|
|
108827
|
+
interaction: item.interaction ?? fallbackItem?.interaction,
|
|
108828
|
+
responseDeclarations: item.responseDeclarations ?? fallbackItem?.responseDeclarations,
|
|
108829
|
+
metadata: {
|
|
108830
|
+
...fallbackItem?.metadata,
|
|
108831
|
+
...item.metadata,
|
|
108832
|
+
...metadata2
|
|
108833
|
+
}
|
|
108834
|
+
});
|
|
108835
|
+
}
|
|
108836
|
+
function newPlaycademyQuestionIdentifier(testIdentifier) {
|
|
108837
|
+
return `${testIdentifier}-q${crypto.randomUUID().slice(0, 8)}`;
|
|
108838
|
+
}
|
|
108839
|
+
function parseQtiQuestionCreationInput(input) {
|
|
108840
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
108841
|
+
throw new ValidationError("Question creation input must be an object");
|
|
108842
|
+
}
|
|
108843
|
+
const rawInput = input;
|
|
108844
|
+
if (rawInput.mode === undefined) {
|
|
108845
|
+
return { kind: "authoring", input: rawInput };
|
|
108846
|
+
}
|
|
108847
|
+
if (rawInput.mode !== "copy") {
|
|
108848
|
+
throw new ValidationError("Question creation mode is invalid");
|
|
108849
|
+
}
|
|
108850
|
+
const sourceItemIdentifier = typeof rawInput.sourceItemIdentifier === "string" ? rawInput.sourceItemIdentifier.trim() : "";
|
|
108851
|
+
if (!sourceItemIdentifier) {
|
|
108852
|
+
throw new ValidationError("A source question identifier is required to create a copy");
|
|
108853
|
+
}
|
|
108854
|
+
const unexpectedField = Object.keys(rawInput).find((field) => field !== "mode" && field !== "sourceItemIdentifier");
|
|
108855
|
+
if (unexpectedField) {
|
|
108856
|
+
throw new ValidationError(`Question copy input contains an unexpected “${unexpectedField}” field`);
|
|
108857
|
+
}
|
|
108858
|
+
return { kind: "copy", sourceItemIdentifier };
|
|
108859
|
+
}
|
|
108860
|
+
function nonEmptyMetadataString(value) {
|
|
108861
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
108862
|
+
}
|
|
108863
|
+
function qtiQuestionOwnerTestIdentifier(item) {
|
|
108864
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
108865
|
+
return;
|
|
108866
|
+
}
|
|
108867
|
+
return nonEmptyMetadataString(item.metadata[PLAYCADEMY_QTI_OWNER_TEST_IDENTIFIER_KEY]);
|
|
108868
|
+
}
|
|
108869
|
+
function qtiTestReferencesItem(test, itemIdentifier) {
|
|
108870
|
+
return (test["qti-test-part"] ?? []).some((part) => (part["qti-assessment-section"] ?? []).some((section) => (section["qti-assessment-item-ref"] ?? []).some((reference) => reference.identifier === itemIdentifier)));
|
|
108871
|
+
}
|
|
108872
|
+
function resolveQtiQuestionOwnerGameSlug(item, ownerTest) {
|
|
108873
|
+
if (item.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
108874
|
+
return;
|
|
108875
|
+
}
|
|
108876
|
+
if ("ownerGameSlug" in item.metadata) {
|
|
108877
|
+
return nonEmptyMetadataString(item.metadata.ownerGameSlug);
|
|
108878
|
+
}
|
|
108879
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
108880
|
+
if (!ownerTestIdentifier || !ownerTest || ownerTest.identifier !== ownerTestIdentifier || !qtiTestReferencesItem(ownerTest, item.identifier) || ownerTest.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
108881
|
+
return;
|
|
108882
|
+
}
|
|
108883
|
+
return nonEmptyMetadataString(ownerTest.metadata.ownerGameSlug);
|
|
108884
|
+
}
|
|
108885
|
+
function isQtiQuestionOwnedByGame(item, gameSlug, ownerTest) {
|
|
108886
|
+
return resolveQtiQuestionOwnerGameSlug(item, ownerTest) === gameSlug;
|
|
108887
|
+
}
|
|
108888
|
+
function isQtiQuestionOwnedByContext(item, ownership, ownerTest) {
|
|
108889
|
+
const declaredOwnerTest = qtiQuestionOwnerTestIdentifier(item);
|
|
108890
|
+
if (declaredOwnerTest) {
|
|
108891
|
+
return declaredOwnerTest === ownership.testIdentifier && isQtiQuestionOwnedByGame(item, ownership.gameSlug, ownerTest);
|
|
108892
|
+
}
|
|
108893
|
+
return isQtiItemOwnedByTest(item, ownership.testIdentifier);
|
|
108894
|
+
}
|
|
108895
|
+
function normalizedQtiInteractionType(value) {
|
|
108896
|
+
if (typeof value !== "string") {
|
|
108897
|
+
return;
|
|
108898
|
+
}
|
|
108899
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
108900
|
+
return normalized || undefined;
|
|
108901
|
+
}
|
|
108902
|
+
function qtiXmlInteractionTypes(xml) {
|
|
108903
|
+
const markup = xml.replaceAll(/<!--[\s\S]*?-->/g, "").replaceAll(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
108904
|
+
const interactionPattern = /<(?!\/)(?:[A-Za-z_][\w.-]*:)?qti-([a-z][\w-]*)-interaction\b[^>]*>/gi;
|
|
108905
|
+
return [...markup.matchAll(interactionPattern)].map((match) => match[1].toLowerCase());
|
|
108906
|
+
}
|
|
108907
|
+
function supportedQtiQuestionInteractionType(question) {
|
|
108908
|
+
const interaction = recordValue(question.interaction);
|
|
108909
|
+
const structuredTypes = [
|
|
108910
|
+
normalizedQtiInteractionType(question.type),
|
|
108911
|
+
normalizedQtiInteractionType(interaction?.type)
|
|
108912
|
+
].filter((type) => Boolean(type));
|
|
108913
|
+
const distinctStructuredTypes = [...new Set(structuredTypes)];
|
|
108914
|
+
if (distinctStructuredTypes.length > 1) {
|
|
108915
|
+
return;
|
|
108916
|
+
}
|
|
108917
|
+
const rawXml = typeof question.rawXml === "string" ? question.rawXml : undefined;
|
|
108918
|
+
if (rawXml) {
|
|
108919
|
+
const xmlTypes = qtiXmlInteractionTypes(rawXml);
|
|
108920
|
+
if (xmlTypes.length !== 1) {
|
|
108921
|
+
return;
|
|
108922
|
+
}
|
|
108923
|
+
const [xmlType] = xmlTypes;
|
|
108924
|
+
const [structuredType2] = distinctStructuredTypes;
|
|
108925
|
+
if (structuredType2 && structuredType2 !== xmlType) {
|
|
108926
|
+
return;
|
|
108927
|
+
}
|
|
108928
|
+
return playcademySupportedQtiInteractionType(xmlType);
|
|
108929
|
+
}
|
|
108930
|
+
const [structuredType] = distinctStructuredTypes;
|
|
108931
|
+
return playcademySupportedQtiInteractionType(structuredType);
|
|
108932
|
+
}
|
|
108933
|
+
function assertSupportedQtiQuestionInteraction(question) {
|
|
108934
|
+
if (!supportedQtiQuestionInteractionType(question)) {
|
|
108935
|
+
throw new ValidationError("This QTI interaction type is not supported by the Playcademy question editor.");
|
|
108936
|
+
}
|
|
108937
|
+
}
|
|
108938
|
+
function invalidQtiCopyXml() {
|
|
108939
|
+
throw new ValidationError("The source question does not contain valid QTI item XML");
|
|
108940
|
+
}
|
|
108941
|
+
function markupEnd(xml, start2, allowInternalSubset) {
|
|
108942
|
+
let quote = null;
|
|
108943
|
+
let subsetDepth = 0;
|
|
108944
|
+
for (let index2 = start2;index2 < xml.length; index2 += 1) {
|
|
108945
|
+
const character = xml[index2];
|
|
108946
|
+
if (quote) {
|
|
108947
|
+
if (character === quote) {
|
|
108948
|
+
quote = null;
|
|
108949
|
+
}
|
|
108950
|
+
} else if (character === '"' || character === "'") {
|
|
108951
|
+
quote = character;
|
|
108952
|
+
} else if (allowInternalSubset && character === "[") {
|
|
108953
|
+
subsetDepth += 1;
|
|
108954
|
+
} else if (allowInternalSubset && character === "]") {
|
|
108955
|
+
subsetDepth = Math.max(0, subsetDepth - 1);
|
|
108956
|
+
} else if (character === ">" && subsetDepth === 0) {
|
|
108957
|
+
return index2;
|
|
108958
|
+
}
|
|
108959
|
+
}
|
|
108960
|
+
return invalidQtiCopyXml();
|
|
108961
|
+
}
|
|
108962
|
+
function skipXmlWhitespace(xml, start2, end = xml.length) {
|
|
108963
|
+
let cursor2 = start2;
|
|
108964
|
+
while (cursor2 < end && /\s/.test(xml[cursor2] ?? "")) {
|
|
108965
|
+
cursor2 += 1;
|
|
108966
|
+
}
|
|
108967
|
+
return cursor2;
|
|
108968
|
+
}
|
|
108969
|
+
function xmlPreambleEnd(xml, cursor2) {
|
|
108970
|
+
if (xml.startsWith("<?", cursor2)) {
|
|
108971
|
+
const end = xml.indexOf("?>", cursor2 + 2);
|
|
108972
|
+
if (end === -1) {
|
|
108973
|
+
return invalidQtiCopyXml();
|
|
108974
|
+
}
|
|
108975
|
+
return end + 2;
|
|
108976
|
+
}
|
|
108977
|
+
if (xml.startsWith("<!--", cursor2)) {
|
|
108978
|
+
const end = xml.indexOf("-->", cursor2 + 4);
|
|
108979
|
+
if (end === -1) {
|
|
108980
|
+
return invalidQtiCopyXml();
|
|
108981
|
+
}
|
|
108982
|
+
return end + 3;
|
|
108983
|
+
}
|
|
108984
|
+
if (/^<!DOCTYPE\b/i.test(xml.slice(cursor2))) {
|
|
108985
|
+
return markupEnd(xml, cursor2 + 2, true) + 1;
|
|
108986
|
+
}
|
|
108987
|
+
return null;
|
|
108988
|
+
}
|
|
108989
|
+
function qtiRootAttributes(xml, start2, end) {
|
|
108990
|
+
const attributes = [];
|
|
108991
|
+
let cursor2 = start2;
|
|
108992
|
+
while (cursor2 < end) {
|
|
108993
|
+
cursor2 = skipXmlWhitespace(xml, cursor2, end);
|
|
108994
|
+
if (xml[cursor2] === "/") {
|
|
108995
|
+
cursor2 += 1;
|
|
108996
|
+
} else if (cursor2 < end) {
|
|
108997
|
+
const nameMatch = /^[A-Za-z_][\w.:-]*/.exec(xml.slice(cursor2, end));
|
|
108998
|
+
if (!nameMatch) {
|
|
108999
|
+
return invalidQtiCopyXml();
|
|
109000
|
+
}
|
|
109001
|
+
const name3 = nameMatch[0];
|
|
109002
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + name3.length, end);
|
|
109003
|
+
if (xml[cursor2] !== "=") {
|
|
109004
|
+
return invalidQtiCopyXml();
|
|
109005
|
+
}
|
|
109006
|
+
cursor2 = skipXmlWhitespace(xml, cursor2 + 1, end);
|
|
109007
|
+
const quote = xml[cursor2];
|
|
109008
|
+
if (quote !== '"' && quote !== "'") {
|
|
109009
|
+
return invalidQtiCopyXml();
|
|
109010
|
+
}
|
|
109011
|
+
const valueStart = cursor2 + 1;
|
|
109012
|
+
const valueEnd = xml.indexOf(quote, valueStart);
|
|
109013
|
+
if (valueEnd === -1 || valueEnd > end) {
|
|
109014
|
+
return invalidQtiCopyXml();
|
|
109015
|
+
}
|
|
109016
|
+
attributes.push({ name: name3, quote, valueStart, valueEnd });
|
|
109017
|
+
cursor2 = valueEnd + 1;
|
|
109018
|
+
}
|
|
109019
|
+
}
|
|
109020
|
+
return attributes;
|
|
109021
|
+
}
|
|
109022
|
+
function qtiItemStartTag(xml) {
|
|
109023
|
+
let cursor2 = xml.charCodeAt(0) === 65279 ? 1 : 0;
|
|
109024
|
+
while (cursor2 < xml.length) {
|
|
109025
|
+
cursor2 = skipXmlWhitespace(xml, cursor2);
|
|
109026
|
+
const preambleEnd = xmlPreambleEnd(xml, cursor2);
|
|
109027
|
+
if (preambleEnd !== null) {
|
|
109028
|
+
cursor2 = preambleEnd;
|
|
109029
|
+
} else {
|
|
109030
|
+
const root = /^<([A-Za-z_][\w.:-]*)/.exec(xml.slice(cursor2));
|
|
109031
|
+
if (!root || root[1].split(":").at(-1) !== "qti-assessment-item") {
|
|
109032
|
+
return invalidQtiCopyXml();
|
|
109033
|
+
}
|
|
109034
|
+
const attributesStart = cursor2 + root[0].length;
|
|
109035
|
+
const end = markupEnd(xml, attributesStart, false);
|
|
109036
|
+
const attributes = qtiRootAttributes(xml, attributesStart, end);
|
|
109037
|
+
let insertionPoint = skipXmlWhitespaceBackward(xml, end);
|
|
109038
|
+
if (xml[insertionPoint - 1] === "/") {
|
|
109039
|
+
insertionPoint -= 1;
|
|
109040
|
+
}
|
|
109041
|
+
return { attributes, insertionPoint };
|
|
109042
|
+
}
|
|
109043
|
+
}
|
|
109044
|
+
return invalidQtiCopyXml();
|
|
109045
|
+
}
|
|
109046
|
+
function skipXmlWhitespaceBackward(xml, start2) {
|
|
109047
|
+
let cursor2 = start2;
|
|
109048
|
+
while (/\s/.test(xml[cursor2 - 1] ?? "")) {
|
|
109049
|
+
cursor2 -= 1;
|
|
109050
|
+
}
|
|
109051
|
+
return cursor2;
|
|
109052
|
+
}
|
|
109053
|
+
function escapeXmlAttribute(value, quote) {
|
|
109054
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(quote, quote === '"' ? """ : "'");
|
|
109055
|
+
}
|
|
109056
|
+
function rewriteQtiItemIdentity(xml, identifier, title) {
|
|
109057
|
+
if (!xml.trim() || !identifier.trim() || !title.trim()) {
|
|
109058
|
+
return invalidQtiCopyXml();
|
|
109059
|
+
}
|
|
109060
|
+
const root = qtiItemStartTag(xml);
|
|
109061
|
+
const replacements = [];
|
|
109062
|
+
const targetAttributes = new Map([
|
|
109063
|
+
["identifier", identifier],
|
|
109064
|
+
["title", title]
|
|
109065
|
+
]);
|
|
109066
|
+
for (const [name3, value] of targetAttributes) {
|
|
109067
|
+
const matches = root.attributes.filter((attribute2) => attribute2.name === name3);
|
|
109068
|
+
if (matches.length > 1) {
|
|
109069
|
+
return invalidQtiCopyXml();
|
|
109070
|
+
}
|
|
109071
|
+
const attribute = matches[0];
|
|
109072
|
+
if (attribute) {
|
|
109073
|
+
replacements.push({
|
|
109074
|
+
start: attribute.valueStart,
|
|
109075
|
+
end: attribute.valueEnd,
|
|
109076
|
+
value: escapeXmlAttribute(value, attribute.quote)
|
|
109077
|
+
});
|
|
109078
|
+
} else {
|
|
109079
|
+
replacements.push({
|
|
109080
|
+
start: root.insertionPoint,
|
|
109081
|
+
end: root.insertionPoint,
|
|
109082
|
+
value: ` ${name3}="${escapeXmlAttribute(value, '"')}"`
|
|
109083
|
+
});
|
|
109084
|
+
}
|
|
109085
|
+
}
|
|
109086
|
+
return replacements.toSorted((left, right) => right.start - left.start).reduce((rewritten, replacement) => `${rewritten.slice(0, replacement.start)}${replacement.value}${rewritten.slice(replacement.end)}`, xml);
|
|
109087
|
+
}
|
|
109088
|
+
function buildQtiQuestionCopyInput(input) {
|
|
109089
|
+
assertSupportedQtiQuestionInteraction(input.source);
|
|
109090
|
+
const title = input.source.title?.trim() || input.source.identifier;
|
|
109091
|
+
if (!input.source.rawXml) {
|
|
109092
|
+
return invalidQtiCopyXml();
|
|
109093
|
+
}
|
|
109094
|
+
const sourceMetadata = { ...input.source.metadata };
|
|
109095
|
+
if (sourceMetadata.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM) {
|
|
109096
|
+
Reflect.deleteProperty(sourceMetadata, PLAYCADEMY_QTI_AUTHORING_METADATA_KEY);
|
|
109097
|
+
}
|
|
109098
|
+
const metadata2 = buildOwnedQtiQuestionMetadata({
|
|
109099
|
+
metadata: {
|
|
109100
|
+
copiedFromItemIdentifier: input.source.identifier,
|
|
109101
|
+
integrationId: input.integrationId
|
|
109102
|
+
}
|
|
109103
|
+
}, {
|
|
109104
|
+
gameSlug: input.gameSlug,
|
|
109105
|
+
testIdentifier: input.targetTestIdentifier
|
|
109106
|
+
}, sourceMetadata);
|
|
109107
|
+
return {
|
|
109108
|
+
identifier: input.targetIdentifier,
|
|
109109
|
+
title,
|
|
109110
|
+
xml: rewriteQtiItemIdentity(input.source.rawXml, input.targetIdentifier, title),
|
|
109111
|
+
metadata: metadata2
|
|
109112
|
+
};
|
|
109113
|
+
}
|
|
109114
|
+
function assertQtiQuestionEditable(item, ownership, ownerTest) {
|
|
109115
|
+
if (!isQtiQuestionOwnedByContext(item, ownership, ownerTest)) {
|
|
109116
|
+
throw new ValidationError("Shared question references are read-only. Copy the question to create an editable independent item.");
|
|
109117
|
+
}
|
|
109118
|
+
}
|
|
109119
|
+
function buildNumericQuestionXml(input, identifier, title) {
|
|
109120
|
+
if (input.format === "xml") {
|
|
109121
|
+
throw new ValidationError("Raw question XML is not accepted at this API boundary");
|
|
109122
|
+
}
|
|
109123
|
+
const candidate = input.numericTextEntry;
|
|
109124
|
+
if (candidate === undefined) {
|
|
109125
|
+
return null;
|
|
109126
|
+
}
|
|
109127
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
109128
|
+
throw new ValidationError("Numeric text-entry data is invalid");
|
|
109129
|
+
}
|
|
109130
|
+
const prompt = "prompt" in candidate ? candidate.prompt : undefined;
|
|
109131
|
+
const numeric3 = parseNumericTextEntry({
|
|
109132
|
+
baseType: "baseType" in candidate ? candidate.baseType : undefined,
|
|
109133
|
+
answer: "answer" in candidate ? candidate.answer : undefined,
|
|
109134
|
+
comparison: "comparison" in candidate ? candidate.comparison : undefined
|
|
109135
|
+
});
|
|
109136
|
+
if (!numeric3.success) {
|
|
109137
|
+
throw new ValidationError(numeric3.message);
|
|
109138
|
+
}
|
|
109139
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
109140
|
+
throw new ValidationError("Numeric questions require a title and prompt");
|
|
109141
|
+
}
|
|
109142
|
+
return numericTextEntryXml({ identifier, title, prompt: prompt.trim(), numeric: numeric3.value });
|
|
109143
|
+
}
|
|
109144
|
+
function buildExactMatchQuestionXml(input) {
|
|
109145
|
+
const responseIdentifier = escapeXml(input.responseIdentifier);
|
|
109146
|
+
const correctValues = input.correctIdentifiers.map((identifier) => ` <qti-value>${escapeXml(identifier)}</qti-value>`).join(`
|
|
109147
|
+
`);
|
|
109148
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
109149
|
+
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" identifier="${escapeXml(input.identifier)}" title="${escapeXml(input.title)}" adaptive="false" time-dependent="false">
|
|
109150
|
+
<qti-response-declaration identifier="${responseIdentifier}" cardinality="${input.cardinality}" base-type="identifier">
|
|
109151
|
+
<qti-correct-response>
|
|
109152
|
+
${correctValues}
|
|
109153
|
+
</qti-correct-response>
|
|
109154
|
+
</qti-response-declaration>
|
|
109155
|
+
<qti-outcome-declaration identifier="FEEDBACK" cardinality="single" base-type="identifier" />
|
|
109156
|
+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
|
|
109157
|
+
<qti-default-value><qti-value>0</qti-value></qti-default-value>
|
|
109158
|
+
</qti-outcome-declaration>
|
|
109159
|
+
<qti-item-body>
|
|
109160
|
+
${input.itemBody}
|
|
109161
|
+
</qti-item-body>
|
|
109162
|
+
<qti-response-processing>
|
|
109163
|
+
<qti-response-condition>
|
|
109164
|
+
<qti-response-if>
|
|
109165
|
+
<qti-match><qti-variable identifier="${responseIdentifier}" /><qti-correct identifier="${responseIdentifier}" /></qti-match>
|
|
109166
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">CORRECT</qti-base-value></qti-set-outcome-value>
|
|
109167
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">1</qti-base-value></qti-set-outcome-value>
|
|
109168
|
+
</qti-response-if>
|
|
109169
|
+
<qti-response-else>
|
|
109170
|
+
<qti-set-outcome-value identifier="FEEDBACK"><qti-base-value base-type="identifier">INCORRECT</qti-base-value></qti-set-outcome-value>
|
|
109171
|
+
<qti-set-outcome-value identifier="SCORE"><qti-base-value base-type="float">0</qti-base-value></qti-set-outcome-value>
|
|
109172
|
+
</qti-response-else>
|
|
109173
|
+
</qti-response-condition>
|
|
109174
|
+
</qti-response-processing>
|
|
109175
|
+
</qti-assessment-item>`;
|
|
109176
|
+
}
|
|
109177
|
+
function parseInlineChoices(value) {
|
|
109178
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
109179
|
+
throw new ValidationError("Inline-choice questions require at least two choices");
|
|
109180
|
+
}
|
|
109181
|
+
const choices = value.map((choice) => {
|
|
109182
|
+
const record3 = recordValue(choice);
|
|
109183
|
+
const identifier = record3?.identifier;
|
|
109184
|
+
const content = record3?.content;
|
|
109185
|
+
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
|
|
109186
|
+
throw new ValidationError("Inline-choice options are invalid");
|
|
109187
|
+
}
|
|
109188
|
+
return { identifier: identifier.trim(), content: content.trim() };
|
|
109189
|
+
});
|
|
109190
|
+
if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
|
|
109191
|
+
throw new ValidationError("Inline-choice option identifiers must be unique");
|
|
109192
|
+
}
|
|
109193
|
+
return choices;
|
|
109194
|
+
}
|
|
109195
|
+
function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
|
|
109196
|
+
const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
|
|
109197
|
+
const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
|
|
109198
|
+
const correctResponse = recordValue(declaration?.correctResponse);
|
|
109199
|
+
const rawValues = correctResponse?.value;
|
|
109200
|
+
const values = Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : [];
|
|
109201
|
+
if (declaration?.cardinality !== "single" || declaration.baseType !== "identifier") {
|
|
109202
|
+
throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
|
|
109203
|
+
}
|
|
109204
|
+
if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
|
|
109205
|
+
throw new ValidationError("Inline-choice questions require one valid correct option");
|
|
109206
|
+
}
|
|
109207
|
+
return values[0];
|
|
109208
|
+
}
|
|
109209
|
+
function buildInlineChoiceQuestionXml(input, identifier, title) {
|
|
109210
|
+
const interaction = recordValue(input.interaction);
|
|
109211
|
+
const interactionType = normalizedQtiInteractionType(interaction?.type ?? input.type);
|
|
109212
|
+
if (interactionType !== "inline-choice") {
|
|
109213
|
+
return null;
|
|
109214
|
+
}
|
|
109215
|
+
const responseIdentifier = interaction?.responseIdentifier;
|
|
109216
|
+
const structure = recordValue(interaction?.questionStructure);
|
|
109217
|
+
const prompt = structure?.prompt;
|
|
109218
|
+
if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
109219
|
+
throw new ValidationError("Inline-choice question data is invalid");
|
|
109220
|
+
}
|
|
109221
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
109222
|
+
throw new ValidationError("Inline-choice questions require a title and prompt");
|
|
109223
|
+
}
|
|
109224
|
+
const normalizedPrompt = prompt.trim();
|
|
109225
|
+
const promptParts = normalizedPrompt.split(INLINE_CHOICE_BLANK);
|
|
109226
|
+
if (promptParts.length !== 2) {
|
|
109227
|
+
throw new ValidationError("Inline-choice questions require exactly one blank");
|
|
109228
|
+
}
|
|
109229
|
+
const choices = parseInlineChoices(structure.inlineChoices);
|
|
109230
|
+
const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
|
|
109231
|
+
const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
|
|
109232
|
+
const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
|
|
109233
|
+
`);
|
|
109234
|
+
const [before = "", after = ""] = promptParts;
|
|
109235
|
+
const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
|
|
109236
|
+
${choicesXml}
|
|
109237
|
+
</qti-inline-choice-interaction>${escapeXml(after)}</p>`;
|
|
109238
|
+
return buildExactMatchQuestionXml({
|
|
109239
|
+
identifier,
|
|
109240
|
+
title,
|
|
109241
|
+
responseIdentifier,
|
|
109242
|
+
cardinality: "single",
|
|
109243
|
+
correctIdentifiers: [correctIdentifier],
|
|
109244
|
+
itemBody
|
|
109245
|
+
});
|
|
109246
|
+
}
|
|
109247
|
+
function parseHottextSegments(value) {
|
|
109248
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
109249
|
+
throw new ValidationError("Hottext questions require passage segments");
|
|
109250
|
+
}
|
|
109251
|
+
const segments = value.map((segment) => {
|
|
109252
|
+
const record3 = recordValue(segment);
|
|
109253
|
+
const identifier = record3?.identifier;
|
|
109254
|
+
const content = record3?.content;
|
|
109255
|
+
const mode = record3?.mode;
|
|
109256
|
+
if (typeof identifier !== "string" || typeof content !== "string" || !content.trim() || !["plain", "option", "correct"].includes(String(mode))) {
|
|
109257
|
+
throw new ValidationError("Hottext passage segments are invalid");
|
|
109258
|
+
}
|
|
109259
|
+
return {
|
|
109260
|
+
identifier: identifier.trim(),
|
|
109261
|
+
content: content.trim(),
|
|
109262
|
+
mode
|
|
109263
|
+
};
|
|
109264
|
+
});
|
|
109265
|
+
const selectable = segments.filter((segment) => segment.mode !== "plain");
|
|
109266
|
+
const correct = selectable.filter((segment) => segment.mode === "correct");
|
|
109267
|
+
const identifiers = selectable.map((segment) => segment.identifier);
|
|
109268
|
+
if (selectable.length < 2) {
|
|
109269
|
+
throw new ValidationError("Hottext questions require at least two selectable phrases");
|
|
109270
|
+
}
|
|
109271
|
+
if (identifiers.some((identifier) => !identifier)) {
|
|
109272
|
+
throw new ValidationError("Hottext selectable phrases require identifiers");
|
|
109273
|
+
}
|
|
109274
|
+
if (new Set(identifiers).size !== identifiers.length) {
|
|
109275
|
+
throw new ValidationError("Hottext selectable phrase identifiers must be unique");
|
|
109276
|
+
}
|
|
109277
|
+
if (correct.length === 0) {
|
|
109278
|
+
throw new ValidationError("Hottext questions require a correct phrase");
|
|
109279
|
+
}
|
|
109280
|
+
return { segments, selectable, correct };
|
|
109281
|
+
}
|
|
109282
|
+
function buildHottextQuestionXml(input, identifier, title) {
|
|
109283
|
+
const candidate = input.hottext;
|
|
109284
|
+
if (candidate === undefined) {
|
|
109285
|
+
return null;
|
|
109286
|
+
}
|
|
109287
|
+
const hottext = recordValue(candidate);
|
|
109288
|
+
if (!hottext) {
|
|
109289
|
+
throw new ValidationError("Hottext question data is invalid");
|
|
109290
|
+
}
|
|
109291
|
+
const prompt = hottext.prompt;
|
|
109292
|
+
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
109293
|
+
throw new ValidationError("Hottext questions require a title and prompt");
|
|
109294
|
+
}
|
|
109295
|
+
const { segments, selectable, correct } = parseHottextSegments(hottext.segments);
|
|
109296
|
+
const multiple = correct.length > 1;
|
|
109297
|
+
const maxChoices = hottextMaxChoices(selectable.length, multiple);
|
|
109298
|
+
const passage = segments.map((segment) => segment.mode === "plain" ? escapeXml(segment.content) : `<qti-hottext identifier="${escapeXml(segment.identifier)}">${escapeXml(segment.content)}</qti-hottext>`).join(" ");
|
|
109299
|
+
const itemBody = ` <qti-hottext-interaction response-identifier="RESPONSE" max-choices="${maxChoices}">
|
|
109300
|
+
<qti-prompt>${escapeXml(prompt.trim())}</qti-prompt>
|
|
109301
|
+
<p>${passage}</p>
|
|
109302
|
+
</qti-hottext-interaction>`;
|
|
109303
|
+
return buildExactMatchQuestionXml({
|
|
109304
|
+
identifier,
|
|
109305
|
+
title,
|
|
109306
|
+
responseIdentifier: "RESPONSE",
|
|
109307
|
+
cardinality: multiple ? "multiple" : "single",
|
|
109308
|
+
correctIdentifiers: correct.map((segment) => segment.identifier),
|
|
109309
|
+
itemBody
|
|
109310
|
+
});
|
|
109311
|
+
}
|
|
109312
|
+
function buildQuestionXml(input, identifier, title) {
|
|
109313
|
+
for (const build2 of QUESTION_XML_BUILDERS) {
|
|
109314
|
+
const xml = build2(input, identifier, title);
|
|
109315
|
+
if (xml !== null) {
|
|
109316
|
+
return xml;
|
|
109317
|
+
}
|
|
109318
|
+
}
|
|
109319
|
+
return null;
|
|
109320
|
+
}
|
|
109321
|
+
function buildCreatedQtiQuestionReference(input) {
|
|
109322
|
+
return {
|
|
109323
|
+
ownership: "owned",
|
|
109324
|
+
reference: {
|
|
109325
|
+
identifier: input.itemIdentifier,
|
|
109326
|
+
href: input.href,
|
|
109327
|
+
testPart: input.partIdentifier,
|
|
109328
|
+
section: input.sectionIdentifier
|
|
109329
|
+
},
|
|
109330
|
+
question: mergeQtiQuestionItem(input.item, input.fallbackItem, input.itemIdentifier, input.metadata)
|
|
109331
|
+
};
|
|
109332
|
+
}
|
|
109333
|
+
function buildHydratedQtiQuestionReference(input) {
|
|
109334
|
+
const identifier = input.reference.reference.identifier;
|
|
109335
|
+
const ownerGameSlug = resolveQtiQuestionOwnerGameSlug(input.item, input.ownerTest);
|
|
109336
|
+
const authoritativeItem = ownerGameSlug ? {
|
|
109337
|
+
...input.item,
|
|
109338
|
+
metadata: { ...input.item.metadata, ownerGameSlug }
|
|
109339
|
+
} : input.item;
|
|
109340
|
+
const question = mergeQtiQuestionItem(authoritativeItem, input.reference.question, identifier);
|
|
109341
|
+
return {
|
|
109342
|
+
...input.reference,
|
|
109343
|
+
ownership: isQtiQuestionOwnedByContext(question, { gameSlug: input.gameSlug, testIdentifier: input.testIdentifier }, input.ownerTest) ? "owned" : "shared",
|
|
109344
|
+
question
|
|
109345
|
+
};
|
|
109346
|
+
}
|
|
109347
|
+
function isQtiTestOwnedByGame(test, gameSlug) {
|
|
109348
|
+
return test.metadata?.ownerSystem === PLAYCADEMY_QTI_OWNER_SYSTEM && test.metadata.ownerGameSlug === gameSlug;
|
|
109349
|
+
}
|
|
109350
|
+
function assertQtiTestOwnedByGame(test, gameSlug) {
|
|
109351
|
+
if (!isQtiTestOwnedByGame(test, gameSlug)) {
|
|
109352
|
+
throw new ValidationError("Shared assessment references are read-only. Copy the assessment to create an editable independent test.");
|
|
109353
|
+
}
|
|
109354
|
+
}
|
|
109355
|
+
function qtiTestParts(test) {
|
|
109356
|
+
const parts2 = test["qti-test-part"];
|
|
109357
|
+
if (!Array.isArray(parts2) || parts2.length === 0) {
|
|
109358
|
+
throw new ValidationError(`Assessment ${test.identifier} has no test parts`);
|
|
109359
|
+
}
|
|
109360
|
+
for (const [partIndex, part] of parts2.entries()) {
|
|
109361
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
|
109362
|
+
throw new ValidationError(`Assessment ${test.identifier} contains an invalid test part at position ${partIndex + 1}`);
|
|
109363
|
+
}
|
|
109364
|
+
const sections = part["qti-assessment-section"];
|
|
109365
|
+
if (!Array.isArray(sections)) {
|
|
109366
|
+
throw new ValidationError(`Assessment ${test.identifier} test part ${partIndex + 1} has no section list`);
|
|
109367
|
+
}
|
|
109368
|
+
}
|
|
109369
|
+
return parts2;
|
|
109370
|
+
}
|
|
109371
|
+
function qtiTestOptionalAttributes(test) {
|
|
109372
|
+
return {
|
|
109373
|
+
...test.qtiVersion ? { qtiVersion: test.qtiVersion } : {},
|
|
109374
|
+
...test.timeLimit !== undefined ? { timeLimit: test.timeLimit } : {},
|
|
109375
|
+
...test.maxAttempts !== undefined ? { maxAttempts: test.maxAttempts } : {},
|
|
109376
|
+
...test.toolsEnabled !== undefined ? { toolsEnabled: test.toolsEnabled } : {}
|
|
109377
|
+
};
|
|
109378
|
+
}
|
|
109379
|
+
function buildQtiAssessmentItemCopyPlan(test, targetTestIdentifier, hrefForIdentifier, createIdentifier = newPlaycademyQuestionIdentifier) {
|
|
109380
|
+
const sourceIdentifiers = [];
|
|
109381
|
+
const seen = new Set;
|
|
109382
|
+
for (const part of qtiTestParts(test)) {
|
|
109383
|
+
for (const section of part["qti-assessment-section"]) {
|
|
109384
|
+
for (const reference of section["qti-assessment-item-ref"] ?? []) {
|
|
109385
|
+
if (!seen.has(reference.identifier)) {
|
|
109386
|
+
seen.add(reference.identifier);
|
|
109387
|
+
sourceIdentifiers.push(reference.identifier);
|
|
109388
|
+
}
|
|
109389
|
+
}
|
|
109390
|
+
}
|
|
109391
|
+
}
|
|
109392
|
+
return sourceIdentifiers.map((sourceIdentifier) => {
|
|
109393
|
+
const targetIdentifier = createIdentifier(targetTestIdentifier);
|
|
109394
|
+
return {
|
|
109395
|
+
sourceIdentifier,
|
|
109396
|
+
targetIdentifier,
|
|
109397
|
+
href: hrefForIdentifier(targetIdentifier)
|
|
109398
|
+
};
|
|
109399
|
+
});
|
|
109400
|
+
}
|
|
109401
|
+
function buildQtiTestStructureInput(test, targetTestIdentifier, itemCopies) {
|
|
109402
|
+
const outcomeDeclarations = test["qti-outcome-declaration"];
|
|
109403
|
+
if (outcomeDeclarations !== undefined && !Array.isArray(outcomeDeclarations)) {
|
|
109404
|
+
throw new ValidationError(`Assessment ${test.identifier} has an invalid outcome declaration list`);
|
|
109405
|
+
}
|
|
109406
|
+
return {
|
|
109407
|
+
"qti-test-part": qtiTestParts(test).map((part, partIndex) => ({
|
|
109408
|
+
identifier: targetTestIdentifier ? `${targetTestIdentifier}-part${partIndex + 1}` : part.identifier,
|
|
109409
|
+
navigationMode: part.navigationMode,
|
|
109410
|
+
submissionMode: part.submissionMode,
|
|
109411
|
+
"qti-assessment-section": part["qti-assessment-section"].map((section, sectionIndex) => {
|
|
109412
|
+
let sectionIdentifier = section.identifier;
|
|
109413
|
+
if (targetTestIdentifier) {
|
|
109414
|
+
sectionIdentifier = partIndex === 0 && sectionIndex === 0 ? `${targetTestIdentifier}-section1` : `${targetTestIdentifier}-part${partIndex + 1}-section${sectionIndex + 1}`;
|
|
109415
|
+
}
|
|
109416
|
+
return {
|
|
109417
|
+
identifier: sectionIdentifier,
|
|
109418
|
+
title: section.title,
|
|
109419
|
+
visible: section.visible ?? true,
|
|
109420
|
+
...section.required !== undefined ? { required: section.required } : {},
|
|
109421
|
+
...section.fixed !== undefined ? { fixed: section.fixed } : {},
|
|
109422
|
+
sequence: section.sequence ?? sectionIndex + 1,
|
|
109423
|
+
...section["qti-assessment-item-ref"] ? {
|
|
109424
|
+
"qti-assessment-item-ref": section["qti-assessment-item-ref"].map((item, itemIndex) => {
|
|
109425
|
+
const copy = itemCopies?.get(item.identifier);
|
|
109426
|
+
if (itemCopies && !copy) {
|
|
109427
|
+
throw new ValidationError(`Assessment copy is missing question ${item.identifier}`);
|
|
109428
|
+
}
|
|
109429
|
+
return {
|
|
109430
|
+
identifier: copy?.targetIdentifier ?? item.identifier,
|
|
109431
|
+
href: copy?.href ?? item.href,
|
|
109432
|
+
sequence: item.sequence ?? itemIndex + 1
|
|
109433
|
+
};
|
|
109434
|
+
})
|
|
109435
|
+
} : {}
|
|
109436
|
+
};
|
|
109437
|
+
})
|
|
109438
|
+
})),
|
|
109439
|
+
...outcomeDeclarations ? {
|
|
109440
|
+
"qti-outcome-declaration": outcomeDeclarations.map((declaration) => ({
|
|
109441
|
+
identifier: declaration.identifier,
|
|
109442
|
+
...declaration.cardinality !== undefined ? { cardinality: declaration.cardinality } : {},
|
|
109443
|
+
baseType: declaration.baseType,
|
|
109444
|
+
...declaration.normalMaximum !== undefined ? { normalMaximum: declaration.normalMaximum } : {},
|
|
109445
|
+
...declaration.normalMinimum !== undefined ? { normalMinimum: declaration.normalMinimum } : {},
|
|
109446
|
+
...declaration.defaultValue ? {
|
|
109447
|
+
defaultValue: declaration.defaultValue.value !== undefined ? { value: declaration.defaultValue.value } : {}
|
|
109448
|
+
} : {}
|
|
109449
|
+
}))
|
|
109450
|
+
} : {}
|
|
109451
|
+
};
|
|
109452
|
+
}
|
|
109453
|
+
function buildQtiTestUpdateInput(test, title) {
|
|
109454
|
+
return {
|
|
109455
|
+
title,
|
|
109456
|
+
...qtiTestOptionalAttributes(test),
|
|
109457
|
+
...test.metadata ? { metadata: test.metadata } : {},
|
|
109458
|
+
...buildQtiTestStructureInput(test)
|
|
109459
|
+
};
|
|
109460
|
+
}
|
|
109461
|
+
function buildQtiTestCopyInput(source, targetTestIdentifier, metadata2, itemCopies) {
|
|
109462
|
+
const copiesBySourceIdentifier = new Map(itemCopies.map((copy) => [copy.sourceIdentifier, copy]));
|
|
109463
|
+
return {
|
|
109464
|
+
identifier: targetTestIdentifier,
|
|
109465
|
+
title: `${source.title} (copy)`,
|
|
109466
|
+
...qtiTestOptionalAttributes(source),
|
|
109467
|
+
metadata: metadata2,
|
|
109468
|
+
...buildQtiTestStructureInput(source, targetTestIdentifier, copiesBySourceIdentifier)
|
|
109469
|
+
};
|
|
109470
|
+
}
|
|
109471
|
+
function validateUniqueQuestionIdentifiers(itemIdentifiers) {
|
|
109472
|
+
if (itemIdentifiers.length === 0 || new Set(itemIdentifiers).size !== itemIdentifiers.length) {
|
|
109473
|
+
throw new ValidationError("Question order must contain unique question identifiers");
|
|
109474
|
+
}
|
|
109475
|
+
}
|
|
109476
|
+
function resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers) {
|
|
109477
|
+
let selectedPart;
|
|
109478
|
+
let selectedSection;
|
|
109479
|
+
for (const part of qtiTestParts(test)) {
|
|
109480
|
+
for (const section of part["qti-assessment-section"]) {
|
|
109481
|
+
if (!itemIdentifier || section["qti-assessment-item-ref"]?.some((item) => item.identifier === itemIdentifier)) {
|
|
109482
|
+
selectedPart = part;
|
|
109483
|
+
selectedSection = section;
|
|
109484
|
+
break;
|
|
109485
|
+
}
|
|
109486
|
+
}
|
|
109487
|
+
if (selectedSection) {
|
|
109488
|
+
break;
|
|
109489
|
+
}
|
|
109490
|
+
}
|
|
109491
|
+
if (!selectedPart || !selectedSection) {
|
|
109492
|
+
throw new ValidationError(itemIdentifier ? `Question ${itemIdentifier} is not referenced by assessment ${qtiTestIdentifier}` : `Assessment ${qtiTestIdentifier} has no section for questions`);
|
|
109493
|
+
}
|
|
109494
|
+
if (expectedItemIdentifiers) {
|
|
109495
|
+
const sectionIdentifiers = selectedSection["qti-assessment-item-ref"]?.map((item) => item.identifier) ?? [];
|
|
109496
|
+
const expectedIdentifiers = new Set(expectedItemIdentifiers);
|
|
109497
|
+
if (expectedIdentifiers.size !== sectionIdentifiers.length || sectionIdentifiers.some((identifier) => !expectedIdentifiers.has(identifier))) {
|
|
109498
|
+
throw new ValidationError("Questions can only be reordered within one complete assessment section");
|
|
109499
|
+
}
|
|
109500
|
+
}
|
|
109501
|
+
return {
|
|
109502
|
+
partIdentifier: selectedPart.identifier,
|
|
109503
|
+
sectionIdentifier: selectedSection.identifier
|
|
109504
|
+
};
|
|
109505
|
+
}
|
|
109506
|
+
var PLAYCADEMY_QTI_AUTHORING_METADATA_KEY = "playcademyAuthoring";
|
|
109507
|
+
var QTI_AUTHORING_FIELDS;
|
|
109508
|
+
var QUESTION_XML_BUILDERS;
|
|
109509
|
+
var init_timeback_qti_authoring_util = __esm(() => {
|
|
109510
|
+
init_timeback3();
|
|
109511
|
+
init_errors();
|
|
109512
|
+
QTI_AUTHORING_FIELDS = [
|
|
109513
|
+
"type",
|
|
109514
|
+
"qtiVersion",
|
|
109515
|
+
"timeDependent",
|
|
109516
|
+
"adaptive",
|
|
109517
|
+
"preInteraction",
|
|
109518
|
+
"interaction",
|
|
109519
|
+
"postInteraction",
|
|
109520
|
+
"responseDeclarations",
|
|
109521
|
+
"outcomeDeclarations",
|
|
109522
|
+
"responseProcessing",
|
|
109523
|
+
"modalFeedback",
|
|
109524
|
+
"feedbackInline",
|
|
109525
|
+
"feedbackBlock",
|
|
109526
|
+
"rubrics",
|
|
109527
|
+
"stimulus",
|
|
109528
|
+
"content"
|
|
109529
|
+
];
|
|
109530
|
+
QUESTION_XML_BUILDERS = [
|
|
109531
|
+
buildNumericQuestionXml,
|
|
109532
|
+
buildHottextQuestionXml,
|
|
109533
|
+
buildInlineChoiceQuestionXml
|
|
109534
|
+
];
|
|
109535
|
+
});
|
|
109536
|
+
function newPlaycademyTestIdentifier() {
|
|
109537
|
+
return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
|
|
109538
|
+
}
|
|
109539
|
+
function playcademySourceWhere() {
|
|
109540
|
+
return {
|
|
109541
|
+
identifier: {
|
|
109542
|
+
gte: PLAYCADEMY_QTI_SOURCE_PREFIX,
|
|
109543
|
+
lt: PLAYCADEMY_QTI_SOURCE_UPPER_BOUND
|
|
109544
|
+
}
|
|
109545
|
+
};
|
|
109546
|
+
}
|
|
109547
|
+
function buildQtiLibraryListPlan(params) {
|
|
109548
|
+
const query = params.query?.trim() || undefined;
|
|
109549
|
+
if (!params.source && !query) {
|
|
109550
|
+
throw new Error("An exact QTI source identifier is required for a global lookup");
|
|
109551
|
+
}
|
|
109552
|
+
let where;
|
|
109553
|
+
if (params.source) {
|
|
109554
|
+
where = playcademySourceWhere();
|
|
109555
|
+
} else if (query) {
|
|
109556
|
+
where = { identifier: query };
|
|
109557
|
+
}
|
|
109558
|
+
return {
|
|
109559
|
+
kind: "direct",
|
|
109560
|
+
params: {
|
|
109561
|
+
...where ? { where } : {},
|
|
109562
|
+
...params.source && query ? { query } : {},
|
|
109563
|
+
page: params.page,
|
|
109564
|
+
limit: params.limit,
|
|
109565
|
+
...params.source ? { sort: "identifier", order: "asc" } : {}
|
|
109566
|
+
}
|
|
109567
|
+
};
|
|
109568
|
+
}
|
|
109569
|
+
var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-";
|
|
109570
|
+
var PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
|
|
108701
109571
|
|
|
108702
109572
|
class TimebackAssessmentsService {
|
|
109573
|
+
static QTI_HYDRATION_CONCURRENCY = 8;
|
|
108703
109574
|
deps;
|
|
108704
109575
|
constructor(deps) {
|
|
108705
109576
|
this.deps = deps;
|
|
@@ -108716,33 +109587,19 @@ class TimebackAssessmentsService {
|
|
|
108716
109587
|
}
|
|
108717
109588
|
async listAssessments(integrationId) {
|
|
108718
109589
|
const client = this.requireClient();
|
|
108719
|
-
await this.
|
|
109590
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
108720
109591
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
108721
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
108722
|
-
orderBy: asc(gameTimebackAssessmentTests.sortOrder)
|
|
109592
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
108723
109593
|
});
|
|
108724
|
-
|
|
108725
|
-
return [];
|
|
108726
|
-
}
|
|
108727
|
-
const assessments = await Promise.all(rows.map(async (row) => {
|
|
109594
|
+
const assessments = await runWithConcurrency(rows, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
108728
109595
|
try {
|
|
108729
109596
|
const test = await client.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
108730
|
-
let itemCount = 0;
|
|
108731
|
-
for (const part of test["qti-test-part"] ?? []) {
|
|
108732
|
-
for (const section of part["qti-assessment-section"] ?? []) {
|
|
108733
|
-
itemCount += section["qti-assessment-item-ref"]?.length ?? 0;
|
|
108734
|
-
}
|
|
108735
|
-
}
|
|
108736
109597
|
return {
|
|
108737
|
-
|
|
108738
|
-
integrationId: row.integrationId,
|
|
108739
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
108740
|
-
bankResourceId: row.bankResourceId,
|
|
108741
|
-
bankActive: row.bankActive,
|
|
108742
|
-
sortOrder: row.sortOrder,
|
|
109598
|
+
...this.associationSummary(row),
|
|
108743
109599
|
title: test.title,
|
|
108744
|
-
questionCount:
|
|
108745
|
-
|
|
109600
|
+
questionCount: countQtiTestItems(test),
|
|
109601
|
+
available: true,
|
|
109602
|
+
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
108746
109603
|
};
|
|
108747
109604
|
} catch (error88) {
|
|
108748
109605
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -108751,319 +109608,420 @@ class TimebackAssessmentsService {
|
|
|
108751
109608
|
"app.error.message": errorMessage2(error88)
|
|
108752
109609
|
});
|
|
108753
109610
|
return {
|
|
108754
|
-
|
|
108755
|
-
integrationId: row.integrationId,
|
|
108756
|
-
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
108757
|
-
bankResourceId: row.bankResourceId,
|
|
108758
|
-
bankActive: row.bankActive,
|
|
108759
|
-
sortOrder: row.sortOrder,
|
|
109611
|
+
...this.associationSummary(row),
|
|
108760
109612
|
title: row.qtiTestIdentifier,
|
|
108761
109613
|
questionCount: 0,
|
|
108762
|
-
|
|
109614
|
+
available: false,
|
|
109615
|
+
editable: false
|
|
108763
109616
|
};
|
|
108764
109617
|
}
|
|
108765
|
-
})
|
|
108766
|
-
return assessments;
|
|
109618
|
+
});
|
|
109619
|
+
return assessments.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
108767
109620
|
}
|
|
108768
109621
|
async createAssessment(integrationId, input) {
|
|
108769
109622
|
const client = this.requireClient();
|
|
108770
|
-
const
|
|
108771
|
-
|
|
108772
|
-
courseId: integration.courseId,
|
|
108773
|
-
subject: integration.subject,
|
|
108774
|
-
grade: integration.grade
|
|
108775
|
-
});
|
|
109623
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
109624
|
+
const { integration } = ownership;
|
|
108776
109625
|
await client.course.createAssessmentTest({
|
|
108777
109626
|
identifier: input.qtiTestIdentifier,
|
|
108778
109627
|
title: input.title,
|
|
108779
109628
|
metadata: {
|
|
109629
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
109630
|
+
ownerGameSlug: ownership.gameSlug,
|
|
108780
109631
|
integrationId,
|
|
108781
109632
|
subject: integration.subject,
|
|
108782
109633
|
grade: String(integration.grade)
|
|
108783
109634
|
}
|
|
108784
109635
|
});
|
|
108785
|
-
const maxSortOrder = await this.getMaxSortOrder(integrationId);
|
|
108786
|
-
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
108787
|
-
integrationId,
|
|
108788
|
-
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
108789
|
-
sortOrder: maxSortOrder + 1
|
|
108790
|
-
}).returning();
|
|
108791
|
-
setAttribute("app.assessment.operation", "create");
|
|
108792
|
-
return row;
|
|
108793
|
-
}
|
|
108794
|
-
async deleteAssessment(integrationId, qtiTestIdentifier) {
|
|
108795
|
-
const client = this.requireClient();
|
|
108796
|
-
const row = await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
108797
|
-
if (row.bankActive) {
|
|
108798
|
-
await this.deactivateAssessment(integrationId, qtiTestIdentifier);
|
|
108799
|
-
}
|
|
108800
109636
|
try {
|
|
108801
|
-
await
|
|
109637
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
109638
|
+
integrationId,
|
|
109639
|
+
qtiTestIdentifier: input.qtiTestIdentifier,
|
|
109640
|
+
purpose: input.purpose,
|
|
109641
|
+
status: "draft"
|
|
109642
|
+
}).returning();
|
|
109643
|
+
setAttribute("app.assessment.operation", "create");
|
|
109644
|
+
return row;
|
|
108802
109645
|
} catch (error88) {
|
|
108803
|
-
|
|
108804
|
-
|
|
108805
|
-
|
|
108806
|
-
|
|
108807
|
-
|
|
109646
|
+
let committedRow;
|
|
109647
|
+
try {
|
|
109648
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
109649
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, input.qtiTestIdentifier))
|
|
109650
|
+
});
|
|
109651
|
+
} catch (verificationError) {
|
|
109652
|
+
addEvent("assessment.qti_test_create_commit_verification_failed", {
|
|
109653
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
109654
|
+
"exception.type": errorType(verificationError),
|
|
109655
|
+
"app.error.message": errorMessage2(verificationError)
|
|
109656
|
+
});
|
|
109657
|
+
throw error88;
|
|
109658
|
+
}
|
|
109659
|
+
if (committedRow) {
|
|
109660
|
+
setAttribute("app.assessment.operation", "create");
|
|
109661
|
+
return committedRow;
|
|
109662
|
+
}
|
|
109663
|
+
try {
|
|
109664
|
+
await client.qtiApi.assessmentTests.delete(input.qtiTestIdentifier);
|
|
109665
|
+
} catch (cleanupError) {
|
|
109666
|
+
if (!isApiError(cleanupError) || cleanupError.statusCode !== 404) {
|
|
109667
|
+
addEvent("assessment.qti_test_create_cleanup_failed", {
|
|
109668
|
+
"app.assessment.qti_test_identifier": input.qtiTestIdentifier,
|
|
109669
|
+
"exception.type": errorType(cleanupError),
|
|
109670
|
+
"app.error.message": errorMessage2(cleanupError)
|
|
109671
|
+
});
|
|
109672
|
+
}
|
|
109673
|
+
}
|
|
109674
|
+
throw error88;
|
|
108808
109675
|
}
|
|
108809
|
-
await this.deps.db.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
108810
|
-
setAttribute("app.assessment.operation", "delete");
|
|
108811
109676
|
}
|
|
108812
|
-
async
|
|
108813
|
-
|
|
108814
|
-
|
|
108815
|
-
|
|
108816
|
-
|
|
108817
|
-
|
|
108818
|
-
|
|
108819
|
-
|
|
108820
|
-
|
|
108821
|
-
|
|
108822
|
-
|
|
108823
|
-
|
|
108824
|
-
|
|
108825
|
-
|
|
108826
|
-
|
|
108827
|
-
|
|
108828
|
-
|
|
108829
|
-
|
|
108830
|
-
|
|
108831
|
-
|
|
109677
|
+
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
109678
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
109679
|
+
const updates = buildAssessmentAssociationUpdates(row, input);
|
|
109680
|
+
if (input.status !== undefined) {
|
|
109681
|
+
validateAssessmentStatusTransition(row.status, input.status);
|
|
109682
|
+
if (isAssessmentPublicationTransition(row.status, input.status)) {
|
|
109683
|
+
await this.validateAssessmentHasQuestions(qtiTestIdentifier);
|
|
109684
|
+
}
|
|
109685
|
+
}
|
|
109686
|
+
if (input.title !== undefined) {
|
|
109687
|
+
assertDraftAssessment(row);
|
|
109688
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
109689
|
+
const client = this.requireClient();
|
|
109690
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
109691
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
109692
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
109693
|
+
await client.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
109694
|
+
}
|
|
109695
|
+
let updated = row;
|
|
109696
|
+
if (Object.keys(updates).length > 0) {
|
|
109697
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ ...updates, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
|
|
109698
|
+
updated = updatedRow ?? row;
|
|
109699
|
+
}
|
|
109700
|
+
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
109701
|
+
return updated;
|
|
108832
109702
|
});
|
|
108833
|
-
return item;
|
|
108834
109703
|
}
|
|
108835
|
-
async
|
|
108836
|
-
|
|
108837
|
-
|
|
108838
|
-
|
|
108839
|
-
|
|
108840
|
-
|
|
108841
|
-
|
|
108842
|
-
|
|
108843
|
-
|
|
108844
|
-
|
|
109704
|
+
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
109705
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
109706
|
+
const plan = planAssessmentRemoval(row.status);
|
|
109707
|
+
if (plan.kind === "delete") {
|
|
109708
|
+
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
109709
|
+
} else if (plan.kind === "archive") {
|
|
109710
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", sortOrder: null, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
109711
|
+
}
|
|
109712
|
+
if (plan.kind !== "none") {
|
|
109713
|
+
setAttribute("app.assessment.operation", plan.operation);
|
|
109714
|
+
}
|
|
109715
|
+
return { action: plan.action };
|
|
109716
|
+
});
|
|
108845
109717
|
}
|
|
108846
|
-
async
|
|
108847
|
-
|
|
108848
|
-
|
|
108849
|
-
const
|
|
108850
|
-
|
|
108851
|
-
|
|
108852
|
-
|
|
108853
|
-
|
|
108854
|
-
|
|
108855
|
-
|
|
109718
|
+
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
109719
|
+
await this.requireIntegration(integrationId);
|
|
109720
|
+
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
109721
|
+
const updatedAt = new Date;
|
|
109722
|
+
await this.deps.db.transaction(async (tx) => {
|
|
109723
|
+
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
109724
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
109725
|
+
});
|
|
109726
|
+
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
109727
|
+
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
109728
|
+
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
109729
|
+
const testIdentifier = row.qtiTestIdentifier;
|
|
109730
|
+
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 });
|
|
109731
|
+
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
109732
|
+
}
|
|
108856
109733
|
});
|
|
109734
|
+
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
108857
109735
|
}
|
|
108858
|
-
async
|
|
109736
|
+
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
108859
109737
|
const client = this.requireClient();
|
|
108860
|
-
|
|
108861
|
-
const
|
|
108862
|
-
|
|
108863
|
-
|
|
108864
|
-
|
|
108865
|
-
|
|
108866
|
-
const qtiTestUrl = `${client.getQtiBaseUrl()}/assessment-tests/${qtiTestIdentifier}`;
|
|
108867
|
-
await client.course.ensureAssessmentBank({
|
|
108868
|
-
courseId: integration.courseId,
|
|
108869
|
-
subject: integration.subject,
|
|
108870
|
-
grade: integration.grade
|
|
108871
|
-
});
|
|
108872
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
108873
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
108874
|
-
let childResourceId = row.bankResourceId;
|
|
108875
|
-
if (childResourceId) {
|
|
109738
|
+
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
109739
|
+
const { gameSlug } = await this.requireQtiTestOwnershipContext(integrationId);
|
|
109740
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
109741
|
+
const ownerTests = new Map;
|
|
109742
|
+
const questions = await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
109743
|
+
let item;
|
|
108876
109744
|
try {
|
|
108877
|
-
await client.
|
|
108878
|
-
status: "active",
|
|
108879
|
-
title: `Assessment Bank Test: ${qtiTestIdentifier}`,
|
|
108880
|
-
vendorResourceId: "",
|
|
108881
|
-
metadata: {
|
|
108882
|
-
type: "qti",
|
|
108883
|
-
subType: "qti-test",
|
|
108884
|
-
url: qtiTestUrl
|
|
108885
|
-
}
|
|
108886
|
-
});
|
|
108887
|
-
if (!currentResources.includes(childResourceId)) {
|
|
108888
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
108889
|
-
status: "active",
|
|
108890
|
-
title: parentResource.title,
|
|
108891
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
108892
|
-
vendorId: parentResource.vendorId,
|
|
108893
|
-
metadata: {
|
|
108894
|
-
...parentResource.metadata,
|
|
108895
|
-
resources: [...currentResources, childResourceId]
|
|
108896
|
-
}
|
|
108897
|
-
});
|
|
108898
|
-
}
|
|
109745
|
+
item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
108899
109746
|
} catch (error88) {
|
|
108900
|
-
addEvent("assessment.
|
|
108901
|
-
"app.assessment.
|
|
109747
|
+
addEvent("assessment.qti_question_hydration_failed", {
|
|
109748
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
109749
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
108902
109750
|
"exception.type": errorType(error88),
|
|
108903
109751
|
"app.error.message": errorMessage2(error88)
|
|
108904
109752
|
});
|
|
108905
|
-
|
|
109753
|
+
return reference;
|
|
108906
109754
|
}
|
|
108907
|
-
|
|
108908
|
-
|
|
108909
|
-
|
|
108910
|
-
|
|
108911
|
-
|
|
108912
|
-
|
|
108913
|
-
|
|
108914
|
-
|
|
108915
|
-
|
|
108916
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
108917
|
-
"app.assessment.child_resource_id": resourceId
|
|
108918
|
-
});
|
|
108919
|
-
break;
|
|
108920
|
-
}
|
|
108921
|
-
} catch {}
|
|
108922
|
-
}
|
|
108923
|
-
if (!childResourceId) {
|
|
108924
|
-
const childResult = await client.api.oneroster.resources.create({
|
|
108925
|
-
status: "active",
|
|
108926
|
-
title: `Assessment Bank Test: ${qtiTestIdentifier}`,
|
|
108927
|
-
vendorResourceId: "",
|
|
108928
|
-
vendorId: undefined,
|
|
108929
|
-
metadata: {
|
|
108930
|
-
type: "qti",
|
|
108931
|
-
subType: "qti-test",
|
|
108932
|
-
url: qtiTestUrl
|
|
108933
|
-
}
|
|
108934
|
-
});
|
|
108935
|
-
childResourceId = childResult.sourcedIdPairs.allocatedSourcedId;
|
|
108936
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
108937
|
-
status: "active",
|
|
108938
|
-
title: parentResource.title,
|
|
108939
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
108940
|
-
vendorId: parentResource.vendorId,
|
|
108941
|
-
metadata: {
|
|
108942
|
-
...parentResource.metadata,
|
|
108943
|
-
resources: [...currentResources, childResourceId]
|
|
108944
|
-
}
|
|
109755
|
+
let ownerTest;
|
|
109756
|
+
try {
|
|
109757
|
+
ownerTest = await this.qtiQuestionOwnerTest(client, item, ownerTests);
|
|
109758
|
+
} catch (error88) {
|
|
109759
|
+
addEvent("assessment.qti_question_owner_test_fetch_failed", {
|
|
109760
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
109761
|
+
"app.assessment.qti_item_identifier": reference.reference.identifier,
|
|
109762
|
+
"exception.type": errorType(error88),
|
|
109763
|
+
"app.error.message": errorMessage2(error88)
|
|
108945
109764
|
});
|
|
108946
109765
|
}
|
|
108947
|
-
|
|
108948
|
-
|
|
108949
|
-
|
|
108950
|
-
|
|
108951
|
-
|
|
109766
|
+
return buildHydratedQtiQuestionReference({
|
|
109767
|
+
reference,
|
|
109768
|
+
item,
|
|
109769
|
+
gameSlug,
|
|
109770
|
+
ownerTest,
|
|
109771
|
+
testIdentifier: qtiTestIdentifier
|
|
109772
|
+
});
|
|
108952
109773
|
});
|
|
109774
|
+
return { ...result, questions };
|
|
108953
109775
|
}
|
|
108954
|
-
async
|
|
109776
|
+
async listQuestionLibrary(integrationId, params) {
|
|
108955
109777
|
const client = this.requireClient();
|
|
108956
|
-
|
|
108957
|
-
|
|
108958
|
-
if (!row.bankActive) {
|
|
108959
|
-
throw new ValidationError("Assessment is already in draft");
|
|
108960
|
-
}
|
|
108961
|
-
if (!row.bankResourceId) {
|
|
108962
|
-
throw new ValidationError("Assessment has no bank resource — activate it first");
|
|
108963
|
-
}
|
|
108964
|
-
const childResourceId = row.bankResourceId;
|
|
108965
|
-
const bankIds = deriveAssessmentBankIds2(integration.courseId);
|
|
108966
|
-
try {
|
|
108967
|
-
const parentResource = await client.api.oneroster.resources.get(bankIds.resource);
|
|
108968
|
-
const currentResources = parentResource.metadata?.resources ?? [];
|
|
108969
|
-
const filtered = currentResources.filter((id) => id !== childResourceId);
|
|
108970
|
-
if (filtered.length !== currentResources.length) {
|
|
108971
|
-
await client.api.oneroster.resources.update(bankIds.resource, {
|
|
108972
|
-
status: "active",
|
|
108973
|
-
title: parentResource.title,
|
|
108974
|
-
vendorResourceId: parentResource.vendorResourceId,
|
|
108975
|
-
vendorId: parentResource.vendorId,
|
|
108976
|
-
metadata: {
|
|
108977
|
-
...parentResource.metadata,
|
|
108978
|
-
resources: filtered
|
|
108979
|
-
}
|
|
108980
|
-
});
|
|
108981
|
-
}
|
|
108982
|
-
} catch (error88) {
|
|
108983
|
-
addEvent("assessment.parent_resource_update_failed", {
|
|
108984
|
-
"app.assessment.bank_resource_id": bankIds.resource,
|
|
108985
|
-
"exception.type": errorType(error88),
|
|
108986
|
-
"app.error.message": errorMessage2(error88)
|
|
108987
|
-
});
|
|
108988
|
-
}
|
|
108989
|
-
try {
|
|
108990
|
-
await client.api.oneroster.resources.delete(childResourceId);
|
|
108991
|
-
} catch (error88) {
|
|
108992
|
-
addEvent("assessment.child_resource_delete_failed", {
|
|
108993
|
-
"app.assessment.child_resource_id": childResourceId,
|
|
108994
|
-
"exception.type": errorType(error88),
|
|
108995
|
-
"app.error.message": errorMessage2(error88)
|
|
108996
|
-
});
|
|
108997
|
-
}
|
|
108998
|
-
await this.deps.db.update(gameTimebackAssessmentTests).set({ bankActive: false }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
108999
|
-
setAttribute("app.assessment.operation", "deactivate");
|
|
109000
|
-
}
|
|
109001
|
-
isAssessmentActive(row) {
|
|
109002
|
-
return row.bankActive;
|
|
109778
|
+
await this.requireIntegration(integrationId);
|
|
109779
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentItems.list(listParams));
|
|
109003
109780
|
}
|
|
109004
|
-
async
|
|
109005
|
-
const
|
|
109006
|
-
|
|
109007
|
-
|
|
109008
|
-
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
109009
|
-
});
|
|
109010
|
-
const activeCount = rows.filter((r) => r.bankActive).length;
|
|
109011
|
-
return {
|
|
109012
|
-
bankIds,
|
|
109013
|
-
totalAssessments: rows.length,
|
|
109014
|
-
activeAssessments: activeCount,
|
|
109015
|
-
draftAssessments: rows.length - activeCount
|
|
109016
|
-
};
|
|
109781
|
+
async listTestLibrary(integrationId, params) {
|
|
109782
|
+
const client = this.requireClient();
|
|
109783
|
+
await this.requireIntegration(integrationId);
|
|
109784
|
+
return this.listQtiLibrary(params, async (listParams) => await client.qtiApi.assessmentTests.list(listParams));
|
|
109017
109785
|
}
|
|
109018
|
-
async
|
|
109786
|
+
async copyAssessment(integrationId, sourceTestIdentifier, targetTestIdentifier, purpose) {
|
|
109019
109787
|
const client = this.requireClient();
|
|
109020
|
-
const
|
|
109021
|
-
const
|
|
109022
|
-
const
|
|
109023
|
-
|
|
109788
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
109789
|
+
const { integration } = ownership;
|
|
109790
|
+
const source = await client.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
109791
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => this.qtiItemHref(client, identifier));
|
|
109792
|
+
const itemCopies = await runWithConcurrency(itemPlan, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
109793
|
+
const sourceItem = await client.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
109794
|
+
return {
|
|
109795
|
+
plan,
|
|
109796
|
+
input: buildQtiQuestionCopyInput({
|
|
109797
|
+
source: sourceItem,
|
|
109798
|
+
targetIdentifier: plan.targetIdentifier,
|
|
109799
|
+
targetTestIdentifier,
|
|
109800
|
+
gameSlug: ownership.gameSlug,
|
|
109801
|
+
integrationId
|
|
109802
|
+
})
|
|
109803
|
+
};
|
|
109024
109804
|
});
|
|
109805
|
+
const testInput = buildQtiTestCopyInput(source, targetTestIdentifier, {
|
|
109806
|
+
...source.metadata,
|
|
109807
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
109808
|
+
ownerGameSlug: ownership.gameSlug,
|
|
109809
|
+
integrationId,
|
|
109810
|
+
subject: integration.subject,
|
|
109811
|
+
grade: String(integration.grade),
|
|
109812
|
+
copiedFromTestIdentifier: sourceTestIdentifier
|
|
109813
|
+
}, itemPlan);
|
|
109814
|
+
const attemptedItemIdentifiers = [];
|
|
109815
|
+
let testCreationAttempted = false;
|
|
109816
|
+
let associationCreationAttempted = false;
|
|
109025
109817
|
try {
|
|
109026
|
-
|
|
109818
|
+
for (const copy of itemCopies) {
|
|
109819
|
+
attemptedItemIdentifiers.push(copy.plan.targetIdentifier);
|
|
109820
|
+
await client.course.createAssessmentItemXml({
|
|
109821
|
+
xml: copy.input.xml,
|
|
109822
|
+
metadata: copy.input.metadata
|
|
109823
|
+
});
|
|
109824
|
+
}
|
|
109825
|
+
testCreationAttempted = true;
|
|
109826
|
+
await client.qtiApi.assessmentTests.create(testInput);
|
|
109827
|
+
associationCreationAttempted = true;
|
|
109828
|
+
const [row] = await this.deps.db.insert(gameTimebackAssessmentTests).values({
|
|
109829
|
+
integrationId,
|
|
109830
|
+
qtiTestIdentifier: targetTestIdentifier,
|
|
109831
|
+
purpose,
|
|
109832
|
+
status: "draft"
|
|
109833
|
+
}).returning();
|
|
109834
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
109835
|
+
return row;
|
|
109027
109836
|
} catch (error88) {
|
|
109028
|
-
|
|
109029
|
-
|
|
109030
|
-
"exception.type": errorType(error88),
|
|
109031
|
-
"app.error.message": errorMessage2(error88)
|
|
109032
|
-
});
|
|
109033
|
-
}
|
|
109034
|
-
for (const row of activeRows) {
|
|
109035
|
-
if (row.bankResourceId) {
|
|
109837
|
+
if (associationCreationAttempted) {
|
|
109838
|
+
let committedRow;
|
|
109036
109839
|
try {
|
|
109037
|
-
await
|
|
109038
|
-
|
|
109039
|
-
|
|
109040
|
-
|
|
109041
|
-
|
|
109042
|
-
"app.
|
|
109840
|
+
committedRow = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
109841
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, targetTestIdentifier))
|
|
109842
|
+
});
|
|
109843
|
+
} catch (verificationError) {
|
|
109844
|
+
addEvent("assessment.qti_test_copy_commit_verification_failed", {
|
|
109845
|
+
"app.assessment.qti_test_identifier": targetTestIdentifier,
|
|
109846
|
+
"exception.type": errorType(verificationError),
|
|
109847
|
+
"app.error.message": errorMessage2(verificationError)
|
|
109043
109848
|
});
|
|
109849
|
+
throw error88;
|
|
109850
|
+
}
|
|
109851
|
+
if (committedRow) {
|
|
109852
|
+
setAttribute("app.assessment.operation", "copy_test");
|
|
109853
|
+
return committedRow;
|
|
109044
109854
|
}
|
|
109045
109855
|
}
|
|
109856
|
+
await this.cleanupQtiAssessmentCopy(client, testCreationAttempted ? targetTestIdentifier : undefined, attemptedItemIdentifiers);
|
|
109857
|
+
throw error88;
|
|
109046
109858
|
}
|
|
109047
|
-
|
|
109048
|
-
|
|
109049
|
-
|
|
109050
|
-
|
|
109051
|
-
|
|
109052
|
-
|
|
109053
|
-
|
|
109859
|
+
}
|
|
109860
|
+
async createQuestion(integrationId, qtiTestIdentifier, input) {
|
|
109861
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
109862
|
+
const client = this.requireClient();
|
|
109863
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
109864
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
109865
|
+
const creation = parseQtiQuestionCreationInput(input);
|
|
109866
|
+
const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
|
|
109867
|
+
let title;
|
|
109868
|
+
let metadata2;
|
|
109869
|
+
let xml;
|
|
109870
|
+
let structuredInput;
|
|
109871
|
+
let fallbackItem;
|
|
109872
|
+
if (creation.kind === "copy") {
|
|
109873
|
+
const source = await client.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
|
|
109874
|
+
const copy = buildQtiQuestionCopyInput({
|
|
109875
|
+
source,
|
|
109876
|
+
targetIdentifier: itemIdentifier,
|
|
109877
|
+
targetTestIdentifier: qtiTestIdentifier,
|
|
109878
|
+
gameSlug: ownership.gameSlug,
|
|
109879
|
+
integrationId
|
|
109880
|
+
});
|
|
109881
|
+
title = copy.title;
|
|
109882
|
+
metadata2 = copy.metadata;
|
|
109883
|
+
xml = copy.xml;
|
|
109884
|
+
fallbackItem = {
|
|
109885
|
+
...source,
|
|
109886
|
+
identifier: itemIdentifier,
|
|
109887
|
+
title,
|
|
109888
|
+
rawXml: xml,
|
|
109889
|
+
metadata: metadata2
|
|
109890
|
+
};
|
|
109891
|
+
} else {
|
|
109892
|
+
structuredInput = creation.input;
|
|
109893
|
+
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
109894
|
+
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
109895
|
+
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
109896
|
+
gameSlug: ownership.gameSlug,
|
|
109897
|
+
testIdentifier: qtiTestIdentifier
|
|
109898
|
+
});
|
|
109899
|
+
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
109900
|
+
fallbackItem = {
|
|
109901
|
+
...structuredInput,
|
|
109902
|
+
identifier: itemIdentifier,
|
|
109903
|
+
title,
|
|
109904
|
+
...xml ? { rawXml: xml } : {}
|
|
109905
|
+
};
|
|
109906
|
+
}
|
|
109907
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
109908
|
+
const href = this.qtiItemHref(client, itemIdentifier);
|
|
109909
|
+
let item;
|
|
109910
|
+
let itemCreationAttempted = false;
|
|
109911
|
+
let referenceCreationAttempted = false;
|
|
109912
|
+
try {
|
|
109913
|
+
itemCreationAttempted = true;
|
|
109914
|
+
item = xml ? await client.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.create({
|
|
109915
|
+
...structuredInput,
|
|
109916
|
+
identifier: itemIdentifier,
|
|
109917
|
+
metadata: metadata2
|
|
109918
|
+
});
|
|
109919
|
+
referenceCreationAttempted = true;
|
|
109920
|
+
await section.items.add({
|
|
109921
|
+
identifier: itemIdentifier,
|
|
109922
|
+
href
|
|
109923
|
+
});
|
|
109924
|
+
} catch (error88) {
|
|
109925
|
+
let referenceRemoved = !referenceCreationAttempted;
|
|
109926
|
+
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
109927
|
+
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
109928
|
+
if (referenceCreationAttempted) {
|
|
109929
|
+
try {
|
|
109930
|
+
await section.items.remove(itemIdentifier);
|
|
109931
|
+
referenceRemoved = true;
|
|
109932
|
+
} catch (cleanupError) {
|
|
109933
|
+
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
109934
|
+
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
109935
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
109936
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
109937
|
+
"exception.type": errorType(cleanupError),
|
|
109938
|
+
"app.error.message": errorMessage2(cleanupError)
|
|
109939
|
+
});
|
|
109940
|
+
}
|
|
109941
|
+
}
|
|
109942
|
+
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
109943
|
+
try {
|
|
109944
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
109945
|
+
} catch (cleanupError) {
|
|
109946
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
109947
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
109948
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
109949
|
+
"exception.type": errorType(cleanupError),
|
|
109950
|
+
"app.error.message": errorMessage2(cleanupError)
|
|
109951
|
+
});
|
|
109952
|
+
}
|
|
109953
|
+
}
|
|
109954
|
+
throw error88;
|
|
109955
|
+
}
|
|
109956
|
+
setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
|
|
109957
|
+
return buildCreatedQtiQuestionReference({
|
|
109958
|
+
item,
|
|
109959
|
+
fallbackItem,
|
|
109960
|
+
itemIdentifier,
|
|
109961
|
+
href,
|
|
109962
|
+
partIdentifier: section.partIdentifier,
|
|
109963
|
+
sectionIdentifier: section.sectionIdentifier,
|
|
109964
|
+
metadata: metadata2
|
|
109054
109965
|
});
|
|
109055
|
-
}
|
|
109056
|
-
|
|
109057
|
-
|
|
109058
|
-
|
|
109059
|
-
|
|
109060
|
-
|
|
109061
|
-
|
|
109062
|
-
|
|
109966
|
+
});
|
|
109967
|
+
}
|
|
109968
|
+
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
109969
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
109970
|
+
const client = this.requireClient();
|
|
109971
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
109972
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
109973
|
+
const [, item] = await Promise.all([
|
|
109974
|
+
this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test),
|
|
109975
|
+
client.qtiApi.assessmentItems.get(itemIdentifier)
|
|
109976
|
+
]);
|
|
109977
|
+
const ownerTest = await this.qtiQuestionOwnerTest(client, item, undefined, ownership.test);
|
|
109978
|
+
const replacesInteraction = "type" in input || "interaction" in input || "numericTextEntry" in input;
|
|
109979
|
+
assertQtiQuestionEditable(item, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, ownerTest);
|
|
109980
|
+
assertSupportedQtiQuestionInteraction(replacesInteraction ? input : item);
|
|
109981
|
+
let title = typeof input.title === "string" ? input.title.trim() : "";
|
|
109982
|
+
if (!title) {
|
|
109983
|
+
title = item.title;
|
|
109984
|
+
}
|
|
109985
|
+
const metadata2 = buildOwnedQtiQuestionMetadata(input, { gameSlug: ownership.gameSlug, testIdentifier: qtiTestIdentifier }, item.metadata);
|
|
109986
|
+
const xml = buildQuestionXml(input, itemIdentifier, title);
|
|
109987
|
+
const updated = xml ? await client.course.updateAssessmentItemXml(itemIdentifier, { xml, metadata: metadata2 }) : await client.qtiApi.assessmentItems.update(itemIdentifier, {
|
|
109988
|
+
...input,
|
|
109989
|
+
title,
|
|
109990
|
+
metadata: metadata2
|
|
109991
|
+
});
|
|
109992
|
+
setAttribute("app.assessment.operation", "update_question");
|
|
109993
|
+
return updated;
|
|
109994
|
+
});
|
|
109995
|
+
}
|
|
109996
|
+
async removeQuestion(integrationId, qtiTestIdentifier, itemIdentifier) {
|
|
109997
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
109998
|
+
const client = this.requireClient();
|
|
109999
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
110000
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
110001
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, undefined, ownership.test);
|
|
110002
|
+
await section.items.remove(itemIdentifier);
|
|
110003
|
+
setAttribute("app.assessment.operation", "remove_question");
|
|
110004
|
+
});
|
|
110005
|
+
}
|
|
110006
|
+
async reorderQuestions(integrationId, qtiTestIdentifier, items) {
|
|
110007
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
110008
|
+
const client = this.requireClient();
|
|
110009
|
+
assertAllAssessmentAssociationsDraft(associations);
|
|
110010
|
+
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
110011
|
+
const identifiers = items.map((item) => item.identifier);
|
|
110012
|
+
validateUniqueQuestionIdentifiers(identifiers);
|
|
110013
|
+
const firstIdentifier = identifiers[0];
|
|
110014
|
+
const section = await this.qtiSectionItems(client, qtiTestIdentifier, firstIdentifier, identifiers, ownership.test);
|
|
110015
|
+
const result = await section.items.reorder({
|
|
110016
|
+
items: items.map((item) => ({
|
|
110017
|
+
identifier: item.identifier,
|
|
110018
|
+
href: item.href ?? this.qtiItemHref(client, item.identifier),
|
|
110019
|
+
sequence: item.sequence
|
|
110020
|
+
}))
|
|
109063
110021
|
});
|
|
109064
|
-
|
|
109065
|
-
|
|
109066
|
-
|
|
110022
|
+
setAttribute("app.assessment.operation", "reorder_questions");
|
|
110023
|
+
return result;
|
|
110024
|
+
});
|
|
109067
110025
|
}
|
|
109068
110026
|
requireClient() {
|
|
109069
110027
|
if (!this.deps.timeback) {
|
|
@@ -109071,8 +110029,18 @@ class TimebackAssessmentsService {
|
|
|
109071
110029
|
}
|
|
109072
110030
|
return this.deps.timeback;
|
|
109073
110031
|
}
|
|
109074
|
-
async
|
|
109075
|
-
|
|
110032
|
+
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
110033
|
+
return this.deps.db.transaction(async (tx) => {
|
|
110034
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
110035
|
+
const row = rows.find((candidate) => candidate.integrationId === integrationId);
|
|
110036
|
+
if (!row) {
|
|
110037
|
+
throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
|
|
110038
|
+
}
|
|
110039
|
+
return action(row, tx, rows);
|
|
110040
|
+
});
|
|
110041
|
+
}
|
|
110042
|
+
async requireIntegration(integrationId, db2 = this.deps.db) {
|
|
110043
|
+
const integration = await db2.query.gameTimebackIntegrations.findFirst({
|
|
109076
110044
|
where: and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())
|
|
109077
110045
|
});
|
|
109078
110046
|
if (!integration) {
|
|
@@ -109080,6 +110048,17 @@ class TimebackAssessmentsService {
|
|
|
109080
110048
|
}
|
|
109081
110049
|
return integration;
|
|
109082
110050
|
}
|
|
110051
|
+
async requireQtiTestOwnershipContext(integrationId, db2 = this.deps.db) {
|
|
110052
|
+
const integration = await this.requireIntegration(integrationId, db2);
|
|
110053
|
+
const game2 = await db2.query.games.findFirst({
|
|
110054
|
+
where: eq(games.id, integration.gameId),
|
|
110055
|
+
columns: { slug: true }
|
|
110056
|
+
});
|
|
110057
|
+
if (!game2) {
|
|
110058
|
+
throw new NotFoundError(`Game not found for integration: ${integrationId}`);
|
|
110059
|
+
}
|
|
110060
|
+
return { integration, gameSlug: game2.slug };
|
|
110061
|
+
}
|
|
109083
110062
|
async requireAssessmentRow(integrationId, qtiTestIdentifier) {
|
|
109084
110063
|
const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
109085
110064
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
|
|
@@ -109089,27 +110068,125 @@ class TimebackAssessmentsService {
|
|
|
109089
110068
|
}
|
|
109090
110069
|
return row;
|
|
109091
110070
|
}
|
|
109092
|
-
|
|
109093
|
-
|
|
110071
|
+
async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
|
|
110072
|
+
const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
110073
|
+
assertDraftAssessment(row);
|
|
110074
|
+
return row;
|
|
109094
110075
|
}
|
|
109095
|
-
async
|
|
109096
|
-
const
|
|
109097
|
-
|
|
109098
|
-
|
|
110076
|
+
async requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, lockedRow, db2 = this.deps.db) {
|
|
110077
|
+
const client = this.requireClient();
|
|
110078
|
+
await this.requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow);
|
|
110079
|
+
const test = await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
110080
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, db2);
|
|
110081
|
+
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
110082
|
+
return { ...ownership, test };
|
|
110083
|
+
}
|
|
110084
|
+
async qtiQuestionOwnerTest(client, item, cache, knownTest) {
|
|
110085
|
+
if (item.metadata && "ownerGameSlug" in item.metadata) {
|
|
110086
|
+
return;
|
|
110087
|
+
}
|
|
110088
|
+
const ownerTestIdentifier = qtiQuestionOwnerTestIdentifier(item);
|
|
110089
|
+
if (!ownerTestIdentifier) {
|
|
110090
|
+
return;
|
|
110091
|
+
}
|
|
110092
|
+
if (knownTest?.identifier === ownerTestIdentifier) {
|
|
110093
|
+
return knownTest;
|
|
110094
|
+
}
|
|
110095
|
+
function loadOwnerTest(identifier) {
|
|
110096
|
+
return client.qtiApi.assessmentTests.get(identifier);
|
|
110097
|
+
}
|
|
110098
|
+
if (!cache) {
|
|
110099
|
+
return loadOwnerTest(ownerTestIdentifier);
|
|
110100
|
+
}
|
|
110101
|
+
let ownerTest = cache.get(ownerTestIdentifier);
|
|
110102
|
+
if (!ownerTest) {
|
|
110103
|
+
ownerTest = loadOwnerTest(ownerTestIdentifier);
|
|
110104
|
+
cache.set(ownerTestIdentifier, ownerTest);
|
|
110105
|
+
}
|
|
110106
|
+
return ownerTest;
|
|
110107
|
+
}
|
|
110108
|
+
async assertQtiTestQuestionsSupported(client, qtiTestIdentifier, questions) {
|
|
110109
|
+
const result = questions ?? await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
110110
|
+
await runWithConcurrency(result.questions, TimebackAssessmentsService.QTI_HYDRATION_CONCURRENCY, async (reference) => {
|
|
110111
|
+
const item = await client.qtiApi.assessmentItems.get(reference.reference.identifier);
|
|
110112
|
+
assertSupportedQtiQuestionInteraction(item);
|
|
109099
110113
|
});
|
|
109100
|
-
|
|
109101
|
-
|
|
110114
|
+
}
|
|
110115
|
+
async qtiSectionItems(client, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers, knownTest) {
|
|
110116
|
+
const test = knownTest ?? await client.qtiApi.assessmentTests.get(qtiTestIdentifier);
|
|
110117
|
+
const section = resolveQtiQuestionSection(test, qtiTestIdentifier, itemIdentifier, expectedItemIdentifiers);
|
|
110118
|
+
return {
|
|
110119
|
+
...section,
|
|
110120
|
+
items: client.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
110121
|
+
};
|
|
110122
|
+
}
|
|
110123
|
+
qtiItemHref(client, itemIdentifier) {
|
|
110124
|
+
return `${client.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
110125
|
+
}
|
|
110126
|
+
async cleanupQtiAssessmentCopy(client, testIdentifier, itemIdentifiers) {
|
|
110127
|
+
let testDeleted = !testIdentifier;
|
|
110128
|
+
if (testIdentifier) {
|
|
110129
|
+
try {
|
|
110130
|
+
await client.qtiApi.assessmentTests.delete(testIdentifier);
|
|
110131
|
+
testDeleted = true;
|
|
110132
|
+
} catch (error88) {
|
|
110133
|
+
testDeleted = isApiError(error88) && error88.statusCode === 404;
|
|
110134
|
+
addEvent("assessment.qti_test_copy_cleanup_failed", {
|
|
110135
|
+
"app.assessment.qti_test_identifier": testIdentifier,
|
|
110136
|
+
"exception.type": errorType(error88),
|
|
110137
|
+
"app.error.message": errorMessage2(error88)
|
|
110138
|
+
});
|
|
110139
|
+
}
|
|
110140
|
+
}
|
|
110141
|
+
if (!testDeleted) {
|
|
110142
|
+
return;
|
|
110143
|
+
}
|
|
110144
|
+
for (const itemIdentifier of itemIdentifiers.toReversed()) {
|
|
110145
|
+
try {
|
|
110146
|
+
await client.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
110147
|
+
} catch (error88) {
|
|
110148
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
110149
|
+
...testIdentifier ? { "app.assessment.qti_test_identifier": testIdentifier } : {},
|
|
110150
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
110151
|
+
"exception.type": errorType(error88),
|
|
110152
|
+
"app.error.message": errorMessage2(error88)
|
|
110153
|
+
});
|
|
110154
|
+
}
|
|
109102
110155
|
}
|
|
109103
|
-
|
|
110156
|
+
}
|
|
110157
|
+
associationSummary(row) {
|
|
110158
|
+
return {
|
|
110159
|
+
id: row.id,
|
|
110160
|
+
integrationId: row.integrationId,
|
|
110161
|
+
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
110162
|
+
purpose: row.purpose,
|
|
110163
|
+
status: row.status,
|
|
110164
|
+
sortOrder: row.sortOrder,
|
|
110165
|
+
createdAt: row.createdAt,
|
|
110166
|
+
updatedAt: row.updatedAt
|
|
110167
|
+
};
|
|
110168
|
+
}
|
|
110169
|
+
async listQtiLibrary(params, list) {
|
|
110170
|
+
const plan = buildQtiLibraryListPlan(params);
|
|
110171
|
+
return list(plan.params);
|
|
110172
|
+
}
|
|
110173
|
+
async validateAssessmentHasQuestions(qtiTestIdentifier) {
|
|
110174
|
+
const client = this.requireClient();
|
|
110175
|
+
const result = await client.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
|
|
110176
|
+
assertAssessmentHasQuestions(result.questions);
|
|
110177
|
+
await this.assertQtiTestQuestionsSupported(client, qtiTestIdentifier, result);
|
|
109104
110178
|
}
|
|
109105
110179
|
}
|
|
109106
|
-
var init_timeback_assessments_service = __esm(() => {
|
|
110180
|
+
var init_timeback_assessments_service = __esm(async () => {
|
|
109107
110181
|
init_drizzle_orm();
|
|
109108
110182
|
init_helpers_index();
|
|
109109
110183
|
init_tables_index();
|
|
109110
110184
|
init_spans();
|
|
109111
|
-
|
|
110185
|
+
init_timeback3();
|
|
109112
110186
|
init_errors();
|
|
110187
|
+
init_timeback_assessment_rules_util();
|
|
110188
|
+
init_timeback_qti_authoring_util();
|
|
110189
|
+
await init_errors8();
|
|
109113
110190
|
});
|
|
109114
110191
|
function buildTimebackBaseConfigFromExistingConfig(config4) {
|
|
109115
110192
|
return {
|
|
@@ -111181,10 +112258,10 @@ var init_platform2 = __esm(async () => {
|
|
|
111181
112258
|
init_kv_service();
|
|
111182
112259
|
init_secrets_service();
|
|
111183
112260
|
init_seed_service();
|
|
111184
|
-
init_timeback_assessments_service();
|
|
111185
112261
|
init_upload_service();
|
|
111186
112262
|
await __promiseAll([
|
|
111187
112263
|
init_timeback_admin_service(),
|
|
112264
|
+
init_timeback_assessments_service(),
|
|
111188
112265
|
init_timeback_service()
|
|
111189
112266
|
]);
|
|
111190
112267
|
});
|
|
@@ -170393,16 +171470,6 @@ function requireAnonymous(handler) {
|
|
|
170393
171470
|
return handler(ctx);
|
|
170394
171471
|
};
|
|
170395
171472
|
}
|
|
170396
|
-
function requireAdmin(handler) {
|
|
170397
|
-
return async (ctx) => {
|
|
170398
|
-
assertAuthenticatedRequest(ctx);
|
|
170399
|
-
rejectDashboardWorkerKey(ctx);
|
|
170400
|
-
if (ctx.user.role !== "admin") {
|
|
170401
|
-
throw ApiError.forbidden("Admin access required");
|
|
170402
|
-
}
|
|
170403
|
-
return handler(ctx);
|
|
170404
|
-
};
|
|
170405
|
-
}
|
|
170406
171473
|
function requireDeveloper(handler) {
|
|
170407
171474
|
return async (ctx) => {
|
|
170408
171475
|
assertAuthenticatedRequest(ctx);
|
|
@@ -171587,6 +172654,26 @@ var init_session_controller = __esm(() => {
|
|
|
171587
172654
|
mintToken
|
|
171588
172655
|
});
|
|
171589
172656
|
});
|
|
172657
|
+
function parseQtiLibraryParams(searchParams) {
|
|
172658
|
+
const requestedPage = Number(searchParams.get("page"));
|
|
172659
|
+
const requestedLimit = Number(searchParams.get("limit"));
|
|
172660
|
+
const page = Number.isInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
|
172661
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 50) : 20;
|
|
172662
|
+
const query = searchParams.get("q")?.trim() || undefined;
|
|
172663
|
+
const requestedSource = searchParams.get("source");
|
|
172664
|
+
if (requestedSource && requestedSource !== "playcademy") {
|
|
172665
|
+
throw ApiError.badRequest("Unsupported QTI library source");
|
|
172666
|
+
}
|
|
172667
|
+
if (requestedSource !== "playcademy" && !query) {
|
|
172668
|
+
throw ApiError.badRequest("An exact QTI source identifier is required for a global lookup");
|
|
172669
|
+
}
|
|
172670
|
+
return {
|
|
172671
|
+
query,
|
|
172672
|
+
source: requestedSource === "playcademy" ? "playcademy" : undefined,
|
|
172673
|
+
page,
|
|
172674
|
+
limit
|
|
172675
|
+
};
|
|
172676
|
+
}
|
|
171590
172677
|
var populateStudent;
|
|
171591
172678
|
var getUser;
|
|
171592
172679
|
var getUserEnrollments;
|
|
@@ -171628,17 +172715,17 @@ var unenrollStudent;
|
|
|
171628
172715
|
var reactivateEnrollment;
|
|
171629
172716
|
var listAssessments;
|
|
171630
172717
|
var createAssessment;
|
|
171631
|
-
var
|
|
172718
|
+
var updateAssessment;
|
|
171632
172719
|
var reorderAssessments;
|
|
171633
172720
|
var reorderQuestions;
|
|
171634
|
-
var
|
|
171635
|
-
var deactivateAssessment;
|
|
172721
|
+
var removeAssessment;
|
|
171636
172722
|
var listQuestions;
|
|
172723
|
+
var listQuestionLibrary;
|
|
172724
|
+
var listTestLibrary;
|
|
172725
|
+
var copyAssessment;
|
|
171637
172726
|
var createQuestion;
|
|
171638
172727
|
var updateQuestion;
|
|
171639
|
-
var
|
|
171640
|
-
var getAssessmentBankStatus;
|
|
171641
|
-
var destroyAssessmentBank;
|
|
172728
|
+
var removeQuestion;
|
|
171642
172729
|
var timeback2;
|
|
171643
172730
|
var init_timeback_controller = __esm(() => {
|
|
171644
172731
|
init_esm();
|
|
@@ -172154,7 +173241,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
172154
173241
|
const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
|
|
172155
173242
|
return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
|
|
172156
173243
|
});
|
|
172157
|
-
listAssessments =
|
|
173244
|
+
listAssessments = requireDeveloper(async (ctx) => {
|
|
172158
173245
|
const { gameId, courseId } = ctx.params;
|
|
172159
173246
|
if (!gameId || !courseId) {
|
|
172160
173247
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
@@ -172162,40 +173249,40 @@ var init_timeback_controller = __esm(() => {
|
|
|
172162
173249
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172163
173250
|
return ctx.services.timebackAssessments.listAssessments(integrationId);
|
|
172164
173251
|
});
|
|
172165
|
-
createAssessment =
|
|
173252
|
+
createAssessment = requireDeveloper(async (ctx) => {
|
|
172166
173253
|
const { gameId, courseId } = ctx.params;
|
|
172167
173254
|
if (!gameId || !courseId) {
|
|
172168
173255
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
172169
173256
|
}
|
|
172170
173257
|
const body2 = await parseRequestBody(ctx.request, CreateAssessmentRequestSchema);
|
|
172171
173258
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172172
|
-
const
|
|
172173
|
-
const qtiTestIdentifier = `assessment-${shortId}`;
|
|
173259
|
+
const qtiTestIdentifier = newPlaycademyTestIdentifier();
|
|
172174
173260
|
return ctx.services.timebackAssessments.createAssessment(integrationId, {
|
|
172175
173261
|
title: body2.title,
|
|
173262
|
+
purpose: body2.purpose,
|
|
172176
173263
|
qtiTestIdentifier
|
|
172177
173264
|
});
|
|
172178
173265
|
});
|
|
172179
|
-
|
|
173266
|
+
updateAssessment = requireDeveloper(async (ctx) => {
|
|
172180
173267
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
172181
173268
|
if (!gameId || !courseId || !testIdentifier) {
|
|
172182
173269
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
172183
173270
|
}
|
|
172184
173271
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172185
|
-
await ctx.
|
|
172186
|
-
return
|
|
173272
|
+
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
173273
|
+
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
172187
173274
|
});
|
|
172188
|
-
reorderAssessments =
|
|
173275
|
+
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
172189
173276
|
const { gameId, courseId } = ctx.params;
|
|
172190
173277
|
if (!gameId || !courseId) {
|
|
172191
173278
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
172192
173279
|
}
|
|
172193
173280
|
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
172194
173281
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172195
|
-
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.
|
|
173282
|
+
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.purpose, body2.testIdentifiers);
|
|
172196
173283
|
return { success: true };
|
|
172197
173284
|
});
|
|
172198
|
-
reorderQuestions =
|
|
173285
|
+
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
172199
173286
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
172200
173287
|
if (!gameId || !courseId || !testIdentifier) {
|
|
172201
173288
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -172205,33 +173292,51 @@ var init_timeback_controller = __esm(() => {
|
|
|
172205
173292
|
await ctx.services.timebackAssessments.reorderQuestions(integrationId, testIdentifier, body2.items);
|
|
172206
173293
|
return { success: true };
|
|
172207
173294
|
});
|
|
172208
|
-
|
|
173295
|
+
removeAssessment = requireDeveloper(async (ctx) => {
|
|
172209
173296
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
172210
173297
|
if (!gameId || !courseId || !testIdentifier) {
|
|
172211
173298
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
172212
173299
|
}
|
|
172213
173300
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172214
|
-
|
|
172215
|
-
return { success: true };
|
|
173301
|
+
return ctx.services.timebackAssessments.removeAssessment(integrationId, testIdentifier);
|
|
172216
173302
|
});
|
|
172217
|
-
|
|
173303
|
+
listQuestions = requireDeveloper(async (ctx) => {
|
|
172218
173304
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
172219
173305
|
if (!gameId || !courseId || !testIdentifier) {
|
|
172220
173306
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
172221
173307
|
}
|
|
172222
173308
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172223
|
-
|
|
172224
|
-
return { success: true };
|
|
173309
|
+
return ctx.services.timebackAssessments.listQuestions(integrationId, testIdentifier);
|
|
172225
173310
|
});
|
|
172226
|
-
|
|
172227
|
-
const { gameId, courseId
|
|
172228
|
-
if (!gameId || !courseId
|
|
172229
|
-
throw ApiError.badRequest("Missing gameId
|
|
173311
|
+
listQuestionLibrary = requireDeveloper(async (ctx) => {
|
|
173312
|
+
const { gameId, courseId } = ctx.params;
|
|
173313
|
+
if (!gameId || !courseId) {
|
|
173314
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
172230
173315
|
}
|
|
173316
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
172231
173317
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172232
|
-
return ctx.services.timebackAssessments.
|
|
173318
|
+
return ctx.services.timebackAssessments.listQuestionLibrary(integrationId, params);
|
|
173319
|
+
});
|
|
173320
|
+
listTestLibrary = requireDeveloper(async (ctx) => {
|
|
173321
|
+
const { gameId, courseId } = ctx.params;
|
|
173322
|
+
if (!gameId || !courseId) {
|
|
173323
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
173324
|
+
}
|
|
173325
|
+
const params = parseQtiLibraryParams(ctx.url.searchParams);
|
|
173326
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
173327
|
+
return ctx.services.timebackAssessments.listTestLibrary(integrationId, params);
|
|
172233
173328
|
});
|
|
172234
|
-
|
|
173329
|
+
copyAssessment = requireDeveloper(async (ctx) => {
|
|
173330
|
+
const { gameId, courseId } = ctx.params;
|
|
173331
|
+
if (!gameId || !courseId) {
|
|
173332
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
173333
|
+
}
|
|
173334
|
+
const body2 = await parseRequestBody(ctx.request, CopyAssessmentRequestSchema);
|
|
173335
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
173336
|
+
const targetTestIdentifier = newPlaycademyTestIdentifier();
|
|
173337
|
+
return ctx.services.timebackAssessments.copyAssessment(integrationId, body2.testIdentifier, targetTestIdentifier, body2.purpose);
|
|
173338
|
+
});
|
|
173339
|
+
createQuestion = requireDeveloper(async (ctx) => {
|
|
172235
173340
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
172236
173341
|
if (!gameId || !courseId || !testIdentifier) {
|
|
172237
173342
|
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
@@ -172240,7 +173345,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
172240
173345
|
const body2 = await ctx.request.json();
|
|
172241
173346
|
return ctx.services.timebackAssessments.createQuestion(integrationId, testIdentifier, body2);
|
|
172242
173347
|
});
|
|
172243
|
-
updateQuestion =
|
|
173348
|
+
updateQuestion = requireDeveloper(async (ctx) => {
|
|
172244
173349
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
172245
173350
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
172246
173351
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
@@ -172249,30 +173354,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
172249
173354
|
const body2 = await ctx.request.json();
|
|
172250
173355
|
return ctx.services.timebackAssessments.updateQuestion(integrationId, testIdentifier, itemIdentifier, body2);
|
|
172251
173356
|
});
|
|
172252
|
-
|
|
173357
|
+
removeQuestion = requireDeveloper(async (ctx) => {
|
|
172253
173358
|
const { gameId, courseId, testIdentifier, itemIdentifier } = ctx.params;
|
|
172254
173359
|
if (!gameId || !courseId || !testIdentifier || !itemIdentifier) {
|
|
172255
173360
|
throw ApiError.badRequest("Missing gameId, courseId, testIdentifier, or itemIdentifier parameter");
|
|
172256
173361
|
}
|
|
172257
173362
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172258
|
-
await ctx.services.timebackAssessments.
|
|
172259
|
-
return { success: true };
|
|
172260
|
-
});
|
|
172261
|
-
getAssessmentBankStatus = requireGameManagementAccess(async (ctx) => {
|
|
172262
|
-
const { gameId, courseId } = ctx.params;
|
|
172263
|
-
if (!gameId || !courseId) {
|
|
172264
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
172265
|
-
}
|
|
172266
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172267
|
-
return ctx.services.timebackAssessments.getBankStatus(integrationId);
|
|
172268
|
-
});
|
|
172269
|
-
destroyAssessmentBank = requireAdmin(async (ctx) => {
|
|
172270
|
-
const { gameId, courseId } = ctx.params;
|
|
172271
|
-
if (!gameId || !courseId) {
|
|
172272
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
172273
|
-
}
|
|
172274
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
172275
|
-
await ctx.services.timebackAssessments.destroyBank(integrationId);
|
|
173363
|
+
await ctx.services.timebackAssessments.removeQuestion(integrationId, testIdentifier, itemIdentifier);
|
|
172276
173364
|
return { success: true };
|
|
172277
173365
|
});
|
|
172278
173366
|
timeback2 = defineControllerNames("timeback", {
|
|
@@ -172317,17 +173405,17 @@ var init_timeback_controller = __esm(() => {
|
|
|
172317
173405
|
reactivateEnrollment,
|
|
172318
173406
|
listAssessments,
|
|
172319
173407
|
createAssessment,
|
|
172320
|
-
|
|
173408
|
+
updateAssessment,
|
|
172321
173409
|
reorderAssessments,
|
|
173410
|
+
removeAssessment,
|
|
172322
173411
|
reorderQuestions,
|
|
172323
|
-
activateAssessment,
|
|
172324
|
-
deactivateAssessment,
|
|
172325
173412
|
listQuestions,
|
|
173413
|
+
listTestLibrary,
|
|
173414
|
+
copyAssessment,
|
|
173415
|
+
listQuestionLibrary,
|
|
172326
173416
|
createQuestion,
|
|
172327
173417
|
updateQuestion,
|
|
172328
|
-
|
|
172329
|
-
getAssessmentBankStatus,
|
|
172330
|
-
destroyAssessmentBank
|
|
173418
|
+
removeQuestion
|
|
172331
173419
|
});
|
|
172332
173420
|
});
|
|
172333
173421
|
var initiate;
|