Finished quiz and debugging

This commit is contained in:
Vivek Santayana 2022-06-16 10:44:48 +01:00
parent b9d45f94fe
commit 4b08c830a1
15 changed files with 148 additions and 82 deletions

View File

@ -15,30 +15,30 @@
<h5 class="mb-1">Candidate</h5> <h5 class="mb-1">Candidate</h5>
</div> </div>
<h2> <h2>
{{ entry.name.surname}}, {{ entry.name.first_name }} {{ entry.get_surname()}}, {{ entry.get_first_name() }}
</h2> </h2>
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Email Address</h5> <h5 class="mb-1">Email Address</h5>
</div> </div>
{{ entry.email }} {{ entry.get_email() }}
</li> </li>
{% if entry['club'] %} {% if entry.club %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Club</h5> <h5 class="mb-1">Club</h5>
</div> </div>
{{ entry.club }} {{ entry.get_club() }}
</li> </li>
{% endif %} {% endif %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Exam Code</h5> <h5 class="mb-1">Exam Code</h5>
</div> </div>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }} {{ entry.test.get_code() }}
</li> </li>
{% if entry['user_code'] %} {% if entry.user_code %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">User Code</h5> <h5 class="mb-1">User Code</h5>
@ -59,19 +59,19 @@
<span class="badge bg-danger">Late</span> <span class="badge bg-danger">Late</span>
{% endif %} {% endif %}
</div> </div>
{{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }} {{ entry.end_time.strftime('%d %b %Y %H:%M:%S') }}
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Score</h5> <h5 class="mb-1">Score</h5>
</div> </div>
{{ entry.results.score }}&percnt; {{ entry.result.score }}&percnt;
</li> </li>
<li class="list-group-item list-group-item-action {% if entry.results.grade == 'fail' %}list-group-item-danger {% elif entry.results.grade == 'merit' %} list-group-item-success {% endif %}"> <li class="list-group-item list-group-item-action {% if entry.result.grade == 'fail' %}list-group-item-danger {% elif entry.result.grade == 'merit' %} list-group-item-success {% endif %}">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Grade</h5> <h5 class="mb-1">Grade</h5>
</div> </div>
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:]}} {{ entry.result.grade[0]|upper }}{{ entry.result.grade[1:]}}
</li> </li>
</ul> </ul>
<div class="site-footer mt-5"> <div class="site-footer mt-5">

View File

@ -23,8 +23,37 @@
<li class="nav-item" id="nav-results"> <li class="nav-item" id="nav-results">
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a> <a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
</li> </li>
<li class="nav-item" id="nav-tests"> <li class="nav-item dropdown" id="nav-tests">
<a href="{{ url_for('admin._tests') }}" id="link-tests" class="nav-link">Manage Exams</a> <a
class="nav-link dropdown-toggle"
id="dropdown-tests"
role="button"
href="{{ url_for('admin._tests') }}"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Exams
</a>
<ul
class="dropdown-menu"
aria-labelledby="dropdown-settings"
>
<li>
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
</li>
<li>
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
</li>
</ul>
</li> </li>
<li class="nav-item dropdown" id="nav-settings"> <li class="nav-item dropdown" id="nav-settings">
<a <a
@ -42,7 +71,7 @@
aria-labelledby="dropdown-settings" aria-labelledby="dropdown-settings"
> >
<li> <li>
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">Settings</a> <a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
</li> </li>
<li> <li>
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a> <a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>

View File

@ -1,7 +1,7 @@
<div class="navbar navbar-expand-sm navbar-light bg-light"> <div class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid"> <div class="container-fluid">
<div class="expand navbar-expand justify-content-center" id="navbar_secondary"> <div class="expand navbar-expand justify-content-center" id="navbar_secondary">
<ul class="nav"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a> <a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
</li> </li>

View File

