some big changes...
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+133
-71
@@ -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 = $("<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)
|
||||
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 = $("<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);
|
||||
}
|
||||
}
|
||||
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("<span>No cached exams / questions</span>");
|
||||
if ($("#cache-details summary span").length === 0) {
|
||||
$("#cache-details summary").append("<span>No cached exams / questions</span>");
|
||||
}
|
||||
} 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");
|
||||
saved_exams.forEach(async (saved_exam, n) => {
|
||||
console.debug("Check", saved_exam);
|
||||
$("#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}`
|
||||
);
|
||||
//Compared saved exams to those available
|
||||
if (exam_generated_map.hasOwnProperty(saved_exam.eid)) {
|
||||
$("#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}`
|
||||
);
|
||||
// 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];
|
||||
|
||||
+44
-3
@@ -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 = `
|
||||
<span class="sync-dot practice"></span>
|
||||
<span class="sync-text">Practice mode (local only)</span>
|
||||
`;
|
||||
break;
|
||||
case "syncing":
|
||||
html = `
|
||||
<span class="sync-dot syncing"></span>
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user