From ca2cf739eeb15598f2b484c3a7a2db49132cf12a Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Sun, 9 Feb 2025 20:56:55 +0000 Subject: [PATCH 1/4] improve edited exam changes printing format --- src/utils/exam.differences.ts | 73 +++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/src/utils/exam.differences.ts b/src/utils/exam.differences.ts index ed78bac4..40cc518f 100644 --- a/src/utils/exam.differences.ts +++ b/src/utils/exam.differences.ts @@ -1,7 +1,26 @@ import { Exam } from "@/interfaces/exam"; import { diff, Diff } from "deep-diff"; -const EXCLUDED_FIELDS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private"]); +const EXCLUDED_FIELDS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval", "score"]); + +const PATH_LABELS: Record = { + parts: "Parts", + exercises: "Exercises", + userSolutions: "User Solutions", + words: "Words", + options: "Options", + prompt: "Prompt", + text: "Text", + audio: "Audio", + script: "Script", + difficulty: "Difficulty", + shuffle: "Shuffle", + solutions: "Solutions", + variant: "Variant", + prefix: "Prefix", + suffix: "Suffix", + topic: "Topic", +}; export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] { const differences = diff(oldExam, newExam) || []; @@ -17,24 +36,19 @@ function formatDifference(change: Diff): string | undefined { return; } - // Convert path array to something human-readable - const pathString = change.path.join(" \u2192 "); // e.g. "parts → 0 → exercises → 1 → prompt" + const pathString = pathToHumanReadable(change.path); switch (change.kind) { - case "N": - // A new property/element was added - return `\u{2022} Added \`${pathString}\` with value: ${formatValue(change.rhs)}`; + case "N": // A new property/element was added + return `• Added ${pathString} with value: ${formatValue(change.rhs)}\n`; - case "D": - // A property/element was deleted - return `\u{2022} Removed \`${pathString}\` which had value: ${formatValue(change.lhs)}`; + case "D": // A property/element was deleted + return `• Removed ${pathString} which had value: ${formatValue(change.lhs)}\n`; - case "E": - // A property/element was edited - return `\u{2022} Changed \`${pathString}\` from ${formatValue(change.lhs)} to ${formatValue(change.rhs)}`; + case "E": // A property/element was edited + return `• Changed ${pathString} from ${formatValue(change.lhs)} to ${formatValue(change.rhs)}\n`; - case "A": - // An array change; change.item describes what happened at array index change.index + case "A": // An array change; `change.item` describes what happened at array index `change.index` return formatArrayChange(change); default: @@ -44,12 +58,12 @@ function formatDifference(change: Diff): string | undefined { function formatArrayChange(change: Diff): string | undefined { if (!change.path) return; + if (change.path.some((segment) => EXCLUDED_FIELDS.has(segment))) { return; } - - const pathString = change.path.join(" \u2192 "); + const pathString = pathToHumanReadable(change.path); const arrayChange = (change as any).item; const idx = (change as any).index; @@ -57,14 +71,13 @@ function formatArrayChange(change: Diff): string | undefined { switch (arrayChange.kind) { case "N": - return `\u{2022} Added an item at index [${idx}] in \`${pathString}\`: ${formatValue(arrayChange.rhs)}`; + return `• Added an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.rhs)}\n`; case "D": - return `\u{2022} Removed an item at index [${idx}] in \`${pathString}\`: ${formatValue(arrayChange.lhs)}`; + return `• Removed an item at [#${idx + 1}] in ${pathString}: ${formatValue(arrayChange.lhs)}\n`; case "E": - return `\u{2022} Edited an item at index [${idx}] in \`${pathString}\` from ${formatValue(arrayChange.lhs)} to ${formatValue(arrayChange.rhs)}`; + return `• Edited an item at [#${idx + 1}] in ${pathString} from ${formatValue(arrayChange.lhs)} to ${formatValue(arrayChange.rhs)}\n`; case "A": - // Nested array changes could happen theoretically; handle or ignore similarly - return `\u{2022} Complex array change at index [${idx}] in \`${pathString}\`: ${JSON.stringify(arrayChange)}`; + return `• Complex array change at [#${idx + 1}] in ${pathString}: ${JSON.stringify(arrayChange)}\n`; default: return; } @@ -82,3 +95,21 @@ function formatValue(value: any): string { } return JSON.stringify(value); } + +/** + * Convert an array of path segments into a friendlier string. + * Example: + * ["parts", 0, "exercises", 1, "prompt"] + * becomes: + * "Parts → [#1] → Exercises → [#2] → Prompt" + */ +function pathToHumanReadable(pathSegments: Array): string { + return pathSegments + .map((seg) => { + if (typeof seg === "number") { + return `[#${seg + 1}]`; + } + return PATH_LABELS[seg] ?? seg; + }) + .join(" → "); +} From c14f16c97afa90ba469a48564db57ebdce43a842 Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Sun, 9 Feb 2025 21:12:29 +0000 Subject: [PATCH 2/4] improve edited exam changes printing format --- src/utils/exam.differences.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/utils/exam.differences.ts b/src/utils/exam.differences.ts index 40cc518f..6a1cf303 100644 --- a/src/utils/exam.differences.ts +++ b/src/utils/exam.differences.ts @@ -1,7 +1,7 @@ import { Exam } from "@/interfaces/exam"; import { diff, Diff } from "deep-diff"; -const EXCLUDED_FIELDS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval", "score"]); +const EXCLUDED_KEYS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval"]); const PATH_LABELS: Record = { parts: "Parts", @@ -32,25 +32,21 @@ function formatDifference(change: Diff): string | undefined { return; } - if (change.path.some((segment) => EXCLUDED_FIELDS.has(segment))) { + if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) { return; } const pathString = pathToHumanReadable(change.path); switch (change.kind) { - case "N": // A new property/element was added + case "N": // New property/element return `• Added ${pathString} with value: ${formatValue(change.rhs)}\n`; - - case "D": // A property/element was deleted + case "D": // Deleted property/element return `• Removed ${pathString} which had value: ${formatValue(change.lhs)}\n`; - - case "E": // A property/element was edited + case "E": // Edited property/element return `• Changed ${pathString} from ${formatValue(change.lhs)} to ${formatValue(change.rhs)}\n`; - - case "A": // An array change; `change.item` describes what happened at array index `change.index` + case "A": // Array change return formatArrayChange(change); - default: return; } @@ -59,7 +55,7 @@ function formatDifference(change: Diff): string | undefined { function formatArrayChange(change: Diff): string | undefined { if (!change.path) return; - if (change.path.some((segment) => EXCLUDED_FIELDS.has(segment))) { + if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) { return; } @@ -86,13 +82,24 @@ function formatArrayChange(change: Diff): string | undefined { function formatValue(value: any): string { if (value === null) return "null"; if (value === undefined) return "undefined"; + if (typeof value === "object") { try { - return JSON.stringify(value); + return JSON.stringify( + value, + (key, val) => { + if (EXCLUDED_KEYS.has(key)) { + return undefined; + } + return val; + }, + 2 // optional indentation for readability + ); } catch { return String(value); } } + return JSON.stringify(value); } From e214d8b59833d187e3244cacf7e57051afd42d9b Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Mon, 10 Feb 2025 11:30:24 +0000 Subject: [PATCH 3/4] improve edited exam changes again --- src/pages/approval-workflows/[id]/index.tsx | 2 +- src/utils/exam.differences.ts | 65 ++++++++++++++------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/pages/approval-workflows/[id]/index.tsx b/src/pages/approval-workflows/[id]/index.tsx index 64a8026e..c6cdc340 100644 --- a/src/pages/approval-workflows/[id]/index.tsx +++ b/src/pages/approval-workflows/[id]/index.tsx @@ -557,7 +557,7 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor
{currentWorkflow.steps[selectedStepIndex].examChanges?.length ? ( currentWorkflow.steps[selectedStepIndex].examChanges!.map((change, index) => ( -

+

{change}

)) diff --git a/src/utils/exam.differences.ts b/src/utils/exam.differences.ts index 6a1cf303..b860e11f 100644 --- a/src/utils/exam.differences.ts +++ b/src/utils/exam.differences.ts @@ -1,9 +1,10 @@ import { Exam } from "@/interfaces/exam"; import { diff, Diff } from "deep-diff"; -const EXCLUDED_KEYS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "access", "requiresApproval"]); +const EXCLUDED_KEYS = new Set(["_id", "id", "createdAt", "createdBy", "entities", "isDiagnostic", "private", "requiresApproval", "exerciseID", "questionID"]); const PATH_LABELS: Record = { + access: "Access Type", parts: "Parts", exercises: "Exercises", userSolutions: "User Solutions", @@ -20,6 +21,9 @@ const PATH_LABELS: Record = { prefix: "Prefix", suffix: "Suffix", topic: "Topic", + allowRepetition: "Allow Repetition", + maxWords: "Max Words", + minTimer: "Timer", }; export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] { @@ -28,9 +32,7 @@ export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] } function formatDifference(change: Diff): string | undefined { - if (!change.path) { - return; - } + if (!change.path) return; if (change.path.some((segment) => EXCLUDED_KEYS.has(segment))) { return; @@ -85,30 +87,53 @@ function formatValue(value: any): string { if (typeof value === "object") { try { - return JSON.stringify( - value, - (key, val) => { - if (EXCLUDED_KEYS.has(key)) { - return undefined; - } - return val; - }, - 2 // optional indentation for readability - ); + const sanitized = removeExcludedKeysDeep(value, EXCLUDED_KEYS); + + const renamed = renameKeysDeep(sanitized, PATH_LABELS); + + return JSON.stringify(renamed, null, 2); } catch { return String(value); } } - return JSON.stringify(value); } +function removeExcludedKeysDeep(obj: any, excludedKeys: Set): any { + if (Array.isArray(obj)) { + return obj.map((item) => removeExcludedKeysDeep(item, excludedKeys)); + } else if (obj && typeof obj === "object") { + const newObj: any = {}; + for (const key of Object.keys(obj)) { + if (excludedKeys.has(key)) { + // Skip this key entirely + continue; + } + newObj[key] = removeExcludedKeysDeep(obj[key], excludedKeys); + } + return newObj; + } + return obj; +} + +function renameKeysDeep(obj: any, renameMap: Record): any { + if (Array.isArray(obj)) { + return obj.map((item) => renameKeysDeep(item, renameMap)); + } else if (obj && typeof obj === "object") { + const newObj: any = {}; + for (const key of Object.keys(obj)) { + const newKey = renameMap[key] ?? key; // Use friendly label if available + newObj[newKey] = renameKeysDeep(obj[key], renameMap); + } + return newObj; + } + return obj; +} + /** - * Convert an array of path segments into a friendlier string. - * Example: - * ["parts", 0, "exercises", 1, "prompt"] - * becomes: - * "Parts → [#1] → Exercises → [#2] → Prompt" + * Convert an array of path segments into a user-friendly string. + * e.g. ["parts", 0, "exercises", 1, "prompt"] + * → "Parts → [#1] → Exercises → [#2] → Prompt" */ function pathToHumanReadable(pathSegments: Array): string { return pathSegments From df2f83e49630517d3aa49adab304d81a6caada0d Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Mon, 10 Feb 2025 13:25:11 +0000 Subject: [PATCH 4/4] make access confidential when user submits exam with approval process. make access private upon approval workflow completed. --- src/components/ExamEditor/SettingsEditor/index.tsx | 11 +++++++---- src/components/ExamEditor/SettingsEditor/level.tsx | 2 +- .../ExamEditor/SettingsEditor/listening/index.tsx | 2 +- .../ExamEditor/SettingsEditor/reading/index.tsx | 2 +- .../ExamEditor/SettingsEditor/speaking/index.tsx | 2 +- .../ExamEditor/SettingsEditor/writing/index.tsx | 2 +- src/pages/api/exam/[module]/index.ts | 4 ++-- src/pages/approval-workflows/[id]/index.tsx | 4 ++-- 8 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/ExamEditor/SettingsEditor/index.tsx b/src/components/ExamEditor/SettingsEditor/index.tsx index be98e4b4..fedbd0c7 100644 --- a/src/components/ExamEditor/SettingsEditor/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/index.tsx @@ -159,7 +159,7 @@ const SettingsEditor: React.FC = ({ disabled={!canSubmit} > - Submit Module as Exam + Submit module as exam for approval
diff --git a/src/components/ExamEditor/SettingsEditor/level.tsx b/src/components/ExamEditor/SettingsEditor/level.tsx index 76b82bb6..36dc8796 100644 --- a/src/components/ExamEditor/SettingsEditor/level.tsx +++ b/src/components/ExamEditor/SettingsEditor/level.tsx @@ -196,7 +196,7 @@ const LevelSettings: React.FC = () => { }; }).filter(part => part.exercises.length > 0), requiresApproval: requiresApproval, - isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + isDiagnostic: false, minTimer, module: "level", id: title, diff --git a/src/components/ExamEditor/SettingsEditor/listening/index.tsx b/src/components/ExamEditor/SettingsEditor/listening/index.tsx index c2a862d3..fa0ffc3e 100644 --- a/src/components/ExamEditor/SettingsEditor/listening/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/listening/index.tsx @@ -139,7 +139,7 @@ const ListeningSettings: React.FC = () => { }; }), requiresApproval: requiresApproval, - isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + isDiagnostic: false, minTimer, module: "listening", id: title, diff --git a/src/components/ExamEditor/SettingsEditor/reading/index.tsx b/src/components/ExamEditor/SettingsEditor/reading/index.tsx index 15615a8b..4c4f69b5 100644 --- a/src/components/ExamEditor/SettingsEditor/reading/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/reading/index.tsx @@ -75,7 +75,7 @@ const ReadingSettings: React.FC = () => { }; }), requiresApproval: requiresApproval, - isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + isDiagnostic: false, minTimer, module: "reading", id: title, diff --git a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx index 6d2be4df..186b2c53 100644 --- a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx @@ -182,7 +182,7 @@ const SpeakingSettings: React.FC = () => { module: "speaking", id: title, requiresApproval: requiresApproval, - isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + isDiagnostic: false, variant: undefined, difficulty, instructorGender: "varied", diff --git a/src/components/ExamEditor/SettingsEditor/writing/index.tsx b/src/components/ExamEditor/SettingsEditor/writing/index.tsx index 8c6b79c1..9bbed3a8 100644 --- a/src/components/ExamEditor/SettingsEditor/writing/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/writing/index.tsx @@ -132,7 +132,7 @@ const WritingSettings: React.FC = () => { module: "writing", id: title, requiresApproval: requiresApproval, - isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + isDiagnostic: false, variant: undefined, difficulty, access, diff --git a/src/pages/api/exam/[module]/index.ts b/src/pages/api/exam/[module]/index.ts index 966bf502..6643d81d 100644 --- a/src/pages/api/exam/[module]/index.ts +++ b/src/pages/api/exam/[module]/index.ts @@ -78,8 +78,8 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { throw new Error("Name already exists"); } - if (isAdmin(user)) { - exam.isDiagnostic = false; + if (exam.requiresApproval === true) { + exam.access = "confidential"; } await db.collection(module).updateOne( diff --git a/src/pages/approval-workflows/[id]/index.tsx b/src/pages/approval-workflows/[id]/index.tsx index c6cdc340..2b512ba0 100644 --- a/src/pages/approval-workflows/[id]/index.tsx +++ b/src/pages/approval-workflows/[id]/index.tsx @@ -150,7 +150,7 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor const handleApproveStep = () => { const isLastStep = (selectedStepIndex + 1 === currentWorkflow.steps.length); if (isLastStep) { - if (!confirm(`Are you sure you want to approve the last step? Doing so will approve the exam.`)) return; + if (!confirm(`Are you sure you want to approve the last step? Doing so will change the access type of the exam from confidential to private.`)) return; } const updatedWorkflow: ApprovalWorkflow = { @@ -192,7 +192,7 @@ export default function Home({ user, initialWorkflow, id, workflowAssignees, wor const examId = currentWorkflow.examId; axios - .patch(`/api/exam/${examModule}/${examId}`, { isDiagnostic: false }) + .patch(`/api/exam/${examModule}/${examId}`, { access: "private" }) .then(() => toast.success(`The exam was successfuly approved and this workflow has been completed.`)) .catch((reason) => { if (reason.response.status === 404) {