@ -69,13 +69,13 @@
{% for result in recent_results %} {% for result in recent_results %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('admin._view_entry', id=result.id) }}">{{ result.name.surname }}, {{ result.name.first_name }}</a> <a href="{{ url_for('admin._view_entry', id=result.id) }}">{{ result.get_surname() }}, {{ result.get_first_name() }}</a>
</td> </td>
<td> <td>
{{ result.submission_time.strftime('%d %b %Y %H:%M') }} {{ result.end_time.strftime('%d %b %Y %H:%M') }}
</td> </td>
<td> <td>
{{ result.percent }}&percnt; ({{ result.results.grade }}) {{ (100*result.result['score']/result.result['max'])|round|int }}&percnt; ({{ result.result.grade }})
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -13,30 +13,30 @@
<h5 class="mb-1">Candidate</h5> <h5 class="mb-1">Candidate</h5>
</div> </div>
<h2> <h2>
{{ entry.name.surname }}, {{ entry.name.first_name }} {{ entry.get_surname() }}, {{ entry.get_first_name() }}
</h2> </h2>
</li> </li>
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Email Address</h5> <h5 class="mb-1">Email Address</h5>
</div> </div>
{{ entry.email }} {{ entry.get_email() }}
</li> </li>
{% if entry['club'] %} {% if entry.club %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Club</h5> <h5 class="mb-1">Club</h5>
</div> </div>
{{ entry.club }} {{ entry.get_club() }}
</li> </li>
{% endif %} {% endif %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Exam Code</h5> <h5 class="mb-1">Exam Code</h5>
</div> </div>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }} {{ entry.test.get_code() }}
</li> </li>
{% if entry['user_code'] %} {% if entry.user_code %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">User Code</h5> <h5 class="mb-1">User Code</h5>
@ -44,7 +44,7 @@
{{ entry.user_code }} {{ entry.user_code }}
</li> </li>
{% endif %} {% endif %}
{% if 'start_time' in entry %} {% if entry.start_time %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Start Time</h5> <h5 class="mb-1">Start Time</h5>
@ -59,28 +59,28 @@
<span class="badge bg-danger">Late</span> <span class="badge bg-danger">Late</span>
{% endif %} {% endif %}
</div> </div>
{% if 'submission_time' in entry %} {% if entry.end_time %}
{{ entry.submission_time.strftime('%d %b %Y %H:%M:%S') }} {{ entry.end_time.strftime('%d %b %Y %H:%M:%S') }}
{% else %} {% else %}
Incomplete Incomplete
{% endif %} {% endif %}
</li> </li>
{% if 'results' in entry %} {% if entry.result %}
<li class="list-group-item list-group-item-action"> <li class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Score</h5> <h5 class="mb-1">Score</h5>
</div> </div>
{{ entry.results.score }}&percnt; {{ entry.result.score }}&percnt;
</li> </li>
<li class="list-group-item list-group-item-action {% if entry.results.grade == 'fail' %}list-group-item-danger {% elif entry.results.grade == 'merit' %} list-group-item-success {% endif %}"> <li class="list-group-item list-group-item-action {% if entry.result.grade == 'fail' %}list-group-item-danger {% elif entry.result.grade == 'merit' %} list-group-item-success {% endif %}">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Grade</h5> <h5 class="mb-1">Grade</h5>
</div> </div>
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:]}} {{ entry.result.grade[0]|upper }}{{ entry.result.grade[1:]}}
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
{% if 'results' in entry %} {% if entry.result %}
<div class="accordion" id="results-breakdown"> <div class="accordion" id="results-breakdown">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="by-category"> <h2 class="accordion-header" id="by-category">
@ -105,7 +105,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for tag, scores in entry.results.tags.items() %} {% for tag, scores in entry.result.tags.items() %}
<tr> <tr>
<td> <td>
{{ tag }} {{ tag }}
@ -149,8 +149,8 @@
{{ question }} {{ question }}
</td> </td>
<td> <td>
{{ answer }} {{ answers[question|int][answer|int] }}
{% if not correct[question] == answer %} {% if not correct[question] == answer|int %}
<span class="badge badge-pill bg-danger badge-danger">Incorrect</span> <span class="badge badge-pill bg-danger badge-danger">Incorrect</span>
{% endif %} {% endif %}
</td> </td>

View File

