7 Commits

9 changed files with 128 additions and 163 deletions

View File

@@ -17,7 +17,6 @@ services:
- ./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/view/static:/usr/share/nginx/html/view/static:ro
- ./ref-test/app/analysis/static:/usr/share/nginx/html/analysis/static:ro
ports:
- 80:80
- 443:443

View File

@@ -45,11 +45,6 @@ server {
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
location / {
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/
ENV DATA=$DATA
WORKDIR /ref-test

View File

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

View File

@@ -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) }} &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>
<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 %}

View File

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

View File

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

View File

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

View File

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