Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 13x 61x 45x 61x 53x 48x 44x 44x 44x 44x 44x 88x 55x 55x 44x 32x | /**
* Builds the instructions/rubric portion of the create-assignment payload.
*
* Extracted from NewAssignment.jsx's `saveOperation` (EI-3182) so the
* normalization is unit-tested against the real production code instead of a
* re-implemented copy.
*
* Contract (EI-3182): forward an imported global assignment's `instructions` /
* `rubric` when present, and OMIT each field entirely when it is null/undefined,
* so assignments without them are unaffected. Values are forwarded **as-is** —
* this helper does NOT trim instructions or collapse an empty-criteria rubric to
* null. (If the product later wants empty values normalized to null on save,
* that is a deliberate behaviour change to make here.)
*
* @param {{ instructions?: any, rubric?: any }} [assignmentData]
* @returns {{ instructions?: any, rubric?: any }} an object with each field
* present only when its source value is non-null.
*/
export function buildInstructionsRubricFields(assignmentData) {
return {
...(assignmentData?.instructions != null && { instructions: assignmentData.instructions }),
...(assignmentData?.rubric != null && { rubric: assignmentData.rubric }),
};
}
/**
* Resolves the Step-3 class picker's DISPLAY strings back to class ids.
*
* Step 3 stores each chosen class as `"Title (class_code)"` — the label the
* MUI Select renders — not as an id, so saving has to map them back.
*
* Unmatched entries are DROPPED rather than passed through as `undefined`. The
* previous inline version returned undefined for a miss, and `JSON.stringify`
* omits undefined array members silently: the request went out assigning the
* assignment to fewer classes than the teacher picked, with nothing to show that
* anything had been lost. Dropping them is the same end state, but it is now
* countable — callers can compare lengths to detect it.
*
* @param {string[]} [assignedClass] display labels from Step 3
* @param {Array<{_id: string, title: string, class_code: string}>} [classData]
* @returns {string[]} class ids, in the order chosen
*
* Developer: Allan Ninal
*/
export function resolveAssignedClassIds(assignedClass, classData) {
if (!Array.isArray(assignedClass)) return [];
const classes = Array.isArray(classData) ? classData : [];
return assignedClass
.map((label) => classes.find((c) => `${c?.title} (${c?.class_code})` === label)?._id)
.filter((id) => id != null);
}
/**
* Builds the part of the create/update-assignment payload that both
* NewAssignment flows share — the in-class wizard and the reuse-a-global-test
* wizard. Each caller adds its own `_id` / `copy_of` and its own
* instructions/rubric handling, which differ deliberately: the in-class flow
* must send them explicitly so clearing persists, while the reuse flow omits
* absent fields (see buildInstructionsRubricFields above).
*
* Every settings key is a snake_case rename of a camelCase Step-3 field. A
* mis-rename does not fail loudly — the backend simply keeps its default and the
* teacher's choice is silently ignored — so the mapping is asserted key by key.
*
* @param {object} args
* @param {object} args.assignmentData the wizard's redux state (step1/2/3)
* @param {string} args.assignmentType
* @param {Array} args.classData classes available to this teacher
* @returns {object} the shared payload fields
*
* Developer: Allan Ninal
*/
export function buildAssignmentCorePayload({ assignmentData, assignmentType, classData } = {}) {
const step1 = assignmentData?.step1 ?? {};
const step2 = assignmentData?.step2 ?? {};
const step3 = assignmentData?.step3 ?? {};
const dates = step3.dateAssigned ?? {};
/** Dates are stored with a local offset by the Step-3 picker; normalise to UTC. */
const toUtcOrNull = (value) => {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
};
return {
title: step1.title,
description: step1.description,
type: assignmentType,
category: "current",
semester: "spring",
assigned_class: resolveAssignedClassIds(step3.assignedClass, classData),
date_open: toUtcOrNull(dates.dateOpen),
date_close: toUtcOrNull(dates.dateClose),
format: step3.testFormat,
questions: (step2.selectedQuestions ?? []).map((q) => ({
id: q?._id,
category: q?.category,
topic: q?.questionTopic,
})),
settings: {
allow_calculator: step3.allowCalculator,
allow_feedback_after_submit: step3.allowFeedback,
allowed_attempts: step3.totalAttempts,
time_allowed: step3.timeAllowed,
show_correct_answers_after_submit: step3.displayCorrectAnswers,
show_score_after_submit: step3.displayScore,
shuffle_choices: step3.shuffleChoices,
shuffle_questions: step3.shuffleQuestions,
// EI-1210: late-submission teacher-approval setting
allow_late_submissions: step3.allowLateSubmissions ?? false,
},
passing_grade: step3.passingGrade,
};
}
|