feat: enhance error handling with submission error overlay and logging level controls

This commit is contained in:
Ross
2026-07-05 21:59:12 +01:00
parent 348cdbf9d8
commit 58deaee5ce
4 changed files with 317 additions and 132 deletions
+73 -1
View File
@@ -1138,7 +1138,7 @@ select option:disabled {
} }
} }
#submit-error-overlay, #view-answers-overlay { #view-answers-overlay {
opacity: 0.9; opacity: 0.9;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -1151,6 +1151,78 @@ select option:disabled {
z-index: 5000; z-index: 5000;
} }
#submit-error-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
}
#submit-error-overlay .error-card {
background-color: #1a1a1a;
border: 1px solid #c62828;
border-radius: 8px;
max-width: 600px;
width: 100%;
padding: 25px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
color: #eee;
font-family: sans-serif;
}
#submit-error-overlay .error-card h3 {
margin-top: 0;
color: #ef5350;
font-size: 20px;
border-bottom: 1px solid #333;
padding-bottom: 10px;
margin-bottom: 15px;
}
#submit-error-overlay .error-card button {
cursor: pointer;
background-color: #311B92;
color: white;
border: none;
padding: 10px 18px;
font-size: 14px;
border-radius: 4px;
transition: background-color 0.2s;
font-weight: bold;
}
#submit-error-overlay .error-card button:hover {
background-color: #4527A0;
}
#submit-error-overlay .error-card button.btn-secondary {
background-color: #444;
}
#submit-error-overlay .error-card button.btn-secondary:hover {
background-color: #555;
}
#submit-error-overlay .error-card button.btn-danger {
background-color: #c62828;
}
#submit-error-overlay .error-card button.btn-danger:hover {
background-color: #d32f2f;
}
.cache-out-of-date { .cache-out-of-date {
color: red; color: red;
} }
+24
View File
@@ -255,6 +255,16 @@
View local saved answers View local saved answers
</button> </button>
</p> </p>
<div id="logging-controls" style="margin-top: 15px; border-top: 1px solid #444; padding-top: 10px; font-family: sans-serif; font-size: 13px;">
<label for="select-log-level" style="font-weight: bold; color: #fff;">Logging Level:</label>
<select id="select-log-level" style="background:#222; color:#fff; border:1px solid #555; padding: 2px 5px; border-radius: 3px; font-size: 13px; margin-left: 5px; cursor: pointer;">
<option value="silent">Disabled (Silent)</option>
<option value="error">Error</option>
<option value="warn">Warn</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
</details> </details>
</div> </div>
<div id="review-overlay" class="fullscreen-overlay"> <div id="review-overlay" class="fullscreen-overlay">
@@ -358,6 +368,20 @@
<button id="btn-login" class="navigation dialog-yes">Login</button> <button id="btn-login" class="navigation dialog-yes">Login</button>
</div> </div>
<div id="resume-dialog" class="dialog modal noclose">
<h3 class="dialog-title">Resume Session?</h3>
<div class="dialog-text">
<p id="resume-text">Choose a previous session to continue, or start a new one:</p>
<div id="resume-sessions-list" style="margin: 15px 0; max-height: 200px; overflow-y: auto;">
<!-- Session options list -->
</div>
</div>
<div class="dialog-buttons" style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 15px;">
<button id="btn-resume-start-new" class="navigation dialog-no" style="margin: 0;">Start New</button>
<button id="btn-resume-continue" class="navigation dialog-yes" style="margin: 0;" disabled>Continue</button>
</div>
</div>
<div id="loading" class="fullscreen-overlay"> <div id="loading" class="fullscreen-overlay">
<div class="progress-block"> <div class="progress-block">
<div id="progress"></div> <div id="progress"></div>
+69 -46
View File
@@ -91,74 +91,97 @@ export function postAnswers(ans, URLS, exam_details) {
alert(`Answers sucessfully submitted.`); alert(`Answers sucessfully submitted.`);
} }
} else { } else {
submissionError(data, ans, exam_details); submissionError(data, ans, exam_details, URLS);
} }
}) })
.fail((e) => { .fail((e) => {
// Will occur with server error such as 500 // Will occur with server error such as 500
console.debug("error", e); console.debug("error", e);
submissionError(e, ans, exam_details); submissionError(e, ans, exam_details, URLS);
}); });
// $.post( "http://localhost:8000/submit_answers", JSON.stringify(ans)); // $.post( "http://localhost:8000/submit_answers", JSON.stringify(ans));
} }
export function submissionError(data, answer_json, exam_details) { export function submissionError(data, answer_json, exam_details, URLS) {
// error will not be defined with server errors // Clear any existing overlay
if (data.error != undefined) { $("#submit-error-overlay").remove();
alert(`Error submitting answers: ${data.error}`);
} else {
alert(`Error submitting answers`);
}
var docHeight = $(document).height();
let answers = JSON.parse(answer_json.answers) let answers = JSON.parse(answer_json.answers);
let answer_map = {};
let answer_map = {} answers.forEach((ans) => {
answers.forEach((ans, n) => {
if (!answer_map.hasOwnProperty(ans.qid)) { if (!answer_map.hasOwnProperty(ans.qid)) {
answer_map[ans.qid] = []; answer_map[ans.qid] = [];
} }
answer_map[ans.qid].push(ans); answer_map[ans.qid].push(ans);
}); });
let html = $("<ul></ul>"); let $list = $("<ol style='margin-left: 20px; font-size: 13px; max-height: 150px; overflow-y: auto; color: #ccc;'></ol>");
exam_details.question_order.forEach((qid, idx) => {
exam_details.question_order.forEach((i, j) => { if (qid in answer_map) {
console.debug(i, answer_map) let ans_array = answer_map[qid];
if (i in answer_map) { ans_array.forEach((ans_obj, sub_idx) => {
console.debug("YES", i, answer_map) $list.append(`<li><strong>Question ${idx + 1}.${sub_idx + 1}:</strong> ${ans_obj.ans}</li>`);
let ans_array = answer_map[i];
ans_array.forEach((x, y) => {
$(html).append(`<li><b>Question ${j+1}.${y}:</b> ${x.ans}</li>`);
}); });
} }
});
}) let rawJsonStr = JSON.stringify(answer_json, null, 2);
console.debug(exam_details.question_order); let errorMsg = "An error occurred while submitting your answers.";
console.debug(answer_map); if (data && data.error) {
console.debug(html); errorMsg = `Server returned error: ${data.error}`;
} else if (data && data.statusText) {
if ($("#submit-error-overlay").length < 1) { errorMsg = `Network error: ${data.statusText} (${data.status})`;
$("body").append(
`<div id='submit-error-overlay'><span style='color: white'><p>An error has occurred when submitting your answers. A copy of your answers are displayed below, you may wish to <a id="save-failed-answers" href="#">save a copy</a> if subsequent submissions still fail. Please try submitting again or refresh this page to continue (answers will be saved locally in the browser unless you clear local storage).</p><p>${html.get(0).innerHTML}</p><p>${JSON.stringify(
answer_json
)}</p></span></div>`
);
$("#btn-submit-nav").prependTo("#submit-error-overlay");
$("#submit-error-overlay").height(docHeight);
let file = new Blob([JSON.stringify(answer_json)], {type: "application/json"})
let link = document.getElementById("save-failed-answers");
link.href = URL.createObjectURL(file);
link.download = "answers.json";
} }
let $overlay = $(`
<div id="submit-error-overlay">
<div class="error-card">
<h3>Submission Failed</h3>
<p style="margin-bottom: 15px; color: #ef5350; font-weight: bold;">${errorMsg}</p>
<p style="font-size: 13px; color: #bbb; line-height: 1.4;">
Your answers are saved locally in the browser's database and will not be lost unless you clear your local data.
Please try to submit again, or save a backup file of your answers and contact support.
</p>
<div style="margin: 15px 0;">
<h4 style="margin: 0 0 5px 0; font-size: 14px; color: #eee;">Submitted Answers:</h4>
<div style="background: #111; border: 1px solid #333; padding: 10px; border-radius: 4px;">
${$list.prop("outerHTML")}
</div>
</div>
<details style="margin: 15px 0; font-size: 12px; cursor: pointer; color: #aaa;">
<summary style="font-weight: bold; margin-bottom: 5px;">View Raw Submission JSON</summary>
<pre style="background: #111; border: 1px solid #333; padding: 10px; overflow: auto; max-height: 150px; text-align: left; font-family: monospace; white-space: pre-wrap; margin: 0; color: #a5d6a7;">${rawJsonStr}</pre>
</details>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
<button id="btn-err-close" class="btn-secondary">Close</button>
<a id="btn-err-backup" class="button" style="text-decoration: none;"><button class="btn-secondary">Save Backup</button></a>
<button id="btn-err-retry">Retry Submit</button>
</div>
</div>
</div>
`);
$("body").append($overlay);
// Setup download backup link
let fileBlob = new Blob([JSON.stringify(answer_json)], {type: "application/json"});
let backupUrl = URL.createObjectURL(fileBlob);
$overlay.find("#btn-err-backup").attr("href", backupUrl).attr("download", `answers_backup_${exam_details.eid.replace(/\//g, "_")}.json`);
// Close handler
$overlay.find("#btn-err-close").on("click", function() {
$("#submit-error-overlay").remove();
});
// Retry handler
$overlay.find("#btn-err-retry").on("click", function() {
postAnswers(answer_json, URLS, exam_details);
});
} }
export function getQuestion(url, question_number, question_total) { export function getQuestion(url, question_number, question_total) {
+151 -85
View File
@@ -7,8 +7,42 @@ import { db, question_db } from "./db.js";
import { initSync, triggerImmediateSync, updateSyncUI } from "./sync.js"; import { initSync, triggerImmediateSync, updateSyncUI } from "./sync.js";
// const { v4: uuidv4 } = require('uuid'); // const { v4: uuidv4 } = require('uuid');
log.setDefaultLevel("warn") function initLogging() {
log.setDefaultLevel("debug") try {
const urlParams = new URLSearchParams(window.location.search);
let logLevel = urlParams.get("log");
if (logLevel !== null) {
if (logLevel === "1" || logLevel === "true") {
logLevel = "debug";
} else if (logLevel === "0" || logLevel === "false") {
logLevel = "silent";
}
localStorage.setItem("rts.logLevel", logLevel);
} else {
logLevel = localStorage.getItem("rts.logLevel");
}
if (!logLevel || !["silent", "error", "warn", "info", "debug"].includes(logLevel)) {
logLevel = "warn";
}
log.setLevel(logLevel);
// Redirect console methods through loglevel so they can be silenced consistently
console.debug = log.debug.bind(log);
console.log = log.info.bind(log);
console.warn = log.warn.bind(log);
console.error = log.error.bind(log);
log.info(`RTS logging level initialized to: ${logLevel}`);
} catch (e) {
log.setLevel("warn");
console.warn("Failed to initialize custom log level, falling back to warn:", e);
}
}
initLogging();
let URLS = {}; let URLS = {};
@@ -309,7 +343,7 @@ function showLoadingState($btn, $targetContainer) {
} }
async function retrievePacketList(target = "all", onComplete = null) { async function retrievePacketList(target = "all", onComplete = null) {
log.debug(`Load users from database...`) log.info(`Load users from database...`)
let cid = localStorage.getItem("cid.cid") let cid = localStorage.getItem("cid.cid")
let passcode = localStorage.getItem("cid.passcode") let passcode = localStorage.getItem("cid.passcode")
@@ -327,7 +361,7 @@ async function retrievePacketList(target = "all", onComplete = null) {
// Update the candidate details display (only shows when both values exist) // Update the candidate details display (only shows when both values exist)
showCandidateDetails(); showCandidateDetails();
} else { } else {
log.debug("Try and set CID / Passcode from current URL") log.info("Try and set CID / Passcode from current URL")
try { try {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const cidParam = params.get("cid"); const cidParam = params.get("cid");
@@ -337,8 +371,8 @@ async function retrievePacketList(target = "all", onComplete = null) {
if (cidParam !== null && passParam !== null && cidParam !== "" && passParam !== "") { if (cidParam !== null && passParam !== null && cidParam !== "" && passParam !== "") {
cid = cidParam; cid = cidParam;
passcode = passParam; passcode = passParam;
log.debug("CID and passcode extracted from URL", cid, passcode); log.info("CID and passcode extracted from URL", cid, passcode);
log.debug("adding to local db"); log.info("adding to local db");
localStorage.setItem("cid.cid", cid); localStorage.setItem("cid.cid", cid);
localStorage.setItem("cid.passcode", passcode); localStorage.setItem("cid.passcode", passcode);
@@ -348,10 +382,10 @@ async function retrievePacketList(target = "all", onComplete = null) {
showCandidateDetails(); showCandidateDetails();
} else { } else {
log.debug("CID or passcode missing in URL; not setting local storage"); log.info("CID or passcode missing in URL; not setting local storage");
} }
} catch (e) { } catch (e) {
log.debug("Error parsing URL params for CID/passcode", e); log.error("Error parsing URL params for CID/passcode", e);
} }
} }
@@ -385,7 +419,7 @@ async function retrievePacketList(target = "all", onComplete = null) {
} }
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
log.debug("Unable to load packets, trying legacy '/packets'"); log.info("Unable to load packets, trying legacy '/packets'");
$.getJSON("packets", function(data) { $.getJSON("packets", function(data) {
if (!data.hasOwnProperty("exams")) { if (!data.hasOwnProperty("exams")) {
loadPacketList(data); loadPacketList(data);
@@ -410,7 +444,7 @@ async function retrievePacketList(target = "all", onComplete = null) {
} }
} }
} catch (e) { } catch (e) {
log.debug("Unable to set exam query url", e); log.warn("Unable to set exam query url", e);
} }
if (examUrl) { if (examUrl) {
@@ -506,17 +540,17 @@ async function retrievePacketList(target = "all", onComplete = null) {
async function loadExamList(data, clearExistingList = true, target = "all") { async function loadExamList(data, clearExistingList = true, target = "all") {
// If exam mode was enabled via URL param, hide packets by default // If exam mode was enabled via URL param, hide packets by default
if (typeof url_exam_mode !== "undefined" && url_exam_mode) { if (typeof url_exam_mode !== "undefined" && url_exam_mode) {
log.debug("URL exam mode active - hiding packets list"); log.info("URL exam mode active - hiding packets list");
try { try {
$(".packet-wrapper").hide(); $(".packet-wrapper").hide();
} catch (e) { } catch (e) {
log.debug("Unable to hide packet UI elements", e); log.warn("Unable to hide packet UI elements", e);
} }
} }
log.debug(`Load exam list: ${data}`) log.info(`Load exam list: ${data}`)
log.debug("Loading saved sessions") log.info("Loading saved sessions")
let sessions = await db.session.toArray().catch(function(error) { let sessions = await db.session.toArray().catch(function(error) {
log.debug("Error loading session", error); log.error("Error loading session", error);
$("#database-error").text( $("#database-error").text(
"Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database." "Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database."
); );
@@ -539,7 +573,7 @@ async function loadExamList(data, clearExistingList = true, target = "all") {
let exams_started = []; let exams_started = [];
let exams_completed = []; let exams_completed = [];
log.debug("Check status of saved exams") log.info("Check status of saved exams")
for (const s of sessions) { for (const s of sessions) {
log.debug(`checking status of`, s) log.debug(`checking status of`, s)
if (s.status == "active") { if (s.status == "active") {
@@ -569,7 +603,7 @@ async function loadExamList(data, clearExistingList = true, target = "all") {
$("#cache-details ul").empty(); $("#cache-details ul").empty();
} }
log.debug("Loop available exams") log.info("Loop available exams")
for (const exam of exam_list) { for (const exam of exam_list) {
log.debug(exam) log.debug(exam)
const name = exam["name"]; const name = exam["name"];
@@ -804,10 +838,10 @@ async function loadExamList(data, clearExistingList = true, target = "all") {
async function loadPacketList(data) { async function loadPacketList(data) {
// NOTE: currently the packet id is not available prior ot downloading the packet // NOTE: currently the packet id is not available prior ot downloading the packet
// this severly limits what we can do with it (but to require would limit the utility) // this severly limits what we can do with it (but to require would limit the utility)
log.debug(`loadPacketList`); log.info(`loadPacketList`);
log.debug("Loading saved sessions") log.info("Loading saved sessions")
let sessions = await db.session.toArray().catch(function(error) { let sessions = await db.session.toArray().catch(function(error) {
log.debug(`Error loading session ${error}`); log.error(`Error loading session ${error}`);
$("#database-error").text( $("#database-error").text(
"Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database." "Error loading the database, schema has probably changed and needs updating. You will probably need to delete the local database."
); );
@@ -826,7 +860,7 @@ async function loadPacketList(data) {
// .equals() // .equals()
let packets_started = []; let packets_started = [];
let packets_completed = []; let packets_completed = [];
log.debug("Check status of saved packets") log.info("Check status of saved packets")
sessions.forEach((s) => { sessions.forEach((s) => {
log.debug(`checking status of`, s) log.debug(`checking status of`, s)
if (s.status == "active") { if (s.status == "active") {
@@ -836,16 +870,16 @@ async function loadPacketList(data) {
} }
}); });
log.debug("Packets started") log.info("Packets started")
log.debug(packets_started) log.debug(packets_started)
log.debug("Packets completed") log.info("Packets completed")
log.debug(packets_completed) log.debug(packets_completed)
let packet_list = []; let packet_list = [];
if (data != null) { if (data != null) {
packet_list = data.packets; packet_list = data.packets;
} else { } else {
log.debug("data == null") log.warn("data == null")
} }
log.debug(packet_list) log.debug(packet_list)
@@ -1146,40 +1180,38 @@ function setUpPacket(data, path) {
} }
function loadSession() { function loadSession() {
console.debug("load session", exam_details, db.session); log.info("load session", exam_details, db.session);
// Either continue session or create a new one // Either continue session or create a new one
db.session db.session
// .where("status")
// .equals("active")
.where("[eid+aid]") .where("[eid+aid]")
.between([exam_details.eid, Dexie.minKey], [exam_details.eid, Dexie.maxKey]) .between([exam_details.eid, Dexie.minKey], [exam_details.eid, Dexie.maxKey])
.toArray() .toArray()
.then((sessions) => { .then((sessions) => {
//console.debug("sessions", sessions);
// let active_sessions = [];
// sessions.forEach((s) => {
// if (s.status == "active") {
// active_sessions.push(s);
// }
// });
let load_previous = false;
// Start new session if no session found
if (sessions.length < 1) { if (sessions.length < 1) {
exam_details.aid = uuidv4(); exam_details.aid = uuidv4();
date_started = Date.now(); date_started = Date.now();
} else { exam_details.start_time = date_started / 1000;
let text = "Select session to continue (leave blank to create new):"; setUpQuestions(false);
if (exam_details.exam_mode) { if (!review_mode) {
text = "Session will be continued"; initSync(exam_details, URLS);
} }
//console.debug(sessions.date); } else {
// Show resume dialog
$("#resume-sessions-list").empty();
for (let i = 0; i < sessions.length; i++) { let textPrompt = "Select a session to continue:";
let d = new Date(sessions[i].date); if (exam_details.exam_mode) {
textPrompt = "An exam session is already in progress. You must continue the existing session:";
$("#btn-resume-start-new").hide();
} else {
$("#btn-resume-start-new").show();
}
$("#resume-text").text(textPrompt);
// Populate session items
sessions.forEach((session, i) => {
let d = new Date(session.date);
let formatted_date = let formatted_date =
d.getFullYear() + d.getFullYear() +
"-" + "-" +
@@ -1193,58 +1225,83 @@ function loadSession() {
":" + ":" +
d.getSeconds(); d.getSeconds();
if (sessions[i].status == "active") { let label = `${formatted_date}`;
text = text + `\r${i}: ${formatted_date} [In progress]`; if (session.status == "active") {
label += " [In progress]";
} else { } else {
if (!exam_details.exam_mode) { if (!exam_details.exam_mode) {
text = label += ` [Complete - score ${session.score}/${session.max_score}]`;
text +
`\r${i}: ${formatted_date} [Complete - score ${sessions[i].score}/${sessions[i].max_score}]`;
} else { } else {
text = label += " [Complete]";
text +
`\r${i}: ${formatted_date} [Complete]`;
} }
} }
}
// If in exam mode we don't want to allow multiple sessions let $item = $(`
let s; <div class="resume-session-item" data-index="${i}" style="padding: 8px; margin: 4px 0; border: 1px solid #444; border-radius: 4px; cursor: pointer; background: #222; color: #eee; display: flex; align-items: center; gap: 10px;">
<input type="radio" name="resume-session-select" id="session-opt-${i}" value="${i}" style="cursor: pointer; margin: 0;">
<label for="session-opt-${i}" style="cursor: pointer; display: inline; font-size: 13px; margin: 0; color: #ccc;">${label}</label>
</div>
`);
$item.on("click", function() {
$(this).find("input[type=radio]").prop("checked", true);
$(".resume-session-item").css("border-color", "#444");
$(this).css("border-color", "#311B92");
$("#btn-resume-continue").prop("disabled", false);
});
$("#resume-sessions-list").append($item);
});
// Auto-select the first one by default if exam_mode is active
if (exam_details.exam_mode) { if (exam_details.exam_mode) {
s = "0"; $(".resume-session-item").first().click();
alert(text);
} else { } else {
s = prompt(text, "0"); $("#btn-resume-continue").prop("disabled", true);
} }
if (s != null && s != "") { // Open the dialog
load_previous = true; $("#resume-dialog").modal({
escapeClose: false,
clickClose: false,
showClose: false
});
exam_details.aid = sessions[parseInt(s)].aid; // Unbind previous click events to prevent duplicates
exam_details.cid = sessions[parseInt(s)].cid; $("#btn-resume-start-new").off("click").on("click", function() {
date_started = sessions[parseInt(s)].date; $.modal.close();
let time_left = sessions[parseInt(s)].time_left;
exam_time = time_left;
exam_details.question_order = sessions[parseInt(s)].question_order;
if (sessions[parseInt(s)].status == "complete") {
review_mode = true;
}
} else {
// If cancel is pressed or input is blank
exam_details.aid = uuidv4(); exam_details.aid = uuidv4();
date_started = Date.now(); date_started = Date.now();
//console.debug("new date", date_started); exam_details.start_time = date_started / 1000;
} setUpQuestions(false);
exam_details.start_time = date_started / 1000; if (!review_mode) {
} initSync(exam_details, URLS);
}
});
setUpQuestions(load_previous); $("#btn-resume-continue").off("click").on("click", function() {
if (!review_mode) { let selectedIdx = parseInt($("input[name=resume-session-select]:checked").val());
initSync(exam_details, URLS); if (isNaN(selectedIdx)) return;
$.modal.close();
let selectedSession = sessions[selectedIdx];
exam_details.aid = selectedSession.aid;
exam_details.cid = selectedSession.cid;
date_started = selectedSession.date;
exam_time = selectedSession.time_left;
exam_details.question_order = selectedSession.question_order;
if (selectedSession.status == "complete") {
review_mode = true;
}
exam_details.start_time = date_started / 1000;
setUpQuestions(true);
if (!review_mode) {
initSync(exam_details, URLS);
}
});
} }
}); });
} }
@@ -3287,6 +3344,15 @@ $("#btn-reset-local").click(function(evt) {
localStorage.removeItem("cid.passcode") localStorage.removeItem("cid.passcode")
} else {} } else {}
}); });
// Initialize logging controls
$("#select-log-level").val(localStorage.getItem("rts.logLevel") || "warn");
$("#select-log-level").on("change", function() {
let level = $(this).val();
log.setLevel(level);
localStorage.setItem("rts.logLevel", level);
log.info(`Log level changed to: ${level}`);
});
$("#btn-delete-current").click(function(evt) { $("#btn-delete-current").click(function(evt) {
if (exam_details.question_order.length < 1) { if (exam_details.question_order.length < 1) {