diff --git a/css/main.css b/css/main.css index e3b5edf..43c609c 100644 --- a/css/main.css +++ b/css/main.css @@ -1290,6 +1290,11 @@ h2:has(+ #exam-list:empty), h2:has(+ #packet-list:empty){ animation: pulse-failed 1s infinite alternate; } +.sync-dot.practice { + background-color: #2196F3; + box-shadow: 0 0 8px #2196F3; +} + .sync-text { letter-spacing: 0.2px; } diff --git a/js/interact.js b/js/interact.js index fd25bce..310f8f0 100644 --- a/js/interact.js +++ b/js/interact.js @@ -4,6 +4,10 @@ import * as config from "./config.js"; * Submits answers */ export function submitAnswers(exam_details, db, URLS) { + if (!exam_details || !exam_details.exam_mode) { + alert("Answers can only be submitted when the exam is in exam mode."); + return; + } getJsonAnswers(exam_details, db) .then((a) => { let json = { diff --git a/js/main.js b/js/main.js index 0b69039..5346651 100644 --- a/js/main.js +++ b/js/main.js @@ -109,6 +109,43 @@ let global_cid = null; let global_passcode = null; let global_uid = null; let global_username = null; +let userInfoSet = false; + +/** + * Set user info in the UI independently of packets/exams. + */ +function setUserInfo(data) { + if (userInfoSet || !data || !data.user) return; + userInfoSet = true; + + $("#user").empty().append(`User: ${data.user}`); + global_uid = data.user_id; + global_username = data.user; + + if (global_cid == null) { + global_cid = "u-" + global_uid; + } + + $("#btn-user-login").hide(); + + let logout = $("[x]"); + logout.css({ cursor: "pointer", "margin-left": "8px" }); + logout.click(() => { + localStorage.removeItem("cid.cid"); + localStorage.removeItem("cid.passcode"); + window.location = URLS.logout_url; + }); + + if ($("#user-actions").length) { + $("#user-actions").empty().append(logout); + if (!allow_users) { + $("#user-actions").hide(); + } + } else { + $("#user").append(logout); + } +} + // Map of eid -> friendly exam name (populated when exam list is loaded) let examNameMap = {}; @@ -304,47 +341,92 @@ async function retrievePacketList() { log.debug("this will default to packets/packets.json") } - $.ajax({ - dataType: "json", - cache: false, - url: url, - success: function(data) { - if (data.hasOwnProperty("exams")) { - loadExamList(data); + if (url && (url.includes("exam/json") || url.endsWith("exam/json"))) { + const types = ["anatomy", "rapid", "short", "long"]; + $("#packet-list").empty(); + $("#exam-list").empty(); + $("#cache-details summary").empty(); + $("#cache-details ul").empty(); + + types.forEach(type => { + let typeUrl = url; + if (url.includes("exam/json/unbased")) { + typeUrl = url.replace("exam/json/unbased", `exam/json/${type}/unbased`); } else { - loadPacketList(data); + typeUrl = url.replace("exam/json", `exam/json/${type}`); } - }, - error: function(httpObj, textStatus) { - console.debug(httpObj); - if (httpObj.status == 401 || httpObj.status == 404) { - // First we remove any saved cid items from local storage so the url can take precedence - localStorage.removeItem("cid.cid"); - localStorage.removeItem("cid.passcode"); - $.notify("Unable to login", "error"); - $.notify("This page will now reload", "error"); - setTimeout(() => { - window.location.reload(); - }, 3000); - $("#options-panel").show(); - $("#candidate-details").addClass("invalid-login"); - } - }, - }) - .done(function() {}) - .fail(function() { - log.debug("Unable to load packets / exams, try loading '/packets' (legacy)") - $.getJSON("packets", function(data) { + + $.ajax({ + dataType: "json", + cache: false, + url: typeUrl, + success: function(data) { + if (data.hasOwnProperty("exams")) { + setUserInfo(data); + + loadExamList(data, false); + } else { + loadPacketList(data); + } + }, + error: function(httpObj) { + console.debug(httpObj); + if (httpObj.status == 401 || httpObj.status == 404) { + localStorage.removeItem("cid.cid"); + localStorage.removeItem("cid.passcode"); + $.notify("Unable to login", "error"); + $.notify("This page will now reload", "error"); + setTimeout(() => { + window.location.reload(); + }, 3000); + $("#options-panel").show(); + $("#candidate-details").addClass("invalid-login"); + } + } + }); + }); + } else { + $.ajax({ + dataType: "json", + cache: false, + url: url, + success: function(data) { if (data.hasOwnProperty("exams")) { - loadExamList(data); + loadExamList(data, true); } else { loadPacketList(data); } - }).fail(function(jqXHR, textStatus, errorThrown) { - console.warn("No packet list available"); - }); + }, + error: function(httpObj, textStatus) { + console.debug(httpObj); + if (httpObj.status == 401 || httpObj.status == 404) { + localStorage.removeItem("cid.cid"); + localStorage.removeItem("cid.passcode"); + $.notify("Unable to login", "error"); + $.notify("This page will now reload", "error"); + setTimeout(() => { + window.location.reload(); + }, 3000); + $("#options-panel").show(); + $("#candidate-details").addClass("invalid-login"); + } + }, }) - .always(function() {}); + .done(function() {}) + .fail(function() { + log.debug("Unable to load packets / exams, try loading '/packets' (legacy)") + $.getJSON("packets", function(data) { + if (data.hasOwnProperty("exams")) { + loadExamList(data, true); + } else { + loadPacketList(data); + } + }).fail(function(jqXHR, textStatus, errorThrown) { + console.warn("No packet list available"); + }); + }) + .always(function() {}); + } } /** @@ -353,7 +435,7 @@ async function retrievePacketList() { * * @param {JSON} data - json containing available exams */ -async function loadExamList(data) { +async function loadExamList(data, clearExistingList = true) { // If exam mode was enabled via URL param, hide packets by default if (typeof url_exam_mode !== "undefined" && url_exam_mode) { log.debug("URL exam mode active - hiding packets list"); @@ -384,36 +466,7 @@ async function loadExamList(data) { //Display user info if it exists // If a user is loggged into the server it any exam responses // should have the details embedded - if (data.hasOwnProperty("user") && data.user) { - $("#user").append(`User: ${data.user}`); - //$(".exam-wrapper").show(); - global_uid = data.user_id; - global_username = data.user; - - - if (global_cid == null) { - global_cid = "u-" + global_uid; - } - - $("#btn-user-login").hide(); - - let logout = $("[x]") - logout.css({ cursor: "pointer", "margin-left": "8px" }); - logout.click(() => { - localStorage.removeItem("cid.cid"); - localStorage.removeItem("cid.passcode"); - window.location = URLS.logout_url; - }) - // append logout to actions area so actions can be hidden when `allow_users` is false - if ($("#user-actions").length) { - $("#user-actions").append(logout); - if (!allow_users) { - $("#user-actions").hide(); - } - } else { - $("#user").append(logout); - } - } + setUserInfo(data); let exams_started = []; @@ -437,7 +490,12 @@ async function loadExamList(data) { // exam_generated_map stores the exam and its json ID to // enable invalidation when the question is updated on the server let exam_generated_map = {}; - $("#packet-list").empty(); + if (clearExistingList) { + $("#packet-list").empty(); + $("#exam-list").empty(); + $("#cache-details summary").empty(); + $("#cache-details ul").empty(); + } log.debug("Loop available exams") for (const exam of exam_list) { @@ -515,18 +573,22 @@ async function loadExamList(data) { // Check the database for exams that have been saved question_db.saved_exams.toArray().then((saved_exams) => { if (saved_exams.length < 1) { - $("#cache-details summary").append("No cached exams / questions"); + if ($("#cache-details summary span").length === 0) { + $("#cache-details summary").append("No cached exams / questions"); + } } else { - $("#cache-details summary").append("Cached exams / questions:"); + if ($("#cache-details summary span").length === 0 || $("#cache-details summary span").text().includes("No cached")) { + $("#cache-details summary").empty().append("Cached exams / questions:"); + } } console.debug("Loop through saved exams"); saved_exams.forEach(async (saved_exam, n) => { console.debug("Check", saved_exam); - $("#cache-details ul").append( - `
  • Exam: ${saved_exam.exam_name} [${saved_exam.eid}]: ${saved_exam.exam_json_id}` - ); //Compared saved exams to those available if (exam_generated_map.hasOwnProperty(saved_exam.eid)) { + $("#cache-details ul").append( + `
  • Exam: ${saved_exam.exam_name} [${saved_exam.eid}]: ${saved_exam.exam_json_id}` + ); // If the json_id s differ delete and force a refresh const exam_json_id = exam_generated_map[saved_exam.eid][0]; const question_json_id_hash = exam_generated_map[saved_exam.eid][1]; diff --git a/js/sync.js b/js/sync.js index 83ce00f..59bb54d 100644 --- a/js/sync.js +++ b/js/sync.js @@ -4,11 +4,11 @@ import { db } from "./db.js"; let syncInterval = null; let debounceTimeout = null; -let currentStatus = "saved"; // "saved", "syncing", "failed" +let currentStatus = "saved"; // "saved", "syncing", "failed", "practice" /** * Update the sync UI state - * @param {string} status - "saved", "syncing", "failed" + * @param {string} status - "saved", "syncing", "failed", "practice" */ export function updateSyncUI(status) { currentStatus = status; @@ -17,6 +17,12 @@ export function updateSyncUI(status) { let html = ""; switch (status) { + case "practice": + html = ` + + Practice mode (local only) + `; + break; case "syncing": html = ` @@ -44,7 +50,7 @@ export function updateSyncUI(status) { * Perform background synchronization of unsynced answers */ export async function performSync(exam_details, URLS) { - if (!exam_details || !exam_details.eid) return; + if (!exam_details || !exam_details.exam_mode || !exam_details.eid) return; // Find all unsynced answers for this exam/cid/aid const unsynced = await db.answers @@ -108,6 +114,8 @@ export async function performSync(exam_details, URLS) { * Trigger an immediate, debounced sync (2s delay after last change) */ export function triggerImmediateSync(exam_details, URLS) { + if (!exam_details || !exam_details.exam_mode) return; + if (debounceTimeout) { clearTimeout(debounceTimeout); } @@ -125,6 +133,39 @@ export function initSync(exam_details, URLS) { clearInterval(syncInterval); } + // Setup click redirect to remote user answers on the server if in exam mode + const container = document.getElementById("sync-status"); + if (container) { + if (exam_details && exam_details.exam_mode) { + $(container).css("cursor", "pointer"); + $(container).off("click").on("click", () => { + let url = URLS.exam_results_url; + if (url) { + if (exam_details.cid) { + if (exam_details.cid.startsWith("u-")) { + url = URLS.exam_user_results_url || url; + } else { + url = `${url}${exam_details.cid}`; + const passcode = localStorage.getItem("cid.passcode"); + if (passcode) { + url = `${url}/${passcode}`; + } + } + } + window.open(url, "_blank"); + } + }); + } else { + $(container).css("cursor", "default"); + $(container).off("click"); + } + } + + if (!exam_details || !exam_details.exam_mode) { + updateSyncUI("practice"); + return; + } + // Check if there are unsynced answers at startup db.answers .where({ diff --git a/tests/sync.test.js b/tests/sync.test.js index c18cc10..3ed1631 100644 --- a/tests/sync.test.js +++ b/tests/sync.test.js @@ -74,6 +74,7 @@ describe("RTS Sync Unit Tests", () => { cid: "9999", eid: "rapid/1", start_time: 1234567, + exam_mode: true, }; const URLS = { exam_submit_url: "/submit-url",