@playcademy/sandbox 0.10.1-beta.4 → 0.10.1-beta.6
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 +357 -110
- package/dist/server.js +357 -110
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1128,7 +1128,7 @@ var package_default;
|
|
|
1128
1128
|
var init_package = __esm(() => {
|
|
1129
1129
|
package_default = {
|
|
1130
1130
|
name: "@playcademy/sandbox",
|
|
1131
|
-
version: "0.10.1-beta.
|
|
1131
|
+
version: "0.10.1-beta.6",
|
|
1132
1132
|
description: "Local development server for Playcademy game development",
|
|
1133
1133
|
type: "module",
|
|
1134
1134
|
exports: {
|
|
@@ -47989,7 +47989,7 @@ function rejectSystemManagedReview(input, context2) {
|
|
|
47989
47989
|
});
|
|
47990
47990
|
}
|
|
47991
47991
|
}
|
|
47992
|
-
var 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, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47992
|
+
var 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, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47993
47993
|
var init_schemas4 = __esm(() => {
|
|
47994
47994
|
init_esm();
|
|
47995
47995
|
init_src();
|
|
@@ -48272,6 +48272,11 @@ var init_schemas4 = __esm(() => {
|
|
|
48272
48272
|
requireMasteryStandard(input, context2);
|
|
48273
48273
|
rejectSystemManagedReview(input, context2);
|
|
48274
48274
|
});
|
|
48275
|
+
ReviseAssessmentRequestSchema = exports_external.object({
|
|
48276
|
+
currentQtiTestIdentifier: exports_external.string().trim().min(1, "The current assessment identifier is required"),
|
|
48277
|
+
title: exports_external.string().trim().min(1, "Assessment title is required"),
|
|
48278
|
+
questions: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).min(1, "A revision must contain at least one question").max(500, "A revision cannot contain more than 500 questions")
|
|
48279
|
+
});
|
|
48275
48280
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
48276
48281
|
assessmentKey: AssessmentKeySchema,
|
|
48277
48282
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
@@ -94023,8 +94028,20 @@ function compareEnrollmentsByRecency(a, b) {
|
|
|
94023
94028
|
}
|
|
94024
94029
|
return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
|
|
94025
94030
|
}
|
|
94031
|
+
function resolvePrimaryAccountRole(roles2) {
|
|
94032
|
+
if (!roles2?.length) {
|
|
94033
|
+
return null;
|
|
94034
|
+
}
|
|
94035
|
+
return ACCOUNT_ROLE_PRECEDENCE.find((role) => roles2.some((entry) => entry.role === role)) ?? null;
|
|
94036
|
+
}
|
|
94037
|
+
var ACCOUNT_ROLE_PRECEDENCE;
|
|
94026
94038
|
var init_timeback_admin_util = __esm(() => {
|
|
94027
94039
|
init_errors2();
|
|
94040
|
+
ACCOUNT_ROLE_PRECEDENCE = [
|
|
94041
|
+
"administrator",
|
|
94042
|
+
"teacher",
|
|
94043
|
+
"student"
|
|
94044
|
+
];
|
|
94028
94045
|
});
|
|
94029
94046
|
|
|
94030
94047
|
// ../api-core/src/utils/timeback-discrepancy-queue.util.ts
|
|
@@ -98734,6 +98751,7 @@ class TimebackAdminService {
|
|
|
98734
98751
|
className: rosterEntry.class?.title || null,
|
|
98735
98752
|
name: name3,
|
|
98736
98753
|
email: rosterEntry.user?.email || null,
|
|
98754
|
+
primaryRole: resolvePrimaryAccountRole(rosterEntry.user?.roles),
|
|
98737
98755
|
analyticsUnavailable,
|
|
98738
98756
|
totalXp: summary?.totalXp ?? 0,
|
|
98739
98757
|
todayXp: summary?.todayXp ?? 0,
|
|
@@ -99697,15 +99715,22 @@ class TimebackAdminService {
|
|
|
99697
99715
|
});
|
|
99698
99716
|
pastEnrollmentsByStudent.set(studentId, list);
|
|
99699
99717
|
}
|
|
99700
|
-
const students = allUsers.
|
|
99718
|
+
const students = allUsers.flatMap((entry) => {
|
|
99719
|
+
const primaryRole = resolvePrimaryAccountRole(entry.roles);
|
|
99720
|
+
if (!entry.sourcedId || !primaryRole) {
|
|
99721
|
+
return [];
|
|
99722
|
+
}
|
|
99701
99723
|
const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
|
|
99702
|
-
return
|
|
99703
|
-
|
|
99704
|
-
|
|
99705
|
-
|
|
99706
|
-
|
|
99707
|
-
|
|
99708
|
-
|
|
99724
|
+
return [
|
|
99725
|
+
{
|
|
99726
|
+
studentId: entry.sourcedId,
|
|
99727
|
+
name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
|
|
99728
|
+
email: entry.email || null,
|
|
99729
|
+
primaryRole,
|
|
99730
|
+
alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
|
|
99731
|
+
...past.length > 0 ? { pastEnrollments: past } : {}
|
|
99732
|
+
}
|
|
99733
|
+
];
|
|
99709
99734
|
});
|
|
99710
99735
|
setAttribute("app.timeback.result_count", students.length);
|
|
99711
99736
|
return { students };
|
|
@@ -106222,6 +106247,69 @@ var init_timeback_assessment_import_util = __esm(() => {
|
|
|
106222
106247
|
init_timeback_qti_authoring_util();
|
|
106223
106248
|
});
|
|
106224
106249
|
|
|
106250
|
+
// ../api-core/src/utils/timeback-assessment-revision.util.ts
|
|
106251
|
+
function assertRevisableAssessment(row, reviewBankIdentifier) {
|
|
106252
|
+
if (row.purpose === "review" || row.qtiTestIdentifier === reviewBankIdentifier) {
|
|
106253
|
+
throw new ValidationError("The review bank is system-managed. Revise its mastery sources instead.");
|
|
106254
|
+
}
|
|
106255
|
+
if (row.purpose === "diagnostic") {
|
|
106256
|
+
throw new ValidationError("Diagnostics are revised by importing a new revision and archiving the current one.");
|
|
106257
|
+
}
|
|
106258
|
+
if (row.status !== "live") {
|
|
106259
|
+
throw new ValidationError(row.status === "draft" ? "Draft assessments are edited directly. Only live assessments can be revised in place." : "Archived assessments cannot be revised. Restore it to live first.");
|
|
106260
|
+
}
|
|
106261
|
+
if (!row.assessmentKey) {
|
|
106262
|
+
throw new ValidationError("This unmanaged assessment cannot be revised. Republish it with an explicit assessment key.");
|
|
106263
|
+
}
|
|
106264
|
+
}
|
|
106265
|
+
function assertRevisableTestStructure(current) {
|
|
106266
|
+
const parts2 = qtiTestParts(current);
|
|
106267
|
+
const sectionCount = parts2.reduce((count, part) => count + part["qti-assessment-section"].length, 0);
|
|
106268
|
+
if (parts2.length !== 1 || sectionCount !== 1) {
|
|
106269
|
+
throw new ValidationError(`Assessment ${current.identifier} has ${parts2.length} test part(s) and ${sectionCount} section(s). Revisions can only replace content in single-section assessments.`);
|
|
106270
|
+
}
|
|
106271
|
+
}
|
|
106272
|
+
function buildRevisionSourceTestInput(input) {
|
|
106273
|
+
assertRevisableTestStructure(input.current);
|
|
106274
|
+
const { "qti-outcome-declaration": outcomeDeclarations, ...structure } = buildQtiTestStructureInput(input.current, input.sourceIdentifier);
|
|
106275
|
+
return {
|
|
106276
|
+
identifier: input.sourceIdentifier,
|
|
106277
|
+
title: input.title,
|
|
106278
|
+
...qtiTestOptionalAttributes(input.current),
|
|
106279
|
+
metadata: {
|
|
106280
|
+
...sourceMetadata(input.current.metadata),
|
|
106281
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
106282
|
+
ownerGameSlug: input.gameSlug,
|
|
106283
|
+
assessmentKey: input.assessmentKey,
|
|
106284
|
+
integrationId: input.integrationId,
|
|
106285
|
+
subject: input.subject,
|
|
106286
|
+
grade: String(input.grade),
|
|
106287
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.sourceIdentifier)
|
|
106288
|
+
},
|
|
106289
|
+
...outcomeDeclarations && outcomeDeclarations.length > 0 ? { "qti-outcome-declaration": outcomeDeclarations } : {},
|
|
106290
|
+
"qti-test-part": structure["qti-test-part"].map((part) => ({
|
|
106291
|
+
...part,
|
|
106292
|
+
"qti-assessment-section": part["qti-assessment-section"].map((section) => ({
|
|
106293
|
+
...section,
|
|
106294
|
+
"qti-assessment-item-ref": []
|
|
106295
|
+
}))
|
|
106296
|
+
}))
|
|
106297
|
+
};
|
|
106298
|
+
}
|
|
106299
|
+
function revisionConflict(requestedIdentifier, currentIdentifier) {
|
|
106300
|
+
return new ConflictError("This assessment was revised by someone else. Reload to see the current version before revising again.", {
|
|
106301
|
+
requestedQtiTestIdentifier: requestedIdentifier,
|
|
106302
|
+
currentQtiTestIdentifier: currentIdentifier
|
|
106303
|
+
});
|
|
106304
|
+
}
|
|
106305
|
+
var init_timeback_assessment_revision_util = __esm(() => {
|
|
106306
|
+
init_assessment_runtime2();
|
|
106307
|
+
init_timeback3();
|
|
106308
|
+
init_errors2();
|
|
106309
|
+
init_timeback_assessment_publication_util();
|
|
106310
|
+
init_timeback_qti_authoring_util();
|
|
106311
|
+
});
|
|
106312
|
+
|
|
106225
106313
|
// ../api-core/src/utils/timeback-qti-library.util.ts
|
|
106226
106314
|
function newPlaycademyTestIdentifier() {
|
|
106227
106315
|
return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
|
|
@@ -106647,6 +106735,109 @@ class TimebackAssessmentsService {
|
|
|
106647
106735
|
throw error88;
|
|
106648
106736
|
}
|
|
106649
106737
|
}
|
|
106738
|
+
async reviseAssessment(integrationId, input) {
|
|
106739
|
+
const client2 = this.requireClient();
|
|
106740
|
+
const reviewBankIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
106741
|
+
const current = await this.requireCurrentRevisionRow(integrationId, input.currentQtiTestIdentifier);
|
|
106742
|
+
assertRevisableAssessment(current, reviewBankIdentifier);
|
|
106743
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
106744
|
+
const { integration } = ownership;
|
|
106745
|
+
const creations = input.questions.map((question) => {
|
|
106746
|
+
const creation = parseQtiQuestionCreationInput(question);
|
|
106747
|
+
if (creation.kind === "authoring") {
|
|
106748
|
+
assertSupportedQtiQuestionInteraction(creation.input);
|
|
106749
|
+
}
|
|
106750
|
+
return creation;
|
|
106751
|
+
});
|
|
106752
|
+
assertAssessmentHasQuestions(creations);
|
|
106753
|
+
const currentTest = await client2.qtiApi.assessmentTests.get(current.qtiTestIdentifier);
|
|
106754
|
+
const sourceIdentifier = input.sourceTestIdentifier;
|
|
106755
|
+
const sourceInput = buildRevisionSourceTestInput({
|
|
106756
|
+
sourceIdentifier,
|
|
106757
|
+
title: input.title,
|
|
106758
|
+
current: currentTest,
|
|
106759
|
+
assessmentKey: current.assessmentKey,
|
|
106760
|
+
integrationId,
|
|
106761
|
+
gameSlug: ownership.gameSlug,
|
|
106762
|
+
subject: integration.subject,
|
|
106763
|
+
grade: integration.grade
|
|
106764
|
+
});
|
|
106765
|
+
const createdItemIdentifiers = [];
|
|
106766
|
+
let sourceCreateAttempted = false;
|
|
106767
|
+
try {
|
|
106768
|
+
sourceCreateAttempted = true;
|
|
106769
|
+
await client2.qtiApi.assessmentTests.create(sourceInput);
|
|
106770
|
+
const sourceTest = await client2.qtiApi.assessmentTests.get(sourceIdentifier);
|
|
106771
|
+
for (const creation of creations) {
|
|
106772
|
+
const created = await this.createQuestionInOwnedTest({
|
|
106773
|
+
client: client2,
|
|
106774
|
+
integrationId,
|
|
106775
|
+
qtiTestIdentifier: sourceIdentifier,
|
|
106776
|
+
test: sourceTest,
|
|
106777
|
+
gameSlug: ownership.gameSlug,
|
|
106778
|
+
creation
|
|
106779
|
+
});
|
|
106780
|
+
createdItemIdentifiers.push(created.reference.identifier);
|
|
106781
|
+
}
|
|
106782
|
+
const loaded = await loadHydratedQtiTest(client2, sourceIdentifier);
|
|
106783
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
106784
|
+
const publication = await this.publishManagedAssessment(current, current.purpose, null, loaded);
|
|
106785
|
+
if (!publication.artifactsAvailable) {
|
|
106786
|
+
throw new ValidationError("The revised assessment could not be compiled for the runtime. The current version stays live; try again.");
|
|
106787
|
+
}
|
|
106788
|
+
const result = await this.deps.db.transaction(async (tx) => {
|
|
106789
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, current.assessmentKey))).for("update");
|
|
106790
|
+
const row = rows[0];
|
|
106791
|
+
if (!row) {
|
|
106792
|
+
throw new NotFoundError(`Assessment not found: ${input.currentQtiTestIdentifier}`);
|
|
106793
|
+
}
|
|
106794
|
+
if (row.qtiTestIdentifier !== input.currentQtiTestIdentifier) {
|
|
106795
|
+
throw revisionConflict(input.currentQtiTestIdentifier, row.qtiTestIdentifier);
|
|
106796
|
+
}
|
|
106797
|
+
assertRevisableAssessment(row, reviewBankIdentifier);
|
|
106798
|
+
const previousQtiTestIdentifier = row.qtiTestIdentifier;
|
|
106799
|
+
const unchanged = publication.qtiTestIdentifier === previousQtiTestIdentifier;
|
|
106800
|
+
if (unchanged) {
|
|
106801
|
+
setAttribute("app.assessment.operation", "revise_unchanged");
|
|
106802
|
+
return { row, previousQtiTestIdentifier, unchanged };
|
|
106803
|
+
}
|
|
106804
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({
|
|
106805
|
+
qtiTestIdentifier: publication.qtiTestIdentifier,
|
|
106806
|
+
updatedAt: new Date
|
|
106807
|
+
}).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
|
|
106808
|
+
setAttribute("app.assessment.operation", "revise");
|
|
106809
|
+
addEvent("assessment.revised", {
|
|
106810
|
+
"app.assessment.assessment_key": row.assessmentKey,
|
|
106811
|
+
"app.assessment.previous_qti_test_identifier": previousQtiTestIdentifier,
|
|
106812
|
+
"app.assessment.qti_test_identifier": publication.qtiTestIdentifier
|
|
106813
|
+
});
|
|
106814
|
+
return { row: updatedRow ?? row, previousQtiTestIdentifier, unchanged };
|
|
106815
|
+
});
|
|
106816
|
+
let warning;
|
|
106817
|
+
if (!result.unchanged && result.row.purpose === "mastery") {
|
|
106818
|
+
try {
|
|
106819
|
+
const sync = await this.synchronizeReviewBank(integrationId);
|
|
106820
|
+
if (sync.warnings.length > 0) {
|
|
106821
|
+
warning = `Review bank: ${sync.warnings.map((entry) => entry.message).join(" ")}`;
|
|
106822
|
+
}
|
|
106823
|
+
} catch (error88) {
|
|
106824
|
+
addEvent("assessment.revision_review_bank_sync_failed", {
|
|
106825
|
+
"app.assessment.qti_test_identifier": result.row.qtiTestIdentifier,
|
|
106826
|
+
"exception.type": errorType(error88),
|
|
106827
|
+
"app.error.message": errorMessage(error88)
|
|
106828
|
+
});
|
|
106829
|
+
warning = `The review bank could not be synchronized: ${errorMessage(error88)}. Use Sync Review Bank to retry.`;
|
|
106830
|
+
}
|
|
106831
|
+
}
|
|
106832
|
+
return {
|
|
106833
|
+
...this.associationSummary(result.row, warning),
|
|
106834
|
+
previousQtiTestIdentifier: result.previousQtiTestIdentifier,
|
|
106835
|
+
unchanged: result.unchanged
|
|
106836
|
+
};
|
|
106837
|
+
} finally {
|
|
106838
|
+
await this.cleanupQtiAssessmentCopy(client2, sourceCreateAttempted ? sourceIdentifier : undefined, createdItemIdentifiers);
|
|
106839
|
+
}
|
|
106840
|
+
}
|
|
106650
106841
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
106651
106842
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
106652
106843
|
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
@@ -106919,106 +107110,118 @@ class TimebackAssessmentsService {
|
|
|
106919
107110
|
assertAllAssessmentAssociationsDraft(associations);
|
|
106920
107111
|
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
106921
107112
|
const creation = parseQtiQuestionCreationInput(input);
|
|
106922
|
-
const
|
|
106923
|
-
|
|
106924
|
-
|
|
106925
|
-
|
|
106926
|
-
|
|
106927
|
-
|
|
106928
|
-
|
|
106929
|
-
|
|
106930
|
-
const copy = buildQtiQuestionCopyInput({
|
|
106931
|
-
source,
|
|
106932
|
-
targetIdentifier: itemIdentifier,
|
|
106933
|
-
targetTestIdentifier: qtiTestIdentifier,
|
|
106934
|
-
gameSlug: ownership.gameSlug,
|
|
106935
|
-
integrationId
|
|
106936
|
-
});
|
|
106937
|
-
title = copy.title;
|
|
106938
|
-
metadata2 = copy.metadata;
|
|
106939
|
-
xml = copy.xml;
|
|
106940
|
-
fallbackItem = {
|
|
106941
|
-
...source,
|
|
106942
|
-
identifier: itemIdentifier,
|
|
106943
|
-
title,
|
|
106944
|
-
rawXml: xml,
|
|
106945
|
-
metadata: metadata2
|
|
106946
|
-
};
|
|
106947
|
-
} else {
|
|
106948
|
-
structuredInput = creation.input;
|
|
106949
|
-
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
106950
|
-
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
106951
|
-
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
106952
|
-
gameSlug: ownership.gameSlug,
|
|
106953
|
-
testIdentifier: qtiTestIdentifier
|
|
106954
|
-
});
|
|
106955
|
-
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
106956
|
-
fallbackItem = {
|
|
106957
|
-
...structuredInput,
|
|
106958
|
-
identifier: itemIdentifier,
|
|
106959
|
-
title,
|
|
106960
|
-
...xml ? { rawXml: xml } : {}
|
|
106961
|
-
};
|
|
106962
|
-
}
|
|
106963
|
-
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
106964
|
-
const href = qtiItemHref(client2, itemIdentifier);
|
|
106965
|
-
let item;
|
|
106966
|
-
let itemCreationAttempted = false;
|
|
106967
|
-
let referenceCreationAttempted = false;
|
|
106968
|
-
try {
|
|
106969
|
-
itemCreationAttempted = true;
|
|
106970
|
-
item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
|
|
106971
|
-
...structuredInput,
|
|
106972
|
-
identifier: itemIdentifier,
|
|
106973
|
-
metadata: metadata2
|
|
106974
|
-
});
|
|
106975
|
-
referenceCreationAttempted = true;
|
|
106976
|
-
await section.items.add({
|
|
106977
|
-
identifier: itemIdentifier,
|
|
106978
|
-
href
|
|
106979
|
-
});
|
|
106980
|
-
} catch (error88) {
|
|
106981
|
-
let referenceRemoved = !referenceCreationAttempted;
|
|
106982
|
-
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
106983
|
-
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
106984
|
-
if (referenceCreationAttempted) {
|
|
106985
|
-
try {
|
|
106986
|
-
await section.items.remove(itemIdentifier);
|
|
106987
|
-
referenceRemoved = true;
|
|
106988
|
-
} catch (cleanupError) {
|
|
106989
|
-
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
106990
|
-
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
106991
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
106992
|
-
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
106993
|
-
"exception.type": errorType(cleanupError),
|
|
106994
|
-
"app.error.message": errorMessage(cleanupError)
|
|
106995
|
-
});
|
|
106996
|
-
}
|
|
106997
|
-
}
|
|
106998
|
-
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
106999
|
-
try {
|
|
107000
|
-
await client2.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
107001
|
-
} catch (cleanupError) {
|
|
107002
|
-
addEvent("assessment.qti_question_cleanup_failed", {
|
|
107003
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107004
|
-
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107005
|
-
"exception.type": errorType(cleanupError),
|
|
107006
|
-
"app.error.message": errorMessage(cleanupError)
|
|
107007
|
-
});
|
|
107008
|
-
}
|
|
107009
|
-
}
|
|
107010
|
-
throw error88;
|
|
107011
|
-
}
|
|
107113
|
+
const created = await this.createQuestionInOwnedTest({
|
|
107114
|
+
client: client2,
|
|
107115
|
+
integrationId,
|
|
107116
|
+
qtiTestIdentifier,
|
|
107117
|
+
test: ownership.test,
|
|
107118
|
+
gameSlug: ownership.gameSlug,
|
|
107119
|
+
creation
|
|
107120
|
+
});
|
|
107012
107121
|
setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
|
|
107013
|
-
return
|
|
107014
|
-
|
|
107015
|
-
|
|
107016
|
-
|
|
107017
|
-
|
|
107018
|
-
|
|
107019
|
-
|
|
107122
|
+
return created;
|
|
107123
|
+
});
|
|
107124
|
+
}
|
|
107125
|
+
async createQuestionInOwnedTest(input) {
|
|
107126
|
+
const { client: client2, integrationId, qtiTestIdentifier, creation } = input;
|
|
107127
|
+
const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
|
|
107128
|
+
let title;
|
|
107129
|
+
let metadata2;
|
|
107130
|
+
let xml;
|
|
107131
|
+
let structuredInput;
|
|
107132
|
+
let fallbackItem;
|
|
107133
|
+
if (creation.kind === "copy") {
|
|
107134
|
+
const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
|
|
107135
|
+
const copy = buildQtiQuestionCopyInput({
|
|
107136
|
+
source,
|
|
107137
|
+
targetIdentifier: itemIdentifier,
|
|
107138
|
+
targetTestIdentifier: qtiTestIdentifier,
|
|
107139
|
+
gameSlug: input.gameSlug,
|
|
107140
|
+
integrationId
|
|
107141
|
+
});
|
|
107142
|
+
title = copy.title;
|
|
107143
|
+
metadata2 = copy.metadata;
|
|
107144
|
+
xml = copy.xml;
|
|
107145
|
+
fallbackItem = {
|
|
107146
|
+
...source,
|
|
107147
|
+
identifier: itemIdentifier,
|
|
107148
|
+
title,
|
|
107149
|
+
rawXml: xml,
|
|
107150
|
+
metadata: metadata2
|
|
107151
|
+
};
|
|
107152
|
+
} else {
|
|
107153
|
+
structuredInput = creation.input;
|
|
107154
|
+
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
107155
|
+
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
107156
|
+
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
107157
|
+
gameSlug: input.gameSlug,
|
|
107158
|
+
testIdentifier: qtiTestIdentifier
|
|
107159
|
+
});
|
|
107160
|
+
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
107161
|
+
fallbackItem = {
|
|
107162
|
+
...structuredInput,
|
|
107163
|
+
identifier: itemIdentifier,
|
|
107164
|
+
title,
|
|
107165
|
+
...xml ? { rawXml: xml } : {}
|
|
107166
|
+
};
|
|
107167
|
+
}
|
|
107168
|
+
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, input.test);
|
|
107169
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
107170
|
+
let item;
|
|
107171
|
+
let itemCreationAttempted = false;
|
|
107172
|
+
let referenceCreationAttempted = false;
|
|
107173
|
+
try {
|
|
107174
|
+
itemCreationAttempted = true;
|
|
107175
|
+
item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
|
|
107176
|
+
...structuredInput,
|
|
107177
|
+
identifier: itemIdentifier,
|
|
107020
107178
|
metadata: metadata2
|
|
107021
107179
|
});
|
|
107180
|
+
referenceCreationAttempted = true;
|
|
107181
|
+
await section.items.add({
|
|
107182
|
+
identifier: itemIdentifier,
|
|
107183
|
+
href
|
|
107184
|
+
});
|
|
107185
|
+
} catch (error88) {
|
|
107186
|
+
let referenceRemoved = !referenceCreationAttempted;
|
|
107187
|
+
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
107188
|
+
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
107189
|
+
if (referenceCreationAttempted) {
|
|
107190
|
+
try {
|
|
107191
|
+
await section.items.remove(itemIdentifier);
|
|
107192
|
+
referenceRemoved = true;
|
|
107193
|
+
} catch (cleanupError) {
|
|
107194
|
+
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
107195
|
+
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
107196
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107197
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107198
|
+
"exception.type": errorType(cleanupError),
|
|
107199
|
+
"app.error.message": errorMessage(cleanupError)
|
|
107200
|
+
});
|
|
107201
|
+
}
|
|
107202
|
+
}
|
|
107203
|
+
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
107204
|
+
try {
|
|
107205
|
+
await client2.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
107206
|
+
} catch (cleanupError) {
|
|
107207
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
107208
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107209
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107210
|
+
"exception.type": errorType(cleanupError),
|
|
107211
|
+
"app.error.message": errorMessage(cleanupError)
|
|
107212
|
+
});
|
|
107213
|
+
}
|
|
107214
|
+
}
|
|
107215
|
+
throw error88;
|
|
107216
|
+
}
|
|
107217
|
+
return buildCreatedQtiQuestionReference({
|
|
107218
|
+
item,
|
|
107219
|
+
fallbackItem,
|
|
107220
|
+
itemIdentifier,
|
|
107221
|
+
href,
|
|
107222
|
+
partIdentifier: section.partIdentifier,
|
|
107223
|
+
sectionIdentifier: section.sectionIdentifier,
|
|
107224
|
+
metadata: metadata2
|
|
107022
107225
|
});
|
|
107023
107226
|
}
|
|
107024
107227
|
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
@@ -107266,6 +107469,22 @@ class TimebackAssessmentsService {
|
|
|
107266
107469
|
}
|
|
107267
107470
|
return row;
|
|
107268
107471
|
}
|
|
107472
|
+
async requireCurrentRevisionRow(integrationId, qtiTestIdentifier) {
|
|
107473
|
+
const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
107474
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
|
|
107475
|
+
});
|
|
107476
|
+
if (row) {
|
|
107477
|
+
return row;
|
|
107478
|
+
}
|
|
107479
|
+
const assessmentKey = assessmentKeyFromManagedQtiIdentifier(qtiTestIdentifier);
|
|
107480
|
+
const successor = assessmentKey ? await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
107481
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, assessmentKey))
|
|
107482
|
+
}) : undefined;
|
|
107483
|
+
if (successor) {
|
|
107484
|
+
throw revisionConflict(qtiTestIdentifier, successor.qtiTestIdentifier);
|
|
107485
|
+
}
|
|
107486
|
+
throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
|
|
107487
|
+
}
|
|
107269
107488
|
async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
|
|
107270
107489
|
const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
107271
107490
|
assertDraftAssessment(row);
|
|
@@ -107413,9 +107632,21 @@ class TimebackAssessmentsService {
|
|
|
107413
107632
|
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
107414
107633
|
});
|
|
107415
107634
|
const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
|
|
107416
|
-
const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
107635
|
+
const artifactsAvailable = await this.managedArtifactsStored(plan.qtiTestIdentifier) || await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
107417
107636
|
const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
|
|
107418
|
-
return { ...plan, ...warning ? { warning } : {} };
|
|
107637
|
+
return { ...plan, artifactsAvailable, ...warning ? { warning } : {} };
|
|
107638
|
+
}
|
|
107639
|
+
async managedArtifactsStored(qtiTestIdentifier) {
|
|
107640
|
+
try {
|
|
107641
|
+
return await this.deps.assessmentArtifacts.loadManagedPlayable(qtiTestIdentifier) !== null;
|
|
107642
|
+
} catch (error88) {
|
|
107643
|
+
addEvent("assessment.artifact_lookup_failed", {
|
|
107644
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107645
|
+
"exception.type": errorType(error88),
|
|
107646
|
+
"app.error.message": errorMessage(error88)
|
|
107647
|
+
});
|
|
107648
|
+
return false;
|
|
107649
|
+
}
|
|
107419
107650
|
}
|
|
107420
107651
|
async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
|
|
107421
107652
|
let testDeleted = !testIdentifier;
|
|
@@ -107542,6 +107773,7 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
107542
107773
|
init_timeback_assessment_diagnostic_util();
|
|
107543
107774
|
init_timeback_assessment_import_util();
|
|
107544
107775
|
init_timeback_assessment_publication_util();
|
|
107776
|
+
init_timeback_assessment_revision_util();
|
|
107545
107777
|
init_timeback_assessment_rules_util();
|
|
107546
107778
|
init_timeback_qti_authoring_util();
|
|
107547
107779
|
init_timeback_qti_hydration_util();
|
|
@@ -167551,7 +167783,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
167551
167783
|
limit
|
|
167552
167784
|
};
|
|
167553
167785
|
}
|
|
167554
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
167786
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
167555
167787
|
var init_timeback_controller = __esm(() => {
|
|
167556
167788
|
init_esm();
|
|
167557
167789
|
init_src();
|
|
@@ -168296,6 +168528,20 @@ var init_timeback_controller = __esm(() => {
|
|
|
168296
168528
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
168297
168529
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
168298
168530
|
});
|
|
168531
|
+
reviseAssessment = requireDeveloper(async (ctx) => {
|
|
168532
|
+
const { gameId, courseId } = ctx.params;
|
|
168533
|
+
if (!gameId || !courseId) {
|
|
168534
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
168535
|
+
}
|
|
168536
|
+
const body2 = await parseRequestBody(ctx.request, ReviseAssessmentRequestSchema);
|
|
168537
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
168538
|
+
return ctx.services.timebackAssessments.reviseAssessment(integrationId, {
|
|
168539
|
+
currentQtiTestIdentifier: body2.currentQtiTestIdentifier,
|
|
168540
|
+
sourceTestIdentifier: newPlaycademyTestIdentifier(),
|
|
168541
|
+
title: body2.title,
|
|
168542
|
+
questions: body2.questions
|
|
168543
|
+
});
|
|
168544
|
+
});
|
|
168299
168545
|
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
168300
168546
|
const { gameId, courseId } = ctx.params;
|
|
168301
168547
|
if (!gameId || !courseId) {
|
|
@@ -168446,6 +168692,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
168446
168692
|
createAssessment,
|
|
168447
168693
|
attachExistingAssessments,
|
|
168448
168694
|
updateAssessment,
|
|
168695
|
+
reviseAssessment,
|
|
168449
168696
|
synchronizeReviewBank,
|
|
168450
168697
|
removeAssessment,
|
|
168451
168698
|
reorderQuestions,
|
package/dist/server.js
CHANGED
|
@@ -1127,7 +1127,7 @@ var package_default;
|
|
|
1127
1127
|
var init_package = __esm(() => {
|
|
1128
1128
|
package_default = {
|
|
1129
1129
|
name: "@playcademy/sandbox",
|
|
1130
|
-
version: "0.10.1-beta.
|
|
1130
|
+
version: "0.10.1-beta.6",
|
|
1131
1131
|
description: "Local development server for Playcademy game development",
|
|
1132
1132
|
type: "module",
|
|
1133
1133
|
exports: {
|
|
@@ -47988,7 +47988,7 @@ function rejectSystemManagedReview(input, context2) {
|
|
|
47988
47988
|
});
|
|
47989
47989
|
}
|
|
47990
47990
|
}
|
|
47991
|
-
var 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, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47991
|
+
var 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, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47992
47992
|
var init_schemas4 = __esm(() => {
|
|
47993
47993
|
init_esm();
|
|
47994
47994
|
init_src();
|
|
@@ -48271,6 +48271,11 @@ var init_schemas4 = __esm(() => {
|
|
|
48271
48271
|
requireMasteryStandard(input, context2);
|
|
48272
48272
|
rejectSystemManagedReview(input, context2);
|
|
48273
48273
|
});
|
|
48274
|
+
ReviseAssessmentRequestSchema = exports_external.object({
|
|
48275
|
+
currentQtiTestIdentifier: exports_external.string().trim().min(1, "The current assessment identifier is required"),
|
|
48276
|
+
title: exports_external.string().trim().min(1, "Assessment title is required"),
|
|
48277
|
+
questions: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).min(1, "A revision must contain at least one question").max(500, "A revision cannot contain more than 500 questions")
|
|
48278
|
+
});
|
|
48274
48279
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
48275
48280
|
assessmentKey: AssessmentKeySchema,
|
|
48276
48281
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
@@ -94022,8 +94027,20 @@ function compareEnrollmentsByRecency(a, b) {
|
|
|
94022
94027
|
}
|
|
94023
94028
|
return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
|
|
94024
94029
|
}
|
|
94030
|
+
function resolvePrimaryAccountRole(roles2) {
|
|
94031
|
+
if (!roles2?.length) {
|
|
94032
|
+
return null;
|
|
94033
|
+
}
|
|
94034
|
+
return ACCOUNT_ROLE_PRECEDENCE.find((role) => roles2.some((entry) => entry.role === role)) ?? null;
|
|
94035
|
+
}
|
|
94036
|
+
var ACCOUNT_ROLE_PRECEDENCE;
|
|
94025
94037
|
var init_timeback_admin_util = __esm(() => {
|
|
94026
94038
|
init_errors2();
|
|
94039
|
+
ACCOUNT_ROLE_PRECEDENCE = [
|
|
94040
|
+
"administrator",
|
|
94041
|
+
"teacher",
|
|
94042
|
+
"student"
|
|
94043
|
+
];
|
|
94027
94044
|
});
|
|
94028
94045
|
|
|
94029
94046
|
// ../api-core/src/utils/timeback-discrepancy-queue.util.ts
|
|
@@ -98733,6 +98750,7 @@ class TimebackAdminService {
|
|
|
98733
98750
|
className: rosterEntry.class?.title || null,
|
|
98734
98751
|
name: name3,
|
|
98735
98752
|
email: rosterEntry.user?.email || null,
|
|
98753
|
+
primaryRole: resolvePrimaryAccountRole(rosterEntry.user?.roles),
|
|
98736
98754
|
analyticsUnavailable,
|
|
98737
98755
|
totalXp: summary?.totalXp ?? 0,
|
|
98738
98756
|
todayXp: summary?.todayXp ?? 0,
|
|
@@ -99696,15 +99714,22 @@ class TimebackAdminService {
|
|
|
99696
99714
|
});
|
|
99697
99715
|
pastEnrollmentsByStudent.set(studentId, list);
|
|
99698
99716
|
}
|
|
99699
|
-
const students = allUsers.
|
|
99717
|
+
const students = allUsers.flatMap((entry) => {
|
|
99718
|
+
const primaryRole = resolvePrimaryAccountRole(entry.roles);
|
|
99719
|
+
if (!entry.sourcedId || !primaryRole) {
|
|
99720
|
+
return [];
|
|
99721
|
+
}
|
|
99700
99722
|
const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
|
|
99701
|
-
return
|
|
99702
|
-
|
|
99703
|
-
|
|
99704
|
-
|
|
99705
|
-
|
|
99706
|
-
|
|
99707
|
-
|
|
99723
|
+
return [
|
|
99724
|
+
{
|
|
99725
|
+
studentId: entry.sourcedId,
|
|
99726
|
+
name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
|
|
99727
|
+
email: entry.email || null,
|
|
99728
|
+
primaryRole,
|
|
99729
|
+
alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
|
|
99730
|
+
...past.length > 0 ? { pastEnrollments: past } : {}
|
|
99731
|
+
}
|
|
99732
|
+
];
|
|
99708
99733
|
});
|
|
99709
99734
|
setAttribute("app.timeback.result_count", students.length);
|
|
99710
99735
|
return { students };
|
|
@@ -106221,6 +106246,69 @@ var init_timeback_assessment_import_util = __esm(() => {
|
|
|
106221
106246
|
init_timeback_qti_authoring_util();
|
|
106222
106247
|
});
|
|
106223
106248
|
|
|
106249
|
+
// ../api-core/src/utils/timeback-assessment-revision.util.ts
|
|
106250
|
+
function assertRevisableAssessment(row, reviewBankIdentifier) {
|
|
106251
|
+
if (row.purpose === "review" || row.qtiTestIdentifier === reviewBankIdentifier) {
|
|
106252
|
+
throw new ValidationError("The review bank is system-managed. Revise its mastery sources instead.");
|
|
106253
|
+
}
|
|
106254
|
+
if (row.purpose === "diagnostic") {
|
|
106255
|
+
throw new ValidationError("Diagnostics are revised by importing a new revision and archiving the current one.");
|
|
106256
|
+
}
|
|
106257
|
+
if (row.status !== "live") {
|
|
106258
|
+
throw new ValidationError(row.status === "draft" ? "Draft assessments are edited directly. Only live assessments can be revised in place." : "Archived assessments cannot be revised. Restore it to live first.");
|
|
106259
|
+
}
|
|
106260
|
+
if (!row.assessmentKey) {
|
|
106261
|
+
throw new ValidationError("This unmanaged assessment cannot be revised. Republish it with an explicit assessment key.");
|
|
106262
|
+
}
|
|
106263
|
+
}
|
|
106264
|
+
function assertRevisableTestStructure(current) {
|
|
106265
|
+
const parts2 = qtiTestParts(current);
|
|
106266
|
+
const sectionCount = parts2.reduce((count, part) => count + part["qti-assessment-section"].length, 0);
|
|
106267
|
+
if (parts2.length !== 1 || sectionCount !== 1) {
|
|
106268
|
+
throw new ValidationError(`Assessment ${current.identifier} has ${parts2.length} test part(s) and ${sectionCount} section(s). Revisions can only replace content in single-section assessments.`);
|
|
106269
|
+
}
|
|
106270
|
+
}
|
|
106271
|
+
function buildRevisionSourceTestInput(input) {
|
|
106272
|
+
assertRevisableTestStructure(input.current);
|
|
106273
|
+
const { "qti-outcome-declaration": outcomeDeclarations, ...structure } = buildQtiTestStructureInput(input.current, input.sourceIdentifier);
|
|
106274
|
+
return {
|
|
106275
|
+
identifier: input.sourceIdentifier,
|
|
106276
|
+
title: input.title,
|
|
106277
|
+
...qtiTestOptionalAttributes(input.current),
|
|
106278
|
+
metadata: {
|
|
106279
|
+
...sourceMetadata(input.current.metadata),
|
|
106280
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
106281
|
+
ownerGameSlug: input.gameSlug,
|
|
106282
|
+
assessmentKey: input.assessmentKey,
|
|
106283
|
+
integrationId: input.integrationId,
|
|
106284
|
+
subject: input.subject,
|
|
106285
|
+
grade: String(input.grade),
|
|
106286
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.sourceIdentifier)
|
|
106287
|
+
},
|
|
106288
|
+
...outcomeDeclarations && outcomeDeclarations.length > 0 ? { "qti-outcome-declaration": outcomeDeclarations } : {},
|
|
106289
|
+
"qti-test-part": structure["qti-test-part"].map((part) => ({
|
|
106290
|
+
...part,
|
|
106291
|
+
"qti-assessment-section": part["qti-assessment-section"].map((section) => ({
|
|
106292
|
+
...section,
|
|
106293
|
+
"qti-assessment-item-ref": []
|
|
106294
|
+
}))
|
|
106295
|
+
}))
|
|
106296
|
+
};
|
|
106297
|
+
}
|
|
106298
|
+
function revisionConflict(requestedIdentifier, currentIdentifier) {
|
|
106299
|
+
return new ConflictError("This assessment was revised by someone else. Reload to see the current version before revising again.", {
|
|
106300
|
+
requestedQtiTestIdentifier: requestedIdentifier,
|
|
106301
|
+
currentQtiTestIdentifier: currentIdentifier
|
|
106302
|
+
});
|
|
106303
|
+
}
|
|
106304
|
+
var init_timeback_assessment_revision_util = __esm(() => {
|
|
106305
|
+
init_assessment_runtime2();
|
|
106306
|
+
init_timeback3();
|
|
106307
|
+
init_errors2();
|
|
106308
|
+
init_timeback_assessment_publication_util();
|
|
106309
|
+
init_timeback_qti_authoring_util();
|
|
106310
|
+
});
|
|
106311
|
+
|
|
106224
106312
|
// ../api-core/src/utils/timeback-qti-library.util.ts
|
|
106225
106313
|
function newPlaycademyTestIdentifier() {
|
|
106226
106314
|
return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
|
|
@@ -106646,6 +106734,109 @@ class TimebackAssessmentsService {
|
|
|
106646
106734
|
throw error88;
|
|
106647
106735
|
}
|
|
106648
106736
|
}
|
|
106737
|
+
async reviseAssessment(integrationId, input) {
|
|
106738
|
+
const client2 = this.requireClient();
|
|
106739
|
+
const reviewBankIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
106740
|
+
const current = await this.requireCurrentRevisionRow(integrationId, input.currentQtiTestIdentifier);
|
|
106741
|
+
assertRevisableAssessment(current, reviewBankIdentifier);
|
|
106742
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
106743
|
+
const { integration } = ownership;
|
|
106744
|
+
const creations = input.questions.map((question) => {
|
|
106745
|
+
const creation = parseQtiQuestionCreationInput(question);
|
|
106746
|
+
if (creation.kind === "authoring") {
|
|
106747
|
+
assertSupportedQtiQuestionInteraction(creation.input);
|
|
106748
|
+
}
|
|
106749
|
+
return creation;
|
|
106750
|
+
});
|
|
106751
|
+
assertAssessmentHasQuestions(creations);
|
|
106752
|
+
const currentTest = await client2.qtiApi.assessmentTests.get(current.qtiTestIdentifier);
|
|
106753
|
+
const sourceIdentifier = input.sourceTestIdentifier;
|
|
106754
|
+
const sourceInput = buildRevisionSourceTestInput({
|
|
106755
|
+
sourceIdentifier,
|
|
106756
|
+
title: input.title,
|
|
106757
|
+
current: currentTest,
|
|
106758
|
+
assessmentKey: current.assessmentKey,
|
|
106759
|
+
integrationId,
|
|
106760
|
+
gameSlug: ownership.gameSlug,
|
|
106761
|
+
subject: integration.subject,
|
|
106762
|
+
grade: integration.grade
|
|
106763
|
+
});
|
|
106764
|
+
const createdItemIdentifiers = [];
|
|
106765
|
+
let sourceCreateAttempted = false;
|
|
106766
|
+
try {
|
|
106767
|
+
sourceCreateAttempted = true;
|
|
106768
|
+
await client2.qtiApi.assessmentTests.create(sourceInput);
|
|
106769
|
+
const sourceTest = await client2.qtiApi.assessmentTests.get(sourceIdentifier);
|
|
106770
|
+
for (const creation of creations) {
|
|
106771
|
+
const created = await this.createQuestionInOwnedTest({
|
|
106772
|
+
client: client2,
|
|
106773
|
+
integrationId,
|
|
106774
|
+
qtiTestIdentifier: sourceIdentifier,
|
|
106775
|
+
test: sourceTest,
|
|
106776
|
+
gameSlug: ownership.gameSlug,
|
|
106777
|
+
creation
|
|
106778
|
+
});
|
|
106779
|
+
createdItemIdentifiers.push(created.reference.identifier);
|
|
106780
|
+
}
|
|
106781
|
+
const loaded = await loadHydratedQtiTest(client2, sourceIdentifier);
|
|
106782
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
106783
|
+
const publication = await this.publishManagedAssessment(current, current.purpose, null, loaded);
|
|
106784
|
+
if (!publication.artifactsAvailable) {
|
|
106785
|
+
throw new ValidationError("The revised assessment could not be compiled for the runtime. The current version stays live; try again.");
|
|
106786
|
+
}
|
|
106787
|
+
const result = await this.deps.db.transaction(async (tx) => {
|
|
106788
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, current.assessmentKey))).for("update");
|
|
106789
|
+
const row = rows[0];
|
|
106790
|
+
if (!row) {
|
|
106791
|
+
throw new NotFoundError(`Assessment not found: ${input.currentQtiTestIdentifier}`);
|
|
106792
|
+
}
|
|
106793
|
+
if (row.qtiTestIdentifier !== input.currentQtiTestIdentifier) {
|
|
106794
|
+
throw revisionConflict(input.currentQtiTestIdentifier, row.qtiTestIdentifier);
|
|
106795
|
+
}
|
|
106796
|
+
assertRevisableAssessment(row, reviewBankIdentifier);
|
|
106797
|
+
const previousQtiTestIdentifier = row.qtiTestIdentifier;
|
|
106798
|
+
const unchanged = publication.qtiTestIdentifier === previousQtiTestIdentifier;
|
|
106799
|
+
if (unchanged) {
|
|
106800
|
+
setAttribute("app.assessment.operation", "revise_unchanged");
|
|
106801
|
+
return { row, previousQtiTestIdentifier, unchanged };
|
|
106802
|
+
}
|
|
106803
|
+
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({
|
|
106804
|
+
qtiTestIdentifier: publication.qtiTestIdentifier,
|
|
106805
|
+
updatedAt: new Date
|
|
106806
|
+
}).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
|
|
106807
|
+
setAttribute("app.assessment.operation", "revise");
|
|
106808
|
+
addEvent("assessment.revised", {
|
|
106809
|
+
"app.assessment.assessment_key": row.assessmentKey,
|
|
106810
|
+
"app.assessment.previous_qti_test_identifier": previousQtiTestIdentifier,
|
|
106811
|
+
"app.assessment.qti_test_identifier": publication.qtiTestIdentifier
|
|
106812
|
+
});
|
|
106813
|
+
return { row: updatedRow ?? row, previousQtiTestIdentifier, unchanged };
|
|
106814
|
+
});
|
|
106815
|
+
let warning;
|
|
106816
|
+
if (!result.unchanged && result.row.purpose === "mastery") {
|
|
106817
|
+
try {
|
|
106818
|
+
const sync = await this.synchronizeReviewBank(integrationId);
|
|
106819
|
+
if (sync.warnings.length > 0) {
|
|
106820
|
+
warning = `Review bank: ${sync.warnings.map((entry) => entry.message).join(" ")}`;
|
|
106821
|
+
}
|
|
106822
|
+
} catch (error88) {
|
|
106823
|
+
addEvent("assessment.revision_review_bank_sync_failed", {
|
|
106824
|
+
"app.assessment.qti_test_identifier": result.row.qtiTestIdentifier,
|
|
106825
|
+
"exception.type": errorType(error88),
|
|
106826
|
+
"app.error.message": errorMessage(error88)
|
|
106827
|
+
});
|
|
106828
|
+
warning = `The review bank could not be synchronized: ${errorMessage(error88)}. Use Sync Review Bank to retry.`;
|
|
106829
|
+
}
|
|
106830
|
+
}
|
|
106831
|
+
return {
|
|
106832
|
+
...this.associationSummary(result.row, warning),
|
|
106833
|
+
previousQtiTestIdentifier: result.previousQtiTestIdentifier,
|
|
106834
|
+
unchanged: result.unchanged
|
|
106835
|
+
};
|
|
106836
|
+
} finally {
|
|
106837
|
+
await this.cleanupQtiAssessmentCopy(client2, sourceCreateAttempted ? sourceIdentifier : undefined, createdItemIdentifiers);
|
|
106838
|
+
}
|
|
106839
|
+
}
|
|
106649
106840
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
106650
106841
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
106651
106842
|
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
@@ -106918,106 +107109,118 @@ class TimebackAssessmentsService {
|
|
|
106918
107109
|
assertAllAssessmentAssociationsDraft(associations);
|
|
106919
107110
|
const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
|
|
106920
107111
|
const creation = parseQtiQuestionCreationInput(input);
|
|
106921
|
-
const
|
|
106922
|
-
|
|
106923
|
-
|
|
106924
|
-
|
|
106925
|
-
|
|
106926
|
-
|
|
106927
|
-
|
|
106928
|
-
|
|
106929
|
-
const copy = buildQtiQuestionCopyInput({
|
|
106930
|
-
source,
|
|
106931
|
-
targetIdentifier: itemIdentifier,
|
|
106932
|
-
targetTestIdentifier: qtiTestIdentifier,
|
|
106933
|
-
gameSlug: ownership.gameSlug,
|
|
106934
|
-
integrationId
|
|
106935
|
-
});
|
|
106936
|
-
title = copy.title;
|
|
106937
|
-
metadata2 = copy.metadata;
|
|
106938
|
-
xml = copy.xml;
|
|
106939
|
-
fallbackItem = {
|
|
106940
|
-
...source,
|
|
106941
|
-
identifier: itemIdentifier,
|
|
106942
|
-
title,
|
|
106943
|
-
rawXml: xml,
|
|
106944
|
-
metadata: metadata2
|
|
106945
|
-
};
|
|
106946
|
-
} else {
|
|
106947
|
-
structuredInput = creation.input;
|
|
106948
|
-
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
106949
|
-
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
106950
|
-
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
106951
|
-
gameSlug: ownership.gameSlug,
|
|
106952
|
-
testIdentifier: qtiTestIdentifier
|
|
106953
|
-
});
|
|
106954
|
-
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
106955
|
-
fallbackItem = {
|
|
106956
|
-
...structuredInput,
|
|
106957
|
-
identifier: itemIdentifier,
|
|
106958
|
-
title,
|
|
106959
|
-
...xml ? { rawXml: xml } : {}
|
|
106960
|
-
};
|
|
106961
|
-
}
|
|
106962
|
-
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
106963
|
-
const href = qtiItemHref(client2, itemIdentifier);
|
|
106964
|
-
let item;
|
|
106965
|
-
let itemCreationAttempted = false;
|
|
106966
|
-
let referenceCreationAttempted = false;
|
|
106967
|
-
try {
|
|
106968
|
-
itemCreationAttempted = true;
|
|
106969
|
-
item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
|
|
106970
|
-
...structuredInput,
|
|
106971
|
-
identifier: itemIdentifier,
|
|
106972
|
-
metadata: metadata2
|
|
106973
|
-
});
|
|
106974
|
-
referenceCreationAttempted = true;
|
|
106975
|
-
await section.items.add({
|
|
106976
|
-
identifier: itemIdentifier,
|
|
106977
|
-
href
|
|
106978
|
-
});
|
|
106979
|
-
} catch (error88) {
|
|
106980
|
-
let referenceRemoved = !referenceCreationAttempted;
|
|
106981
|
-
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
106982
|
-
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
106983
|
-
if (referenceCreationAttempted) {
|
|
106984
|
-
try {
|
|
106985
|
-
await section.items.remove(itemIdentifier);
|
|
106986
|
-
referenceRemoved = true;
|
|
106987
|
-
} catch (cleanupError) {
|
|
106988
|
-
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
106989
|
-
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
106990
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
106991
|
-
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
106992
|
-
"exception.type": errorType(cleanupError),
|
|
106993
|
-
"app.error.message": errorMessage(cleanupError)
|
|
106994
|
-
});
|
|
106995
|
-
}
|
|
106996
|
-
}
|
|
106997
|
-
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
106998
|
-
try {
|
|
106999
|
-
await client2.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
107000
|
-
} catch (cleanupError) {
|
|
107001
|
-
addEvent("assessment.qti_question_cleanup_failed", {
|
|
107002
|
-
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107003
|
-
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107004
|
-
"exception.type": errorType(cleanupError),
|
|
107005
|
-
"app.error.message": errorMessage(cleanupError)
|
|
107006
|
-
});
|
|
107007
|
-
}
|
|
107008
|
-
}
|
|
107009
|
-
throw error88;
|
|
107010
|
-
}
|
|
107112
|
+
const created = await this.createQuestionInOwnedTest({
|
|
107113
|
+
client: client2,
|
|
107114
|
+
integrationId,
|
|
107115
|
+
qtiTestIdentifier,
|
|
107116
|
+
test: ownership.test,
|
|
107117
|
+
gameSlug: ownership.gameSlug,
|
|
107118
|
+
creation
|
|
107119
|
+
});
|
|
107011
107120
|
setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
|
|
107012
|
-
return
|
|
107013
|
-
|
|
107014
|
-
|
|
107015
|
-
|
|
107016
|
-
|
|
107017
|
-
|
|
107018
|
-
|
|
107121
|
+
return created;
|
|
107122
|
+
});
|
|
107123
|
+
}
|
|
107124
|
+
async createQuestionInOwnedTest(input) {
|
|
107125
|
+
const { client: client2, integrationId, qtiTestIdentifier, creation } = input;
|
|
107126
|
+
const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
|
|
107127
|
+
let title;
|
|
107128
|
+
let metadata2;
|
|
107129
|
+
let xml;
|
|
107130
|
+
let structuredInput;
|
|
107131
|
+
let fallbackItem;
|
|
107132
|
+
if (creation.kind === "copy") {
|
|
107133
|
+
const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
|
|
107134
|
+
const copy = buildQtiQuestionCopyInput({
|
|
107135
|
+
source,
|
|
107136
|
+
targetIdentifier: itemIdentifier,
|
|
107137
|
+
targetTestIdentifier: qtiTestIdentifier,
|
|
107138
|
+
gameSlug: input.gameSlug,
|
|
107139
|
+
integrationId
|
|
107140
|
+
});
|
|
107141
|
+
title = copy.title;
|
|
107142
|
+
metadata2 = copy.metadata;
|
|
107143
|
+
xml = copy.xml;
|
|
107144
|
+
fallbackItem = {
|
|
107145
|
+
...source,
|
|
107146
|
+
identifier: itemIdentifier,
|
|
107147
|
+
title,
|
|
107148
|
+
rawXml: xml,
|
|
107149
|
+
metadata: metadata2
|
|
107150
|
+
};
|
|
107151
|
+
} else {
|
|
107152
|
+
structuredInput = creation.input;
|
|
107153
|
+
assertSupportedQtiQuestionInteraction(structuredInput);
|
|
107154
|
+
title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
|
|
107155
|
+
metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
|
|
107156
|
+
gameSlug: input.gameSlug,
|
|
107157
|
+
testIdentifier: qtiTestIdentifier
|
|
107158
|
+
});
|
|
107159
|
+
xml = buildQuestionXml(structuredInput, itemIdentifier, title);
|
|
107160
|
+
fallbackItem = {
|
|
107161
|
+
...structuredInput,
|
|
107162
|
+
identifier: itemIdentifier,
|
|
107163
|
+
title,
|
|
107164
|
+
...xml ? { rawXml: xml } : {}
|
|
107165
|
+
};
|
|
107166
|
+
}
|
|
107167
|
+
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, input.test);
|
|
107168
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
107169
|
+
let item;
|
|
107170
|
+
let itemCreationAttempted = false;
|
|
107171
|
+
let referenceCreationAttempted = false;
|
|
107172
|
+
try {
|
|
107173
|
+
itemCreationAttempted = true;
|
|
107174
|
+
item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
|
|
107175
|
+
...structuredInput,
|
|
107176
|
+
identifier: itemIdentifier,
|
|
107019
107177
|
metadata: metadata2
|
|
107020
107178
|
});
|
|
107179
|
+
referenceCreationAttempted = true;
|
|
107180
|
+
await section.items.add({
|
|
107181
|
+
identifier: itemIdentifier,
|
|
107182
|
+
href
|
|
107183
|
+
});
|
|
107184
|
+
} catch (error88) {
|
|
107185
|
+
let referenceRemoved = !referenceCreationAttempted;
|
|
107186
|
+
const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
|
|
107187
|
+
const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
|
|
107188
|
+
if (referenceCreationAttempted) {
|
|
107189
|
+
try {
|
|
107190
|
+
await section.items.remove(itemIdentifier);
|
|
107191
|
+
referenceRemoved = true;
|
|
107192
|
+
} catch (cleanupError) {
|
|
107193
|
+
referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
|
|
107194
|
+
addEvent("assessment.qti_question_reference_cleanup_failed", {
|
|
107195
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107196
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107197
|
+
"exception.type": errorType(cleanupError),
|
|
107198
|
+
"app.error.message": errorMessage(cleanupError)
|
|
107199
|
+
});
|
|
107200
|
+
}
|
|
107201
|
+
}
|
|
107202
|
+
if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
|
|
107203
|
+
try {
|
|
107204
|
+
await client2.qtiApi.assessmentItems.delete(itemIdentifier);
|
|
107205
|
+
} catch (cleanupError) {
|
|
107206
|
+
addEvent("assessment.qti_question_cleanup_failed", {
|
|
107207
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107208
|
+
"app.assessment.qti_item_identifier": itemIdentifier,
|
|
107209
|
+
"exception.type": errorType(cleanupError),
|
|
107210
|
+
"app.error.message": errorMessage(cleanupError)
|
|
107211
|
+
});
|
|
107212
|
+
}
|
|
107213
|
+
}
|
|
107214
|
+
throw error88;
|
|
107215
|
+
}
|
|
107216
|
+
return buildCreatedQtiQuestionReference({
|
|
107217
|
+
item,
|
|
107218
|
+
fallbackItem,
|
|
107219
|
+
itemIdentifier,
|
|
107220
|
+
href,
|
|
107221
|
+
partIdentifier: section.partIdentifier,
|
|
107222
|
+
sectionIdentifier: section.sectionIdentifier,
|
|
107223
|
+
metadata: metadata2
|
|
107021
107224
|
});
|
|
107022
107225
|
}
|
|
107023
107226
|
async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
|
|
@@ -107265,6 +107468,22 @@ class TimebackAssessmentsService {
|
|
|
107265
107468
|
}
|
|
107266
107469
|
return row;
|
|
107267
107470
|
}
|
|
107471
|
+
async requireCurrentRevisionRow(integrationId, qtiTestIdentifier) {
|
|
107472
|
+
const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
107473
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
|
|
107474
|
+
});
|
|
107475
|
+
if (row) {
|
|
107476
|
+
return row;
|
|
107477
|
+
}
|
|
107478
|
+
const assessmentKey = assessmentKeyFromManagedQtiIdentifier(qtiTestIdentifier);
|
|
107479
|
+
const successor = assessmentKey ? await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
|
|
107480
|
+
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, assessmentKey))
|
|
107481
|
+
}) : undefined;
|
|
107482
|
+
if (successor) {
|
|
107483
|
+
throw revisionConflict(qtiTestIdentifier, successor.qtiTestIdentifier);
|
|
107484
|
+
}
|
|
107485
|
+
throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
|
|
107486
|
+
}
|
|
107268
107487
|
async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
|
|
107269
107488
|
const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
107270
107489
|
assertDraftAssessment(row);
|
|
@@ -107412,9 +107631,21 @@ class TimebackAssessmentsService {
|
|
|
107412
107631
|
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
107413
107632
|
});
|
|
107414
107633
|
const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
|
|
107415
|
-
const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
107634
|
+
const artifactsAvailable = await this.managedArtifactsStored(plan.qtiTestIdentifier) || await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
107416
107635
|
const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
|
|
107417
|
-
return { ...plan, ...warning ? { warning } : {} };
|
|
107636
|
+
return { ...plan, artifactsAvailable, ...warning ? { warning } : {} };
|
|
107637
|
+
}
|
|
107638
|
+
async managedArtifactsStored(qtiTestIdentifier) {
|
|
107639
|
+
try {
|
|
107640
|
+
return await this.deps.assessmentArtifacts.loadManagedPlayable(qtiTestIdentifier) !== null;
|
|
107641
|
+
} catch (error88) {
|
|
107642
|
+
addEvent("assessment.artifact_lookup_failed", {
|
|
107643
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
107644
|
+
"exception.type": errorType(error88),
|
|
107645
|
+
"app.error.message": errorMessage(error88)
|
|
107646
|
+
});
|
|
107647
|
+
return false;
|
|
107648
|
+
}
|
|
107418
107649
|
}
|
|
107419
107650
|
async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
|
|
107420
107651
|
let testDeleted = !testIdentifier;
|
|
@@ -107541,6 +107772,7 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
107541
107772
|
init_timeback_assessment_diagnostic_util();
|
|
107542
107773
|
init_timeback_assessment_import_util();
|
|
107543
107774
|
init_timeback_assessment_publication_util();
|
|
107775
|
+
init_timeback_assessment_revision_util();
|
|
107544
107776
|
init_timeback_assessment_rules_util();
|
|
107545
107777
|
init_timeback_qti_authoring_util();
|
|
107546
107778
|
init_timeback_qti_hydration_util();
|
|
@@ -167550,7 +167782,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
167550
167782
|
limit
|
|
167551
167783
|
};
|
|
167552
167784
|
}
|
|
167553
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
167785
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
167554
167786
|
var init_timeback_controller = __esm(() => {
|
|
167555
167787
|
init_esm();
|
|
167556
167788
|
init_src();
|
|
@@ -168295,6 +168527,20 @@ var init_timeback_controller = __esm(() => {
|
|
|
168295
168527
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
168296
168528
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
168297
168529
|
});
|
|
168530
|
+
reviseAssessment = requireDeveloper(async (ctx) => {
|
|
168531
|
+
const { gameId, courseId } = ctx.params;
|
|
168532
|
+
if (!gameId || !courseId) {
|
|
168533
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
168534
|
+
}
|
|
168535
|
+
const body2 = await parseRequestBody(ctx.request, ReviseAssessmentRequestSchema);
|
|
168536
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
168537
|
+
return ctx.services.timebackAssessments.reviseAssessment(integrationId, {
|
|
168538
|
+
currentQtiTestIdentifier: body2.currentQtiTestIdentifier,
|
|
168539
|
+
sourceTestIdentifier: newPlaycademyTestIdentifier(),
|
|
168540
|
+
title: body2.title,
|
|
168541
|
+
questions: body2.questions
|
|
168542
|
+
});
|
|
168543
|
+
});
|
|
168298
168544
|
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
168299
168545
|
const { gameId, courseId } = ctx.params;
|
|
168300
168546
|
if (!gameId || !courseId) {
|
|
@@ -168445,6 +168691,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
168445
168691
|
createAssessment,
|
|
168446
168692
|
attachExistingAssessments,
|
|
168447
168693
|
updateAssessment,
|
|
168694
|
+
reviseAssessment,
|
|
168448
168695
|
synchronizeReviewBank,
|
|
168449
168696
|
removeAssessment,
|
|
168450
168697
|
reorderQuestions,
|