From b9d45f94fe9eaea3b9b2f71edd4a2214dfeeb156 Mon Sep 17 00:00:00 2001 From: viveksantayana Date: Thu, 16 Jun 2022 01:03:06 +0100 Subject: [PATCH] Finished Quiz Console --- ref-test/app/api/views.py | 4 +- ref-test/app/forms/quiz.py | 11 +++ ref-test/app/models/entry.py | 3 + ref-test/app/quiz/static/js/script.js | 2 +- ref-test/app/quiz/templates/quiz/result.html | 6 +- .../quiz/{start-quiz.html => start_quiz.html} | 0 ref-test/app/quiz/views.py | 80 +++++++++++++++++-- ref-test/app/tools/test.py | 17 +++- 8 files changed, 110 insertions(+), 13 deletions(-) rename ref-test/app/quiz/templates/quiz/{start-quiz.html => start_quiz.html} (100%) diff --git a/ref-test/app/api/views.py b/ref-test/app/api/views.py index 546a329..d806562 100644 --- a/ref-test/app/api/views.py +++ b/ref-test/app/api/views.py @@ -49,8 +49,8 @@ def _submit_quiz(): id = request.get_json()['id'] answers = request.get_json()['answers'] entry = Entry.query.filter_by(id=id).first() - if not entry: return jsonify({'error': 'Unrecognised ID.'}), 400 - test = entry['test'] + if not entry: return jsonify({'error': 'Unrecognised Entry.'}), 400 + test = entry.test dataset = test.dataset success, message = dataset.check_file() if not success: return jsonify({'error': message}), 500 diff --git a/ref-test/app/forms/quiz.py b/ref-test/app/forms/quiz.py index e69de29..bda67bd 100644 --- a/ref-test/app/forms/quiz.py +++ b/ref-test/app/forms/quiz.py @@ -0,0 +1,11 @@ +from flask_wtf import FlaskForm +from wtforms import StringField +from wtforms.validators import InputRequired, Length, Email, Optional + +class StartQuiz(FlaskForm): + first_name = StringField('First Name(s)', validators=[InputRequired(), Length(max=30)]) + surname = StringField('Surname', validators=[InputRequired(), Length(max=30)]) + email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)]) + club = StringField('Affiliated Club (Optional)', validators=[Optional(), Length(max=50)]) + test_code = StringField('Exam Code', validators=[InputRequired(), Length(min=14, max=14)]) + user_code = StringField('User Code (Optional)', validators=[Optional(), Length(min=6, max=6)]) \ No newline at end of file diff --git a/ref-test/app/models/entry.py b/ref-test/app/models/entry.py index 5a9132c..80b9d55 100644 --- a/ref-test/app/models/entry.py +++ b/ref-test/app/models/entry.py @@ -67,10 +67,12 @@ class Entry(db.Model): def get_club(self): return decrypt(self.club) def start(self): + self.generate_id() self.start_time = datetime.now() self.status = 'started' write('tests.log', f'New test started by {self.get_first_name()} {self.get_surname()}.') db.session.commit() + return True, f'New test started with id {self.id}.' def complete(self, answers:dict=None, result:dict=None): self.end_time = datetime.now() @@ -84,6 +86,7 @@ class Entry(db.Model): self.status = 'late' self.valid = False db.session.commit() + return True, f'Test entry completed for id {self.id}.' def validate(self): if self.valid: return False, f'The entry is already valid.' diff --git a/ref-test/app/quiz/static/js/script.js b/ref-test/app/quiz/static/js/script.js index 468866d..e5f0ed1 100644 --- a/ref-test/app/quiz/static/js/script.js +++ b/ref-test/app/quiz/static/js/script.js @@ -25,7 +25,7 @@ $('form[name=form-quiz-start]').submit(function(event) { success: function(response) { var id = response.id window.localStorage.setItem('id', id); - window.location.href = `/test/`; + window.location.href = `/quiz/`; }, error: function(response) { error_response(response); diff --git a/ref-test/app/quiz/templates/quiz/result.html b/ref-test/app/quiz/templates/quiz/result.html index 2603d6b..b358070 100644 --- a/ref-test/app/quiz/templates/quiz/result.html +++ b/ref-test/app/quiz/templates/quiz/result.html @@ -6,13 +6,13 @@

Candidate Results

- {{ entry.name.surname }}, {{ entry.name.first_name }} + {{ entry.get_surname() }}, {{ entry.get_first_name() }}

- Email Address: {{ entry.email }}
+ Email Address: {{ entry.get_email() }}
{% if entry.club %} - Club: {{ entry.club }}
+ Club: {{ entry.get_club() }}
{% endif%} {% if entry.status == 'late' %} diff --git a/ref-test/app/quiz/templates/quiz/start-quiz.html b/ref-test/app/quiz/templates/quiz/start_quiz.html similarity index 100% rename from ref-test/app/quiz/templates/quiz/start-quiz.html rename to ref-test/app/quiz/templates/quiz/start_quiz.html diff --git a/ref-test/app/quiz/views.py b/ref-test/app/quiz/views.py index be97555..86d8b4c 100644 --- a/ref-test/app/quiz/views.py +++ b/ref-test/app/quiz/views.py @@ -1,4 +1,11 @@ -from flask import Blueprint +from ..forms.quiz import StartQuiz +from ..models import Entry, Test +from ..tools.test import redirect_if_started + +from flask import abort, Blueprint, jsonify, redirect, render_template, request, session +from flask.helpers import flash, url_for + +from datetime import datetime quiz = Blueprint( name='quiz', @@ -9,21 +16,82 @@ quiz = Blueprint( @quiz.route('/') @quiz.route('/home/') +@redirect_if_started def _home(): - return 'Quiz Home Page' + return render_template('/quiz/index.html') @quiz.route('/instructions/') def _instructions(): - return 'Instructions' + return render_template('/quiz/instructions.html') @quiz.route('/start/') def _start(): - return 'Start Quiz' + form = StartQuiz() + if request.method == 'POST': + if form.validate_on_submit(): + entry = Entry() + entry.set_first_name(request.form.get('first_name')) + entry.set_surname(request.form.get('surname')) + entry.set_club(request.form.get('club')) + code = request.form.get('test_code').replace('—', '').lower() + test = Test.query.filter_by(code=code) + entry.test = test + entry.user_code = request.form.get('user_code') + entry.user_code = None if entry.user_code == '' else entry.user_code.lower() + if not test: return jsonify({'error': 'The exam code you entered is invalid.'}), 400 + if entry.user_code and entry.user_code not in test.adjustments: return jsonify({'error': f'The user code you entered is not valid.'}), 400 + if test.end_date < datetime.now(): return jsonify({'error': f'The exam code you entered expired on {test["expiry_date"].strftime("%d %b %Y %H:%M")}.'}), 400 + if test.start_date > datetime.now(): return jsonify({'error': f'The exam has not yet opened. Your exam code will be valid from {test["start_date"].strftime("%d %b %Y %H:%M")}.'}), 400 + success, message = entry.start() + if success: + session['id'] = entry.id + return jsonify({ + 'success': 'Received and validated test and/or user code. Redirecting to test client.', + 'id': entry.id + }), 200 + return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400 + errors = [*form.test_code.errors, *form.user_code.errors, *form.first_name.errors, *form.surname.errors, *form.email.errors, *form.club.errors] + return jsonify({ 'error': errors}), 400 + render_template('/quiz/start_quiz.html', form = form) @quiz.route('/quiz/') def _quiz(): - return 'Quiz Console' + id = session.get('id') + if not id or not Entry.query.filter_by(id=id).first(): + flash('Your session was not recognised. Please sign in to the quiz again.', 'error') + session.pop('id') + return redirect(url_for('quiz_views.start')) + return render_template('/quiz/client.html') @quiz.route('/result/') def _result(): - return 'Quiz Result' \ No newline at end of file + id = session.get('id') + entry = Entry.query.filter_by('id').first() + 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_tags = sorted(tags_low.items(), key=lambda x: x[1], reverse=True) + tag_output = [ tag[0] for tag in sorted_low_tags[0:3] if tag[1] > 3] + revision_plain = '' + revision_html = '' + if entry.results['grade'] == 'pass': + flavour_text_plain = """Well done on successfully completing the refereeing theory exam. We really appreciate members of our community taking the time to get qualified to referee. + """ + elif entry.results['grade'] == 'merit': + flavour_text_plain = """Congratulations on achieving a merit in the refereeing exam. We are delighted that members of our community work so hard with refereeing. + """ + elif entry.results['grade'] == 'fail': + flavour_text_plain = """Unfortunately, you were not successful in passing the theory exam in this attempt. We hope that this does not dissuade you, and that you try again in the future. + """ + revision_plain = f"""Based on your answers, we would also suggest you brush up on the following topics for your next attempt:\n\n + {','.join(tag_output)}\n\n + """ + revision_html = f"""

Based on your answers, we would also suggest you brush up on the following topics for your next attempt:

+ + """ + if not entry['status'] == 'late': + pass + return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output) \ No newline at end of file diff --git a/ref-test/app/tools/test.py b/ref-test/app/tools/test.py index 469d445..6981f6d 100644 --- a/ref-test/app/tools/test.py +++ b/ref-test/app/tools/test.py @@ -1,4 +1,10 @@ from .data import randomise_list +from ..models import Entry + +from flask import redirect, request, session +from flask.helpers import url_for + +from functools import wraps def parse_test_code(code): return code.replace('—', '').lower() @@ -101,4 +107,13 @@ def get_correct_answers(dataset:list): if block['type'] == 'block': for question in block['questions']: output[str(question['q_no'])] = question['options'][question['correct']] - return output \ No newline at end of file + return output + +def redirect_if_started(function): + @wraps(function) + def wrapper(*args, **kwargs): + id = session.get('id') + if request.method == 'GET' and id and Entry.query.filter_by(id=id).first(): + return redirect(url_for('quiz._quiz')) + return function(*args, **kwargs) + return wrapper