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;
|
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,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
@@ -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
@@ -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({
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user