some big changes...

This commit is contained in:
Ross
2026-07-04 22:40:56 +01:00
parent f74aba0e9c
commit 8ab8a7e8db
5 changed files with 187 additions and 74 deletions
+5
View File
@@ -1290,6 +1290,11 @@ h2:has(+ #exam-list:empty), h2:has(+ #packet-list:empty){
animation: pulse-failed 1s infinite alternate; animation: pulse-failed 1s infinite alternate;
} }
.sync-dot.practice {
background-color: #2196F3;
box-shadow: 0 0 8px #2196F3;
}
.sync-text { .sync-text {
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
+4
View File
@@ -4,6 +4,10 @@ import * as config from "./config.js";
* Submits answers * Submits answers
*/ */
export function submitAnswers(exam_details, db, URLS) { 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) getJsonAnswers(exam_details, db)
.then((a) => { .then((a) => {
let json = { let json = {
+99 -37
View File
@@ -109,6 +109,43 @@ let global_cid = null;
let global_passcode = null; let global_passcode = null;
let global_uid = null; let global_uid = null;
let global_username = 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 = $("<span>[x]</span>");
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) // Map of eid -> friendly exam name (populated when exam list is loaded)
let examNameMap = {}; let examNameMap = {};
@@ -304,13 +341,58 @@ async function retrievePacketList() {
log.debug("this will default to packets/packets.json") log.debug("this will default to packets/packets.json")
} }
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 {
typeUrl = url.replace("exam/json", `exam/json/${type}`);
}
$.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({ $.ajax({
dataType: "json", dataType: "json",
cache: false, cache: false,
url: url, url: url,
success: function(data) { success: function(data) {
if (data.hasOwnProperty("exams")) { if (data.hasOwnProperty("exams")) {
loadExamList(data); loadExamList(data, true);
} else { } else {
loadPacketList(data); loadPacketList(data);
} }
@@ -318,7 +400,6 @@ async function retrievePacketList() {
error: function(httpObj, textStatus) { error: function(httpObj, textStatus) {
console.debug(httpObj); console.debug(httpObj);
if (httpObj.status == 401 || httpObj.status == 404) { 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.cid");
localStorage.removeItem("cid.passcode"); localStorage.removeItem("cid.passcode");
$.notify("Unable to login", "error"); $.notify("Unable to login", "error");
@@ -336,7 +417,7 @@ async function retrievePacketList() {
log.debug("Unable to load packets / exams, try loading '/packets' (legacy)") log.debug("Unable to load packets / exams, try loading '/packets' (legacy)")
$.getJSON("packets", function(data) { $.getJSON("packets", function(data) {
if (data.hasOwnProperty("exams")) { if (data.hasOwnProperty("exams")) {
loadExamList(data); loadExamList(data, true);
} else { } else {
loadPacketList(data); loadPacketList(data);
} }
@@ -345,6 +426,7 @@ async function retrievePacketList() {
}); });
}) })
.always(function() {}); .always(function() {});
}
} }
/** /**
@@ -353,7 +435,7 @@ async function retrievePacketList() {
* *
* @param {JSON} data - json containing available exams * @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 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.debug("URL exam mode active - hiding packets list");
@@ -384,36 +466,7 @@ async function loadExamList(data) {
//Display user info if it exists //Display user info if it exists
// If a user is loggged into the server it any exam responses // If a user is loggged into the server it any exam responses
// should have the details embedded // should have the details embedded
if (data.hasOwnProperty("user") && data.user) { setUserInfo(data);
$("#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 = $("<span>[x]</span>")
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);
}
}
let exams_started = []; let exams_started = [];
@@ -437,7 +490,12 @@ async function loadExamList(data) {
// exam_generated_map stores the exam and its json ID to // exam_generated_map stores the exam and its json ID to
// enable invalidation when the question is updated on the server // enable invalidation when the question is updated on the server
let exam_generated_map = {}; let exam_generated_map = {};
if (clearExistingList) {
$("#packet-list").empty(); $("#packet-list").empty();
$("#exam-list").empty();
$("#cache-details summary").empty();
$("#cache-details ul").empty();
}
log.debug("Loop available exams") log.debug("Loop available exams")
for (const exam of exam_list) { for (const exam of exam_list) {
@@ -515,18 +573,22 @@ async function loadExamList(data) {
// Check the database for exams that have been saved // Check the database for exams that have been saved
question_db.saved_exams.toArray().then((saved_exams) => { question_db.saved_exams.toArray().then((saved_exams) => {
if (saved_exams.length < 1) { if (saved_exams.length < 1) {
if ($("#cache-details summary span").length === 0) {
$("#cache-details summary").append("<span>No cached exams / questions</span>"); $("#cache-details summary").append("<span>No cached exams / questions</span>");
}
} else { } else {
$("#cache-details summary").append("<span>Cached exams / questions:</span>"); if ($("#cache-details summary span").length === 0 || $("#cache-details summary span").text().includes("No cached")) {
$("#cache-details summary").empty().append("<span>Cached exams / questions:</span>");
}
} }
console.debug("Loop through saved exams"); console.debug("Loop through saved exams");
saved_exams.forEach(async (saved_exam, n) => { saved_exams.forEach(async (saved_exam, n) => {
console.debug("Check", saved_exam); console.debug("Check", saved_exam);
//Compared saved exams to those available
if (exam_generated_map.hasOwnProperty(saved_exam.eid)) {
$("#cache-details ul").append( $("#cache-details ul").append(
`<li class="cache-item" data-eid=${saved_exam.eid}>Exam: ${saved_exam.exam_name} [${saved_exam.eid}]: ${saved_exam.exam_json_id}` `<li class="cache-item" data-eid=${saved_exam.eid}>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)) {
// If the json_id s differ delete and force a refresh // If the json_id s differ delete and force a refresh
const exam_json_id = exam_generated_map[saved_exam.eid][0]; const exam_json_id = exam_generated_map[saved_exam.eid][0];
const question_json_id_hash = exam_generated_map[saved_exam.eid][1]; const question_json_id_hash = exam_generated_map[saved_exam.eid][1];
+44 -3
View File
@@ -4,11 +4,11 @@ import { db } from "./db.js";
let syncInterval = null; let syncInterval = null;
let debounceTimeout = null; let debounceTimeout = null;
let currentStatus = "saved"; // "saved", "syncing", "failed" let currentStatus = "saved"; // "saved", "syncing", "failed", "practice"
/** /**
* Update the sync UI state * Update the sync UI state
* @param {string} status - "saved", "syncing", "failed" * @param {string} status - "saved", "syncing", "failed", "practice"
*/ */
export function updateSyncUI(status) { export function updateSyncUI(status) {
currentStatus = status; currentStatus = status;
@@ -17,6 +17,12 @@ export function updateSyncUI(status) {
let html = ""; let html = "";
switch (status) { switch (status) {
case "practice":
html = `
<span class="sync-dot practice"></span>
<span class="sync-text">Practice mode (local only)</span>
`;
break;
case "syncing": case "syncing":
html = ` html = `
<span class="sync-dot syncing"></span> <span class="sync-dot syncing"></span>
@@ -44,7 +50,7 @@ export function updateSyncUI(status) {
* Perform background synchronization of unsynced answers * Perform background synchronization of unsynced answers
*/ */
export async function performSync(exam_details, URLS) { 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 // Find all unsynced answers for this exam/cid/aid
const unsynced = await db.answers 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) * Trigger an immediate, debounced sync (2s delay after last change)
*/ */
export function triggerImmediateSync(exam_details, URLS) { export function triggerImmediateSync(exam_details, URLS) {
if (!exam_details || !exam_details.exam_mode) return;
if (debounceTimeout) { if (debounceTimeout) {
clearTimeout(debounceTimeout); clearTimeout(debounceTimeout);
} }
@@ -125,6 +133,39 @@ export function initSync(exam_details, URLS) {
clearInterval(syncInterval); 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 // Check if there are unsynced answers at startup
db.answers db.answers
.where({ .where({
+1
View File
@@ -74,6 +74,7 @@ describe("RTS Sync Unit Tests", () => {
cid: "9999", cid: "9999",
eid: "rapid/1", eid: "rapid/1",
start_time: 1234567, start_time: 1234567,
exam_mode: true,
}; };
const URLS = { const URLS = {
exam_submit_url: "/submit-url", exam_submit_url: "/submit-url",