`
).click(function(evt) {
//console.debug("packets/" + packet);
evt.stopPropagation();
})
)
);
});
$("#options-panel").show();
}
/**
* Loads the requisite packet into the quiz system
* @param {string} path - relative path to the packet json file
*/
function loadPacketFromAjax(path, eid, exam_json_id) {
if (eid != undefined) {
// try and load the exam from the local indexedDB
question_db.saved_exams
.get(eid)
.then((exam) => {
if (exam == undefined || (exam["exam_json_id"] != exam_json_id)) {
console.debug("AjaxRequestPacket:", eid);
ajaxRequestionPacket(true);
} else {
// We have an up to date exam in the database
// TODO: check the questions are valid
console.debug("LoadLocalPacket:", eid);
use_local_question_cache = true;
setUpPacket(exam, path);
$("#options-panel").hide();
}
})
.catch(function(error) {
// Will be triggered if eid does not exist in database
console.debug("Unable to load exam:", eid);
console.debug("error-", error);
console.debug("Exam will be downloaded");
ajaxRequestionPacket();
});
return;
}
function ajaxRequestionPacket(force_refresh) {
//console.debug("loading packet from " + path);
$("#progress").html(`Requesting packet: ${path}`);
let browser_cache = true;
if (force_refresh) {
browser_cache = false;
}
$.ajax({
dataType: "json",
url: path,
cache: browser_cache,
progress: function(e) {
if (e.lengthComputable) {
var completedPercentage = Math.round((e.loaded * 100) / e.total);
$("#progress").html(
`${completedPercentage}% ${helper.formatBytes(e.total)}`
);
}
},
})
.done(function(data) {
setUpPacket(data, path);
$("#options-panel").hide();
})
.fail(function() {
console.debug("Unable to load packet at: " + path);
});
}
}
/**
* Parse the packet and extract metadata (if it exists)
* Will then call setUpQuestions() to load the packet
* @param {JSON} data
*/
function setUpPacket(data, path) {
packet_name = path;
// Load the exam name from the json packet (if it exists)
if (data.hasOwnProperty("exam_name")) {
packet_name = data.exam_name;
}
$(".exam-name").text("Exam: " + packet_name);
if (data.hasOwnProperty("eid")) {
exam_details.eid = data.eid;
} else {
exam_details.eid = packet_name
}
if (data.hasOwnProperty("exam_type")) {
question_type = data.exam_type;
}
if (data.hasOwnProperty("exam_json_id")) {
packet_json_id = data.exam_json_id;
}
if (data.hasOwnProperty("exam_mode")) {
exam_details.exam_mode = data.exam_mode;
}
if (data.hasOwnProperty("questions")) {
questions = data.questions;
} else {
questions = data;
}
if (data.hasOwnProperty("exam_order")) {
exam_details.question_order = data.exam_order;
} else {
exam_details.question_order = [];
Object.keys(questions).forEach(function(e) {
exam_details.question_order.push(e);
// Make sure answers are arrays
if (!Array.isArray(questions[e]["answers"])) {
questions[e]["answers"] = [questions[e]["answers"]];
}
});
let randomise_order = true;
if (config.randomise_order != undefined) {
randomise_order = config.randomise_order;
}
if (randomise_order) {
exam_details.question_order = helper.shuffleArray(
exam_details.question_order
);
}
// We add this here so the question order is maintained
// when loading from cache
data["exam_order"] = exam_details.question_order;
}
if (data.hasOwnProperty("exam_time")) {
exam_time = data.exam_time;
// packet time is the time specificed by the packet
packet_time = data.exam_time;
}
//if (use_local_question_cache && force_question_refresh.length < 1) {
// // If loading form cache nothing else to do here
// loadSession();
// return;
//}
if (!use_local_question_cache) {
// Save the details to the question database
var clone_data = Object.assign({}, data, {
questions: "cached"
});
console.debug(clone_data)
//clone_data["cached_questions"] = Object.keys(data["questions"]);
question_db.saved_exams.put(clone_data);
}
// If question_requests is set we need to do a request for each question
if (
data.hasOwnProperty("question_requests") &&
Object.keys(data["question_requests"]).length > 0
) {
// Will happen if loading from cache
//if (data["questions"] == "cached") {
// let a = {};
// data["cached_questions"].forEach((q, n) => {
// let s = JSON.stringify()
// if (force_question_refresh.includes(s)) {
// a[q] = 1;
// }
// });
// data["questions"] = a;
//}
let question_total = Object.keys(data["question_requests"]).length;
let question_number = 0;
var requests = [];
var request_numbers = [];
// For loop to generate requests
(async () => {
for (let n in data["question_requests"]) {
question_number++;
n = String(n);
let obj = {
qid: n,
type: question_type
};
let question_in_db = await question_db.question_data.get(obj);
// If the question is in the db we can load
// invalid questions should have already been removed
if (question_in_db != undefined) {
continue;
}
const question_url = `${path}/${n}`;
//console.debug("Creating ", question_url, question_number, question_total);
//$("#progress").html(`Downloading [${question_number}/${question_total}]`);
let question_json = {};
question_json = await interact
.getQuestion(question_url, question_number, question_total)
.fail((jqXHR, textStatus, errorThrown) => {
//console.debug(jqXHR, textStatus, errorThrown);
console.debug(`error loading question: ${n}`);
//data["questions"][n] = {};
});
if (question_json.hasOwnProperty("cached") && question_json["cached"]) {
console.debug("loading cached packet");
}
if (
question_json.hasOwnProperty("images_json") &&
question_json["images_json"]
) {}
//requests.push(request)
//request_numbers.push(n)
// Using indexed db means that we can avoid loading all data into memory
//data["questions"][n] = question_json;
// Store question data into dexie
let d = {
//eid: exam_details.eid,
qid: n,
data: question_json,
type: question_json.type,
};
question_db.question_data.put(d);
}
// Once all questions have been downloaded we carry on loading
use_local_question_cache = true;
loadSession();
})().catch((error) => {
alert("Error downloading, try refreshing");
console.debug(error);
});
} else {
// Just carry on loading
loadSession();
}
}
function loadSession() {
console.debug("load session", exam_details, db.session);
// Either continue session or create a new one
db.session
// .where("status")
// .equals("active")
.where("[eid+aid]")
.between([exam_details.eid, Dexie.minKey], [exam_details.eid, Dexie.maxKey])
.toArray()
.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) {
exam_details.aid = uuidv4();
date_started = Date.now();
} else {
let text = "Select session to continue (leave blank to create new):";
if (exam_details.exam_mode) {
text = "Session will be continued";
}
//console.debug(sessions.date);
for (let i = 0; i < sessions.length; i++) {
let d = new Date(sessions[i].date);
let formatted_date =
d.getFullYear() +
"-" +
(d.getMonth() + 1) +
"-" +
d.getDate() +
" " +
d.getHours() +
":" +
d.getMinutes() +
":" +
d.getSeconds();
if (sessions[i].status == "active") {
text = text + `\r${i}: ${formatted_date} [In progress]`;
} else {
if (!exam_details.exam_mode) {
text =
text +
`\r${i}: ${formatted_date} [Complete - score ${sessions[i].score}/${sessions[i].max_score}]`;
} else {
text =
text +
`\r${i}: ${formatted_date} [Complete]`;
}
}
}
// If in exam mode we don't want to allow multiple sessions
let s;
if (exam_details.exam_mode) {
s = "0";
alert(text);
} else {
s = prompt(text, "0");
}
if (s != null && s != "") {
load_previous = true;
exam_details.aid = sessions[parseInt(s)].aid;
exam_details.cid = sessions[parseInt(s)].cid;
date_started = sessions[parseInt(s)].date;
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();
date_started = Date.now();
//console.debug("new date", date_started);
}
}
setUpQuestions(load_previous);
});
}
/**
* Build the currently loaded quiz
*/
function setUpQuestions(load_previous) {
if (questions == undefined) {
//return;
} else {
if (!use_local_question_cache) {
Object.keys(questions).forEach(function(e) {
// Store question data into dexie
let d = {
//eid: exam_details.eid,
qid: String(e),
type: questions[e].type,
data: questions[e],
//timestamp: questions[e]["exam_json_id"],
};
question_db.question_data.put(d);
});
}
// Set an order for the questions
if (!load_previous) {}
}
exam_details.number_of_questions = exam_details.question_order.length;
review_mode = false;
// Horrible way to get type of questions
// We assume they are all of the same type....
//question_type = questions[Object.keys(questions)[0]].type;
if (exam_details.exam_mode) {
$("#options-button, #review-overlay-button").hide();
} else {
$(".submit-button").hide();
}
questions = null;
// exam_time can be defined in packets
if (!load_previous && exam_time == null) {
if (question_type == "rapid") {
exam_time = 35 * 60;
} else if (question_type == "anatomy") {
exam_time = 90 * 60;
} else if (question_type == "long") {
exam_time = 75 * 60;
}
}
$(".exam-time")
.val(exam_time / 60)
.change(() => {
exam_time = $(".exam-time").val() * 60;
});
if (exam_details.exam_mode) {
// NOTE: changing the CID when restoring a session will not affect the time left
//$("#candidate-number2")
// .val(exam_details.cid)
// .change(() => {
// exam_details.cid = parseInt($("#candidate-number2").val());
// });
console.debug("OTNUHOTNU", global_cid)
exam_details.cid = global_cid;
$("#candidate-number2").val(global_cid)
exam_details.passcode = global_passcode;
if (global_uid != null) {
$("#exam-candidate-number").append(`User: ${global_username} / ${global_uid}`);
}
$("#exam-candidate-number").show();
$("#start-dialog").addClass("no-close");
$("#start-dialog .exam-time").prop("disabled", "true");
$("#exam-candidate-number").show();
$(".packet-database-options").hide();
$("#start-dialog").modal({
closeExisting: false, // Close existing modals. Set this to false if you need to stack multiple modal instances.
escapeClose: false, // Allows the user to close the modal by pressing `ESC`
clickClose: false, // Allows the user to close the modal by clicking the overlay
showClose: false,
});
} else {
$("#start-dialog").modal();
}
loadQuestion(0, 1, true);
createQuestionListPanel();
}
/**
* Loads a specific question
* @param {number} n - Question number to load
* @param {number} section - Question section to focus
* @param {boolean} force_reload - Force question to be reloaded if already loaded
*/
async function loadQuestion(n, section = 1, force_reload = false) {
$("#question-loading").show();
const aid = exam_details.aid;
const cid = exam_details.cid;
const eid = exam_details.eid;
const passcode = exam_details.passcode;
n = parseInt(n);
//console.debug("loading question (n)", n);
if (n == loaded_question && force_reload == false) {
// Question already loaded
setFocus(section);
$("#question-loading").hide();
return;
}
console.debug(exam_details, n);
const qid = exam_details.question_order[n];
console.debug("qid", qid)
let q = {
qid: String(qid),
type: question_type
};
console.debug(q)
console.debug(question_db.question_data)
let return_data = await question_db.question_data.get(q);
const question_data = return_data.data;
return_data = null;
loaded_question = n;
// Set up question navigation
$(".navigation[value='next']").off();
$(".navigation[value='previous']").off();
if (n == 0) {
$(".navigation[value='previous']").attr("disabled", "disabled");
} else {
$(".navigation[value='previous']")
.removeAttr("disabled")
.click(function() {
loadQuestion(n - 1);
});
}
if (n == exam_details.number_of_questions - 1) {
$(".navigation[value='next']").attr("disabled", "disabled");
} else {
$(".navigation[value='next']")
.removeAttr("disabled")
.click(function() {
loadQuestion(n + 1);
});
}
const display_n = n + 1;
if (question_data.title) {
$(".question .title").get(0).innerHTML =
'' +
display_n +
" " +
question_data.title;
} else {
$(".question .title").get(0).innerHTML =
'' + display_n + "";
}
if (question_data.history) {
$(".question .stem").text(question_data.history);
} else {
$(".question .stem").empty();
}
// Close any open figures
//let el = document.getElementById("dicom-image");
//if (el != undefined) {
// cornerstone.disable(el);
//}
// disable any further enabled elements
let enabled_elements = cornerstone.getEnabledElements();
for (var i = enabled_elements.length - 1; i >= 0; i--) {
//console.debug("disable, ", enabled_elements[i]);
cornerstone.disable(enabled_elements[i].element);
}
$(".canvas-panel").remove();
cornerstone.imageCache.purgeCache();
cornerstoneWADOImageLoader.default.wadouri.dataSetCacheManager.purge();
// Remove feedback images
$(".feedback-image").remove();
// Set up thumbnails
const thumbnails = $(".thumbs");
// Why are we checking this?
thumbnails.empty();
// TODO: figure captions (need to extend base json)
function createThumbnail(image, id, caption, thumbnails) {
//console.debug("create thumb", image);
// For thumbnails we only want a single image
thumbnails.append(
`
${caption}
`
);
// const thumbnail = $(".figure .thumbnail").get(id);
// cornerstone.enable(thumbnail)
// based_img = current_question.images[id];
const based_img = image;
// based (image) data url, just load the image directly
if (based_img.startsWith("data:image/")) {
const img = $("", {
src: based_img,
id: "thumb-" + id,
class: "thumbnail",
title: "Click on the thumbnail to view and manipulate the image.",
draggable: "false",
style: "height: 100px;",
});
$("#figure-" + id).append(img);
// otherwise try to load it as a dicom (if it starts with data)
// or ends with dcm
} else if (based_img.startsWith("data:") || based_img.endsWith(".dcm") || /(?:\/|^)[^.\/]+$/.test(based_img)) {
// convert the data url to a file
viewer
.urltoFile(based_img, "dicom", "application/dicom")
.then(function(dfile) {
// load the file using cornerstoneWADO file loader
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(
dfile
);
const img = $("").get(0);
img.id = "thumb-" + id;
img.class = "thumbnail";
img.title = "Click on the thumbnail to view and manipulate the image.";
img.draggable = "false";
img.style = "height: 100px; width: 100px";
$("#figure-" + id).append(img);
const element = document.getElementById("thumb-" + id);
cornerstone.enable(element);
//cornerstone.loadImage(imageId).then(function (image) {
cornerstone.loadAndCacheImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
cornerstone.resize(element);
}); // .catch( function(error) {
});
} else {
const img = $("", {
src: based_img,
id: "thumb-" + id,
class: "thumbnail",
title: "Click on the thumbnail to view and manipulate the image.",
draggable: "false",
style: "height: 100px;",
});
$("#figure-" + id).append(img);
}
}
question_data.images.forEach((images, n) => {
let image;
if (Array.isArray(images)) {
image = images[0]; // Do we want the middle image?
} else {
image = images;
}
let caption = "...";
if (question_data.image_titles != undefined) {
caption = question_data.image_titles[n];
}
createThumbnail(image, n, caption, thumbnails);
});
function handleThumbnailClicks(id, thumbnail) {
$(thumbnail).click(function(event) {
viewer.openMainImage(question_data, event, this);
});
}
$(".thumbs .figure").each((id, thumbnail) => {
handleThumbnailClicks(id, thumbnail);
});
// Answer setup
const ap = $(".answer-panel");
ap.empty();
switch (question_data.type) {
case "rapid": {
ap.append(
'
' +
display_n +
'.1Case normal/abnormal
'
);
ap.append(
'
' +
display_n +
'.2Reason
'
);
// Handle changing display of optional answer fields and saving of data
$("#rapid-option").change(function(evt) {
if (evt.target.value == "Abnormal") {
$("#rapid-text").css("display", "block");
} else {
$("#rapid-text").css("display", "none");
}
let answer = {
aid: aid,
cid: cid,
eid: eid,
qid: qid,
qidn: "1",
passcode: passcode,
ans: evt.target.value,
};
db.answers.put(answer);
console.debug("Save ", answer)
saveSession();
updateQuestionListPanel(answer);
});
// Save long answers on textchange
$(".long-answer").change(function(evt) {
// ignore blank text and delete any stored value
if (evt.target.value.length < 1) {
db.answers.delete([aid, cid, eid, qid, "2"]);
$(
"#question-list-item-" +
exam_details.question_order.indexOf(qid) +
"-2"
).removeClass("question-saved-localdb");
return;
}
let answer = {
aid: aid,
cid: cid,
eid: eid,
qid: qid,
qidn: "2",
passcode: passcode,
ans: evt.target.value,
};
// db.answers.put({aid: [cid, eid, qidn], ans: evt.target.value});
console.debug("Save ", answer)
db.answers.put(answer);
$(
"#question-list-item-" +
exam_details.question_order.indexOf(qid) +
"-2"
).addClass("question-saved-localdb");
saveSession();
updateQuestionListPanel(answer);
});
// We chain our db requests as we can only check answers once
// they have been loaded (should probably use then or after)
console.debug("Load rapid answers")
db.answers
.get({
aid: aid,
cid: cid,
eid: eid,
qid: qid,
passcode: passcode,
qidn: "1"
})
.then(function(answer) {
if (answer != undefined) {
$("#rapid-option option:contains(" + answer.ans + ")").prop(
"selected",
true
);
// For some reason a change event is not fired...
if (answer.ans == "Abnormal") {
$("#rapid-text").css("display", "block");
}
$("#rapid-option-not-answered").remove();
}
})
.catch(function(error) {
console.debug("DB", cid, eid, qid);
console.debug("error-", error);
})
//.then(function() {
db.answers
.get({
aid: aid,
cid: cid,
eid: eid,
qid: qid,
qidn: "2",
passcode: passcode,
})
.then(function(answer) {
if (answer != undefined) {
$(".long-answer").text(answer.ans);
}
markAnswer(qid, question_data, n);
})
.catch(function(error) {
console.debug("error-", error);
})
//});
addFlagEvents();
loadFlagsFromDb(qid, "1");
loadFlagsFromDb(qid, "2");
break;
}
case "anatomy": {
ap.append(
'
'
);
// Save long answers on textchange
$(".long-answer").change(function(evt) {
const qidn = evt.target.dataset.qidn.toString();
// ignore blank text and delete any stored value
if (evt.target.value.length < 1) {
db.answers.delete([aid, cid, eid, qid, qidn]);
$(
"#question-list-item-" +
exam_details.question_order.indexOf(qid) +
"-" +
qidn
).removeClass("question-saved-localdb");
return;
}
const answer = {
aid: aid,
cid: cid,
eid: eid,
qid: qid,
qidn: qidn,
passcode: passcode,
ans: evt.target.value,
};
db.answers.put(answer);
$(
"#question-list-item-" +
exam_details.question_order.indexOf(qid) +
"-" +
qidn
).addClass("question-saved-localdb");
saveSession();
updateQuestionListPanel(answer);
});
// Loop through the 5 text areas and retrieve from db
// could all be retrieved at once with an addition key index
//
for (let qidn_count = 1; qidn_count < 6; qidn_count++) {
let obj = {
aid: aid,
cid: cid,
eid: eid,
qid: qid,
qidn: qidn_count.toString(),
};
db.answers
.get(obj)
.then(function(answer) {
if (answer != undefined) {
$(".long-answer")
.eq(parseInt(answer.qidn) - 1)
.text(answer.ans);
}
// We only want to mark once...
if (qidn_count == 5) {
markAnswer(qid, question_data, n);
}
})
.catch(function(error) {
console.debug(error);
});
}
addFlagEvents();
loadFlagsFromDb(qid, "1");
loadFlagsFromDb(qid, "2");
loadFlagsFromDb(qid, "3");
loadFlagsFromDb(qid, "4");
loadFlagsFromDb(qid, "5");
break;
}
}
//rebuildQuestionListPanel();
setTimeout(() => {
setFocus(section);
}, 200);
$("#question-loading").hide();
}
/**
* Sets the focus to the required question answer section
* @param {number} section - section to focus
*/
function setFocus(section) {
// Horrible (but it works)
//setTimeout(function () {
// In pratique it selects the end of a textarea but I'm not sure if I can be bothered...
// Apparently I can...
const el = $("*[data-answer-section-qidn=" + section + "]");
// If the target is a textarea we move focus and reapply the value
// to move the cursor to the end
if (el.length < 1) {
return;
}
if (el.get(0).type == "textarea") {
const val = el.val();
el.focus().val("").val(val);
// else we just focus
} else {
el.focus();
}
//}, 200);
}
function updateQuestionListPanel(answer) {
$(
"#question-list-item-" +
exam_details.question_order.indexOf(answer.qid) +
"-" +
answer.qidn
).addClass("question-saved-localdb");
if (question_type == "rapid" && answer.qidn == "1") {
if (answer.ans == "Abnormal") {
$(
"#question-list-item-" +
exam_details.question_order.indexOf(answer.qid) +
"-2"
).css("display", "block");
} else {
$(
"#question-list-item-" +
exam_details.question_order.indexOf(answer.qid) +
"-2"
).css("display", "none");
}
}
}
function deleteQuestionListPanelFlags(answer) {
$(
"#question-list-item-" +
exam_details.question_order.indexOf(answer.qid) +
"-" +
answer.qidn +
" span"
).css("visibility", "hidden");
}
function updateQuestionListPanelFlags(answer) {
$(
"#question-list-item-" +
exam_details.question_order.indexOf(answer.qid) +
"-" +
answer.qidn +
" span"
).css("visibility", "visible");
}
/**
* Updates the question list panel
* Test with just a global update (may need to do these individually
* if it gets slow...)
*/
function rebuildQuestionListPanel() {
const aid = exam_details.aid;
const cid = exam_details.cid;
const eid = exam_details.eid;
db.answers
.where({
aid: aid,
cid: cid,
eid: eid
})
.toArray()
.then(function(answers) {
// Reset all classes (of question-list-item s)
$(".question-list-panel div")
.slice(1)
.attr("class", "question-list-item");
// Loop through each saved answer
answers.forEach(function(answer, n) {
updateQuestionListPanel(answer);
});
// $(".long-answer").text(answer.ans);
})
.catch(function(error) {
console.debug("error - ", error);
});
db.flags
.where({
aid: aid,
cid: cid,
eid: eid
})
.toArray()
.then(function(answers) {
answers.forEach(function(answer, n) {
updateQuestionListPanelFlags(answer);
});
// $(".long-answer").text(answer.ans);
})
.catch(function(error) {
console.debug("error - ", error);
});
if (review_mode == true) {
$(".question-panel-ans").remove();
for (const qid in questions_correct) {
let q_no = exam_details.question_order.indexOf(qid);
//console.debug("q", q_no, qid, questions_correct[qid]);
let question_list_elements = $(`.question-list-item[data-qid='${q_no}']`);
//console.debug(question_list_elements, questions_correct[qid]);
if (questions_correct[qid]) {
$(`.question-list-item[data-qid='${q_no}']`).append(
"β"
);
} else {
$(`.question-list-item[data-qid='${q_no}']`).append(
"β"
);
}
}
}
}
/**
* Builds the QuestionListPanel
*/
function createQuestionListPanel() {
$(".question-list-panel").empty();
$(".question-list-panel").append(
'
Questions
'
);
/**
* Appends QuestionList item
* @param {number} n - question number
* @param {number} qidn - question section number
* @return {*} el - element that has been created
*/
function appendQuestionListItem(n, qidn) {
const qn = n - 1;
const el = $(
'
' +
n +
"." +
qidn +
'β
'
);
$(".question-list-panel").append(el);
// return the new element so it can be hidden if necessary
return el;
}
if (question_type == "rapid") {
// Loop through all questions and append list items
for (let n = 1; n < exam_details.number_of_questions + 1; n++) {
appendQuestionListItem(n, "1");
appendQuestionListItem(n, "2").hide();
}
} else if (question_type == "anatomy") {
for (let n = 1; n < exam_details.number_of_questions + 1; n++) {
appendQuestionListItem(n, "1");
}
} else if (question_type == "long") {
for (let n = 1; n < exam_details.number_of_questions + 1; n++) {
appendQuestionListItem(n, "1");
appendQuestionListItem(n, "2");
appendQuestionListItem(n, "3");
appendQuestionListItem(n, "4");
appendQuestionListItem(n, "5");
}
}
$(".question-list-item").click(function(evt) {
loadQuestion($(this).attr("data-qid"), $(this).attr("data-qidn"));
});
rebuildQuestionListPanel();
}
$("#btn-local-file-load").click(function(evt) {
loadLocalQuestionSet();
});
$(".submit-button").click(function(evt) {
interact.submitAnswers(exam_details, db, config);
});
$("#review-button").click(function(evt) {
$(".question-list-panel").toggle();
});
$("#options-button").click(function(evt) {
loadPacketList(null);
$("#options-panel").toggle();
});
$("#review-overlay-button").click(function(evt) {
if (review_mode == true) {
reviewQuestions();
} else {
$("#finish-dialog").modal();
}
});
$("#fullscreen-overlay-button").click(function(evt) {
if (document.fullscreen == false) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});
$("#finish-exam, #time-up-review-button").click(function(evt) {
review_mode = true;
reviewQuestions();
$.modal.close();
});
$("#time-up-continue-button").click(function(evt) {
$.modal.close();
});
$("#finish-cancel").click(function(evt) {
$.modal.close();
});
$("#overlay-close").click(function(evt) {
$("#options-panel").hide();
});
$("#review-overlay-close").click(function(evt) {
$("#review-overlay").hide();
});
/**
* Loads a local question set (from a file)
* This is call in index.html
*/
function loadLocalQuestionSet() {
let input;
let file;
let fr;
if (typeof window.FileReader !== "function") {
alert("The file API isn't supported on this browser yet.");
return;
}
input = document.getElementById("fileinput");
if (!input) {
alert("No fileinput element.");
} else if (!input.files) {
alert(
"This browser doesn't seem to support the `files` property of file inputs."
);
} else if (!input.files[0]) {
alert("Please select a file before clicking 'Load'");
} else {
file = input.files[0];
fr = new FileReader();
fr.onload = receivedText;
fr.readAsText(file);
}
/**
* Reads text from event and loads the packet
* @param {*} e - event
*/
function receivedText(e) {
const lines = e.target.result;
let j = JSON.parse(lines);
setUpPacket(j, file.name);
$("#options-panel").hide();
}
}
/**
* Displays the review question panel with a summary of marks
*/
function reviewQuestions() {
// Stop timer (if it's running)
if (timer != null) {
timer.stop();
}
const aid = exam_details.aid;
const cid = exam_details.cid;
const eid = exam_details.eid;
$("#review-overlay").show();
$("#review-answer-list").empty();
$("#review-answer-table").empty();
$("#review-score").empty();
$("#exam-stats").hide();
loadQuestion(0, 0, true);
db.answers
.where({
aid: aid,
cid: cid,
eid: eid
})
.toArray()
.then(function(answers) {
let current_answers = {};
answers.forEach(function(arr, n) {
let answer = arr["ans"];
if (answer == undefined) {
answer = "Not answered";
}
current_answers[[arr["qid"], arr["qidn"]]] = answer;
});
let correct_count = 0;
let not_answered = 0;
let answered_normal = 0;
let answered_abnormal = 0;
let undercall_number = 0;
let overcall_number = 0;
let incorrectcall_number = 0;
exam_details.question_order.forEach(async function(qid, n) {
const q_object = {
qid: String(qid),
type: question_type
};
let question = await question_db.question_data.get(q_object);
question = question.data;
if (question_type == "long") {
$("#review-answer-table").append(
$(
`
"
);
$(`#review-answer-list a[data-qid=${n}]`).click(function(evt) {
loadQuestion(this.dataset.qid);
$("#review-overlay").hide();
});
console.debug(db.user_answers)
console.debug({
type: question_type,
qid: qid
})
db.user_answers
.where({
type: question_type,
qid: qid
})
.toArray()
.then(function(answers) {
// Merge the question answers with the user saved answers
let question_answers = question["answers"];
if (answers != undefined) {
for (let i = 0; i < answers.length; i++) {
question_answers = question_answers.concat(answers[i].ans);
}
}
let section_1_answer = current_answers[[qid, "1"]];
let section_2_answer = current_answers[[qid, "2"]];
if (section_1_answer == undefined) {
section_1_answer = "Not Answered";
not_answered++;
}
if (section_2_answer == undefined) {
if (section_1_answer == "Normal") {
section_2_answer = "Normal";
answered_normal++;
} else {
section_2_answer = "Not Answered";
}
} else {
answered_abnormal++;
}
let el = $(`#review-answer-${qid} span`);
/**
* Helper function to define how review items are displayed
* Yes it is a bit shit
* @param {*} el - element
* @param {*} c - class
* @param {*} user_answer -
* @param {*} normal -
* @param {*} question_answers -
*/
function setReviewAnswer(
el,
c,
user_answer,
normal,
question_answers
) {
if (normal) {
el.html(
`Answer: ${user_answer} (Normal)`
);
} else {
el.html(
`Answer: ${user_answer} (Abnormal: ${question_answers.join(', ')})`
);
}
}
function setAnatomyReviewAnswer(
el,
c,
user_answer,
question_answers
) {
el.html(
`Answer: ${user_answer} (Correct answers: ${question_answers.join(', ')})`
)
}
if (question_type == "rapid") {
// First check normal vs abnormal
if (question["normal"] == true) {
if (section_1_answer == "Normal") {
setReviewAnswer(
el,
"correct",
section_1_answer,
true,
question_answers
);
correct_count++;
questions_correct[qid] = true;
} else {
if (section_1_answer != "Not Answered") {
overcall_number++;
}
setReviewAnswer(
el,
"incorrect",
section_1_answer,
true,
question_answers
);
questions_correct[qid] = false;
}
} else {
if (answerInArray(question_answers, section_2_answer)) {
correct_count++;
setReviewAnswer(
el,
"correct",
section_2_answer,
false,
question_answers
);
questions_correct[qid] = true;
} else {
setReviewAnswer(
el,
"incorrect",
section_2_answer,
false,
question_answers
);
questions_correct[qid] = false;
if (section_1_answer == "Not Answered") {} else if (section_1_answer == "Normal") {
undercall_number++;
} else {
// Incorrect calls could be correct if
// the answer is not in the database
incorrectcall_number++;
el.append(
"[Mark correct]"
).click(() => {
markCorrect(qid, section_2_answer, question_type);
reviewQuestions();
});
}
}
}
} else if (question_type == "anatomy") {
// Anatomy answers are either correct or incorrect
if (answerInArray(question_answers, section_1_answer)) {
correct_count++;
setAnatomyReviewAnswer(
el,
"correct",
section_1_answer,
question_answers
);
questions_correct[qid] = true;
} else {
setAnatomyReviewAnswer(
el,
"incorrect",
section_1_answer,
question_answers
);
questions_correct[qid] = false;
}
}
score = correct_count;
$("#review-score").text(
"Score: " +
correct_count +
" out of " +
exam_details.question_order.length
);
if (question_type == "rapid") {
$("#exam-stats").show();
$("#unanswered-number").html(not_answered);
$("#normal-number").html(answered_normal);
$("#abnormal-number").html(answered_abnormal);
$("#undercall-number").html(undercall_number);
$("#overcall-number").html(overcall_number);
$("#incorrectcall-number").html(incorrectcall_number);
} else {
$("#exam-stats").show();
}
})
.catch(function(error) {
console.debug("error-", error);
})
.finally(() => {
// This will lead to saveSession being called after each question is marked
saveSession();
rebuildQuestionListPanel();
$("#review-answer-list li")
.sort(function(a, b) {
return a.dataset.qid > b.dataset.qid ?
1 :
a.dataset.qid < b.dataset.qid ?
-1 :
0;
})
.appendTo("#review-answer-list");
});
});
})
.catch(function(error) {
console.debug("error - ", error);
});
}
/**
* Marks the loaded question and updates display
* @param {*} qid -
* @param {*} current_question -
*/
function markAnswer(qid, current_question, n) {
const type = current_question.type;
console.debug("Mark", current_question);
let option = null;
if (review_mode == true) {
// Disable all possible answer elements
$("#rapid-option").attr("disabled", "true");
$(".long-answer").attr("readonly", "true");
$(".long-answer").addClass("long-answer-marked");
if (type == "rapid") {
option = document.getElementById("rapid-option");
// If the current question is normal
if (current_question.normal == true) {
$(".answer-panel").append(
"
This is normal
"
);
if (option.value == "Normal") {
option.classList.add("correct");
} else {
option.classList.add("incorrect");
}
// If answer is normal we have nothing else to add.
addFeedback(current_question);
return;
}
}
if (type == "long") {
// For long cases we simple disable the texareas and append the
// model answers
let model_answers = current_question.answers[0];
// TODO: FIX THIS
if (model_answers == undefined) {
model_answers = current_question.answers;
}
for (let key of Object.keys(model_answers)) {
$("textarea[name*='" + key + "' i]").after(model_answers[key]);
}
return;
}
$(".answer-panel").append(
"
Correct answer(s):
"
);
let ul = $("#answer-list");
let textarea = $(".long-answer");
textarea.addClass("incorrect");
let user_answer = textarea.val();
// Load user saved answers
db.user_answers
.where({
type: type,
qid: qid
})
.toArray()
.then(function(saved_user_answers) {
//console.debug("saved user answers", saved_user_answers)
// check if given answer matches a user saved answer
if (saved_user_answers != undefined) {
//console.debug(saved_user_answers);
saved_user_answers.forEach(function(saved_answer_object, n) {
let saved_answer = saved_answer_object.ans;
//console.debug("ua", user_answer, saved_answer);
ul.append("
" + saved_answer + "
");
if (compareString(saved_answer, user_answer)) {
textarea.removeClass("incorrect").addClass("correct");
}
if (
saved_answer_object.submitted == undefined ||
saved_answer_object.submitted != true
) {
ul.append(
$("").click((e) => {
interact.postSavedAnswer(
type,
qid,
saved_answer,
e,
saved_answer_object,
db
);
})
);
}
});
}
// check if given answer matches an answer from the question
current_question.answers.forEach(function(answer, n) {
ul.append("
" + answer + "
");
if (compareString(answer, user_answer)) {
textarea.removeClass("incorrect").addClass("correct");
}
});
// If the given answer is not abnormal it must be incorrect
if (type == "rapid" && option.value != "Abnormal") {
option.classList.add("incorrect");
} else {
// If it is blank it must also be incorrect
if (
textarea.hasClass("incorrect") == true &&
user_answer.replace(/\s/g, "") != ""
) {
// Otherwise there is a chance that it may be correct
// so we give the option to mark it correct
if (allow_self_marking) {
$(".answer-panel").append(
$("").click(
function() {
markCorrect(qid, user_answer, type);
loadQuestion(n, 0, true);
}
)
);
}
}
}
})
.catch(function(error) {
console.debug("error-", error);
});
addFeedback(current_question, qid);
rebuildQuestionListPanel();
}
}
function addFeedback(current_question, qid) {
console.debug(current_question);
if (
current_question.hasOwnProperty("feedback") &&
current_question.feedback.length > 0
) {
$(".answer-panel").append(
$(
"
Feedback
" +
current_question.feedback +
"
"
)
);
}
// TODO: render in dicom viewer
if (
current_question.hasOwnProperty("feedback_images") &&
current_question.feedback_images.length > 0
) {
// TODO: finish (load in dicom viewer)
current_question.feedback_images.forEach((img) => {
$(".question").append($(``));
});
}
// Add link to submit question feedback
$(".answer-panel").append(
$(
`