@ -37,34 +37,34 @@
{% for entry in entries %} {% for entry in entries %}
<tr class="table-row"> <tr class="table-row">
<td> <td>
{{ entry.name.surname }}, {{ entry.name.first_name }} {{ entry.get_surname() }}, {{ entry.get_first_name() }}
</td> </td>
<td> <td>
{% if 'club' in entry %} {% if entry.club %}
{{ entry.club }} {{ entry.get_club() }}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ '—'.join([entry.test_code[:4], entry.test_code[4:8], entry.test_code[8:]]) }} {{ entry.test.get_code() }}
</td> </td>
<td> <td>
{% if 'status' in entry %} {% if entry.status %}
{{ entry.status }} {{ entry.status }}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if 'submission_time' in entry %} {% if entry.end_time %}
{{ entry.submission_time.strftime('%d %b %Y') }} {{ entry.end_time.strftime('%d %b %Y') }}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if 'results' in entry %} {% if entry.result %}
{{ entry.results.score }}&percnt; {{ entry.result.score }}&percnt;
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if 'results' in entry %} {% if entry.result %}
{{ entry.results.grade }} {{ entry.result.grade }}
{% endif %} {% endif %}
</td> </td>
<td class="row-actions"> <td class="row-actions">

View File

@ -3,7 +3,7 @@ from ..models import Dataset, Entry, Test, User
from ..tools.auth import disable_if_logged_in, require_account_creation from ..tools.auth import disable_if_logged_in, require_account_creation
from ..tools.forms import get_dataset_choices, get_time_options from ..tools.forms import get_dataset_choices, get_time_options
from ..tools.data import check_is_json, validate_json from ..tools.data import check_is_json, validate_json
from ..tools.test import get_correct_answers from ..tools.test import answer_options, get_correct_answers
from flask import Blueprint, jsonify, render_template, redirect, request, session from flask import Blueprint, jsonify, render_template, redirect, request, session
from flask.helpers import flash, url_for from flask.helpers import flash, url_for
@ -33,8 +33,6 @@ def _home():
upcoming_tests.sort(key= lambda x: x.start_date) upcoming_tests.sort(key= lambda x: x.start_date)
recent_results = [result for result in results if not result.status == 'started' ] recent_results = [result for result in results if not result.status == 'started' ]
recent_results.sort(key= lambda x: x.end_time, reverse=True) recent_results.sort(key= lambda x: x.end_time, reverse=True)
for result in recent_results:
result['percent'] = round(100*result['result']['score']/result['result']['max'])
return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results) return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results)
@admin.route('/settings/') @admin.route('/settings/')
@ -255,7 +253,7 @@ def _tests(filter:str=None):
if not datasets: if not datasets:
flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error') flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error')
return redirect(url_for('admin._questions')) return redirect(url_for('admin._questions'))
if filter not in [None, '', 'create','active','scheduled','expired','all']: return redirect(url_for('admin._tests')) if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active'))
if filter == 'create': if filter == 'create':
form = CreateTest() form = CreateTest()
form.time_limit.choices = get_time_options() form.time_limit.choices = get_time_options()
@ -337,7 +335,7 @@ def _view_test(id:str=None):
return jsonify({'error': form.time.errors }), 400 return jsonify({'error': form.time.errors }), 400
if not test: if not test:
flash('Invalid test ID.', 'error') flash('Invalid test ID.', 'error')
return redirect(url_for('admin._tests')) return redirect(url_for('admin._tests', filter='active'))
return render_template('/admin/test.html', test = test, form = form) return render_template('/admin/test.html', test = test, form = form)
@admin.route('/test/<string:id>/delete-adjustment/', methods=['POST']) @admin.route('/test/<string:id>/delete-adjustment/', methods=['POST'])
@ -359,7 +357,7 @@ def _view_entries():
@admin.route('/results/<string:id>/', methods = ['GET', 'POST']) @admin.route('/results/<string:id>/', methods = ['GET', 'POST'])
@login_required @login_required
def _view_entry(id:str=None): def _view_entry(id:str=None):
entry = Entry.query.filter_by(id=id) entry = Entry.query.filter_by(id=id).first()
if request.method == 'POST': if request.method == 'POST':
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404 if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
action = request.get_json()['action'] action = request.get_json()['action']
@ -375,13 +373,14 @@ def _view_entry(id:str=None):
if not entry: if not entry:
flash('Invalid entry ID.', 'error') flash('Invalid entry ID.', 'error')
return redirect(url_for('admin._view_entries')) return redirect(url_for('admin._view_entries'))
test = entry['test'] test = entry.test
dataset = test.dataset dataset = test.dataset
dataset_path = dataset.get_file() dataset_path = dataset.get_file()
with open(dataset_path, 'r') as _dataset: with open(dataset_path, 'r') as _dataset:
data = loads(_dataset.read()) data = loads(_dataset.read())
correct = get_correct_answers(dataset=data) correct = get_correct_answers(dataset=data)
return render_template('/admin/result-detail.html', entry = entry, correct = correct) answers = answer_options(dataset=data)
return render_template('/admin/result-detail.html', entry = entry, correct = correct, answers = answers)
@admin.route('/certificate/',methods=['POST']) @admin.route('/certificate/',methods=['POST'])
@login_required @login_required

