20 Commits

Author SHA1 Message Date
2bed84e60b Merge branch 'development' 2025-10-06 16:15:20 +01:00
25fb3fdda4 Update python version number 2025-10-06 16:14:04 +01:00
fa104d7d1b Updated for before_first_request decorator deprecation 2025-10-06 16:11:57 +01:00
cb747b4832 Removed deprecated cryptography method 2025-10-06 16:11:07 +01:00
845fdcdf8d Updated dependencies 2025-10-06 16:09:57 +01:00
7c4a844101 Merge branch 'development' 2025-10-06 15:15:02 +01:00
54e1653cb5 Fixed typo 2025-10-06 15:14:07 +01:00
2235539314 Merge branch 'development' 2025-10-06 14:52:36 +01:00
716206dc65 Disable randomisation 2025-10-06 14:50:18 +01:00
a8eda4078d Added Barrowland Bears Korfball Club to list of clubs dropdown 2025-10-06 14:50:00 +01:00
502e694a17 Merge branch 'development' 2023-10-20 20:59:35 +01:00
d28cd6daed Updated instructions for the test 2023-10-20 20:58:50 +01:00
58782f6db7 Merge branch 'development' 2023-07-01 21:49:21 +01:00
57b25cd214 Formatted DataTable date to ISO-8601 for sorting 2023-07-01 21:48:36 +01:00
666e12253e Merge branch 'development' 2023-07-01 21:33:04 +01:00
8013a776a9 Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into development 2023-07-01 21:26:24 +01:00
aa1f46ee62 Bugfix: response to invalid exam codes 2023-07-01 21:24:29 +01:00
dbd8d6bbe3 Serve static files for analysis directly 2023-03-07 11:38:04 +00:00
fed46eaa1e Bugfix: stdev exception when only one test 2023-03-07 11:21:41 +00:00
79ad96a93f Added additional statistics and improvements to UI 2023-03-05 03:05:42 +00:00
13 changed files with 194 additions and 138 deletions

View File

@@ -17,6 +17,7 @@ services:
- ./ref-test/app/editor/static:/usr/share/nginx/html/editor/static:ro - ./ref-test/app/editor/static:/usr/share/nginx/html/editor/static:ro
- ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static:ro - ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static:ro
- ./ref-test/app/view/static:/usr/share/nginx/html/view/static:ro - ./ref-test/app/view/static:/usr/share/nginx/html/view/static:ro
- ./ref-test/app/analysis/static:/usr/share/nginx/html/analysis/static:ro
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443

View File

