Finished client result API.
Need to work on adjustment user codes and server email notifications.
This commit is contained in:
parent
70f362015c
commit
795545e8af
@ -47,14 +47,8 @@ class UpdateAccountForm(FlaskForm):
|
|||||||
|
|
||||||
class CreateTest(FlaskForm):
|
class CreateTest(FlaskForm):
|
||||||
start_date = DateField('Start Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() )
|
start_date = DateField('Start Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() )
|
||||||
time_options = [
|
|
||||||
('none', 'None'),
|
|
||||||
('60', '1 hour'),
|
|
||||||
('90', '1 hour 30 minutes'),
|
|
||||||
('120', '2 hours')
|
|
||||||
]
|
|
||||||
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
||||||
time_limit = SelectField('Time Limit', choices=time_options)
|
time_limit = SelectField('Time Limit')
|
||||||
dataset = SelectField('Question Dataset')
|
dataset = SelectField('Question Dataset')
|
||||||
|
|
||||||
class UploadDataForm(FlaskForm):
|
class UploadDataForm(FlaskForm):
|
||||||
|
@ -14,7 +14,7 @@ class Test:
|
|||||||
self._id = _id
|
self._id = _id
|
||||||
self.start_date = start_date
|
self.start_date = start_date
|
||||||
self.expiry_date = expiry_date
|
self.expiry_date = expiry_date
|
||||||
self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit
|
self.time_limit = None if time_limit == 'none' or time_limit == '' else int(time_limit)
|
||||||
self.creator = creator
|
self.creator = creator
|
||||||
self.dataset = dataset
|
self.dataset = dataset
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ class Test:
|
|||||||
test['expiry_date'] = self.expiry_date
|
test['expiry_date'] = self.expiry_date
|
||||||
updated.append('expiry date')
|
updated.append('expiry date')
|
||||||
if not self.time_limit == '' and self.time_limit is not None:
|
if not self.time_limit == '' and self.time_limit is not None:
|
||||||
test['time_limit'] = self.time_limit
|
test['time_limit'] = int(self.time_limit)
|
||||||
updated.append('time limit')
|
updated.append('time limit')
|
||||||
output = ''
|
output = ''
|
||||||
if len(updated) == 0:
|
if len(updated) == 0:
|
||||||
|
@ -16,7 +16,7 @@ import secrets
|
|||||||
from main import mail
|
from main import mail
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from .models.tests import Test
|
from .models.tests import Test
|
||||||
from common.data_tools import get_default_dataset
|
from common.data_tools import get_default_dataset, get_time_options
|
||||||
|
|
||||||
views = Blueprint(
|
views = Blueprint(
|
||||||
'admin_views',
|
'admin_views',
|
||||||
@ -339,6 +339,7 @@ def tests(filter=''):
|
|||||||
if filter == 'create':
|
if filter == 'create':
|
||||||
from .models.forms import CreateTest
|
from .models.forms import CreateTest
|
||||||
form = CreateTest()
|
form = CreateTest()
|
||||||
|
form.time_limit.choices = get_time_options()
|
||||||
form.dataset.choices=available_datasets()
|
form.dataset.choices=available_datasets()
|
||||||
form.time_limit.default='none'
|
form.time_limit.default='none'
|
||||||
form.dataset.default=get_default_dataset()
|
form.dataset.default=get_default_dataset()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from json import dump, loads
|
from json import dump, loads
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask.json import jsonify
|
from flask.json import jsonify
|
||||||
from main import app
|
from main import app
|
||||||
@ -99,4 +99,93 @@ def generate_questions(dataset:dict):
|
|||||||
else:
|
else:
|
||||||
question['options'] = _question['options'].copy()
|
question['options'] = _question['options'].copy()
|
||||||
output.append(question)
|
output.append(question)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def evaluate_answers(dataset: dict, answers: dict):
|
||||||
|
score = 0
|
||||||
|
max = 0
|
||||||
|
tags = {}
|
||||||
|
for block in dataset['questions']:
|
||||||
|
if block['type'] == 'question':
|
||||||
|
max += 1
|
||||||
|
q_no = block['q_no']
|
||||||
|
if str(q_no) in answers:
|
||||||
|
correct = block['correct']
|
||||||
|
correct_answer = block['options'][correct]
|
||||||
|
if answers[str(q_no)] == correct_answer:
|
||||||
|
score += 1
|
||||||
|
for tag in block['tags']:
|
||||||
|
if tag not in tags:
|
||||||
|
tags[tag] = {
|
||||||
|
'scored': 1,
|
||||||
|
'max': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tags[tag]['scored'] += 1
|
||||||
|
tags[tag]['max'] += 1
|
||||||
|
else:
|
||||||
|
for tag in block['tags']:
|
||||||
|
if tag not in tags:
|
||||||
|
tags[tag] = {
|
||||||
|
'scored': 0,
|
||||||
|
'max': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tags[tag]['max'] += 1
|
||||||
|
if block['type'] == 'block':
|
||||||
|
for question in block['questions']:
|
||||||
|
max += 1
|
||||||
|
q_no = question['q_no']
|
||||||
|
if str(q_no) in answers:
|
||||||
|
correct = question['correct']
|
||||||
|
correct_answer = question['options'][correct]
|
||||||
|
if answers[str(q_no)] == correct_answer:
|
||||||
|
score += 1
|
||||||
|
for tag in question['tags']:
|
||||||
|
if tag not in tags:
|
||||||
|
tags[tag] = {
|
||||||
|
'scored': 1,
|
||||||
|
'max': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tags[tag]['scored'] += 1
|
||||||
|
tags[tag]['max'] += 1
|
||||||
|
else:
|
||||||
|
for tag in question['tags']:
|
||||||
|
if tag not in tags:
|
||||||
|
tags[tag] = {
|
||||||
|
'scored': 0,
|
||||||
|
'max': 1
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tags[tag]['max'] += 1
|
||||||
|
|
||||||
|
grade = 'merit' if score/max >= .85 else 'pass' if score/max >= .70 else 'fail'
|
||||||
|
return {
|
||||||
|
'grade': grade,
|
||||||
|
'tags': tags,
|
||||||
|
'score': score,
|
||||||
|
'max': max
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags_list(dataset:dict):
|
||||||
|
output = []
|
||||||
|
blocks = dataset['questions']
|
||||||
|
for block in blocks:
|
||||||
|
if block['type'] == 'question':
|
||||||
|
output = list(set(output) | set(block['tags']))
|
||||||
|
if block['type'] == 'block':
|
||||||
|
for question in block['questions']:
|
||||||
|
output = list(set(output) | set(question['tags']))
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_options():
|
||||||
|
time_options = [
|
||||||
|
('none', 'None'),
|
||||||
|
('60', '1 hour'),
|
||||||
|
('90', '1 hour 30 minutes'),
|
||||||
|
('120', '2 hours')
|
||||||
|
]
|
||||||
|
return time_options
|
@ -37,11 +37,22 @@ def encrypt(input):
|
|||||||
def decrypt(input):
|
def decrypt(input):
|
||||||
if not check_keyfile_exists():
|
if not check_keyfile_exists():
|
||||||
raise EncryptionKeyMissing
|
raise EncryptionKeyMissing
|
||||||
input = input.encode()
|
|
||||||
_encryption_key = load_key()
|
_encryption_key = load_key()
|
||||||
fernet = Fernet(_encryption_key)
|
fernet = Fernet(_encryption_key)
|
||||||
output = fernet.decrypt(input)
|
if type(input) == str:
|
||||||
return output.decode()
|
input = input.encode()
|
||||||
|
output = fernet.decrypt(input)
|
||||||
|
return output.decode()
|
||||||
|
if type(input) == dict:
|
||||||
|
output = {}
|
||||||
|
for key, value in input.items():
|
||||||
|
if type(value) == dict:
|
||||||
|
output[key] = decrypt(value)
|
||||||
|
else:
|
||||||
|
value = value.encode()
|
||||||
|
output[key] = fernet.decrypt(value)
|
||||||
|
output[key] = output[key].decode()
|
||||||
|
return output
|
||||||
|
|
||||||
class EncryptionKeyMissing(Exception):
|
class EncryptionKeyMissing(Exception):
|
||||||
def __init__(self, message='There is no encryption keyfile.'):
|
def __init__(self, message='There is no encryption keyfile.'):
|
||||||
|
@ -15,7 +15,7 @@ def decrypt_find(collection:collection, query:dict):
|
|||||||
if not query:
|
if not query:
|
||||||
output_list.append(decrypted_document)
|
output_list.append(decrypted_document)
|
||||||
else:
|
else:
|
||||||
if set(query.items()).issubset(set(decrypted_document.items())):
|
if query.items() <= decrypted_document.items():
|
||||||
output_list.append(decrypted_document)
|
output_list.append(decrypted_document)
|
||||||
return output_list
|
return output_list
|
||||||
|
|
||||||
|
@ -130,7 +130,20 @@
|
|||||||
|
|
||||||
.control-button-container {
|
.control-button-container {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: 0 auto;
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button-container a {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigator-help {
|
||||||
|
margin: 4rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigator-container {
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout for Mobile Devices */
|
/* Layout for Mobile Devices */
|
||||||
|
@ -4,6 +4,11 @@ body {
|
|||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#dismiss-cookie-alert {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
@ -133,6 +138,31 @@ body {
|
|||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results-name {
|
||||||
|
margin: 3rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-name .surname {
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-score {
|
||||||
|
margin: 2rem auto;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-score::after {
|
||||||
|
content: '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grade {
|
||||||
|
margin: 2rem auto;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 26pt;
|
||||||
|
}
|
||||||
|
|
||||||
/* Change Autocomplete styles in Chrome*/
|
/* Change Autocomplete styles in Chrome*/
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:hover,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Click Listeners
|
// Bind Listeners
|
||||||
|
|
||||||
$("input[name='font-select']").change(function(){
|
$("input[name='font-select']").change(function(){
|
||||||
let $choice = $(this).val();
|
let $choice = $(this).val();
|
||||||
@ -16,6 +16,8 @@ $("input[name='bg-select']").change(function(){
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#btn-toggle-navigator").click(function(event){
|
$("#btn-toggle-navigator").click(function(event){
|
||||||
|
check_answered();
|
||||||
|
update_navigator();
|
||||||
if ($quiz_navigator.is(":hidden")) {
|
if ($quiz_navigator.is(":hidden")) {
|
||||||
if ($quiz_settings.is(":visible")) {
|
if ($quiz_settings.is(":visible")) {
|
||||||
toggle_settings = true;
|
toggle_settings = true;
|
||||||
@ -23,6 +25,8 @@ $("#btn-toggle-navigator").click(function(event){
|
|||||||
}
|
}
|
||||||
$quiz_render.hide();
|
$quiz_render.hide();
|
||||||
$quiz_navigator.show();
|
$quiz_navigator.show();
|
||||||
|
$(".navigator-text").show();
|
||||||
|
$(".review-text").hide();
|
||||||
toggle_navigator = false;
|
toggle_navigator = false;
|
||||||
} else {
|
} else {
|
||||||
$quiz_navigator.hide();
|
$quiz_navigator.hide();
|
||||||
@ -57,12 +61,35 @@ $("#btn-toggle-settings").click(function(event){
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".btn-quiz-return").click(function(event){
|
||||||
|
$quiz_navigator.hide();
|
||||||
|
$quiz_settings.hide();
|
||||||
|
$quiz_render.show();
|
||||||
|
toggle_settings = false;
|
||||||
|
toggle_navigator = false;
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
$(".btn-dummy").click(function(event){
|
$(".btn-dummy").click(function(event){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#navigator-container").on("click", ".q-navigator-button", function(event){
|
||||||
|
check_answered();
|
||||||
|
update_navigator();
|
||||||
|
current_question = parseInt($(this).attr("name"));
|
||||||
|
$quiz_render.show();
|
||||||
|
$quiz_navigator.hide();
|
||||||
|
toggle_navigator = false;
|
||||||
|
toggle_settings = false;
|
||||||
|
render_question();
|
||||||
|
check_flag();
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
$(".q-question-nav").click(function(event){
|
$(".q-question-nav").click(function(event){
|
||||||
check_answered();
|
check_answered();
|
||||||
|
update_navigator();
|
||||||
if ($(this).attr("id") == "q-nav-next") {
|
if ($(this).attr("id") == "q-nav-next") {
|
||||||
if (current_question < questions.length) {
|
if (current_question < questions.length) {
|
||||||
current_question ++;
|
current_question ++;
|
||||||
@ -71,6 +98,12 @@ $(".q-question-nav").click(function(event){
|
|||||||
if (current_question > 0) {
|
if (current_question > 0) {
|
||||||
current_question --;
|
current_question --;
|
||||||
}
|
}
|
||||||
|
} else if ($(this).hasClass("q-navigator-button")) {
|
||||||
|
current_question = $(this).attr("name");
|
||||||
|
$quiz_render.show();
|
||||||
|
$quiz_navigator.hide();
|
||||||
|
toggle_navigator = false;
|
||||||
|
toggle_settings = false;
|
||||||
}
|
}
|
||||||
render_question();
|
render_question();
|
||||||
check_flag();
|
check_flag();
|
||||||
@ -81,10 +114,14 @@ $("#q-nav-flag").click(function(event){
|
|||||||
if (question_status[current_question] != 1) {
|
if (question_status[current_question] != 1) {
|
||||||
question_status[current_question] = 1;
|
question_status[current_question] = 1;
|
||||||
$(this).removeClass().addClass("btn btn-warning");
|
$(this).removeClass().addClass("btn btn-warning");
|
||||||
|
$(this).attr("title", "Question Flagged for revision. Click to un-flag.");
|
||||||
} else {
|
} else {
|
||||||
question_status[current_question] = 0;
|
question_status[current_question] = 0;
|
||||||
$(this).removeClass().addClass("btn btn-secondary");
|
$(this).removeClass().addClass("btn btn-secondary");
|
||||||
|
$(this).attr("title", "Question Un-Flagged. Click to flag for revision.");
|
||||||
}
|
}
|
||||||
|
window.localStorage.setItem('question_status', JSON.stringify(question_status));
|
||||||
|
update_navigator();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,12 +139,22 @@ $("#btn-start-quiz").click(function(event){
|
|||||||
data: JSON.stringify({'_id': _id}),
|
data: JSON.stringify({'_id': _id}),
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
time_limit_data = response.time_limit;
|
time_limit = response.time_limit;
|
||||||
|
start_time = response.start_time;
|
||||||
questions = response.questions;
|
questions = response.questions;
|
||||||
total_questions = questions.length;
|
total_questions = questions.length;
|
||||||
window.localStorage.setItem('questions', JSON.stringify(questions));
|
window.localStorage.setItem('questions', JSON.stringify(questions));
|
||||||
|
window.localStorage.setItem('start_time', JSON.stringify(start_time));
|
||||||
|
window.localStorage.setItem('time_limit', JSON.stringify(time_limit));
|
||||||
render_question();
|
render_question();
|
||||||
|
build_navigator();
|
||||||
check_flag();
|
check_flag();
|
||||||
|
if (time_limit != 'null') {
|
||||||
|
time_remaining = get_time_remaining();
|
||||||
|
clock = setInterval(timer, 1000);
|
||||||
|
} else {
|
||||||
|
$("#q-timer-widget").hide();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function(response) {
|
error: function(response) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
@ -117,6 +164,79 @@ $("#btn-start-quiz").click(function(event){
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#quiz-question-options").on("change", ".quiz-option", function(event){
|
||||||
|
$name = parseInt($(this).attr("name"));
|
||||||
|
$value = $(this).attr("value");
|
||||||
|
answers[$name] = $value;
|
||||||
|
window.localStorage.setItem('answers', JSON.stringify(answers));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#q-review-answers").click(function(event){
|
||||||
|
check_answered();
|
||||||
|
update_navigator();
|
||||||
|
if ($quiz_navigator.is(":hidden")) {
|
||||||
|
if ($quiz_settings.is(":visible")) {
|
||||||
|
toggle_settings = true;
|
||||||
|
$quiz_settings.hide();
|
||||||
|
}
|
||||||
|
$quiz_render.hide();
|
||||||
|
$quiz_navigator.show();
|
||||||
|
$(".navigator-text").hide();
|
||||||
|
$(".review-text").show();
|
||||||
|
toggle_navigator = false;
|
||||||
|
} else {
|
||||||
|
$quiz_navigator.hide();
|
||||||
|
if (toggle_settings) {
|
||||||
|
$quiz_settings.show();
|
||||||
|
toggle_settings = false;
|
||||||
|
} else {
|
||||||
|
$quiz_render.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".quiz-button-submit").click(function(event){
|
||||||
|
let submission = {
|
||||||
|
'_id': _id,
|
||||||
|
'answers': answers
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/submit/`,
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify(submission),
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function(response) {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.location.href = `/result/`;
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
|
|
||||||
function set_font(value = 'osdefault') {
|
function set_font(value = 'osdefault') {
|
||||||
@ -229,41 +349,204 @@ function render_question() {
|
|||||||
$question_text.html(question.text);
|
$question_text.html(question.text);
|
||||||
$question_title.html(`Question ${current_question + 1} of ${ questions.length }.`);
|
$question_title.html(`Question ${current_question + 1} of ${ questions.length }.`);
|
||||||
|
|
||||||
|
var q_no = question['q_no'];
|
||||||
var options = question.options;
|
var options = question.options;
|
||||||
var options_output = '';
|
var options_output = '';
|
||||||
for (let i = 0; i < options.length; i ++) {
|
for (let i = 0; i < options.length; i ++) {
|
||||||
|
var add_checked = ''
|
||||||
|
if (q_no in answers) {
|
||||||
|
if (answers[q_no] == options[i]) {
|
||||||
|
add_checked = 'checked';
|
||||||
|
}
|
||||||
|
}
|
||||||
options_output += `<div class="form-check">
|
options_output += `<div class="form-check">
|
||||||
<input type="radio" class="form-check-input quiz-option" id="q${current_question}-${i}" name="${question.q_no}" value="${options[i]}">
|
<input type="radio" class="form-check-input quiz-option" id="q${current_question}-${i}" name="${q_no}" value="${options[i]}" ${add_checked}>
|
||||||
<label for="q${current_question}-${i}" class="form-check-label">${options[i]}</label>
|
<label for="q${current_question}-${i}" class="form-check-label">${options[i]}</label>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
$question_options.html(options_output);
|
$question_options.html(options_output);
|
||||||
|
$question_title.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_answered() {
|
function check_answered() {
|
||||||
var question = questions[current_question];
|
var question = questions[current_question];
|
||||||
var name = question.q_no;
|
var name = question.q_no;
|
||||||
if (!$(`input[name='${name}']:checked`).val() && question_status[current_question] == 0) {
|
if (question_status[current_question] == 0) {
|
||||||
question_status[current_question] = -1;
|
if (!$(`input[name='${name}']:checked`).val()) {
|
||||||
|
question_status[current_question] = -1;
|
||||||
|
} else {
|
||||||
|
question_status[current_question] = 2;
|
||||||
|
}
|
||||||
|
window.localStorage.setItem('question_status', JSON.stringify(question_status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_flag() {
|
function check_flag() {
|
||||||
if (!(current_question in question_status)) {
|
if (!(current_question in question_status)) {
|
||||||
question_status[current_question] = 0;
|
question_status[current_question] = 0;
|
||||||
|
window.localStorage.setItem('question_status', JSON.stringify(question_status));
|
||||||
}
|
}
|
||||||
switch (question_status[current_question]) {
|
switch (question_status[current_question]) {
|
||||||
case -1:
|
case -1:
|
||||||
$nav_flag.removeClass().addClass('btn btn-danger');
|
$nav_flag.removeClass().addClass('btn btn-danger progress-bar-striped');
|
||||||
|
$nav_flag.attr("title", "Question Incomplete. Click to flag for revision.");
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
$nav_flag.removeClass().addClass('btn btn-warning');
|
$nav_flag.removeClass().addClass('btn btn-warning');
|
||||||
|
$nav_flag.attr("title", "Question Flagged for revision. Click to un-flag.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$nav_flag.removeClass().addClass('btn btn-success');
|
||||||
|
$nav_flag.attr("title", "Question Answered. Click to flag for revision.");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$nav_flag.removeClass().addClass('btn btn-secondary');
|
$nav_flag.removeClass().addClass('btn btn-secondary');
|
||||||
|
$nav_flag.attr("title", "Question Un-Flagged. Click to flag for revision.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function build_navigator() {
|
||||||
|
$nav_container.html('')
|
||||||
|
var output = ''
|
||||||
|
for (let i = 0; i < questions.length; i ++) {
|
||||||
|
let add_class, add_href, add_status = '';
|
||||||
|
switch (question_status[i]) {
|
||||||
|
case -1:
|
||||||
|
add_class = 'btn-danger progress-bar-striped';
|
||||||
|
add_href = 'href="#"';
|
||||||
|
add_status = 'Incomplete';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
add_class = 'btn-warning';
|
||||||
|
add_href = 'href="#"';
|
||||||
|
add_status = 'Flagged';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
add_class = 'btn-success';
|
||||||
|
add_href = 'href="#"';
|
||||||
|
add_status = 'Answered';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
add_class = 'btn-secondary disabled';
|
||||||
|
add_href = '';
|
||||||
|
add_status = 'Unseen';
|
||||||
|
}
|
||||||
|
output += `<a ${add_href} class="q-navigator-button btn ${add_class}" name=${i} title="Question ${i+1}: ${add_status}">Q${i + 1}</a>`;
|
||||||
|
}
|
||||||
|
$nav_container.html(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_navigator() {
|
||||||
|
let button = $(`.q-navigator-button[name=${current_question}]`)
|
||||||
|
if (current_question in question_status) {
|
||||||
|
switch (question_status[current_question]) {
|
||||||
|
case -1:
|
||||||
|
button.removeClass().addClass("q-navigator-button btn btn-danger progress-bar-striped");
|
||||||
|
button.attr("title", `Question ${current_question + 1}: Incomplete`);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
button.removeClass().addClass("q-navigator-button btn btn-warning");
|
||||||
|
button.attr("title", `Question ${current_question + 1}: Flagged`);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
button.removeClass().addClass("q-navigator-button btn btn-success");
|
||||||
|
button.attr("title", `Question ${current_question + 1}: Answered`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
button.removeClass().addClass("q-navigator-button btn btn-secondary disabled");
|
||||||
|
button.attr("title", `Question ${current_question + 1}: Unseen`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
$("#btn-start-quiz").hide();
|
||||||
|
$(".btn-quiz-return").show();
|
||||||
|
$(".quiz-console").show();
|
||||||
|
$("#quiz-settings").hide();
|
||||||
|
$("#quiz-navigator").hide();
|
||||||
|
$(".quiz-start-text").hide();
|
||||||
|
|
||||||
|
questions = JSON.parse(window.localStorage.getItem('questions'));
|
||||||
|
total_questions = questions.length;
|
||||||
|
start_time = window.localStorage.getItem('start_time');
|
||||||
|
time_limit = window.localStorage.getItem('time_limit');
|
||||||
|
|
||||||
|
let get_answers = window.localStorage.getItem('answers');
|
||||||
|
if (get_answers != null) {
|
||||||
|
answers = JSON.parse(get_answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
let get_status = window.localStorage.getItem('question_status');
|
||||||
|
if (get_status != null) {
|
||||||
|
question_status = JSON.parse(get_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_question();
|
||||||
|
build_navigator();
|
||||||
|
check_flag();
|
||||||
|
if (time_limit != 'null') {
|
||||||
|
time_remaining = get_time_remaining();
|
||||||
|
clock = setInterval(timer, 1000);
|
||||||
|
} else {
|
||||||
|
$("#q-timer-widget").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_started() {
|
||||||
|
let questions = window.localStorage.getItem('questions');
|
||||||
|
let time_limit = window.localStorage.getItem('time_limit');
|
||||||
|
let start_time = window.localStorage.getItem('start_time')
|
||||||
|
if (questions != null && start_time != null && time_limit != null) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_time_remaining() {
|
||||||
|
var end_time = new Date(time_limit).getTime();
|
||||||
|
var _start_time = new Date().getTime();
|
||||||
|
return end_time - _start_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timer() {
|
||||||
|
var hours = Math.floor((time_remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
var minutes = Math.floor((time_remaining % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
var seconds = Math.floor((time_remaining % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
if (time_remaining > 0) {
|
||||||
|
var timer_display = '';
|
||||||
|
if (hours > 0) {
|
||||||
|
timer_display = `${hours.toString()}: `;
|
||||||
|
}
|
||||||
|
if (minutes > 0 || hours > 0) {
|
||||||
|
if (minutes < 10) {
|
||||||
|
timer_display += `0${minutes.toString()}:`;
|
||||||
|
} else {
|
||||||
|
timer_display += `${minutes.toString()}:`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seconds < 10) {
|
||||||
|
timer_display += `0${seconds.toString()}`;
|
||||||
|
} else {
|
||||||
|
timer_display += seconds.toString();
|
||||||
|
}
|
||||||
|
$timer.html(timer_display);
|
||||||
|
time_remaining -= 1000
|
||||||
|
} else {
|
||||||
|
$timer.html('Expired');
|
||||||
|
clearInterval(clock);
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
$quiz_render.hide();
|
||||||
|
$quiz_navigator.hide();
|
||||||
|
$quiz_timeout.show();
|
||||||
|
$("#btn-toggle-navigator").addClass('disabled');
|
||||||
|
$("#btn-toggle-settings").addClass('disabled')
|
||||||
|
}
|
||||||
|
|
||||||
// Variable Definitions
|
// Variable Definitions
|
||||||
|
|
||||||
const _id = window.localStorage.getItem('_id');
|
const _id = window.localStorage.getItem('_id');
|
||||||
@ -272,16 +555,21 @@ var current_question = 0;
|
|||||||
var total_questions = 0;
|
var total_questions = 0;
|
||||||
var question_status = {};
|
var question_status = {};
|
||||||
var answers = {};
|
var answers = {};
|
||||||
var questions = []
|
var questions = [];
|
||||||
var time_limit_data = ''
|
var time_limit, start_time, time_remaining;
|
||||||
|
|
||||||
var display_settings = get_settings_from_storage();
|
var display_settings = get_settings_from_storage();
|
||||||
|
|
||||||
const $quiz_settings = $("#quiz-settings");
|
const $quiz_settings = $("#quiz-settings");
|
||||||
const $quiz_navigator = $("#quiz-navigator");
|
const $quiz_navigator = $("#quiz-navigator");
|
||||||
const $quiz_render = $("#quiz-render");
|
const $quiz_render = $("#quiz-render");
|
||||||
|
const $quiz_timeout = $("#quiz-timeout");
|
||||||
const $nav_flag = $("#q-nav-flag");
|
const $nav_flag = $("#q-nav-flag");
|
||||||
const $nav_next = $("#q-nav-next");
|
const $nav_next = $("#q-nav-next");
|
||||||
const $nav_prev = $("#q-nav-prev");
|
const $nav_prev = $("#q-nav-prev");
|
||||||
|
const $nav_container = $("#navigator-container");
|
||||||
|
const $timer = $("#q-timer-display");
|
||||||
|
var clock
|
||||||
|
|
||||||
var toggle_settings = false;
|
var toggle_settings = false;
|
||||||
var toggle_navigator = false;
|
var toggle_navigator = false;
|
||||||
@ -294,9 +582,7 @@ const $question_options = $("#quiz-question-options");
|
|||||||
// Execution on Load
|
// Execution on Load
|
||||||
|
|
||||||
apply_settings(display_settings);
|
apply_settings(display_settings);
|
||||||
|
check_started();
|
||||||
|
|
||||||
// TODO Build navigator
|
// TODO Timeout Function
|
||||||
// TODO Navigator Link button behaviour
|
// TODO Send data to server
|
||||||
// TODO Resume Exam button
|
|
||||||
// TODO Load state from storage
|
|
||||||
// TODO Answer Registry
|
|
||||||
|
@ -54,4 +54,26 @@ $('form[name=form-quiz-start]').submit(function(event) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dismiss Cookie Alert
|
||||||
|
$('#dismiss-cookie-alert').click(function(event){
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/cookies/',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
time: Date.now()
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response){
|
||||||
|
console.log(response);
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
})
|
@ -8,7 +8,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="container quiz-panel" id="quiz-settings">
|
<div class="container quiz-panel" id="quiz-settings">
|
||||||
<h1>Adjust Display Settings</h1>
|
<h1>Adjust Display Settings</h1>
|
||||||
<div class="container quiz-start-text">
|
<div class="container quiz-start-text">
|
||||||
@ -141,6 +140,10 @@
|
|||||||
<input type="radio" class="form-check-input" id="sample2" name="sample" value="2">
|
<input type="radio" class="form-check-input" id="sample2" name="sample" value="2">
|
||||||
<label for="sample2" class="form-check-label">The <i>Dungeons & Dragons Fifth Edition Monster Manual</i>.</label>
|
<label for="sample2" class="form-check-label">The <i>Dungeons & Dragons Fifth Edition Monster Manual</i>.</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="radio" class="form-check-input" id="sample3" name="sample" value="3" checked>
|
||||||
|
<label for="sample3" class="form-check-label">All of the above</i></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
@ -154,24 +157,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container quiz-panel" style="display: none;" id="quiz-navigator">
|
<div class="container quiz-panel" style="display: none;" id="quiz-navigator">
|
||||||
<h1>
|
<h1 class="navigator-text">
|
||||||
Navigator
|
Navigator
|
||||||
</h1>
|
</h1>
|
||||||
<div id="navigator-container">
|
<h1 class="review-text" style="display: none;">
|
||||||
<a href="#" class="q-navigator-button btn btn-success">Q666</a>
|
Review Your Answers
|
||||||
<a href="#" class="q-navigator-button btn btn-warning">Q666</a>
|
</h1>
|
||||||
<a href="#" class="q-navigator-button btn btn-secondary disabled">Q666</a>
|
<div class="navigator-text">
|
||||||
|
This is the exam navigator. It displays the progress you have on the exam so far. Each question is represented by an icon below, and you can click on each icon to skip to that question.
|
||||||
|
|
||||||
|
The icons below are colour-coded to represent the status of each question.
|
||||||
</div>
|
</div>
|
||||||
<div class="control-button-container">
|
<div class="review-text" style="display: none;">
|
||||||
|
You can use this panel to review your answers before you submit the exam. You will not be able to amend your answers after you submit.
|
||||||
|
|
||||||
|
Each question is represented by an icon below, and you can click on each icon to skip to that question. The icons below are colour-coded to represent the status of each question.
|
||||||
|
</div>
|
||||||
|
<table class="navigator-help">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="q-navigator-button btn btn-danger progress-bar-striped btn-dummy" title="Question: Incomplete">Q</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
A red and striped icon represents a question that you have skipped, and have not otherwise flagged for revision.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="q-navigator-button btn btn-warning btn-dummy" title="Question: Flagged">Q</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
A yellow icon represents a question that you have flagged for revision.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="q-navigator-button btn btn-success btn-dummy" title="Question: Answered">Q</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
A green icon represents a question that you have answered, and have not otherwise flagged for revision.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="q-navigator-button btn btn-secondary disabled btn-dummy" title="Question: Unseen">Q</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
A greyed-out icon represents a question that you have not yet seen.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div id="navigator-container">
|
||||||
|
</div>
|
||||||
|
<div class="control-button-container navigator-text">
|
||||||
<a href="#" class="btn btn-success btn-quiz-control btn-quiz-return">Resume Exam</a>
|
<a href="#" class="btn btn-success btn-quiz-control btn-quiz-return">Resume Exam</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-button-container review-text">
|
||||||
|
<a href="#" class="btn btn-danger btn-quiz-control btn-quiz-return">Back to Exam</a>
|
||||||
|
<a href="#" class="btn btn-success quiz-button-submit">Submit Exam</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container quiz-panel quiz-console" style="display: none" id="quiz-render">
|
<div class="container quiz-panel quiz-console" style="display: none" id="quiz-render">
|
||||||
<h1>
|
<h1>
|
||||||
Exam Console
|
Exam Console
|
||||||
</h1>
|
</h1>
|
||||||
<div class="container question-container">
|
<div class="container question-container">
|
||||||
<h4 class="question-title" id="quiz-question-title">
|
<h4 class="question-title" id="quiz-question-title" tabindex="-1">
|
||||||
Question x.
|
Question x.
|
||||||
</h4>
|
</h4>
|
||||||
<p class="question-header" id="quiz-question-header">
|
<p class="question-header" id="quiz-question-header">
|
||||||
@ -186,10 +237,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="control-button-container">
|
<div class="control-button-container">
|
||||||
<a href="#" class="btn btn-success q-question-nav" id="q-nav-prev" title="Previous Question"><i class="bi bi-caret-left-square-fill"></i> Back</a>
|
<a href="#" class="btn btn-success q-question-nav" id="q-nav-prev" title="Previous Question"><i class="bi bi-caret-left-square-fill"></i> Back</a>
|
||||||
<a href="#" class="btn btn-secondary" id="q-nav-flag" title="Flag Question"><i class="bi bi-flag-fill"></i></a>
|
<a href="#" class="btn btn-secondary" id="q-nav-flag" title="Question Un-Flagged. Click to flag for revision."><i class="bi bi-flag-fill"></i></a>
|
||||||
<a href="#" class="btn btn-success q-question-nav" id="q-nav-next" title="Next Question">Next <i class="bi bi-caret-right-square-fill"></i></a>
|
<a href="#" class="btn btn-success q-question-nav" id="q-nav-next" title="Next Question">Next <i class="bi bi-caret-right-square-fill"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-button-container">
|
||||||
|
<a href="#" class="btn btn-primary" id="q-review-answers" title="Submit Answers">Submit Answers</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container quiz-panel quiz-timeout" style="display: none;" id="quiz-timeout">
|
||||||
|
<h1>
|
||||||
|
Time Limit Expired
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
The time limit set for this exam has expired. You must submit your answers immediately.
|
||||||
|
</p>
|
||||||
|
<div class="control-button-container">
|
||||||
|
<a href="#" class="btn btn-success quiz-button-submit" title="Submit Exam">Submit Exam</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="alert-box"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script
|
<script
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="quiz-console w-100" style="display: none;" id="q-topbar">
|
<div class="quiz-console w-100" style="display: none;" id="q-topbar">
|
||||||
<div class="d-flex justify-content align-middle">
|
<div class="d-flex justify-content align-middle">
|
||||||
<div class="container d-flex justify-content-center">
|
<div class="container d-flex justify-content-center">
|
||||||
<span class="text-light q-timer" id="q-timer-widget"><i class="bi bi-stopwatch-fill"></i> <span id="q-timer-display">1:58:57</span></span>
|
<span class="text-light q-timer" id="q-timer-widget"><i class="bi bi-stopwatch-fill"></i> <span id="q-timer-display"></span></span>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-warning" aria-title="Navigate" title="Navigate" id="btn-toggle-navigator"><i class="bi bi-table"></i></a>
|
<a href="#" class="btn btn-warning" aria-title="Navigate" title="Navigate" id="btn-toggle-navigator"><i class="bi bi-table"></i></a>
|
||||||
<a href="#" class="btn btn-danger" aria-title="Settings" title="Settings" id="btn-toggle-settings"><i class="bi bi-gear-fill"></i></a>
|
<a href="#" class="btn btn-danger" aria-title="Settings" title="Settings" id="btn-toggle-settings"><i class="bi bi-gear-fill"></i></a>
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% set cookie_flash_flag = namespace(value=False) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
{% if category == "error" %}
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "success" %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "warning" %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "cookie_alert" %}
|
||||||
|
{% if not cookie_flash_flag.value %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
|
||||||
|
</div>
|
||||||
|
{% set cookie_flash_flag.value = True %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
44
ref-test/quiz/templates/quiz/result.html
Normal file
44
ref-test/quiz/templates/quiz/result.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "quiz/components/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>SKA Refereeing Theory Exam</h1>
|
||||||
|
|
||||||
|
<h2>Candidate Results</h2>
|
||||||
|
|
||||||
|
<h3 class="results-name">
|
||||||
|
<span class="surname">{{ entry.name.surname }}</span>, {{ entry.name.first_name }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<strong class="results-details">Email Address</strong>: {{ entry.email }}
|
||||||
|
|
||||||
|
{% if entry.club %}
|
||||||
|
<strong class="results-details">Club</strong>: {{ entry.club }}
|
||||||
|
{% endif%}
|
||||||
|
|
||||||
|
{% if entry.status == 'late' %}
|
||||||
|
|
||||||
|
Your results are invalid because you did not submit your exam in time. Please contact the SKA Refereeing Coordinator.
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="results-score">
|
||||||
|
{{ score }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results-grade">
|
||||||
|
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if entry.results.grade == 'fail' %}
|
||||||
|
Unfortunately, you have not passed the theory exam in this attempt. For your next attempt, it might help to revise the following topics:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for tag in show_low_tags %}
|
||||||
|
<li>{{ tag[0] }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
A copy of these results will be sent to you via email.
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -1,13 +1,16 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, jsonify, session, abort, flash
|
from flask import Blueprint, render_template, request, redirect, jsonify, session, abort, flash
|
||||||
from flask.helpers import url_for
|
from flask.helpers import url_for
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import os
|
import os
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
|
from pymongo.collection import ReturnDocument
|
||||||
|
|
||||||
from main import app, db
|
from main import app, db
|
||||||
from common.security import encrypt
|
from common.security import encrypt
|
||||||
from common.data_tools import generate_questions
|
from common.data_tools import generate_questions, evaluate_answers
|
||||||
|
from common.security.database import decrypt_find_one
|
||||||
|
|
||||||
views = Blueprint(
|
views = Blueprint(
|
||||||
'quiz_views',
|
'quiz_views',
|
||||||
@ -20,6 +23,9 @@ views = Blueprint(
|
|||||||
@views.route('/')
|
@views.route('/')
|
||||||
@views.route('/home/')
|
@views.route('/home/')
|
||||||
def home():
|
def home():
|
||||||
|
_id = session.get('_id')
|
||||||
|
if _id and db.entries.find_one({'_id': _id}):
|
||||||
|
return redirect(url_for('quiz_views.start_quiz'))
|
||||||
return render_template('/quiz/index.html')
|
return render_template('/quiz/index.html')
|
||||||
|
|
||||||
@views.route('/start/', methods = ['GET', 'POST'])
|
@views.route('/start/', methods = ['GET', 'POST'])
|
||||||
@ -27,6 +33,9 @@ def start():
|
|||||||
from .forms import StartQuiz
|
from .forms import StartQuiz
|
||||||
form = StartQuiz()
|
form = StartQuiz()
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
_id = session.get('_id')
|
||||||
|
if _id and db.entries.find_one({'_id': _id}):
|
||||||
|
return redirect(url_for('quiz_views.start_quiz'))
|
||||||
return render_template('/quiz/start-quiz.html', form=form)
|
return render_template('/quiz/start-quiz.html', form=form)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@ -47,12 +56,10 @@ def start():
|
|||||||
'email': encrypt(email),
|
'email': encrypt(email),
|
||||||
'club': encrypt(club),
|
'club': encrypt(club),
|
||||||
'test_code': test_code,
|
'test_code': test_code,
|
||||||
'user_code': user_code,
|
'user_code': user_code
|
||||||
# 'start_time': datetime.utcnow(), TODO move start time to after configuration.
|
|
||||||
# 'status': 'started'
|
|
||||||
}
|
}
|
||||||
if db.entries.insert(entry):
|
if db.entries.insert(entry):
|
||||||
session['_id'] = entry['_id'] # TODO Change this to return _id via JSON so client can access. Client will not be able to decrypt session cookie.
|
session['_id'] = entry['_id']
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': 'Received and validated test and/or user code. Redirecting to test client.',
|
'success': 'Received and validated test and/or user code. Redirecting to test client.',
|
||||||
'_id': entry['_id']
|
'_id': entry['_id']
|
||||||
@ -64,34 +71,86 @@ def start():
|
|||||||
@views.route('/api/questions/', methods=['POST'])
|
@views.route('/api/questions/', methods=['POST'])
|
||||||
def fetch_questions():
|
def fetch_questions():
|
||||||
_id = request.get_json()['_id']
|
_id = request.get_json()['_id']
|
||||||
entry = db.entries.find_one({'_id': _id})
|
entry = db.entries.find_one({'_id': _id})
|
||||||
if not entry:
|
if not entry:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
test_code = entry['test_code']
|
test_code = entry['test_code']
|
||||||
# user_code = entry['user_code'] TODO Implement functionality for adjustments
|
# user_code = entry['user_code'] TODO Implement functionality for adjustments
|
||||||
|
|
||||||
test = db.tests.find_one({'test_code' : test_code})
|
test = db.tests.find_one({'test_code' : test_code})
|
||||||
|
time_limit = test['time_limit']
|
||||||
|
if time_limit:
|
||||||
|
_time_limit = int(time_limit)
|
||||||
|
end_delta = timedelta(minutes=_time_limit)
|
||||||
|
end_time = datetime.utcnow() + end_delta
|
||||||
|
else:
|
||||||
|
end_time = None
|
||||||
|
update = {
|
||||||
|
'start_time': datetime.utcnow(),
|
||||||
|
'status': 'started',
|
||||||
|
'end_time': end_time
|
||||||
|
}
|
||||||
|
entry = db.entries.find_one_and_update({'_id': _id}, {'$set': update}, upsert=False, return_document=ReturnDocument.AFTER)
|
||||||
dataset = test['dataset']
|
dataset = test['dataset']
|
||||||
|
|
||||||
dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset)
|
dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset)
|
||||||
with open(dataset_path, 'r') as data_file:
|
with open(dataset_path, 'r') as data_file:
|
||||||
data = loads(data_file.read())
|
data = loads(data_file.read())
|
||||||
|
|
||||||
questions = generate_questions(data)
|
questions = generate_questions(data)
|
||||||
time_limit = test['time_limit']
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'time_limit': time_limit,
|
'time_limit': end_time,
|
||||||
'questions': questions
|
'questions': questions,
|
||||||
|
'start_time': entry['start_time']
|
||||||
})
|
})
|
||||||
|
|
||||||
@views.route('/test/')
|
@views.route('/test/')
|
||||||
def start_quiz():
|
def start_quiz():
|
||||||
_id = session.get('_id')
|
_id = session.get('_id')
|
||||||
if not _id or not db.entries.find_one({'_id': _id}):
|
if not _id or not db.entries.find_one({'_id': _id}):
|
||||||
|
print('Foo')
|
||||||
flash('Your log in was not recognised. Please sign in to the quiz again.', 'error')
|
flash('Your log in was not recognised. Please sign in to the quiz again.', 'error')
|
||||||
return redirect(url_for('quiz_views.start'))
|
return redirect(url_for('quiz_views.start'))
|
||||||
return render_template('quiz/client.html')
|
return render_template('quiz/client.html')
|
||||||
|
|
||||||
|
@views.route('/api/submit/', methods=['POST'])
|
||||||
|
def submit_quiz():
|
||||||
|
_id = request.get_json()['_id']
|
||||||
|
answers = request.get_json()['answers']
|
||||||
|
entry = db.entries.find_one({'_id': _id})
|
||||||
|
if not entry:
|
||||||
|
return jsonify('Unrecognised ID', 'error'), 400
|
||||||
|
status = 'submitted'
|
||||||
|
if entry['end_time']:
|
||||||
|
if datetime.utcnow() > entry['end_time'] + timedelta(minutes=2):
|
||||||
|
status = 'late'
|
||||||
|
test_code = entry['test_code']
|
||||||
|
test = db.tests.find_one({'test_code' : test_code})
|
||||||
|
dataset = test['dataset']
|
||||||
|
dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset)
|
||||||
|
with open(dataset_path, 'r') as _dataset:
|
||||||
|
data = loads(_dataset.read())
|
||||||
|
results = evaluate_answers(data, answers)
|
||||||
|
entry = db.entries.find_one_and_update({'_id': _id}, {'$set': {
|
||||||
|
'status': status,
|
||||||
|
'submission_time': datetime.utcnow(),
|
||||||
|
'results': results,
|
||||||
|
'answers': answers
|
||||||
|
}})
|
||||||
|
return jsonify({
|
||||||
|
'success': 'Your submission has been processed. Redirecting you to receive your results.',
|
||||||
|
'_id': _id
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
@views.route('/result/')
|
||||||
|
def result():
|
||||||
|
_id = session.get('_id')
|
||||||
|
entry = decrypt_find_one(db.entries, {'_id': _id})
|
||||||
|
if not entry:
|
||||||
|
return abort(404)
|
||||||
|
score = round(100*entry['results']['score']/entry['results']['max'])
|
||||||
|
tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in entry['results']['tags'].items() }
|
||||||
|
sorted_low = sorted(tags_low.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
show_low_tags = sorted_low[0:3]
|
||||||
|
return render_template('/quiz/result.html', entry=entry, score=score, show_low_tags=show_low_tags)
|
||||||
|
|
||||||
@views.route('/privacy/')
|
@views.route('/privacy/')
|
||||||
def privacy():
|
def privacy():
|
||||||
|
Loading…
Reference in New Issue
Block a user