Compare commits
11 Commits
v1.2.2
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
25fb3fdda4 | |||
fa104d7d1b | |||
cb747b4832 | |||
845fdcdf8d | |||
54e1653cb5 | |||
716206dc65 | |||
a8eda4078d | |||
d28cd6daed | |||
57b25cd214 | |||
8013a776a9 | |||
aa1f46ee62 |
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.13-slim
|
||||
ARG DATA=./data/
|
||||
ENV DATA=$DATA
|
||||
WORKDIR /ref-test
|
||||
|
@@ -61,10 +61,8 @@ def create_app():
|
||||
app.register_blueprint(view, url_prefix='/admin/view')
|
||||
app.register_blueprint(analysis, url_prefix='/admin/analysis')
|
||||
|
||||
"""Create Database Tables before First Request"""
|
||||
@app.before_first_request
|
||||
def _create_database_tables():
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
"""Create Database Tables when creating app"""
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
return app
|
@@ -54,7 +54,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if entry.end_time %}
|
||||
{{ entry.end_time.strftime('%d %b %Y') }}
|
||||
{{ entry.end_time.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
@@ -43,7 +43,7 @@
|
||||
{{ element.get_name() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ element.date.strftime('%d %b %Y %H:%M') }}
|
||||
{{ element.date.strftime('%Y-%m-%d %H:%M') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ element.creator.get_username() }}
|
||||
|
@@ -33,13 +33,13 @@
|
||||
{% for test in tests %}
|
||||
<tr class="table-row">
|
||||
<td>
|
||||
{{ test.start_date.strftime('%d %b %y %H:%M') }}
|
||||
{{ test.start_date.strftime('%Y-%m-%d %H:%M') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ test.get_code() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ test.end_date.strftime('%d %b %Y %H:%M') }}
|
||||
{{ test.end_date.strftime('%Y-%m-%d %H:%M') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if test.time_limit == None -%}
|
||||
|
@@ -8,133 +8,105 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Analysis by {{ type[0]|upper }}{{ type[1:] }}</h1>
|
||||
<h1>Analysis</h1>
|
||||
<div class="container">
|
||||
<p class="lead">
|
||||
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.
|
||||
Analysis for {{ type }} {{ subject }}.
|
||||
</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 class="input-group mb-3">
|
||||
<span class="input-group-text">Total Entries</span>
|
||||
<span class="form-control">
|
||||
{{ analysis.entries }}
|
||||
</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) }} %)
|
||||
</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) }} %</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) }} %</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) }} %</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) }} %
|
||||
</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) }} %
|
||||
</span>
|
||||
</div>
|
||||
{% if type == 'exam' %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<h3>
|
||||
Question List
|
||||
</h3>
|
||||
<div class="container dataset-metadata">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Dataset Name</span>
|
||||
<span class="form-control">
|
||||
{{ dataset.get_name() }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<table id="analysis-table" class="table table-striped" style="width:100%">
|
||||
<thead>
|
||||
<th data-priority="1">
|
||||
Question
|
||||
</th>
|
||||
<th data-priority="1">
|
||||
Percent Correct
|
||||
</th>
|
||||
<th data-priority="2">
|
||||
Answers
|
||||
</th>
|
||||
<th data-priority="3">
|
||||
Tags
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for question in questions %}
|
||||
<tr class="table-row">
|
||||
<td>
|
||||
{{ question.q_no + 1 }}
|
||||
</td>
|
||||
<td class="cell-percentage">
|
||||
{{ ((analysis.answers[question.q_no][question.correct] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}
|
||||
</td>
|
||||
<td>
|
||||
<table style="width:100%">
|
||||
{% for option in question.options %}
|
||||
<tr>
|
||||
<td style="width:50%">
|
||||
{{ option[1] }}
|
||||
</td>
|
||||
<td>
|
||||
{% if question.correct == option[0] %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-danger progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for tag in question.tags %}
|
||||
<li>{{ tag|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<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 %}
|
||||
</div>
|
||||
<div class="container">
|
||||
<table id="analysis-table" class="table table-striped" style="width:100%">
|
||||
<thead>
|
||||
<th data-priority="1">
|
||||
Question
|
||||
</th>
|
||||
<th data-priority="1">
|
||||
Percent Correct
|
||||
</th>
|
||||
<th data-priority="2">
|
||||
Answers
|
||||
</th>
|
||||
<th data-priority="3">
|
||||
Tags
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for question in questions %}
|
||||
<tr class="table-row">
|
||||
<td>
|
||||
{{ question.q_no + 1 }}
|
||||
</td>
|
||||
<td class="cell-percentage">
|
||||
{{ ((analysis.answers[question.q_no][question.correct] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}
|
||||
</td>
|
||||
<td>
|
||||
<table style="width:100%">
|
||||
{% for option in question.options %}
|
||||
<tr>
|
||||
<td style="width:50%">
|
||||
{{ option[1] }}
|
||||
</td>
|
||||
<td>
|
||||
{% if question.correct == option[0] %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-danger progress-bar-striped" role="progressbar" style="width: {{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}%;" aria-valuenow="{{ (analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum() }}" aria-valuemin="0" aria-valuemax="100">{{ ((analysis.answers[question.q_no][option[0]] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}%</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for tag in question.tags %}
|
||||
<li>{{ tag|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@@ -43,7 +43,7 @@ def _fetch_questions():
|
||||
data_path = dataset.get_file()
|
||||
with open(data_path, 'r') as data_file:
|
||||
data = loads(data_file.read())
|
||||
questions = generate_questions(data)
|
||||
questions = generate_questions(dataset=data, randomise=False)
|
||||
return jsonify({
|
||||
'time_limit': end_time,
|
||||
'questions': questions,
|
||||
|
@@ -41,7 +41,7 @@ class User(UserMixin, db.Model):
|
||||
def set_password(self): raise AttributeError('set_password is not a readable attribute.')
|
||||
|
||||
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)
|
||||
|
||||
|
@@ -3,13 +3,36 @@
|
||||
{% block content %}
|
||||
<div class="instruction-container">
|
||||
<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>
|
||||
<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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
@@ -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>.
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -46,7 +69,7 @@
|
||||
Results
|
||||
</h4>
|
||||
<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>
|
||||
When you are ready to begin the quiz, click the following button.
|
||||
|
@@ -30,6 +30,7 @@ def _instructions():
|
||||
@quiz.route('/start/', methods=['GET', 'POST'])
|
||||
def _start():
|
||||
clubs = [
|
||||
'Barrowland Bears Korfball Club',
|
||||
'Dundee Korfball Club',
|
||||
'Edinburgh City Korfball Club',
|
||||
'Edinburgh Mavericks Korfball Club',
|
||||
@@ -59,11 +60,11 @@ def _start():
|
||||
except Exception as exception:
|
||||
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||
return abort(500)
|
||||
if not test: return jsonify({'error': 'The exam code you entered is invalid.'}), 400
|
||||
entry.test = test
|
||||
entry.dataset = test.dataset
|
||||
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
|
||||
|
@@ -1,33 +1,33 @@
|
||||
blinker==1.5
|
||||
cffi==1.15.1
|
||||
click==8.1.3
|
||||
cryptography==39.0.2
|
||||
dnspython==2.3.0
|
||||
dominate==2.7.0
|
||||
email-validator==1.3.1
|
||||
Flask==2.2.3
|
||||
blinker==1.9.0
|
||||
cffi==2.0.0
|
||||
click==8.3.0
|
||||
cryptography==46.0.2
|
||||
dnspython==2.8.0
|
||||
dominate==2.9.1
|
||||
email-validator==2.3.0
|
||||
Flask==3.1.2
|
||||
Flask-Bootstrap==3.3.7.1
|
||||
Flask-Login==0.6.2
|
||||
Flask-Mail==0.9.1
|
||||
Flask-SQLAlchemy==3.0.3
|
||||
Flask-WTF==1.1.1
|
||||
greenlet==2.0.2
|
||||
gunicorn==20.1.0
|
||||
idna==3.4
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.2
|
||||
pip==23.0.1
|
||||
pycparser==2.21
|
||||
PyMySQL==1.0.2
|
||||
python-dotenv==1.0.0
|
||||
setuptools==67.4.0
|
||||
six==1.16.0
|
||||
SQLAlchemy==2.0.4
|
||||
sqlalchemy-json==0.5.0
|
||||
SQLAlchemy-Utils==0.40.0
|
||||
typing_extensions==4.5.0
|
||||
Flask-Login==0.6.3
|
||||
Flask-Mail==0.10.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-WTF==1.2.2
|
||||
greenlet==3.2.4
|
||||
gunicorn==23.0.0
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.3
|
||||
packaging==25.0
|
||||
pycparser==2.23
|
||||
PyMySQL==1.1.2
|
||||
python-dotenv==1.1.1
|
||||
setuptools==80.9.0
|
||||
six==1.17.0
|
||||
SQLAlchemy==2.0.43
|
||||
sqlalchemy-json==0.7.0
|
||||
SQLAlchemy-Utils==0.42.0
|
||||
typing_extensions==4.15.0
|
||||
visitor==0.1.3
|
||||
Werkzeug==2.2.3
|
||||
wheel==0.38.4
|
||||
WTForms==3.0.1
|
||||
Werkzeug==3.1.3
|
||||
wheel==0.45.1
|
||||
WTForms==3.2.1
|
Reference in New Issue
Block a user