16 Commits

9 changed files with 162 additions and 129 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,10 +61,8 @@ 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 with app.app_context():
def _create_database_tables(): db.create_all()
with app.app_context():
db.create_all()
return app return app

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

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

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

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