mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
// This will do mandatory call
|
|
const MAX_ATTEMPTS = 10;
|
|
const ATTEMPT_WINDOW_HOURS = 24;
|
|
const COOLDOWN_MINUTES = 60;
|
|
|
|
// Fisher-Yates shuffle of each question's options. Pure — returns new
|
|
// arrays/objects, never mutates input. Grading is unaffected since
|
|
// submitUnitQuiz/submitCourseAssessment always re-fetch questions fresh
|
|
// from the DB and never trust shuffled client-facing order.
|
|
function shuffleOptions(questions) {
|
|
return questions.map((q) => {
|
|
const options = [...(q.options ?? [])];
|
|
for (let i = options.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[options[i], options[j]] = [options[j], options[i]];
|
|
}
|
|
return { ...q, options };
|
|
});
|
|
}
|
|
|
|
// Single source of truth for both the GET-time info fields and the
|
|
// submit-time enforcement check. Doesn't care about input order —
|
|
// derives best/most-recent itself, so callers can just fetch attempts
|
|
// with no ORDER BY.
|
|
function getAttemptStatus(attempts) {
|
|
const now = new Date();
|
|
const windowStart = new Date(now.getTime() - ATTEMPT_WINDOW_HOURS * 60 * 60 * 1000);
|
|
const attemptsInWindow = attempts.filter((a) => new Date(a.createdAt) >= windowStart);
|
|
|
|
const attempt_count = attempts.length; // lifetime — still used for has_passed/best_attempt
|
|
const has_passed = attempts.some((a) => a.passed);
|
|
|
|
const best_attempt = attempts.reduce(
|
|
(best, a) => (!best || a.score > best.score ? a : best),
|
|
null
|
|
);
|
|
|
|
const most_recent = attempts.reduce(
|
|
(latest, a) => (!latest || new Date(a.createdAt) > new Date(latest.createdAt) ? a : latest),
|
|
null
|
|
);
|
|
|
|
let cooldown_until = null;
|
|
if (most_recent) {
|
|
const unlockAt = new Date(new Date(most_recent.createdAt).getTime() + COOLDOWN_MINUTES * 60000);
|
|
if (unlockAt > now) cooldown_until = unlockAt.toISOString();
|
|
}
|
|
|
|
const attempts_remaining = Math.max(0, MAX_ATTEMPTS - attemptsInWindow.length);
|
|
|
|
let window_reset_at = null;
|
|
if (attempts_remaining === 0 && attemptsInWindow.length > 0) {
|
|
const oldestInWindow = attemptsInWindow.reduce(
|
|
(oldest, a) => (!oldest || new Date(a.createdAt) < new Date(oldest.createdAt) ? a : oldest),
|
|
null
|
|
);
|
|
window_reset_at = new Date(
|
|
new Date(oldestInWindow.createdAt).getTime() + ATTEMPT_WINDOW_HOURS * 60 * 60 * 1000
|
|
).toISOString();
|
|
}
|
|
|
|
const can_attempt = attempts_remaining > 0 && !cooldown_until;
|
|
|
|
return { attempt_count, has_passed, best_attempt, attempts_remaining, cooldown_until, window_reset_at, can_attempt };
|
|
}
|
|
|
|
module.exports = { MAX_ATTEMPTS, ATTEMPT_WINDOW_HOURS, COOLDOWN_MINUTES, shuffleOptions, getAttemptStatus }; |