viveksantayana
1f8e4508c3
Moved most of app definitions out of guard function to use wsgi Updated configuration files and referencing of .env values. Local version needs dotenv or exporting of env variables. Dockerised version works fine without load_dotenv. Ready to test now!
233 lines
10 KiB
Python
233 lines
10 KiB
Python
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('—', '')
|
|
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(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"""<h1>SKA Refereeing Theory Exam</h1>
|
|
<h2>Candidate Results</h2>
|
|
<p>Dear {entry['name']['first_name']},</p>
|
|
<p>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.</p>
|
|
<h3>{entry['name']['surname']}, {entry['name']['first_name']}</h3>
|
|
<p><strong>Email Address</strong>: {entry['email']}</p>
|
|
{f"<p><strong>Club</strong>: {entry['club']}</p>" if entry['club'] else ''}
|
|
<p><strong>Date of Test</strong>: {entry['submission_time'].strftime('%d %b %Y')}</p>
|
|
<h1>{score}%</h1>
|
|
<h2>{entry['results']['grade']}</h2>
|
|
<p>{flavour_text_plain}</p>
|
|
<p>Based on your answers, we would also suggest you revise the following topics as you continue refereeing:</p>
|
|
<ul>
|
|
<li>{'</li><li>'.join(tag_output)}</li>
|
|
</ul>
|
|
<p>Thank you for taking the time to get qualified as a referee.</p>
|
|
<p>Best wishes,<br />
|
|
SKA Refereeing</p>
|
|
""",
|
|
)
|
|
mail.send(email)
|
|
return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output)
|
|
|
|
@views.route('/privacy/')
|
|
def privacy():
|
|
return render_template('/quiz/privacy.html') |