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