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 | 4x 4x 351x 336x 330x 330x 422x 210x 205x 67x 58x 177x 177x 13x | /**
* What to show in a submission's grade banner.
*
* The teacher's Individual Answers tab used to render
* `Number(submission.grade).toFixed(2)` behind a `submission.grade &&` check. Two
* things went wrong with that:
*
* * the API returned the literal string "None" for an ungraded submission, which
* is truthy, so the banner rendered "NaN%" as the student's grade; and
* * the withhold check covered `reviewStatus === "pending"` only, so a REJECTED
* late submission displayed its provisional grade — which gradebook.py says
* must never be shown "on either view" (EI-1195 / EI-1210).
*
* The API side is fixed (it now returns null and a `gradeStatus`), but the display
* rule is kept defensive on purpose: this renders a student's grade to their
* teacher, and no input should be able to turn it into "NaN%".
*
* Developer: Allan Ninal
*/
/** Review states whose grade is provisional and must never be displayed. */
const WITHHELD_REVIEW_STATUSES = ["pending", "rejected"];
/** `gradeStatus` values from the API's canonical gradebook resolver. */
const STATUS_LABELS = {
graded: "Graded",
missed: "Not submitted",
incomplete: "In progress",
pending: "Pending review",
rejected: "Rejected",
};
/**
* Parse a grade percentage, rejecting anything that is not a real number.
*
* Returns null — never NaN — so a caller cannot accidentally format it. Note "" and
* null are rejected, but so are "None", "nan" and "abc": `Number("")` is 0, which
* would otherwise read as a legitimate 0%.
*
* @param {number|string|null|undefined} grade
* @returns {number|null}
*/
export function parseGradePercent(grade) {
if (grade === null || grade === undefined) return null;
if (typeof grade === "string" && grade.trim() === "") return null;
const parsed = Number(grade);
return Number.isFinite(parsed) ? parsed : null;
}
/**
* Is this submission's grade safe and permitted to display?
*
* A grade of 0 IS displayed — a student who scored nothing must be visibly
* distinguishable from one who was never graded.
*
* @param {{grade?: unknown, reviewStatus?: string}|null|undefined} submission
* @returns {boolean}
*/
export function canShowGrade(submission) {
if (!submission) return false;
if (WITHHELD_REVIEW_STATUSES.includes(submission.reviewStatus)) return false;
return parseGradePercent(submission.grade) !== null;
}
/**
* The grade as a fixed-2 percentage string, or null when it must not be shown.
*
* @param {{grade?: unknown, reviewStatus?: string}|null|undefined} submission
* @returns {string|null} e.g. "85.50"
*/
export function formatGradePercent(submission) {
if (!canShowGrade(submission)) return null;
return parseGradePercent(submission.grade).toFixed(2);
}
/**
* A clamped 0-100 value for the progress bar.
*
* Clamped because a points override can push a stored grade above 100, and MUI's
* LinearProgress warns and overflows its track for values outside the range.
*
* @param {{grade?: unknown, reviewStatus?: string}|null|undefined} submission
* @returns {number} 0-100
*/
export function gradeProgressValue(submission) {
// No `?? 0` needed: canShowGrade() being true guarantees parseGradePercent
// returns a number, and the ternary supplies 0 otherwise. A `?? 0` here read as
// defensive but was unreachable — removing it killed no test, and coverage
// flagged it as the module's only uncovered branch.
const parsed = canShowGrade(submission) ? parseGradePercent(submission.grade) : 0;
return Math.min(100, Math.max(0, parsed));
}
/**
* Human label for why a grade is not being shown, from the API's `gradeStatus`.
*
* Falls back to a generic phrase rather than rendering a raw enum value, since an
* unrecognised status would otherwise leak into the teacher's UI.
*
* @param {string|null|undefined} gradeStatus
* @returns {string}
*/
export function gradeStatusLabel(gradeStatus) {
return STATUS_LABELS[gradeStatus] ?? "Not graded yet";
}
|