diff --git a/ref-test/admin/models/forms.py b/ref-test/admin/models/forms.py index ee05b74..9ca6e1e 100644 --- a/ref-test/admin/models/forms.py +++ b/ref-test/admin/models/forms.py @@ -47,14 +47,8 @@ class UpdateAccountForm(FlaskForm): class CreateTest(FlaskForm): 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) ) - time_limit = SelectField('Time Limit', choices=time_options) + time_limit = SelectField('Time Limit') dataset = SelectField('Question Dataset') class UploadDataForm(FlaskForm): diff --git a/ref-test/admin/models/tests.py b/ref-test/admin/models/tests.py index a0e44cd..2ba8573 100644 --- a/ref-test/admin/models/tests.py +++ b/ref-test/admin/models/tests.py @@ -14,7 +14,7 @@ class Test: self._id = _id self.start_date = start_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.dataset = dataset @@ -88,7 +88,7 @@ class Test: test['expiry_date'] = self.expiry_date updated.append('expiry date') 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') output = '' if len(updated) == 0: diff --git a/ref-test/admin/views.py b/ref-test/admin/views.py index 5ff95cf..6159f7d 100644 --- a/ref-test/admin/views.py +++ b/ref-test/admin/views.py @@ -16,7 +16,7 @@ import secrets from main import mail from datetime import datetime, date, timedelta 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( 'admin_views', @@ -339,6 +339,7 @@ def tests(filter=''): if filter == 'create': from .models.forms import CreateTest form = CreateTest() + form.time_limit.choices = get_time_options() form.dataset.choices=available_datasets() form.time_limit.default='none' form.dataset.default=get_default_dataset() diff --git a/ref-test/common/data_tools.py b/ref-test/common/data_tools.py index 1497b51..fdf2d71 100644 --- a/ref-test/common/data_tools.py +++ b/ref-test/common/data_tools.py @@ -1,7 +1,7 @@ import os import pathlib from json import dump, loads -from datetime import datetime +from datetime import datetime, timedelta from flask.json import jsonify from main import app @@ -99,4 +99,93 @@ def generate_questions(dataset:dict): else: question['options'] = _question['options'].copy() output.append(question) - return output \ No newline at end of file + 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 \ No newline at end of file diff --git a/ref-test/common/security/__init__.py b/ref-test/common/security/__init__.py index 0272741..e553347 100644 --- a/ref-test/common/security/__init__.py +++ b/ref-test/common/security/__init__.py @@ -37,11 +37,22 @@ def encrypt(input): def decrypt(input): if not check_keyfile_exists(): raise EncryptionKeyMissing - input = input.encode() _encryption_key = load_key() fernet = Fernet(_encryption_key) - output = fernet.decrypt(input) - return output.decode() + if type(input) == str: + 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): def __init__(self, message='There is no encryption keyfile.'): diff --git a/ref-test/common/security/database.py b/ref-test/common/security/database.py index 9adb51e..74a41c3 100644 --- a/ref-test/common/security/database.py +++ b/ref-test/common/security/database.py @@ -15,7 +15,7 @@ def decrypt_find(collection:collection, query:dict): if not query: output_list.append(decrypted_document) else: - if set(query.items()).issubset(set(decrypted_document.items())): + if query.items() <= decrypted_document.items(): output_list.append(decrypted_document) return output_list diff --git a/ref-test/quiz/static/css/quiz.css b/ref-test/quiz/static/css/quiz.css index e416f9b..ef96a56 100644 --- a/ref-test/quiz/static/css/quiz.css +++ b/ref-test/quiz/static/css/quiz.css @@ -130,7 +130,20 @@ .control-button-container { 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 */ diff --git a/ref-test/quiz/static/css/style.css b/ref-test/quiz/static/css/style.css index 9cfdbb6..b955c42 100644 --- a/ref-test/quiz/static/css/style.css +++ b/ref-test/quiz/static/css/style.css @@ -4,6 +4,11 @@ body { font-size: 14pt; } +#dismiss-cookie-alert { + margin-top: 16px; + width: 100%; +} + .site-footer { background-color: lightgray; font-size: small; @@ -133,6 +138,31 @@ body { 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*/ input:-webkit-autofill, input:-webkit-autofill:hover, diff --git a/ref-test/quiz/static/js/quiz.js b/ref-test/quiz/static/js/quiz.js index 73070c9..b71ed0e 100644 --- a/ref-test/quiz/static/js/quiz.js +++ b/ref-test/quiz/static/js/quiz.js @@ -1,4 +1,4 @@ -// Click Listeners +// Bind Listeners $("input[name='font-select']").change(function(){ let $choice = $(this).val(); @@ -16,6 +16,8 @@ $("input[name='bg-select']").change(function(){ }); $("#btn-toggle-navigator").click(function(event){ + check_answered(); + update_navigator(); if ($quiz_navigator.is(":hidden")) { if ($quiz_settings.is(":visible")) { toggle_settings = true; @@ -23,6 +25,8 @@ $("#btn-toggle-navigator").click(function(event){ } $quiz_render.hide(); $quiz_navigator.show(); + $(".navigator-text").show(); + $(".review-text").hide(); toggle_navigator = false; } else { $quiz_navigator.hide(); @@ -57,12 +61,35 @@ $("#btn-toggle-settings").click(function(event){ 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){ 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){ check_answered(); + update_navigator(); if ($(this).attr("id") == "q-nav-next") { if (current_question < questions.length) { current_question ++; @@ -71,6 +98,12 @@ $(".q-question-nav").click(function(event){ if (current_question > 0) { 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(); check_flag(); @@ -81,10 +114,14 @@ $("#q-nav-flag").click(function(event){ if (question_status[current_question] != 1) { question_status[current_question] = 1; $(this).removeClass().addClass("btn btn-warning"); + $(this).attr("title", "Question Flagged for revision. Click to un-flag."); } else { question_status[current_question] = 0; $(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(); }); @@ -102,12 +139,22 @@ $("#btn-start-quiz").click(function(event){ data: JSON.stringify({'_id': _id}), contentType: "application/json", success: function(response) { - time_limit_data = response.time_limit; + time_limit = response.time_limit; + start_time = response.start_time; questions = response.questions; total_questions = questions.length; 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(); + build_navigator(); check_flag(); + if (time_limit != 'null') { + time_remaining = get_time_remaining(); + clock = setInterval(timer, 1000); + } else { + $("#q-timer-widget").hide(); + } }, error: function(response) { console.log(response); @@ -117,6 +164,79 @@ $("#btn-start-quiz").click(function(event){ 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 + ` +