View File

@ -23,7 +23,7 @@ def _fetch_questions():
if time_limit: if time_limit:
_time_limit = int(time_limit) _time_limit = int(time_limit)
if user_code: if user_code:
time_adjustment = test.time_adjustments[user_code] time_adjustment = test.adjustments[user_code]
_time_limit += time_adjustment _time_limit += time_adjustment
end_delta = timedelta(minutes=_time_limit) end_delta = timedelta(minutes=_time_limit)
end_time = datetime.utcnow() + end_delta end_time = datetime.utcnow() + end_delta
@ -40,7 +40,7 @@ def _fetch_questions():
return jsonify({ return jsonify({
'time_limit': end_time, 'time_limit': end_time,
'questions': questions, 'questions': questions,
'start_time': entry['start_time'], 'start_time': entry.start_time,
'time_adjustment': time_adjustment 'time_adjustment': time_adjustment
}), 200 }), 200

View File

@ -75,6 +75,7 @@ class Dataset(db.Model):
filename = secure_filename('.'.join([self.id,'json'])) filename = secure_filename('.'.join([self.id,'json']))
file_path = path.join(data, 'questions', filename) file_path = path.join(data, 'questions', filename)
if not path.isfile(file_path): return False, 'Data file is missing.' if not path.isfile(file_path): return False, 'Data file is missing.'
return True, 'Data file found.'
def get_file(self): def get_file(self):
filename = secure_filename('.'.join([self.id,'json'])) filename = secure_filename('.'.join([self.id,'json']))

View File

@ -18,7 +18,7 @@ class Entry(db.Model):
club = db.Column(db.String(128), nullable=True) club = db.Column(db.String(128), nullable=True)
test_id = db.Column(db.String(36), db.ForeignKey('test.id')) test_id = db.Column(db.String(36), db.ForeignKey('test.id'))
user_code = db.Column(db.String(6), nullable=True) user_code = db.Column(db.String(6), nullable=True)
start_time = db.Column(db.DateTime, nullable=False) start_time = db.Column(db.DateTime, nullable=True)
end_time = db.Column(db.DateTime, nullable=True) end_time = db.Column(db.DateTime, nullable=True)
status = db.Column(db.String(16), nullable=True) status = db.Column(db.String(16), nullable=True)
valid = db.Column(db.Boolean, default=True, nullable=True) valid = db.Column(db.Boolean, default=True, nullable=True)
@ -66,17 +66,24 @@ class Entry(db.Model):
def get_club(self): return decrypt(self.club) def get_club(self): return decrypt(self.club)
def start(self): def ready(self):
self.generate_id() self.generate_id()
db.session.add(self)
db.session.commit()
write('tests.log', f'New test ready for {self.get_first_name()} {self.get_surname()}.')
return True, f'Test ready.'
def start(self):
self.start_time = datetime.now() self.start_time = datetime.now()
self.status = 'started' self.status = 'started'
write('tests.log', f'New test started by {self.get_first_name()} {self.get_surname()}.') write('tests.log', f'Test started by {self.get_first_name()} {self.get_surname()}.')
db.session.commit() db.session.commit()
return True, f'New test started with id {self.id}.' return True, f'New test started with id {self.id}.'
def complete(self, answers:dict=None, result:dict=None): def complete(self, answers:dict=None, result:dict=None):
self.end_time = datetime.now() self.end_time = datetime.now()
self.answers = answers self.answers = answers
self.result = result
write('tests.log', f'Test completed by {self.get_first_name()} {self.get_surname()}.') write('tests.log', f'Test completed by {self.get_first_name()} {self.get_surname()}.')
delta = timedelta(minutes=self.test.time_limit+1) delta = timedelta(minutes=self.test.time_limit+1)
if not self.test.time_limit or self.end_time <= self.start_time + delta: if not self.test.time_limit or self.end_time <= self.start_time + delta:

