8 Commits

7 changed files with 129 additions and 95 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

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

@ -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