from flask import Blueprint, render_template, request, redirect, jsonify, session, abort, flash from flask.helpers import url_for from datetime import datetime, timedelta from uuid import uuid4 import os from json import loads from flask_mail import Message from pymongo.collection import ReturnDocument from common.security import encrypt from common.data_tools import generate_questions, evaluate_answers from common.security.database import decrypt_find_one views = Blueprint( 'quiz_views', __name__, static_url_path='', template_folder='templates', static_folder='static' ) @views.route('/') @views.route('/home/') def home(): from main import db _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') @views.route('/instructions/') def instructions(): from main import db _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/instructions.html') @views.route('/start/', methods = ['GET', 'POST']) def start(): from main import db from .forms import StartQuiz form = StartQuiz() 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) if request.method == 'POST': if form.validate_on_submit(): name = { 'first_name': request.form.get('first_name'), 'surname': request.form.get('surname') } email = request.form.get('email') club = request.form.get('club') test_code = request.form.get('test_code').replace('—', '').upper() user_code = request.form.get('user_code') user_code = None if user_code == '' else user_code.upper() test = db.tests.find_one({'test_code': test_code}) if not test: return jsonify({'error': 'The exam code you entered is invalid.'}), 400 if user_code and user_code not in test['time_adjustments']: return jsonify({'error': f'The user code you entered is not valid.'}), 400 if test['expiry_date'] + timedelta(days=1) < datetime.utcnow(): return jsonify({'error': f'The exam code you entered expired on {test["expiry_date"].strftime("%d %b %Y")} UTC.'}), 400 if test['start_date'] > datetime.utcnow(): 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")} UTC.'}), 400 entry = { '_id': uuid4().hex, 'name': encrypt(name), 'email': encrypt(email), 'club': encrypt(club), 'test_code': test_code, 'user_code': user_code } if db.entries.insert_one(entry): session['_id'] = entry['_id'] return jsonify({ 'success': 'Received and validated test and/or user code. Redirecting to test client.', '_id': entry['_id'] }), 200 else: errors = [*form.errors] return jsonify({ 'error': errors}), 400 @views.route('/api/questions/', methods=['POST']) def fetch_questions(): from main import app, db _id = request.get_json()['_id'] entry = db.entries.find_one({'_id': _id}) if not entry: return jsonify({'error': 'The data that the client sent to the server is invalid. This is possibly because you have already submitted your exam and have tried to access the page again.'}), 400 test_code = entry['test_code'] user_code = entry['user_code'] test = db.tests.find_one({'test_code' : test_code}) time_limit = test['time_limit'] time_adjustment = 0 if time_limit: _time_limit = int(time_limit) if user_code: time_adjustment = test['time_adjustments'][user_code] _time_limit += time_adjustment 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) db.tests.find_one_and_update({'_id': test['_id']}, {'$push': {'entries': _id}}) dataset = test['dataset'] dataset_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], dataset) with open(dataset_path, 'r') as data_file: data = loads(data_file.read()) questions = generate_questions(data) return jsonify({ 'time_limit': end_time, 'questions': questions, 'start_time': entry['start_time'], 'time_adjustment': time_adjustment }) @views.route('/test/') def start_quiz(): from main import db _id = session.get('_id') if not _id or not db.entries.find_one({'_id': _id}): flash('Your log in was not recognised. Please sign in to the quiz again.', 'error') return redirect(url_for('quiz_views.start')) return render_template('quiz/client.html') @views.route('/api/submit/', methods=['POST']) def submit_quiz(): from main import app, db _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(): from main import db, mail _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_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] 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. """ if not entry['status'] == 'late': email = Message( subject="SKA Refereeing Theory Exam Results", recipients=[entry['email']], body=f"""SKA Refereeing Theory Exam\n\n Candidate Results\n\n Dear {entry['name']['first_name']},\n\n This email is to confirm that you have took the SKA Refereeing Theory Exam. Your test has been evaluated and your results have been generated.\n\n {entry['name']['surname']}, {entry['name']['first_name']}\n\n Email Address: {entry['email']}\n {f"Club: {entry['club']}" if entry['club'] else ''}\n Date of Test: {entry['submission_time'].strftime('%d %b %Y')}\n Score: {score}%\n Grade: {entry['results']['grade']}\n\n {flavour_text_plain}\n\n Based on your answers, we would also suggest you brush up on the following topics as you continue refereeing:\n\n {','.join(tag_output)}\n\n Thank you for taking the time to get qualified as a referee.\n\n Best wishes,\n SKA Refereeing """, html=f"""
Dear {entry['name']['first_name']},
This email is to confirm that you have took the SKA Refereeing Theory Exam. Your test has been evaluated and your results have been generated.
Email Address: {entry['email']}
{f"Club: {entry['club']}
" if entry['club'] else ''}Date of Test: {entry['submission_time'].strftime('%d %b %Y')}
{flavour_text_plain}
Based on your answers, we would also suggest you revise the following topics as you continue refereeing:
Thank you for taking the time to get qualified as a referee.
Best wishes,
SKA Refereeing