Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
502e694a17 | |||
d28cd6daed | |||
58782f6db7 | |||
57b25cd214 | |||
666e12253e | |||
8013a776a9 | |||
aa1f46ee62 | |||
dbd8d6bbe3 | |||
fed46eaa1e | |||
79ad96a93f | |||
ba851cb7dc | |||
fcc4d55947 | |||
a56358b8dd | |||
179a608089 | |||
a1289da09c | |||
ea86fd9ae6 | |||
76d60546e2 | |||
9a02048199 | |||
c9ad8e87cd | |||
3714919ba5 | |||
1026cc71a9 | |||
07fb170656 | |||
1ea93994ab | |||
607b132996 | |||
7aa4d81e65 | |||
0ef39dcfbe | |||
e1517b89c0 | |||
d0ed228824 | |||
a2c52a4261 | |||
b2c9bdd7d2 | |||
7536c33a48 | |||
850c2b13b7 | |||
eb69979f59 | |||
95cea46a8f | |||
02a1129390 | |||
438e09f1ec | |||
9241e1c0f7 |
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -51,6 +51,7 @@ def create_app():
|
|||||||
from .views import views
|
from .views import views
|
||||||
from .editor.views import editor
|
from .editor.views import editor
|
||||||
from .view.views import view
|
from .view.views import view
|
||||||
|
from .analysis.views import analysis
|
||||||
|
|
||||||
app.register_blueprint(admin, url_prefix='/admin')
|
app.register_blueprint(admin, url_prefix='/admin')
|
||||||
app.register_blueprint(api, url_prefix='/api')
|
app.register_blueprint(api, url_prefix='/api')
|
||||||
@ -58,6 +59,7 @@ def create_app():
|
|||||||
app.register_blueprint(quiz)
|
app.register_blueprint(quiz)
|
||||||
app.register_blueprint(editor, url_prefix='/admin/editor')
|
app.register_blueprint(editor, url_prefix='/admin/editor')
|
||||||
app.register_blueprint(view, url_prefix='/admin/view')
|
app.register_blueprint(view, url_prefix='/admin/view')
|
||||||
|
app.register_blueprint(analysis, url_prefix='/admin/analysis')
|
||||||
|
|
||||||
"""Create Database Tables before First Request"""
|
"""Create Database Tables before First Request"""
|
||||||
@app.before_first_request
|
@app.before_first_request
|
||||||
|
@ -88,6 +88,19 @@ $('.test-action').click(function(event) {
|
|||||||
})
|
})
|
||||||
} else if (action == 'edit') {
|
} else if (action == 'edit') {
|
||||||
window.location.href = `/admin/test/${id}/`
|
window.location.href = `/admin/test/${id}/`
|
||||||
|
} else if (action == 'analyse') {
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/analysis/`,
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({'id': id, 'class': 'test'}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = response
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
error_response(response)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -123,6 +136,19 @@ $('.edit-question-dataset').click(function(event) {
|
|||||||
window.location.href = `/admin/view/${id}`
|
window.location.href = `/admin/view/${id}`
|
||||||
} else if (action == 'download') {
|
} else if (action == 'download') {
|
||||||
window.location.href = `/admin/settings/questions/download/${id}/`
|
window.location.href = `/admin/settings/questions/download/${id}/`
|
||||||
|
} else if (action == 'analyse') {
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/analysis/`,
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({'id': id, 'class': 'dataset'}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = response
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
error_response(response)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -20,8 +20,28 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<li class="nav-item" id="nav-results">
|
<li class="nav-item dropdown" id="nav-results">
|
||||||
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-results"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._view_entries') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Results
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-results"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown" id="nav-tests">
|
<li class="nav-item dropdown" id="nav-tests">
|
||||||
<a
|
<a
|
||||||
@ -36,7 +56,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul
|
<ul
|
||||||
class="dropdown-menu"
|
class="dropdown-menu"
|
||||||
aria-labelledby="dropdown-settings"
|
aria-labelledby="dropdown-tests"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
||||||
@ -58,7 +78,7 @@
|
|||||||
<li class="nav-item dropdown" id="nav-settings">
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
<a
|
<a
|
||||||
class="nav-link dropdown-toggle"
|
class="nav-link dropdown-toggle"
|
||||||
id="dropdown-account"
|
id="dropdown-settings"
|
||||||
role="button"
|
role="button"
|
||||||
href="{{ url_for('admin._settings') }}"
|
href="{{ url_for('admin._settings') }}"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
{% for tag, scores in entry.result.tags.items() %}
|
{% for tag, scores in entry.result.tags.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{ tag }}
|
{{ tag|safe }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ scores.scored }}
|
{{ scores.scored }}
|
||||||
|
@ -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>
|
||||||
|
@ -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() }}
|
||||||
@ -52,6 +52,15 @@
|
|||||||
{{ element.tests|length }}
|
{{ element.tests|length }}
|
||||||
</td>
|
</td>
|
||||||
<td class="row-actions">
|
<td class="row-actions">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="btn btn-success edit-question-dataset {% if not element.entries %} disabled {% endif %}"
|
||||||
|
data-id="{{ element.id }}"
|
||||||
|
data-action="analyse"
|
||||||
|
title="Analyse Answers"
|
||||||
|
>
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
class="btn btn-primary edit-question-dataset"
|
class="btn btn-primary edit-question-dataset"
|
||||||
@ -63,7 +72,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="javascript:void(0)"
|
href="javascript:void(0)"
|
||||||
class="btn btn-primary view-question-dataset"
|
class="btn btn-primary edit-question-dataset"
|
||||||
data-id="{{ element.id }}"
|
data-id="{{ element.id }}"
|
||||||
data-action="view"
|
data-action="view"
|
||||||
title="View Questions"
|
title="View Questions"
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
{% for entry in test.entries %}
|
{% for entry in test.entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('admin._view_entry', id=entry) }}" >Entry {{ loop.index }}</a>
|
<a href="{{ url_for('admin._view_entry', id=entry.id) }}" >Entry {{ loop.index }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -162,7 +162,7 @@
|
|||||||
{% include "admin/components/client-alerts.html" %}
|
{% include "admin/components/client-alerts.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="container justify-content-center">
|
<div class="container justify-content-center">
|
||||||
<div class="row">
|
<div class="my-3 row">
|
||||||
{% if test.start_date <= now %}
|
{% if test.start_date <= now %}
|
||||||
<a href="#" class="btn btn-warning test-action {% if test.end_date < now %}disabled{% endif %}" data-action="end" data-id="{{ test.id }}">
|
<a href="#" class="btn btn-warning test-action {% if test.end_date < now %}disabled{% endif %}" data-action="end" data-id="{{ test.id }}">
|
||||||
<i class="bi bi-hourglass-bottom button-icon"></i>
|
<i class="bi bi-hourglass-bottom button-icon"></i>
|
||||||
@ -174,6 +174,16 @@
|
|||||||
Start Exam
|
Start Exam
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-success test-analyse test-action {% if not test.entries %} disabled {% endif %}"
|
||||||
|
data-id="{{test.id}}"
|
||||||
|
title="Analyse Exam"
|
||||||
|
data-action="analyse"
|
||||||
|
>
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
Analyse Exam
|
||||||
|
</a>
|
||||||
<a href="#" class="btn btn-danger test-action" data-action="delete" data-id="{{ test.id }}">
|
<a href="#" class="btn btn-danger test-action" data-action="delete" data-id="{{ test.id }}">
|
||||||
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
|
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
|
||||||
Delete Exam
|
Delete Exam
|
||||||
|
@ -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 -%}
|
||||||
@ -58,6 +58,15 @@
|
|||||||
{{ test.entries|length }}
|
{{ test.entries|length }}
|
||||||
</td>
|
</td>
|
||||||
<td class="row-actions">
|
<td class="row-actions">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-success test-analyse test-action {% if not test.entries %} disabled {% endif %}"
|
||||||
|
data-id="{{test.id}}"
|
||||||
|
title="Analyse Exam"
|
||||||
|
data-action="analyse"
|
||||||
|
>
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-primary test-action"
|
class="btn btn-primary test-action"
|
||||||
|
@ -251,7 +251,6 @@ def _questions():
|
|||||||
if success: return jsonify({'success': message}), 200
|
if success: return jsonify({'success': message}), 200
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
return send_errors_to_client(form=form)
|
return send_errors_to_client(form=form)
|
||||||
|
|
||||||
try: data = Dataset.query.all()
|
try: data = Dataset.query.all()
|
||||||
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}')
|
||||||
@ -281,7 +280,7 @@ def _download(id:str):
|
|||||||
return abort(500)
|
return abort(500)
|
||||||
if not dataset: return abort(404)
|
if not dataset: return abort(404)
|
||||||
data_path = path.abspath(dataset.get_file())
|
data_path = path.abspath(dataset.get_file())
|
||||||
return send_file(data_path, as_attachment=True, attachment_filename=f'{dataset.get_name()}.json')
|
return send_file(data_path, as_attachment=True, download_name=f'{dataset.get_name()}.json')
|
||||||
|
|
||||||
@admin.route('/tests/<string:filter>/', methods=['GET'])
|
@admin.route('/tests/<string:filter>/', methods=['GET'])
|
||||||
@admin.route('/tests/', methods=['GET'])
|
@admin.route('/tests/', methods=['GET'])
|
||||||
@ -435,10 +434,7 @@ def _view_entry(id:str=None):
|
|||||||
flash('Invalid entry ID.', 'error')
|
flash('Invalid entry ID.', 'error')
|
||||||
return redirect(url_for('admin._view_entries'))
|
return redirect(url_for('admin._view_entries'))
|
||||||
test = entry.test
|
test = entry.test
|
||||||
dataset = test.dataset
|
data = test.dataset.get_data()
|
||||||
dataset_path = dataset.get_file()
|
|
||||||
with open(dataset_path, 'r') as _dataset:
|
|
||||||
data = loads(_dataset.read())
|
|
||||||
correct = get_correct_answers(dataset=data)
|
correct = get_correct_answers(dataset=data)
|
||||||
answers = answer_options(dataset=data)
|
answers = answer_options(dataset=data)
|
||||||
return render_template('/admin/result-detail.html', entry = entry, correct = correct, answers = answers)
|
return render_template('/admin/result-detail.html', entry = entry, correct = correct, answers = answers)
|
||||||
|
0
ref-test/app/analysis/__init__.py
Normal file
0
ref-test/app/analysis/__init__.py
Normal file
8
ref-test/app/analysis/static/css/analysis.css
Normal file
8
ref-test/app/analysis/static/css/analysis.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#alert-box {
|
||||||
|
margin: 30px auto;
|
||||||
|
max-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-percentage::after {
|
||||||
|
content: '%';
|
||||||
|
}
|
260
ref-test/app/analysis/static/css/style.css
Normal file
260
ref-test/app/analysis/static/css/style.css
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
body {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
background-color: lightgray;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-display {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-heading {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input,
|
||||||
|
.form-label-group label {
|
||||||
|
padding: var(--input-padding-y) var(--input-padding-x);
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0; /* Override default `<label>` margin */
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
cursor: text; /* Match the input under the label */
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: .25rem;
|
||||||
|
transition: all .1s ease-in-out;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0%;
|
||||||
|
border-bottom: 2px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:active, .form-label-group input:focus {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-webkit-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:-ms-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-ms-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-moz-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:not(:placeholder-shown) {
|
||||||
|
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
|
||||||
|
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:not(:placeholder-shown) ~ label {
|
||||||
|
padding-top: calc(var(--input-padding-y) / 3);
|
||||||
|
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input {
|
||||||
|
transform: scale(1.5);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signin-forgot-password {
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button button, .form-submission-button a {
|
||||||
|
margin: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button button span, .form-submission-button button svg, .form-submission-button a span, .form-submission-button a svg {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_wrapper .dt-buttons {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
float:none;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions button, .row-actions a {
|
||||||
|
margin: 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cookie-alert {
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dismiss-cookie-alert {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-db-empty {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
font-size: 14pt;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-date-input, .form-select-input {
|
||||||
|
position: relative;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-date-input input,
|
||||||
|
.form-date-input label, .form-select-input select, .form-select-input label {
|
||||||
|
padding: var(--input-padding-y) var(--input-padding-x);
|
||||||
|
font-size: 16pt;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker::-webkit-calendar-picker-indicator {
|
||||||
|
border: 1px;
|
||||||
|
border-color: gray;
|
||||||
|
border-radius: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-date-input label, .form-select-input label {
|
||||||
|
/* position: absolute; */
|
||||||
|
/* top: 0;
|
||||||
|
left: 0; */
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0; /* Override default `<label>` margin */
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
cursor: text; /* Match the input under the label */
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: .25rem;
|
||||||
|
transition: all .1s ease-in-out;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-upload {
|
||||||
|
margin: 2rem 0;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-action-buttons, .test-action {
|
||||||
|
margin: 5px auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change Autocomplete styles in Chrome*/
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover,
|
||||||
|
textarea:-webkit-autofill:focus,
|
||||||
|
select:-webkit-autofill,
|
||||||
|
select:-webkit-autofill:hover,
|
||||||
|
select:-webkit-autofill:focus {
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for Edge
|
||||||
|
-------------------------------------------------- */
|
||||||
|
@supports (-ms-ime-align: auto) {
|
||||||
|
.form-label-group label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.form-label-group input::-ms-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for IE
|
||||||
|
-------------------------------------------------- */
|
||||||
|
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||||
|
.form-label-group label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.form-label-group input:-ms-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
ref-test/app/analysis/static/js/analysis.js
Normal file
27
ref-test/app/analysis/static/js/analysis.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Analyse Button Listener
|
||||||
|
$('.button-analyse').click(function(event) {
|
||||||
|
|
||||||
|
let buttonClass = $(this).data('class')
|
||||||
|
let id = null
|
||||||
|
|
||||||
|
if (buttonClass == 'test' ) {
|
||||||
|
id = $('#select-test').children('option:selected').val()
|
||||||
|
} else if (buttonClass == 'dataset' ) {
|
||||||
|
id = $('#select-dataset').children('option:selected').val()
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/analysis/`,
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({'id': id, 'class': buttonClass}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = response
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
error_response(response)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
2
ref-test/app/analysis/static/js/jquery-3.6.0.min.js
vendored
Normal file
2
ref-test/app/analysis/static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
115
ref-test/app/analysis/static/js/script.js
Normal file
115
ref-test/app/analysis/static/js/script.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// Menu Highlight Scripts
|
||||||
|
const menuItems = document.getElementsByClassName('nav-link')
|
||||||
|
for(let i = 0; i < menuItems.length; i++) {
|
||||||
|
if(menuItems[i].pathname == window.location.pathname) {
|
||||||
|
menuItems[i].classList.add('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dropdownItems = document.getElementsByClassName('dropdown-item')
|
||||||
|
for(let i = 0; i< dropdownItems.length; i++) {
|
||||||
|
if(dropdownItems[i].pathname == window.location.pathname) {
|
||||||
|
dropdownItems[i].classList.add('active')
|
||||||
|
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// General Post Method Form Processing Script
|
||||||
|
$('form.form-post').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this)
|
||||||
|
var data = $form.serialize()
|
||||||
|
var url = $(this).prop('action')
|
||||||
|
var rel_success = $(this).data('rel-success')
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.redirect_to) {
|
||||||
|
window.location.href = response.redirect_to
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.location.href = rel_success
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
error_response(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
function error_response(response) {
|
||||||
|
|
||||||
|
const $alert = $("#alert-box")
|
||||||
|
$alert.html('')
|
||||||
|
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
$alert.html(`
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
var output = ''
|
||||||
|
for (let i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
output += `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
$alert.html(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$alert.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dismiss Cookie Alert
|
||||||
|
$('#dismiss-cookie-alert').click(function(event){
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/cookies/',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
time: Date.now()
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response){
|
||||||
|
console.log(response)
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
console.log(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create New Dataset
|
||||||
|
$('.create-new-dataset').click(function(event){
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/editor/new/',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
time: Date.now()
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response){
|
||||||
|
if (response.redirect_to) {
|
||||||
|
window.location.href = response.redirect_to
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
console.log(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
198
ref-test/app/analysis/templates/analysis/analysis.html
Normal file
198
ref-test/app/analysis/templates/analysis/analysis.html
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
{% extends "analysis/components/datatable.html" %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('.static', filename='css/analysis.css') }}"
|
||||||
|
/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Analysis by {{ type[0]|upper }}{{ type[1:] }}</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.
|
||||||
|
</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 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>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
const target = "{{ url_for('api._editor') }}"
|
||||||
|
const id = "{{ dataset.id }}"
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ url_for('.static', filename='js/analysis.js') }}"
|
||||||
|
></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block custom_data_script %}
|
||||||
|
<script>
|
||||||
|
console.log($('#analysis-table'))
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#analysis-table').DataTable({
|
||||||
|
'searching': true,
|
||||||
|
'columnDefs': [
|
||||||
|
{'sortable': true, 'targets': [0,1]},
|
||||||
|
{'sortable': false, 'targets': [2,3]},
|
||||||
|
{'searchable': true, 'targets': [0,2,3]}
|
||||||
|
],
|
||||||
|
'order': [[0, 'asc'], [1, 'desc']],
|
||||||
|
'buttons': [
|
||||||
|
{
|
||||||
|
extend: 'print',
|
||||||
|
exportOptions: {
|
||||||
|
columns: [0, 1, 2, 3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extend: 'excel',
|
||||||
|
exportOptions: {
|
||||||
|
columns: [0, 1, 2, 3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extend: 'pdf',
|
||||||
|
exportOptions: {
|
||||||
|
columns: [0, 1, 2, 3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'responsive': 'true',
|
||||||
|
'colReorder': 'true',
|
||||||
|
'fixedHeader': 'true',
|
||||||
|
'searchBuilder': {
|
||||||
|
depthLimit: 2,
|
||||||
|
columns: [2, 3],
|
||||||
|
},
|
||||||
|
dom: 'BQlfrtip'
|
||||||
|
});
|
||||||
|
// $('.buttons-pdf').html('<span class="glyphicon glyphicon-file" data-toggle="tooltip" title="Export To Excel"/>') -->
|
||||||
|
} );
|
||||||
|
$('#analysis-table').show();
|
||||||
|
$(window).trigger('resize');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('.static', filename='css/style.css') }}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('.static', filename='css/view.css') }}"
|
||||||
|
/>
|
||||||
|
{% block style %}
|
||||||
|
{% endblock %}
|
||||||
|
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
|
||||||
|
{% include "analysis/components/og-meta.html" %}
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
{% block navbar %}
|
||||||
|
{% include "analysis/components/navbar.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% block top_alerts %}
|
||||||
|
{% include "analysis/components/server-alerts.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="container site-footer mt-5">
|
||||||
|
{% block footer %}
|
||||||
|
{% include "analysis/components/footer.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- JQuery, Popper, and Bootstrap js dependencies -->
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||||
|
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
|
||||||
|
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<!-- Custom js -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var csrf_token = "{{ csrf_token() }}";
|
||||||
|
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ url_for('.static', filename='js/script.js') }}"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ url_for('.static', filename='js/analysis.js') }}"
|
||||||
|
></script>
|
||||||
|
{% block script %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block datatable_scripts %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_data_script %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1 @@
|
|||||||
|
<div id="alert-box" tabindex="-1"></div>
|
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "analysis/components/base.html" %}
|
||||||
|
{% block datatable_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/colreorder/1.5.5/css/colReorder.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/fixedheader/3.2.0/css/fixedHeader.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/keytable/2.6.4/css/keyTable.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/searchbuilder/1.3.0/css/searchBuilder.dataTables.min.css"/>
|
||||||
|
{% endblock %}
|
||||||
|
{% block datatable_scripts %}
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/2.5.0/jszip.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/dataTables.buttons.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.bootstrap5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.colVis.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.print.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/colreorder/1.5.5/js/dataTables.colReorder.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/fixedheader/3.2.0/js/dataTables.fixedHeader.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/keytable/2.6.4/js/dataTables.keyTable.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap5.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/searchbuilder/1.3.0/js/dataTables.searchBuilder.min.js"></script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,2 @@
|
|||||||
|
<p>This web app was developed and is maintained by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at <a href="https://git.vsnt.uk/viveksantayana/ska-referee-test">Vivek’s personal GIT repository</a> under an MIT License.</p>
|
||||||
|
<p>All questions in the test are © The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
|
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "analysis/components/base.html" %}
|
||||||
|
{% import "bootstrap/wtf.html" as wtf %}
|
||||||
|
{% block top_alerts %}
|
||||||
|
{% endblock %}
|
137
ref-test/app/analysis/templates/analysis/components/navbar.html
Normal file
137
ref-test/app/analysis/templates/analysis/components/navbar.html
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a href="{{ url_for('admin._home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbar"
|
||||||
|
aria-controls="navbar"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle Navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse justify-content-end" id="navbar">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<li class="nav-item" id="nav-login">
|
||||||
|
<a href="{{ url_for('admin._login') }}" id="link-login" class="nav-link">Log In</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown" id="nav-results">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-results"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._view_entries') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Results
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-results"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" id="nav-tests">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-tests"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._tests') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Exams
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-tests"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._tests', filter='scheduled') }}" id="link-scheduled" class="dropdown-item">Scheduled</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._tests', filter='expired') }}" id="link-expired" class="dropdown-item">Expired</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._tests', filter='all') }}" id="link-all" class="dropdown-item">All</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._tests', filter='create') }}" id="link-create" class="dropdown-item">Create</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-settings"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._settings') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-settings"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._settings') }}" id="link-settings" class="dropdown-item">View Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._users') }}" id="link-users" class="dropdown-item">Users</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('view._view') }}" id="link-editor" class="dropdown-item">View Questions</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('editor._editor') }}" id="link-editor" class="dropdown-item">Edit Questions</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" id="nav-account">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-account"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._update_user', id=current_user.id) }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-account"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._update_user', id=current_user.id) }}" id="link-account" class="dropdown-item">Account Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
@ -0,0 +1,18 @@
|
|||||||
|
<meta name="description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
|
||||||
|
<meta property="og:locale" content="en_UK" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
|
||||||
|
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
|
||||||
|
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
|
||||||
|
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
|
||||||
|
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
|
||||||
|
<meta property="og:image:width" content="512" />
|
||||||
|
<meta property="og:image:height" content="512" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
|
||||||
|
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
|
||||||
|
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
|
||||||
|
<meta name="twitter:creator" content="@viveksantayana" />
|
||||||
|
<meta name="twitter:site" content="@viveksantayana" />
|
||||||
|
<meta name="theme-color" content="#343a40" />
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">
|
@ -0,0 +1,23 @@
|
|||||||
|
<div class="navbar navbar-expand-sm navbar-light bg-light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="expand navbar-expand justify-content-center" id="navbar_secondary">
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin._tests', filter='active') }}">Active</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin._tests', filter='scheduled') }}">Scheduled</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin._tests', filter='expired') }}">Expired</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin._tests', filter='all') }}">All</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin._tests', filter='create') }}">Create</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% set cookie_flash_flag = namespace(value=False) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
{% if category == "error" %}
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Error" aria-title="Error"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "success" %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check2-circle" title="Success" aria-title="Success"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "warning" %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" aria-title="Warning" title="Warning"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "cookie_alert" %}
|
||||||
|
{% if not cookie_flash_flag.value %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Cookie Alert" aria-title="Cookie Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<div class="d-flex justify-content-center w-100">
|
||||||
|
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% set cookie_flash_flag.value = True %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
54
ref-test/app/analysis/templates/analysis/index.html
Normal file
54
ref-test/app/analysis/templates/analysis/index.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "analysis/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Analysis</h1>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="card m-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Exams</h5>
|
||||||
|
<div class="card-text">
|
||||||
|
<div class="form-select-input">
|
||||||
|
<select name="select-test" id="select-test">
|
||||||
|
{% for test in tests %}
|
||||||
|
<option value="{{ test.id }}">{{ test.get_code() }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<a href="{{ url_for('analysis._test') }}" class="btn btn-primary button-analyse" data-class="test">
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
Analyse
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="card m-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Datasets</h5>
|
||||||
|
<div class="card-text">
|
||||||
|
<div class="form-select-input">
|
||||||
|
<select name="select-dataset" id="select-dataset">
|
||||||
|
{% for dataset in datasets %}
|
||||||
|
<option value="{{ dataset.id }}">{{ dataset.get_name() }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="my-3">
|
||||||
|
<a href="{{ url_for('analysis._dataset') }}" class="btn btn-primary button-analyse" data-class="dataset">
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
Analyse
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "analysis/components/client-alerts.html" %}
|
||||||
|
{% endblock %}
|
85
ref-test/app/analysis/views.py
Normal file
85
ref-test/app/analysis/views.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from ..models import Dataset, Test
|
||||||
|
from ..tools.data import analyse, check_dataset_exists, check_test_exists
|
||||||
|
from ..tools.logs import write
|
||||||
|
from ..tools.data import parse_questions
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
|
from flask.helpers import abort, flash, redirect, url_for
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
analysis = Blueprint(
|
||||||
|
name='analysis',
|
||||||
|
import_name=__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
||||||
|
|
||||||
|
@analysis.route('/', methods=['GET','POST'])
|
||||||
|
@login_required
|
||||||
|
@check_dataset_exists
|
||||||
|
@check_test_exists
|
||||||
|
def _analysis():
|
||||||
|
try:
|
||||||
|
_tests = Test.query.all()
|
||||||
|
_datasets = Dataset.query.all()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||||
|
return abort(500)
|
||||||
|
tests = [ test for test in _tests if test.entries ]
|
||||||
|
datasets = [ dataset for dataset in _datasets if dataset.entries ]
|
||||||
|
if request.method == 'POST':
|
||||||
|
selection = request.get_json()
|
||||||
|
if selection['class'] == 'test':
|
||||||
|
try:
|
||||||
|
test = Test.query.filter_by(id=selection['id']).first()
|
||||||
|
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': 'Invalid entry ID.'}), 404
|
||||||
|
return url_for('analysis._test', id=selection['id']), 200
|
||||||
|
if selection['class'] == 'dataset':
|
||||||
|
try:
|
||||||
|
dataset = Dataset.query.filter_by(id=selection['id']).first()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||||
|
return abort(500)
|
||||||
|
if not dataset: return jsonify({'error': 'Invalid entry ID.'}), 404
|
||||||
|
return url_for('analysis._dataset', id=selection['id']), 200
|
||||||
|
return jsonify({'error': 'Invalid entry ID.'}), 404
|
||||||
|
return render_template('/analysis/index.html', tests=tests, datasets=datasets)
|
||||||
|
|
||||||
|
@analysis.route('/test/<string:id>')
|
||||||
|
@analysis.route('/test/')
|
||||||
|
@login_required
|
||||||
|
@check_test_exists
|
||||||
|
def _test(id:str=None):
|
||||||
|
if id in [None, '']:
|
||||||
|
flash(message='Please select a valid exam.', category='error')
|
||||||
|
return redirect(url_for('analysis._analysis'))
|
||||||
|
try:
|
||||||
|
test = Test.query.filter_by(id=id).first()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||||
|
return abort(500)
|
||||||
|
if not test:
|
||||||
|
flash('Invalid exam.', 'error')
|
||||||
|
return redirect(url_for('analysis._analysis'))
|
||||||
|
return render_template('/analysis/analysis.html', analysis=analyse(test), subject=test.get_code(), type='exam', dataset=test.dataset, questions=parse_questions(test.dataset.get_data()))
|
||||||
|
|
||||||
|
@analysis.route('/dataset/<string:id>')
|
||||||
|
@analysis.route('/dataset/')
|
||||||
|
@login_required
|
||||||
|
@check_dataset_exists
|
||||||
|
def _dataset(id:str=None):
|
||||||
|
if id in [None, '']:
|
||||||
|
flash(message='Please select a valid dataset.', category='error')
|
||||||
|
return redirect(url_for('analysis._analysis'))
|
||||||
|
try:
|
||||||
|
dataset = Dataset.query.filter_by(id=id).first()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||||
|
return abort(500)
|
||||||
|
if not dataset:
|
||||||
|
flash('Invalid dataset.', 'error')
|
||||||
|
return redirect(url_for('analysis._analysis'))
|
||||||
|
return render_template('/analysis/analysis.html', analysis=analyse(dataset), subject=dataset.get_name(), type='dataset', dataset=dataset, questions=parse_questions(dataset.get_data()))
|
@ -12,6 +12,7 @@ class Config(object):
|
|||||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
SERVER_NAME = os.getenv('SERVER_NAME')
|
SERVER_NAME = os.getenv('SERVER_NAME')
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
|
WTF_CSRF_TIME_LIMIT = None
|
||||||
|
|
||||||
"""Email Engine Configuration"""
|
"""Email Engine Configuration"""
|
||||||
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
||||||
|
@ -20,8 +20,28 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<li class="nav-item" id="nav-results">
|
<li class="nav-item dropdown" id="nav-results">
|
||||||
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-results"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._view_entries') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Results
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-results"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown" id="nav-tests">
|
<li class="nav-item dropdown" id="nav-tests">
|
||||||
<a
|
<a
|
||||||
@ -36,7 +56,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul
|
<ul
|
||||||
class="dropdown-menu"
|
class="dropdown-menu"
|
||||||
aria-labelledby="dropdown-settings"
|
aria-labelledby="dropdown-tests"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
||||||
@ -58,7 +78,7 @@
|
|||||||
<li class="nav-item dropdown" id="nav-settings">
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
<a
|
<a
|
||||||
class="nav-link dropdown-toggle"
|
class="nav-link dropdown-toggle"
|
||||||
id="dropdown-account"
|
id="dropdown-settings"
|
||||||
role="button"
|
role="button"
|
||||||
href="{{ url_for('admin._settings') }}"
|
href="{{ url_for('admin._settings') }}"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
|
@ -8,7 +8,7 @@ from flask_login import current_user
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import dump
|
from json import dump, loads
|
||||||
from os import path, remove
|
from os import path, remove
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -17,6 +17,7 @@ class Dataset(db.Model):
|
|||||||
id = db.Column(db.String(36), index=True, primary_key=True)
|
id = db.Column(db.String(36), index=True, primary_key=True)
|
||||||
name = db.Column(db.String(128), nullable=False)
|
name = db.Column(db.String(128), nullable=False)
|
||||||
tests = db.relationship('Test', backref='dataset')
|
tests = db.relationship('Test', backref='dataset')
|
||||||
|
entries = db.relationship('Entry', backref='dataset')
|
||||||
creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
|
creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
|
||||||
date = db.Column(db.DateTime, nullable=False)
|
date = db.Column(db.DateTime, nullable=False)
|
||||||
default = db.Column(db.Boolean, default=False, nullable=True)
|
default = db.Column(db.Boolean, default=False, nullable=True)
|
||||||
@ -115,6 +116,12 @@ class Dataset(db.Model):
|
|||||||
file_path = path.join(data, 'questions', filename)
|
file_path = path.join(data, 'questions', filename)
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
dataset_path = self.get_file()
|
||||||
|
with open(dataset_path, 'r') as _dataset:
|
||||||
|
data = loads(_dataset.read())
|
||||||
|
return data
|
||||||
|
|
||||||
def update(self, data:list=None, default:bool=False):
|
def update(self, data:list=None, default:bool=False):
|
||||||
self.date = datetime.now()
|
self.date = datetime.now()
|
||||||
if default: self.make_default()
|
if default: self.make_default()
|
||||||
|
@ -2,6 +2,7 @@ from ..extensions import db, mail
|
|||||||
from ..tools.encryption import decrypt, encrypt
|
from ..tools.encryption import decrypt, encrypt
|
||||||
from ..tools.logs import write
|
from ..tools.logs import write
|
||||||
from .test import Test
|
from .test import Test
|
||||||
|
from .dataset import Dataset
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
@ -17,6 +18,7 @@ class Entry(db.Model):
|
|||||||
email = db.Column(db.String(128), nullable=False)
|
email = db.Column(db.String(128), nullable=False)
|
||||||
club = db.Column(db.String(128), nullable=True)
|
club = db.Column(db.String(128), nullable=True)
|
||||||
test_id = db.Column(db.String(36), db.ForeignKey('test.id'))
|
test_id = db.Column(db.String(36), db.ForeignKey('test.id'))
|
||||||
|
dataset_id = db.Column(db.String(36), db.ForeignKey('dataset.id'))
|
||||||
user_code = db.Column(db.String(6), nullable=True)
|
user_code = db.Column(db.String(6), nullable=True)
|
||||||
start_time = db.Column(db.DateTime, index=True, nullable=True)
|
start_time = db.Column(db.DateTime, index=True, nullable=True)
|
||||||
end_time = db.Column(db.DateTime, index=True, nullable=True)
|
end_time = db.Column(db.DateTime, index=True, nullable=True)
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
<div class="container quiz-start-text">
|
<div class="container quiz-start-text">
|
||||||
You can use this panel to adjust the display settings for the exam. Please use the menu below to select the font face and font size. Below is a sample question so you can see how the exam will render with your chosen settings.
|
You can use this panel to adjust the display settings for the exam. Please use the menu below to select the font face and font size. Below is a sample question so you can see how the exam will render with your chosen settings.
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container quiz-start-text">
|
||||||
|
These settings will be stored locally on your browser window. No information about your preferences below will be collected by the app.
|
||||||
|
</div>
|
||||||
<div class="alert alert-primary quiz-start-text" role="alert">
|
<div class="alert alert-primary quiz-start-text" role="alert">
|
||||||
<strong>Note</strong>: Some fonts may not be available depending on your device and/or operating system.
|
<strong>Note</strong>: Some fonts may not be available depending on your device and/or operating system.
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +56,8 @@
|
|||||||
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
|
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
|
<!-- jQuery UI -->
|
||||||
|
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
|
||||||
<!-- Custom js -->
|
<!-- Custom js -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var csrf_token = "{{ csrf_token() }}";
|
var csrf_token = "{{ csrf_token() }}";
|
||||||
|
@ -3,13 +3,36 @@
|
|||||||
{% 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 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>
|
||||||
<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>
|
||||||
|
<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>
|
||||||
@ -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>.
|
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 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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -46,7 +69,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.
|
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>
|
||||||
<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.
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
{% extends "quiz/components/base.html" %}
|
{% extends "quiz/components/base.html" %}
|
||||||
{% import "bootstrap/wtf.html" as wtf %}
|
{% import "bootstrap/wtf.html" as wtf %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<form name="form-quiz-start" class="form-quiz-start">
|
<form name="form-quiz-start" class="form-quiz-start">
|
||||||
@ -43,4 +47,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
$( function() {
|
||||||
|
const clubs = {{ clubs|tojson }}
|
||||||
|
$('#club').autocomplete({
|
||||||
|
source: clubs
|
||||||
|
})
|
||||||
|
} )
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -29,6 +29,23 @@ def _instructions():
|
|||||||
|
|
||||||
@quiz.route('/start/', methods=['GET', 'POST'])
|
@quiz.route('/start/', methods=['GET', 'POST'])
|
||||||
def _start():
|
def _start():
|
||||||
|
clubs = [
|
||||||
|
'Dundee Korfball Club',
|
||||||
|
'Edinburgh City Korfball Club',
|
||||||
|
'Edinburgh Mavericks Korfball Club',
|
||||||
|
'Edinburgh University Korfball Club',
|
||||||
|
'Glasgow Korfball Club',
|
||||||
|
'Saint Andrews University Korfball Club',
|
||||||
|
'Strathclyde University Korfball Club'
|
||||||
|
]
|
||||||
|
try: entries = Entry.query.all()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when processing request \'{request.url}\': {exception}')
|
||||||
|
return abort(500)
|
||||||
|
for entry in entries: clubs.append(entry.get_club())
|
||||||
|
clubs = list(set(clubs))
|
||||||
|
try: clubs.remove('')
|
||||||
|
except: pass
|
||||||
form = StartQuiz()
|
form = StartQuiz()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@ -42,10 +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.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
|
||||||
@ -58,7 +76,7 @@ def _start():
|
|||||||
}), 200
|
}), 200
|
||||||
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400
|
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400
|
||||||
return send_errors_to_client(form=form)
|
return send_errors_to_client(form=form)
|
||||||
return render_template('/quiz/start_quiz.html', form = form)
|
return render_template('/quiz/start_quiz.html', form = form, clubs = clubs)
|
||||||
|
|
||||||
@quiz.route('/quiz/')
|
@quiz.route('/quiz/')
|
||||||
def _quiz():
|
def _quiz():
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from ..models import Dataset
|
from ..models import Dataset, Test
|
||||||
from ..tools.logs import write
|
from ..tools.logs import write
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
@ -7,6 +7,8 @@ from flask.helpers import abort, flash, redirect, url_for
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
from statistics import mean, median, stdev
|
||||||
|
from typing import Union
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
def load(filename:str):
|
def load(filename:str):
|
||||||
@ -84,4 +86,83 @@ def check_dataset_exists(function):
|
|||||||
flash('There are no available question datasets. Please upload a question dataset first, or use the question editor to create a new dataset.', 'error')
|
flash('There are no available question datasets. Please upload a question dataset first, or use the question editor to create a new dataset.', 'error')
|
||||||
return redirect(url_for('admin._questions'))
|
return redirect(url_for('admin._questions'))
|
||||||
return function(*args, **kwargs)
|
return function(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def check_test_exists(function):
|
||||||
|
@wraps(function)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try: tests = Test.query.all()
|
||||||
|
except Exception as exception:
|
||||||
|
write('system.log', f'Database error when checking existing datasets: {exception}')
|
||||||
|
return abort(500)
|
||||||
|
if not tests:
|
||||||
|
flash('There are no exams configured. Please create an exam first.', 'error')
|
||||||
|
return redirect(url_for('admin._tests'))
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def analyse(subject:Union[Dataset,Test]) -> dict:
|
||||||
|
output = {
|
||||||
|
'answers': {},
|
||||||
|
'entries': 0,
|
||||||
|
'grades': {
|
||||||
|
'merit': 0,
|
||||||
|
'pass': 0,
|
||||||
|
'fail': 0
|
||||||
|
},
|
||||||
|
'scores': {
|
||||||
|
'mean': 0,
|
||||||
|
'median': 0,
|
||||||
|
'stdev': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores_raw = []
|
||||||
|
if isinstance(subject, Test):
|
||||||
|
for entry in subject.entries:
|
||||||
|
if entry.answers:
|
||||||
|
for question, answer in entry.answers.items():
|
||||||
|
if int(question) not in output['answers']: output['answers'][int(question)] = {}
|
||||||
|
if int(answer) not in output['answers'][int(question)]: output['answers'][int(question)][int(answer)] = 0
|
||||||
|
output['answers'][int(question)][int(answer)] += 1
|
||||||
|
if entry.result:
|
||||||
|
output['entries'] += 1
|
||||||
|
output['grades'][entry.result['grade']] += 1
|
||||||
|
scores_raw.append(int(entry.result['score']))
|
||||||
|
else:
|
||||||
|
for test in subject.tests:
|
||||||
|
for entry in test.entries:
|
||||||
|
if entry.answers:
|
||||||
|
for question, answer in entry.answers.items():
|
||||||
|
if int(question) not in output['answers']: output['answers'][int(question)] = {}
|
||||||
|
if int(answer) not in output['answers'][int(question)]: output['answers'][int(question)][int(answer)] = 0
|
||||||
|
output['answers'][int(question)][int(answer)] += 1
|
||||||
|
if entry.result:
|
||||||
|
output['entries'] += 1
|
||||||
|
output['grades'][entry.result['grade']] += 1
|
||||||
|
scores_raw.append(entry.result['score'])
|
||||||
|
output['scores']['mean'] = mean(scores_raw)
|
||||||
|
output['scores']['median'] = median(scores_raw)
|
||||||
|
output['scores']['stdev'] = stdev(scores_raw, output['scores']['mean']) if len(scores_raw) > 1 else None
|
||||||
|
return output
|
||||||
|
|
||||||
|
def parse_questions(dataset:list):
|
||||||
|
output = []
|
||||||
|
for block in dataset:
|
||||||
|
if block['type'] == 'question':
|
||||||
|
question = {
|
||||||
|
'q_no': block['q_no'],
|
||||||
|
'tags': block['tags'],
|
||||||
|
'correct': block['correct']
|
||||||
|
}
|
||||||
|
question['options'] = [*enumerate(block['options'])]
|
||||||
|
output.append(question)
|
||||||
|
elif block['type'] == 'block':
|
||||||
|
for _question in block['questions']:
|
||||||
|
question = {
|
||||||
|
'q_no': _question['q_no'],
|
||||||
|
'tags': _question['tags'],
|
||||||
|
'correct': _question['correct']
|
||||||
|
}
|
||||||
|
question['options'] = [*enumerate(_question['options'])]
|
||||||
|
output.append(question)
|
||||||
|
return output
|
@ -10,9 +10,10 @@ from functools import wraps
|
|||||||
def parse_test_code(code):
|
def parse_test_code(code):
|
||||||
return code.replace('—', '').lower()
|
return code.replace('—', '').lower()
|
||||||
|
|
||||||
def generate_questions(dataset:list):
|
def generate_questions(dataset:list, randomise:bool=True):
|
||||||
output = []
|
output = []
|
||||||
for block in randomise_list(dataset):
|
question_dataset = randomise_list(dataset) if randomise else dataset
|
||||||
|
for block in question_dataset:
|
||||||
if block['type'] == 'question':
|
if block['type'] == 'question':
|
||||||
question = {
|
question = {
|
||||||
'type': 'question',
|
'type': 'question',
|
||||||
@ -20,11 +21,12 @@ def generate_questions(dataset:list):
|
|||||||
'question_header': '',
|
'question_header': '',
|
||||||
'text': block['text']
|
'text': block['text']
|
||||||
}
|
}
|
||||||
if block['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(block['options'])])
|
if block['q_type'] == 'Multiple Choice' and randomise: question['options'] = randomise_list([*enumerate(block['options'])])
|
||||||
else: question['options'] = [*enumerate(block['options'])]
|
else: question['options'] = [*enumerate(block['options'])]
|
||||||
output.append(question)
|
output.append(question)
|
||||||
elif block['type'] == 'block':
|
elif block['type'] == 'block':
|
||||||
for key, _question in enumerate(randomise_list(block['questions'])):
|
block_questions = randomise_list(block['questions']) if randomise else block['questions']
|
||||||
|
for key, _question in enumerate(block_questions):
|
||||||
question = {
|
question = {
|
||||||
'type': 'block',
|
'type': 'block',
|
||||||
'q_no': _question['q_no'],
|
'q_no': _question['q_no'],
|
||||||
@ -33,7 +35,7 @@ def generate_questions(dataset:list):
|
|||||||
'block_q_no': key,
|
'block_q_no': key,
|
||||||
'text': _question['text']
|
'text': _question['text']
|
||||||
}
|
}
|
||||||
if _question['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(_question['options'])])
|
if _question['q_type'] == 'Multiple Choice' and randomise: question['options'] = randomise_list([*enumerate(_question['options'])])
|
||||||
else: question['options'] = [*enumerate(_question['options'])]
|
else: question['options'] = [*enumerate(_question['options'])]
|
||||||
output.append(question)
|
output.append(question)
|
||||||
return output
|
return output
|
||||||
|
@ -110,6 +110,27 @@ function parse_data(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analyse Button
|
||||||
|
$('.dataset-analyse').click(function(event) {
|
||||||
|
|
||||||
|
let id = $(this).data('id')
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/analysis/`,
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({'id': id, 'class': 'dataset'}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = response
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
error_response(response)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
// Fetch data once page finishes loading
|
// Fetch data once page finishes loading
|
||||||
$(window).on('load', function() {
|
$(window).on('load', function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -20,8 +20,28 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<li class="nav-item" id="nav-results">
|
<li class="nav-item dropdown" id="nav-results">
|
||||||
<a href="{{ url_for('admin._view_entries') }}" id="link-results" class="nav-link">View Results</a>
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-results"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin._view_entries') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Results
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-results"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin._view_entries', filter='active') }}" id="link-results" class="dropdown-item">View Results</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('analysis._analysis') }}" id="link-analysis" class="dropdown-item">Analysis</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown" id="nav-tests">
|
<li class="nav-item dropdown" id="nav-tests">
|
||||||
<a
|
<a
|
||||||
@ -36,7 +56,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul
|
<ul
|
||||||
class="dropdown-menu"
|
class="dropdown-menu"
|
||||||
aria-labelledby="dropdown-settings"
|
aria-labelledby="dropdown-tests"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
<a href="{{ url_for('admin._tests', filter='active') }}" id="link-active" class="dropdown-item">Active</a>
|
||||||
@ -58,7 +78,7 @@
|
|||||||
<li class="nav-item dropdown" id="nav-settings">
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
<a
|
<a
|
||||||
class="nav-link dropdown-toggle"
|
class="nav-link dropdown-toggle"
|
||||||
id="dropdown-account"
|
id="dropdown-settings"
|
||||||
role="button"
|
role="button"
|
||||||
href="{{ url_for('admin._settings') }}"
|
href="{{ url_for('admin._settings') }}"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
|
@ -100,7 +100,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-success dataset-analyse {% if not dataset.entries %} disabled {% endif %}"
|
||||||
|
data-id="{{dataset.id}}"
|
||||||
|
title="Analyse Answers"
|
||||||
|
data-action="analyse"
|
||||||
|
>
|
||||||
|
<i class="bi bi-search button-icon"></i>
|
||||||
|
Analyse Answers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -38,9 +38,9 @@ def _view_console(id:str=None):
|
|||||||
datasets = Dataset.query.count()
|
datasets = Dataset.query.count()
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
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 dataset:
|
if not dataset:
|
||||||
flash('Invalid dataset ID.', 'error')
|
flash('Invalid dataset ID.', 'error')
|
||||||
return redirect(url_for('admin._questions'))
|
return redirect(url_for('admin._questions'))
|
||||||
return render_template('/view/console.html', dataset=dataset, datasets=datasets, users=users)
|
return render_template('/view/console.html', dataset=dataset)
|
@ -1,32 +1,33 @@
|
|||||||
blinker==1.5
|
blinker==1.5
|
||||||
cffi==1.15.1
|
cffi==1.15.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
cryptography==38.0.1
|
cryptography==39.0.2
|
||||||
dnspython==2.2.1
|
dnspython==2.3.0
|
||||||
dominate==2.7.0
|
dominate==2.7.0
|
||||||
email-validator==1.2.1
|
email-validator==1.3.1
|
||||||
Flask==2.2.2
|
Flask==2.2.3
|
||||||
Flask-Bootstrap==3.3.7.1
|
Flask-Bootstrap==3.3.7.1
|
||||||
Flask-Login==0.6.2
|
Flask-Login==0.6.2
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
Flask-SQLAlchemy==2.5.1
|
Flask-SQLAlchemy==3.0.3
|
||||||
Flask-WTF==1.0.1
|
Flask-WTF==1.1.1
|
||||||
greenlet==1.1.3
|
greenlet==2.0.2
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
idna==3.3
|
idna==3.4
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
MarkupSafe==2.1.1
|
MarkupSafe==2.1.2
|
||||||
pip==22.2.2
|
pip==23.0.1
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
PyMySQL==1.0.2
|
PyMySQL==1.0.2
|
||||||
python-dotenv==0.21.0
|
python-dotenv==1.0.0
|
||||||
setuptools==65.3.0
|
setuptools==67.4.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
SQLAlchemy==1.4.41
|
SQLAlchemy==2.0.4
|
||||||
sqlalchemy-json==0.5.0
|
sqlalchemy-json==0.5.0
|
||||||
SQLAlchemy-Utils==0.38.3
|
SQLAlchemy-Utils==0.40.0
|
||||||
|
typing_extensions==4.5.0
|
||||||
visitor==0.1.3
|
visitor==0.1.3
|
||||||
Werkzeug==2.2.2
|
Werkzeug==2.2.3
|
||||||
wheel==0.37.1
|
wheel==0.38.4
|
||||||
WTForms==3.0.1
|
WTForms==3.0.1
|
||||||
|
Loading…
Reference in New Issue
Block a user