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 | 70x 70x 70x 70x 50x 50x 50x 31x 17x 61x 88x 59x 6x | /**
* The assignment `time_allowed` contract, plus the date-picker's past-date rule.
*
* These were module-private inside the 401-line `step-3/Step3.jsx` at 0% coverage,
* which is awkward because Step 3 is the WRITER of `time_allowed` and the student's
* countdown is the READER (`take-assignment/components/takeAssignmentTimer.js`).
* The two must agree on the format or a student gets the wrong duration, so the
* agreement is asserted directly in the tests for this module.
*
* Backend contract: `time_allowed` is an "HH:MM:SS" string, or null for unlimited.
*
* Developer: Allan Ninal
*/
import dayjs from "dayjs";
/**
* Total minutes -> the backend's "HH:MM:SS" string.
*
* Hours are NOT capped at 24: the input accepts up to 9999 minutes (~6.9 days),
* and the reader parses "166:39:00" correctly.
*
* @param {number|string} minutes
* @returns {string}
*/
export function minutesToTimeAllowed(minutes) {
const totalMinutes = Math.max(0, parseInt(minutes, 10) || 0);
const hours = Math.floor(totalMinutes / 60);
const mins = totalMinutes % 60;
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}:00`;
}
/**
* "HH:MM:SS" -> total minutes, for re-populating the form.
*
* Falls back to 60 for anything that yields zero. That makes the round trip
* ASYMMETRIC at the bottom end: `minutesToTimeAllowed(0)` is "00:00:00", but
* reading "00:00:00" back gives 60, not 0. Deliberate — 60 is the form's default,
* and a zero-minute assessment would expire the moment a student opened it.
*
* Not currently reachable: the minutes field accepts `/^\d{0,4}$/` only and resets
* to 60 on blur when empty or <= 0, so "00:00:00" is never written. The fallback is
* the second line of defence, not the first.
*
* @param {string|null|undefined} timeAllowed
* @returns {number} minutes, never 0
*/
export function timeAllowedToMinutes(timeAllowed) {
const [hours = 0, minutes = 0] = (timeAllowed ?? "").split(":").map(Number);
const total = (Number(hours) || 0) * 60 + (Number(minutes) || 0);
return total || 60;
}
/**
* Is this assignment explicitly unlimited?
*
* `null` means "saved as unlimited"; `undefined` means "Step 3 was never filled in"
* and must NOT be treated as unlimited, or a brand-new wizard would default to no
* time limit. Hence the strict === rather than a falsy check.
*
* @param {string|null|undefined} timeAllowed
* @returns {boolean}
*/
export function isUnlimitedTimeAllowed(timeAllowed) {
return timeAllowed === null;
}
/**
* Should the date picker disable this day?
*
* Day-granular and LOCAL on purpose: the teacher is choosing a calendar day in
* their own zone (Texas by default), not an instant. Contrast
* `helpers/assignmentStatus.js`, which compares instants in UTC because it decides
* whether a window is currently open.
*
* Previously `current < moment().startOf("day")`. moment was imported into Step3
* for this single call and shipped ~428KB into the Step4 chunk; dayjs was already a
* dependency and antd's picker hands us dayjs objects anyway. Verified identical
* under America/Chicago, UTC and Asia/Manila, including both 2026 DST boundaries.
*
* @param {import("dayjs").Dayjs|null|undefined} current
* @returns {boolean}
*/
export function isDateBeforeToday(current) {
return Boolean(current) && current < dayjs().startOf("day");
}
/**
* Should the date picker disable this day, given the range already loaded into the form?
*
* Wraps {@link isDateBeforeToday} but carves out an exception for the dates already
* in `existingRange` (the assignment's currently loaded open/close dates). Without
* this, editing an already-CLOSED assignment — whose open and close dates are both
* in the past — disables its own stored dates. antd's RangePicker treats a `value`
* containing a disabled date as invalid, so opening the picker on a closed
* assignment cleared BOTH dates back to empty, even when the teacher only meant to
* change the close date.
*
* @param {import("dayjs").Dayjs|null|undefined} current
* @param {Array<import("dayjs").Dayjs|null|undefined>} existingRange - the currently loaded [open, close] dates
* @returns {boolean}
*/
export function isDateDisabledForRange(current, existingRange = []) {
if (!current) return false;
const isExistingDate = existingRange.some((date) => date && current.isSame(date, "day"));
if (isExistingDate) return false;
return isDateBeforeToday(current);
}
|