@@ -45,6 +45,11 @@ server {
alias /usr/share/nginx/html/view/static/; alias /usr/share/nginx/html/view/static/;
} }
location ^~ /admin/analysis/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/analysis/static/;
}
# Proxy to the main app for all other requests # Proxy to the main app for all other requests
location / { location / {
include /etc/nginx/conf.d/proxy_headers.conf; include /etc/nginx/conf.d/proxy_headers.conf;

View File

@@ -1,4 +1,4 @@
FROM python:3.10-slim FROM python:3.13-slim
ARG DATA=./data/ ARG DATA=./data/
ENV DATA=$DATA ENV DATA=$DATA
WORKDIR /ref-test WORKDIR /ref-test

View File

@@ -61,9 +61,7 @@ def create_app():
app.register_blueprint(view, url_prefix='/admin/view') app.register_blueprint(view, url_prefix='/admin/view')
app.register_blueprint(analysis, url_prefix='/admin/analysis') app.register_blueprint(analysis, url_prefix='/admin/analysis')
"""Create Database Tables before First Request""" """Create Database Tables when creating app"""
@app.before_first_request
def _create_database_tables():
with app.app_context(): with app.app_context():
db.create_all() db.create_all()

View File

@@ -54,7 +54,7 @@
</td> </td>
<td> <td>
{% if entry.end_time %} {% if entry.end_time %}
{{ entry.end_time.strftime('%d %b %Y') }} {{ entry.end_time.strftime('%Y-%m-%d %H:%M') }}
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@@ -43,7 +43,7 @@
{{ element.get_name() }} {{ element.get_name() }}
</td> </td>
<td> <td>
{{ element.date.strftime('%d %b %Y %H:%M') }} {{ element.date.strftime('%Y-%m-%d %H:%M') }}
</td> </td>
<td> <td>
{{ element.creator.get_username() }} {{ element.creator.get_username() }}

View File

@@ -33,13 +33,13 @@
{% for test in tests %} {% for test in tests %}
<tr class="table-row"> <tr class="table-row">
<td> <td>
{{ test.start_date.strftime('%d %b %y %H:%M') }} {{ test.start_date.strftime('%Y-%m-%d %H:%M') }}
</td> </td>
<td> <td>
{{ test.get_code() }} {{ test.get_code() }}
</td> </td>
<td> <td>
{{ test.end_date.strftime('%d %b %Y %H:%M') }} {{ test.end_date.strftime('%Y-%m-%d %H:%M') }}
</td> </td>
<td> <td>
{% if test.time_limit == None -%} {% if test.time_limit == None -%}

View File

@@ -8,44 +8,73 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Analysis</h1> <h1>Analysis by {{ type[0]|upper }}{{ type[1:] }}</h1>
<div class="container"> <div class="container">
<p class="lead"> <p class="lead">
Analysis for {{ type }} {{ subject }}. The analysis section displays statistics for all test results as well as answers to individual questions.
Analysis reports can be generated per exam or per question dataset to identify common mistakes or patterns in answers.
</p> </p>
<div class="input-group mb-3">
<span class="input-group-text">
{% if type == 'exam' %}
Exam Code
{% elif type == 'dataset' %}
Dataset Name
{% endif %}
</span>
<span class="form-control">
{{ subject }}
</span>
</div> </div>
<div class="container"> <div class="input-group mb-3">
<h3> <span class="input-group-text">Total Entries</span>
Question List <span class="form-control">
</h3> {{ analysis.entries }}
<div class="container dataset-metadata"> </span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Passed</span>
<span class="form-control">
{{ analysis.grades.merit + analysis.grades.pass }} ({{ ((analysis.grades.merit + analysis.grades.pass)*100/analysis.entries)|round(2) }} &percnt;)
</span>
</div>
<div class="mb-3">
<span class="badge rounded-pill progress-bar-striped bg-success">Merit: {{ analysis.grades.merit }}</span> <span class="badge rounded-pill bg-primary progress-bar-striped">Pass: {{ analysis.grades.pass }}</span> <span class="badge rounded-pill progress-bar-striped bg-danger">Fail: {{ analysis.grades.fail }}</span>
<div class="my-1 progress">
<div class="progress-bar progress-bar-striped bg-success" role="progressbar" style="width: {{ (analysis.grades.merit*100/analysis.entries)|round(2) }}%" aria-valuenow="{{ analysis.grades.merit }}" aria-valuemin="0" aria-valuemax="{{ analysis.entries }}">{{ (analysis.grades.merit*100/analysis.entries)|round(2) }} &percnt;</div>
<div class="progress-bar progress-bar-striped" role="progressbar" style="width: {{ (analysis.grades.pass*100/analysis.entries)|round(2) }}%" aria-valuenow="{{ analysis.grades.pass }}" aria-valuemin="0" aria-valuemax="{{ analysis.entries }}">{{ (analysis.grades.pass*100/analysis.entries)|round(2) }} &percnt;</div>
<div class="progress-bar progress-bar-striped bg-danger" role="progressbar" style="width: {{ (analysis.grades.fail*100/analysis.entries)|round(2) }}%" aria-valuenow="{{ analysis.grades.fail }}" aria-valuemin="0" aria-valuemax="{{ analysis.entries }}">{{ (analysis.grades.fail*100/analysis.entries)|round(2) }} &percnt;</div>
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Mean Score</span>
<span class="form-control">
{{ analysis.scores.mean|round(2) }} &percnt;
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Standard Deviation</span>
<span class="form-control">
{% if analysis.scores.stdev %}
{{ analysis.scores.stdev|round(2) }}
{% else %}
{{ None }}
{% endif %}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Median Score</span>
<span class="form-control">
{{ analysis.scores.median|round(2) }} &percnt;
</span>
</div>
{% if type == 'exam' %}
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text">Dataset Name</span> <span class="input-group-text">Dataset Name</span>
<span class="form-control"> <span class="form-control">
{{ dataset.get_name() }} {{ dataset.get_name() }}
</span> </span>
</div> </div>
<div class="input-group mb-3">
<span class="input-group-text">Author</span>
<span class="form-control">
{{ dataset.creator.get_username() }}
</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Last Updated</span>
<span class="form-control">
{{ dataset.date.strftime('%d %b %Y %H:%M') }}
</span>
</div>
{% if dataset.default %}
<div class="input-group mb-3">
<span class="input-group-text">
<input type="checkbox" aria-label="Default" class="dataset-default" checked disabled>
</span>
<span class="form-control">
Default Dataset
</select>
</div>
{% endif %} {% endif %}
</div> </div>
<div class="container"> <div class="container">
@@ -107,7 +136,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}

View File

@@ -43,7 +43,7 @@ def _fetch_questions():
data_path = dataset.get_file() data_path = dataset.get_file()
with open(data_path, 'r') as data_file: with open(data_path, 'r') as data_file:
data = loads(data_file.read()) data = loads(data_file.read())
questions = generate_questions(data) questions = generate_questions(dataset=data, randomise=False)
return jsonify({ return jsonify({
'time_limit': end_time, 'time_limit': end_time,
'questions': questions, 'questions': questions,

View File

@@ -41,7 +41,7 @@ class User(UserMixin, db.Model):
def set_password(self): raise AttributeError('set_password is not a readable attribute.') def set_password(self): raise AttributeError('set_password is not a readable attribute.')
set_password.setter set_password.setter
def set_password(self, password:str): self.password = generate_password_hash(password, method="sha256") def set_password(self, password:str): self.password = generate_password_hash(password, method="scrypt")
def verify_password(self, password:str): return check_password_hash(self.password, password) def verify_password(self, password:str): return check_password_hash(self.password, password)

View File

@@ -3,13 +3,36 @@
{% block content %} {% block content %}
<div class="instruction-container"> <div class="instruction-container">
<h3>Instructions</h3> <h3>Instructions</h3>
<p>
Thank you for putting yourself forward to sit the SKA Referee Theory Exam. Please read the following instructions carefully.
</p>
<h4>
Taking the Exam
</h4>
<ul> <ul>
<li> <li>
The exam comprises 100 multiple-choice questions. The exam consists of 100 questions, all of them multiple choice with two or three options, which are designed to test your knowledge of a wide range of rules. For each question, answer what decision you would give as a referee unless the question instructs otherwise.
</li> </li>
<li> <li>
For each question, answer what decision you would give as a referee unless the question instructs otherwise. It should take around an hour to complete.
</li> </li>
<li>
The exam should be taken under exam conditions. Materials such as the official rules, guidelines, revision resources, or similar should not be consulted during the test.
</li>
<li>
We would remind candidates that whilst we are relying on your honesty in this test, your theory knowledge will make up a part of the practical assessment when you are observed refereeing a game.
</li>
<li>
You also may not discuss the test with any other person while you are sitting it.
</li>
<li>
If you have any queries before the exam or would like further feedback on the test, your emails are welcome.
</li>
</ul>
<h4>
Using the Web App
</h4>
<ul>
<li> <li>
You will be able to customise the display settings of the exam from the settings panel by clicking on the red gear button <a class="btn btn-danger" aria-title="Settings" title="Settings" onclick="return false;"><i class="bi bi-gear-fill"></i></a>. You will be able to customise the display settings of the exam from the settings panel by clicking on the red gear button <a class="btn btn-danger" aria-title="Settings" title="Settings" onclick="return false;"><i class="bi bi-gear-fill"></i></a>.
</li> </li>
@@ -17,7 +40,7 @@
You can view your progress at a glance, as well as navigate to any question in the quiz, using the question grid, accessed via the yellow grid button <a class="btn btn-warning" aria-title="Question Grid" title="Question Grid" onclick="return false;"><i class="bi bi-table"></i></a>. You can view your progress at a glance, as well as navigate to any question in the quiz, using the question grid, accessed via the yellow grid button <a class="btn btn-warning" aria-title="Question Grid" title="Question Grid" onclick="return false;"><i class="bi bi-table"></i></a>.
</li> </li>
<li> <li>
If you are unsure of the answer to a question or would like to revise a question, you can flag the question to review it later on using the flag button button <a class="btn btn-secondary" id="q-nav-flag" title="Flag Button." onclick="return false;"><i class="bi bi-flag-fill"></i></a>. If you are unsure of the answer to a question or would like to return to a question later, you can flag the question using the flag button button <a class="btn btn-secondary" id="q-nav-flag" title="Flag Button." onclick="return false;"><i class="bi bi-flag-fill"></i></a> to serve as a reminder for you to come back to it later.
</li> </li>
</ul> </ul>
</div> </div>
@@ -46,7 +69,7 @@
Results Results
</h4> </h4>
<p> <p>
The results of your exam will be processed immediately and sent to the SKA Refereeing Coordinator. You will also be emailed a copy of your results. The results of your exam will be processed immediately and sent to the SKA Refereeing Coordinator. You will also be emailed a copy of your results. If you do not receive an email, make sure to check your spam folder.
</p> </p>
<p> <p>
When you are ready to begin the quiz, click the following button. When you are ready to begin the quiz, click the following button.

View File

@@ -30,6 +30,7 @@ def _instructions():
@quiz.route('/start/', methods=['GET', 'POST']) @quiz.route('/start/', methods=['GET', 'POST'])
def _start(): def _start():
clubs = [ clubs = [
'Barrowland Bears Korfball Club',
'Dundee Korfball Club', 'Dundee Korfball Club',
'Edinburgh City Korfball Club', 'Edinburgh City Korfball Club',
'Edinburgh Mavericks Korfball Club', 'Edinburgh Mavericks Korfball Club',
@@ -59,11 +60,11 @@ def _start():
except Exception as exception: except Exception as exception:
write('system.log', f'Database error when processing request \'{request.url}\': {exception}') write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
return abort(500) return abort(500)
if not test: return jsonify({'error': 'The exam code you entered is invalid.'}), 400
entry.test = test entry.test = test
entry.dataset = test.dataset entry.dataset = test.dataset
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()
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 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

View File

@@ -1,33 +1,33 @@
blinker==1.5 blinker==1.9.0
cffi==1.15.1 cffi==2.0.0
click==8.1.3 click==8.3.0
cryptography==39.0.2 cryptography==46.0.2
dnspython==2.3.0 dnspython==2.8.0
dominate==2.7.0 dominate==2.9.1
email-validator==1.3.1 email-validator==2.3.0
Flask==2.2.3 Flask==3.1.2
Flask-Bootstrap==3.3.7.1 Flask-Bootstrap==3.3.7.1
Flask-Login==0.6.2 Flask-Login==0.6.3
Flask-Mail==0.9.1 Flask-Mail==0.10.0
Flask-SQLAlchemy==3.0.3 Flask-SQLAlchemy==3.1.1
Flask-WTF==1.1.1 Flask-WTF==1.2.2
greenlet==2.0.2 greenlet==3.2.4
gunicorn==20.1.0 gunicorn==23.0.0
idna==3.4 idna==3.10
itsdangerous==2.1.2 itsdangerous==2.2.0
Jinja2==3.1.2 Jinja2==3.1.6
MarkupSafe==2.1.2 MarkupSafe==3.0.3
pip==23.0.1 packaging==25.0
pycparser==2.21 pycparser==2.23
PyMySQL==1.0.2 PyMySQL==1.1.2
python-dotenv==1.0.0 python-dotenv==1.1.1
setuptools==67.4.0 setuptools==80.9.0
six==1.16.0 six==1.17.0
SQLAlchemy==2.0.4 SQLAlchemy==2.0.43
sqlalchemy-json==0.5.0 sqlalchemy-json==0.7.0
SQLAlchemy-Utils==0.40.0 SQLAlchemy-Utils==0.42.0
typing_extensions==4.5.0 typing_extensions==4.15.0
visitor==0.1.3 visitor==0.1.3
Werkzeug==2.2.3 Werkzeug==3.1.3
wheel==0.38.4 wheel==0.45.1
WTForms==3.0.1 WTForms==3.2.1