View File

@ -13,7 +13,7 @@
The presentation of the questions is just a start, and we acknowledge we still have a long way to go. We welcome any feedback on how we can further improve this test. The presentation of the questions is just a start, and we acknowledge we still have a long way to go. We welcome any feedback on how we can further improve this test.
</p> </p>
<div class="button-container"> <div class="button-container">
<a href="{{ url_for('quiz_views.instructions') }}" class="btn btn-success"> <a href="{{ url_for('quiz._instructions') }}" class="btn btn-success">
<i class="bi bi-book-fill button-icon"></i> <i class="bi bi-book-fill button-icon"></i>
Read the Instructions Read the Instructions
</a> </a>

View File

@ -53,7 +53,7 @@
</p> </p>
</div> </div>
<div class="button-container"> <div class="button-container">
<a href="{{ url_for('quiz_views.start') }}" class="btn btn-success"> <a href="{{ url_for('quiz._start') }}" class="btn btn-success">
<i class="bi bi-pencil-fill button-icon"></i> <i class="bi bi-pencil-fill button-icon"></i>
Take the Exam Take the Exam
</a> </a>

View File

@ -26,10 +26,10 @@
</div> </div>
<div class="results-grade"> <div class="results-grade">
{{ entry.results.grade[0]|upper }}{{ entry.results.grade[1:] }} {{ entry.result.grade[0]|upper }}{{ entry.result.grade[1:] }}
</div> </div>
{% if entry.results.grade == 'fail' %} {% if entry.result.grade == 'fail' %}
Unfortunately, you have not passed the theory exam in this attempt. For your next attempt, it might help to brush up on the following topics: Unfortunately, you have not passed the theory exam in this attempt. For your next attempt, it might help to brush up on the following topics:
<ul> <ul>

View File

