mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
perform test
test to courses Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// This will do mandatory call
|
||||
const MAX_ATTEMPTS = 10;
|
||||
const ATTEMPT_WINDOW_HOURS = 24;
|
||||
const COOLDOWN_MINUTES = 60;
|
||||
// Assessment cooldown policy is now stored per-assessment in the DB (max_attempts / cooldown_hours).
|
||||
// These fallbacks are used only if values are missing (e.g. legacy rows before the migration).
|
||||
const ASSESSMENT_FAILS_BEFORE_COOLDOWN = 3;
|
||||
const ASSESSMENT_COOLDOWN_HOURS = 24;
|
||||
|
||||
// Fisher-Yates shuffle of each question's options. Pure — returns new
|
||||
// arrays/objects, never mutates input. Grading is unaffected since
|
||||
@@ -19,49 +19,64 @@ function shuffleOptions(questions) {
|
||||
}
|
||||
|
||||
// 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(
|
||||
// submit-time enforcement check.
|
||||
//
|
||||
// type = 'quiz' → unit quizzes: no cooldown, no attempt cap, always open
|
||||
// type = 'assessment' → course assessments: maxFails failed attempts → cooldownHours cooldown (rolling cycles)
|
||||
// maxFails / cooldownHours come from the assessment row; fallback to the constants above.
|
||||
function getAttemptStatus(attempts, type = 'quiz', { maxFails, cooldownHours } = {}) {
|
||||
const attempt_count = attempts.length;
|
||||
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();
|
||||
if (type === 'quiz') {
|
||||
return {
|
||||
attempt_count,
|
||||
has_passed,
|
||||
best_attempt,
|
||||
attempts_remaining: null,
|
||||
cooldown_until: null,
|
||||
window_reset_at: null,
|
||||
can_attempt: true,
|
||||
};
|
||||
}
|
||||
|
||||
const attempts_remaining = Math.max(0, MAX_ATTEMPTS - attemptsInWindow.length);
|
||||
// Assessment: simulate rolling cycles — N failed attempts → cooldown (from the assessment's own config)
|
||||
const failLimit = maxFails ?? ASSESSMENT_FAILS_BEFORE_COOLDOWN;
|
||||
const lockHours = cooldownHours ?? ASSESSMENT_COOLDOWN_HOURS;
|
||||
|
||||
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 sorted = [...attempts].sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||
|
||||
let cycle_end = null; // when the current cooldown expires (null = no active or past cooldown)
|
||||
let failed_in_cycle = 0;
|
||||
|
||||
for (const a of sorted) {
|
||||
// Skip attempts that fall inside a previous cooldown window (they shouldn't exist, but guard anyway)
|
||||
if (cycle_end && new Date(a.createdAt) < cycle_end) continue;
|
||||
if (!a.passed) {
|
||||
failed_in_cycle++;
|
||||
if (failed_in_cycle >= failLimit) {
|
||||
cycle_end = new Date(new Date(a.createdAt).getTime() + lockHours * 3600000);
|
||||
failed_in_cycle = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const can_attempt = attempts_remaining > 0 && !cooldown_until;
|
||||
const now = new Date();
|
||||
const cooldown_until = (cycle_end && cycle_end > now) ? cycle_end.toISOString() : null;
|
||||
|
||||
return { attempt_count, has_passed, best_attempt, attempts_remaining, cooldown_until, window_reset_at, can_attempt };
|
||||
return {
|
||||
attempt_count,
|
||||
has_passed,
|
||||
best_attempt,
|
||||
attempts_remaining: null,
|
||||
cooldown_until,
|
||||
window_reset_at: null,
|
||||
can_attempt: !cooldown_until,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { MAX_ATTEMPTS, ATTEMPT_WINDOW_HOURS, COOLDOWN_MINUTES, shuffleOptions, getAttemptStatus };
|
||||
module.exports = { ASSESSMENT_FAILS_BEFORE_COOLDOWN, ASSESSMENT_COOLDOWN_HOURS, shuffleOptions, getAttemptStatus };
|
||||
Reference in New Issue
Block a user