All files / src/utils/helpers assignmentSavePayload.js

100% Statements 19/19
100% Branches 25/25
100% Functions 8/8
100% Lines 16/16

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,
    };
}