@ -11,7 +11,8 @@ quiz = Blueprint(
name='quiz', name='quiz',
import_name=__name__, import_name=__name__,
template_folder='templates', template_folder='templates',
static_folder='static' static_folder='static',
static_url_path='/quiz/static'
) )
@quiz.route('/') @quiz.route('/')
@ -24,7 +25,7 @@ def _home():
def _instructions(): def _instructions():
return render_template('/quiz/instructions.html') return render_template('/quiz/instructions.html')
@quiz.route('/start/') @quiz.route('/start/', methods=['GET', 'POST'])
def _start(): def _start():
form = StartQuiz() form = StartQuiz()
if request.method == 'POST': if request.method == 'POST':
@ -33,8 +34,9 @@ def _start():
entry.set_first_name(request.form.get('first_name')) entry.set_first_name(request.form.get('first_name'))
entry.set_surname(request.form.get('surname')) entry.set_surname(request.form.get('surname'))
entry.set_club(request.form.get('club')) entry.set_club(request.form.get('club'))
entry.set_email(request.form.get('email'))
code = request.form.get('test_code').replace('', '').lower() code = request.form.get('test_code').replace('', '').lower()
test = Test.query.filter_by(code=code) test = Test.query.filter_by(code=code).first()
entry.test = test entry.test = test
entry.user_code = request.form.get('user_code') entry.user_code = request.form.get('user_code')
entry.user_code = None if entry.user_code == '' else entry.user_code.lower() entry.user_code = None if entry.user_code == '' else entry.user_code.lower()
@ -42,7 +44,7 @@ def _start():
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 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.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 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() success, message = entry.ready()
if success: if success:
session['id'] = entry.id session['id'] = entry.id
return jsonify({ return jsonify({
@ -52,36 +54,36 @@ def _start():
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400 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] 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 return jsonify({ 'error': errors}), 400
render_template('/quiz/start_quiz.html', form = form) return render_template('/quiz/start_quiz.html', form = form)
@quiz.route('/quiz/') @quiz.route('/quiz/')
def _quiz(): def _quiz():
id = session.get('id') id = session.get('id')
if not id or not Entry.query.filter_by(id=id).first(): 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') flash('Your session was not recognised. Please sign in to the quiz again.', 'error')
session.pop('id') session.pop('id', None)
return redirect(url_for('quiz_views.start')) return redirect(url_for('quiz._start'))
return render_template('/quiz/client.html') return render_template('/quiz/client.html')
@quiz.route('/result/') @quiz.route('/result/')
def _result(): def _result():
id = session.get('id') id = session.get('id')
entry = Entry.query.filter_by('id').first() entry = Entry.query.filter_by(id=id).first()
if not entry: if not entry: return abort(404)
return abort(404) session.pop('id',None)
score = round(100*entry.results['score']/entry.results['max']) score = round(100*entry.result['score']/entry.result['max'])
tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in entry.results['tags'].items() } tags_low = { tag: tag_result['max'] - tag_result['scored'] for tag, tag_result in entry.result['tags'].items() }
sorted_low_tags = sorted(tags_low.items(), key=lambda x: x[1], reverse=True) 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] tag_output = [ tag[0] for tag in sorted_low_tags[0:3] if tag[1] > 3]
revision_plain = '' revision_plain = ''
revision_html = '' revision_html = ''
if entry.results['grade'] == 'pass': if entry.result['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. 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': elif entry.result['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. 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': elif entry.result['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. 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 revision_plain = f"""Based on your answers, we would also suggest you brush up on the following topics for your next attempt:\n\n
@ -92,6 +94,6 @@ def _result():
<li>{'</li><li>'.join(tag_output)}</li> <li>{'</li><li>'.join(tag_output)}</li>
</ul> </ul>
""" """
if not entry['status'] == 'late': if not entry.status == 'late':
pass pass
return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output) return render_template('/quiz/result.html', entry=entry, score=score, tag_output=tag_output)

View File

@ -20,7 +20,7 @@ def generate_questions(dataset:list):
'text': block['text'] 'text': block['text']
} }
if block['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(block['options'])]) if block['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(block['options'])])
else: question['options'] = block['options'].copy() else: question['options'] = [*enumerate(block['options'])]
output.append(question) output.append(question)
elif block['type'] == 'block': elif block['type'] == 'block':
for key, _question in enumerate(randomise_list(block['questions'])): for key, _question in enumerate(randomise_list(block['questions'])):
@ -33,7 +33,7 @@ def generate_questions(dataset:list):
'text': _question['text'] 'text': _question['text']
} }
if _question['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(_question['options'])]) if _question['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(_question['options'])])
else: question['options'] = _question['options'].copy() else: question['options'] = [*enumerate(_question['options'])]
output.append(question) output.append(question)
return output return output
@ -66,6 +66,14 @@ def evaluate_answers(answers:dict, key:list):
'max': 1 'max': 1
} }
else: tags[tag]['max'] += 1 else: 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
elif block['type'] == 'block': elif block['type'] == 'block':
for question in block['questions']: for question in block['questions']:
max += 1 max += 1
@ -91,6 +99,14 @@ def evaluate_answers(answers:dict, key:list):
'max': 1 'max': 1
} }
else: tags[tag]['max'] += 1 else: 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' grade = 'merit' if score/max >= .85 else 'pass' if score/max >= .70 else 'fail'
return { return {
'grade': grade, 'grade': grade,
@ -103,10 +119,10 @@ def get_correct_answers(dataset:list):
output = {} output = {}
for block in dataset: for block in dataset:
if block['type'] == 'question': if block['type'] == 'question':
output[str(block['q_no'])] = block['options'][block['correct']] output[str(block['q_no'])] = block['correct']
if block['type'] == 'block': if block['type'] == 'block':
for question in block['questions']: for question in block['questions']:
output[str(question['q_no'])] = question['options'][question['correct']] output[str(question['q_no'])] = question['correct']
return output return output
def redirect_if_started(function): def redirect_if_started(function):
@ -117,3 +133,15 @@ def redirect_if_started(function):
return redirect(url_for('quiz._quiz')) return redirect(url_for('quiz._quiz'))
return function(*args, **kwargs) return function(*args, **kwargs)
return wrapper return wrapper
def answer_options(dataset:list):
output = []
for block in dataset:
if block['type'] == 'question':
question = block['options'].copy()
output.append(question)
elif block['type'] == 'block':
for _question in block['questions']:
question = _question['options'].copy()
output.append(question)
return output