Compare commits

..

No commits in common. "master" and "v1.2.0" have entirely different histories.

8 changed files with 99 additions and 156 deletions

View File

@ -17,7 +17,6 @@ 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,11 +45,6 @@ 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

@ -54,7 +54,7 @@
</td> </td>
<td> <td>
{% if entry.end_time %} {% if entry.end_time %}
{{ entry.end_time.strftime('%Y-%m-%d %H:%M') }} {{ entry.end_time.strftime('%d %b %Y') }}
{% 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('%Y-%m-%d %H:%M') }} {{ element.date.strftime('%d %b %Y %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('%Y-%m-%d %H:%M') }} {{ test.start_date.strftime('%d %b %y %H:%M') }}
</td> </td>
<td> <td>
{{ test.get_code() }} {{ test.get_code() }}
</td> </td>
<td> <td>
{{ test.end_date.strftime('%Y-%m-%d %H:%M') }} {{ test.end_date.strftime('%d %b %Y %H:%M') }}
</td> </td>
<td> <td>
{% if test.time_limit == None -%} {% if test.time_limit == None -%}

View File

@ -8,133 +8,105 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Analysis by {{ type[0]|upper }}{{ type[1:] }}</h1> <h1>Analysis</h1>
<div class="container"> <div class="container">
<p class="lead"> <p class="lead">
The analysis section displays statistics for all test results as well as answers to individual questions. Analysis for {{ type }} {{ subject }}.
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"> </div>
<span class="input-group-text"> <div class="container">
{% if type == 'exam' %} <h3>
Exam Code Question List
{% elif type == 'dataset' %} </h3>
Dataset Name <div class="container dataset-metadata">
{% 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) }} &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>
{% endif %} <div class="input-group mb-3">
</div> <span class="input-group-text">Author</span>
<div class="container"> <span class="form-control">
<table id="analysis-table" class="table table-striped" style="width:100%"> {{ dataset.creator.get_username() }}
<thead> </span>
<th data-priority="1"> </div>
Question <div class="input-group mb-3">
</th> <span class="input-group-text">Last Updated</span>
<th data-priority="1"> <span class="form-control">
Percent Correct {{ dataset.date.strftime('%d %b %Y %H:%M') }}
</th> </span>
<th data-priority="2"> </div>
Answers {% if dataset.default %}
</th> <div class="input-group mb-3">
<th data-priority="3"> <span class="input-group-text">
Tags <input type="checkbox" aria-label="Default" class="dataset-default" checked disabled>
</th> </span>
</thead> <span class="form-control">
<tbody> Default Dataset
{% for question in questions %} </select>
<tr class="table-row"> </div>
<td> {% endif %}
{{ question.q_no + 1 }} </div>
</td> <div class="container">
<td class="cell-percentage"> <table id="analysis-table" class="table table-striped" style="width:100%">
{{ ((analysis.answers[question.q_no][question.correct] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }} <thead>
</td> <th data-priority="1">
<td> Question
<table style="width:100%"> </th>
{% for option in question.options %} <th data-priority="1">
<tr> Percent Correct
<td style="width:50%"> </th>
{{ option[1] }} <th data-priority="2">
</td> Answers
<td> </th>
{% if question.correct == option[0] %} <th data-priority="3">
<div class="progress"> Tags
<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> </th>
</div> </thead>
{% else %} <tbody>
<div class="progress"> {% for question in questions %}
<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> <tr class="table-row">
</div> <td>
{% endif %} {{ question.q_no + 1 }}
</td> </td>
</tr> <td class="cell-percentage">
{% endfor %} {{ ((analysis.answers[question.q_no][question.correct] or 0)*100/(analysis.answers[question.q_no].values())|sum())|round(2) }}
</table> </td>
</td> <td>
<td> <table style="width:100%">
<ul> {% for option in question.options %}
{% for tag in question.tags %} <tr>
<li>{{ tag|safe }}</li> <td style="width:50%">
{% endfor %} {{ option[1] }}
</ul> </td>
</td> <td>
</tr> {% if question.correct == option[0] %}
{% endfor %} <div class="progress">
</tbody> <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>
</table> </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> </div>
{% endblock %} {% endblock %}

View File

@ -3,36 +3,13 @@
{% 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 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. The exam comprises 100 multiple-choice questions.
</li> </li>
<li> <li>
It should take around an hour to complete. For each question, answer what decision you would give as a referee unless the question instructs otherwise.
</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>
@ -40,7 +17,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 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. 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>.
</li> </li>
</ul> </ul>
</div> </div>
@ -69,7 +46,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. If you do not receive an email, make sure to check your spam folder. 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.
</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

@ -59,11 +59,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