Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into development

This commit is contained in:
Vivek Santayana 2023-07-01 21:26:24 +01:00
commit 8013a776a9
19 changed files with 331 additions and 236 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,8 @@
.info-panel {
display: none;
}
.control-panel {
margin-left: auto;
margin-right: 0;
width:fit-content;
}
#alert-box { #alert-box {
margin: 30px auto; margin: 30px auto;
max-width: 460px; max-width: 460px;
} }
.block { .cell-percentage::after {
border: 2px solid black; content: '%';
border-radius: 10px;
margin: 10px;
padding: 5px;
}
.question-body, .question-block {
padding: 0px 2em;
}
blockquote {
padding: 0px 2em;
font-style: italic;
} }

View File

@ -1,130 +1,27 @@
// Variable Declarations // Analyse Button Listener
const $control_panel = $('.control-panel') $('.button-analyse').click(function(event) {
const $info_panel = $('.info-panel')
const $viewer_panel = $('.viewer-panel')
var element_index = 0 let buttonClass = $(this).data('class')
let id = null
// Info Button Listener if (buttonClass == 'test' ) {
$control_panel.find('button').click(function(event){ id = $('#select-test').children('option:selected').val()
if ($info_panel.is(":hidden")) { } else if (buttonClass == 'dataset' ) {
$viewer_panel.hide() id = $('#select-dataset').children('option:selected').val()
$info_panel.fadeIn()
$(this).addClass('active')
} else {
$info_panel.hide()
$viewer_panel.fadeIn()
$(this).removeClass('active')
} }
event.preventDefault()
})
function parse_data(data) {
var block
var obj
for (let i = 0; i < data.length; i++) {
block = data[i]
obj = document.createElement('div')
obj.classList = 'block'
if (block['type'] == 'question') {
text = document.createElement('p')
text.innerHTML = `<strong>Question ${block['q_no'] + 1}.</strong> ${block['text']}`
obj.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${block['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let _i = 0; _i < block['options'].length; _i++) {
option = document.createElement('li')
option.innerHTML = block['options'][_i]
if (block['correct'] == _i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let _i = 0; _i < block['tags'].length; _i++) {
tag = document.createElement('li')
tag.innerHTML = block['tags'][_i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
obj.append(question_body)
} else if (block['type'] == 'block') {
meta = document.createElement('p')
meta.innerHTML = `<strong>Block ${i+1}.</strong> ${block['questions'].length} questions.`
obj.append(meta)
header = document.createElement('blockquote')
header.innerText = block['question_header']
obj.append(header)
var block_question = document.createElement('div')
var question
block_question.className = 'question-block'
for (let _i = 0; _i < block['questions'].length; _i++) {
question = block['questions'][_i]
text = document.createElement('p')
text.innerHTML = `<strong>Question ${question['q_no'] + 1}.</strong> ${question['text']}`
block_question.append(text)
question_body = document.createElement('div')
question_body.className ='question-body'
type = document.createElement('p')
type.innerHTML = `<strong>Question Type:</strong> ${question['q_type']}`
question_body.append(type)
options = document.createElement('p')
options.innerHTML = '<strong>Options:</strong>'
option_list = document.createElement('ul')
for (let __i = 0; __i < question['options'].length; __i++) {
option = document.createElement('li')
option.innerHTML = question['options'][__i]
if (question['correct'] == __i) {
option.innerHTML += ' <span class="badge rounded-pill bg-success">Correct</span>'
}
option_list.append(option)
}
options.append(option_list)
question_body.append(options)
tags = document.createElement('p')
tags.innerHTML = `<strong>Tags:</strong>`
tag_list = document.createElement('ul')
for (let __i = 0; __i < question['tags'].length; __i++) {
tag = document.createElement('li')
tag.innerHTML = question['tags'][__i]
tag_list.append(tag)
}
tags.append(tag_list)
question_body.append(tags)
block_question.append(question_body)
obj.append(block_question)
}
}
$viewer_panel.append(obj)
}
}
// Fetch data once page finishes loading
$(window).on('load', function() {
$.ajax({ $.ajax({
url: target, url: `/admin/analysis/`,
type: 'POST', type: 'POST',
data: JSON.stringify({ data: JSON.stringify({'id': id, 'class': buttonClass}),
'id': id,
'action': 'fetch'
}),
contentType: 'application/json', contentType: 'application/json',
success: function(response) { success: function(response) {
parse_data(response['data']) window.location.href = response
},
error: function(response){
error_response(response)
}, },
error: function(response) {
console.log(response)
}
}) })
event.preventDefault()
}) })

View File

@ -1,74 +1,22 @@
{% extends "view/components/base.html" %} {% extends "analysis/components/datatable.html" %}
{% block style %} {% block style %}
<link <link
rel="stylesheet" rel="stylesheet"
href="{{ url_for('.static', filename='css/view.css') }}" href="{{ url_for('.static', filename='css/analysis.css') }}"
/> />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>View Questions</h1> <h1>Analysis</h1>
<div class="container"> <div class="container">
<p class="lead"> <p class="lead">
This page lists all the questions in the selected dataset. Analysis for {{ type }} {{ subject }}.
</p> </p>
</div> </div>
<div class="container control-panel"> <div class="container">
<button class="btn btn-primary" aria-title="Information" title="Information"><i class="bi bi-info-circle-fill"></i></button>
</div>
<div class="container info-panel">
<h3> <h3>
Information Question List
</h3>
<p>
Questions in the test are arranged in blocks. Blocks can be of two types: <strong>Blocks</strong> of multiple related questions, and <strong>Single Questions</strong> that are not part of a block.
You can add, remove, or edit both Blockss and Questions through this editor.
</p>
<p>
<strong>Blocks</strong> are useful when you have a section of the test that contains multiple questions that are related to each other, for example if there is a scenario-based section where a series of questions are about the same situation.
</p>
<p>
Blocks can contain any number of questions within them, but cannot contain nested blocks.
</p>
<p>
When you set up a block, you can also add <strong>header text</strong> that will be displayed with each question.
You can use this to provide common information for a scenario across a series of questions.
</p>
<p>
Questions come in three types:
<ul>
<li>
<strong>Yes/No</strong> for when there is only a yes or no option,
</li>
<li>
<strong>Multiple Choice</strong> for your regular multiple choice questions, and
</li>
<li>
<strong>Ordered List</strong> for multiple choice questions that will be displayed in the same order as listed here.
</li>
</ul>
</p>
<p>
Normally, multiple choice questions will have the order of the options randomised.
</p>
<p>
Questions will be displayed to candidates in a randomised order.
Blocks of questions will be kept together, but the order within the block will also be randomised.
</p>
<p>
Questions can also be categorised using <strong>tags</strong>.
</p>
<p class="lead">
Placeholder for Questions Remaining in a Block
</p>
<p>
In order to show how many questions are remaining inside a block, e.g. to say &lsquo;the next n questions are about a specific scenario&rsquo;, the app uses the placeholder <code>&lt;block_remaining_questions&gt;</code>.
</p>
</div>
<div class="container viewer-panel">
<h3>
Question Dataset
</h3> </h3>
<div class="container dataset-metadata"> <div class="container dataset-metadata">
<div class="input-group mb-3"> <div class="input-group mb-3">
@ -100,7 +48,65 @@
</div> </div>
{% endif %} {% endif %}
</div> </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> </div>
{% endblock %} {% endblock %}
@ -111,6 +117,54 @@
</script> </script>
<script <script
type="text/javascript" type="text/javascript"
src="{{ url_for('.static', filename='js/view.js') }}" src="{{ url_for('.static', filename='js/analysis.js') }}"
></script> ></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 %} {% endblock %}

View File

@ -22,24 +22,24 @@
{% block style %} {% block style %}
{% endblock %} {% endblock %}
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title> <title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
{% include "view/components/og-meta.html" %} {% include "analysis/components/og-meta.html" %}
</head> </head>
<body class="bg-light"> <body class="bg-light">
{% block navbar %} {% block navbar %}
{% include "view/components/navbar.html" %} {% include "analysis/components/navbar.html" %}
{% endblock %} {% endblock %}
<div class="container"> <div class="container">
{% block top_alerts %} {% block top_alerts %}
{% include "view/components/server-alerts.html" %} {% include "analysis/components/server-alerts.html" %}
{% endblock %} {% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<footer class="container site-footer mt-5"> <footer class="container site-footer mt-5">
{% block footer %} {% block footer %}
{% include "view/components/footer.html" %} {% include "analysis/components/footer.html" %}
{% endblock %} {% endblock %}
</footer> </footer>
@ -78,7 +78,15 @@
type="text/javascript" type="text/javascript"
src="{{ url_for('.static', filename='js/script.js') }}" src="{{ url_for('.static', filename='js/script.js') }}"
></script> ></script>
<script
type="text/javascript"
src="{{ url_for('.static', filename='js/analysis.js') }}"
></script>
{% block script %} {% block script %}
{% endblock %} {% endblock %}
{% block datatable_scripts %}
{% endblock %}
{% block custom_data_script %}
{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,4 +1,4 @@
{% extends "view/components/base.html" %} {% extends "analysis/components/base.html" %}
{% block datatable_css %} {% 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/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/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>

View File

@ -1,4 +1,4 @@
{% extends "view/components/base.html" %} {% extends "analysis/components/base.html" %}
{% import "bootstrap/wtf.html" as wtf %} {% import "bootstrap/wtf.html" as wtf %}
{% block top_alerts %} {% block top_alerts %}
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "view/components/input-forms.html" %} {% extends "analysis/components/input-forms.html" %}
{% block content %} {% block content %}
<h1>Analysis</h1> <h1>Analysis</h1>
@ -9,7 +9,19 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Exams</h5> <h5 class="card-title">Exams</h5>
<div class="card-text"> <div class="card-text">
{{ tests }} <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> </div>
@ -19,11 +31,24 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Datasets</h5> <h5 class="card-title">Datasets</h5>
<div class="card-text"> <div class="card-text">
{{ datasets }} <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>
</div> </div>
</div> </div>
</div> </div>
{% include "analysis/components/client-alerts.html" %}
{% endblock %} {% endblock %}

View File

@ -1,8 +1,9 @@
from ..models import Dataset, Test from ..models import Dataset, Test
from ..tools.data import analyse, check_dataset_exists, check_test_exists from ..tools.data import analyse, check_dataset_exists, check_test_exists
from ..tools.logs import write from ..tools.logs import write
from ..tools.data import parse_questions
from flask import Blueprint, jsonify, render_template, request from flask import Blueprint, render_template, request, jsonify
from flask.helpers import abort, flash, redirect, url_for from flask.helpers import abort, flash, redirect, url_for
from flask_login import login_required from flask_login import login_required
@ -18,10 +19,33 @@ analysis = Blueprint(
@check_dataset_exists @check_dataset_exists
@check_test_exists @check_test_exists
def _analysis(): def _analysis():
_tests = Test.query.all() 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 ] tests = [ test for test in _tests if test.entries ]
_datasets = Dataset.query.all()
datasets = [ dataset for dataset in _datasets if dataset.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) return render_template('/analysis/index.html', tests=tests, datasets=datasets)
@analysis.route('/test/<string:id>') @analysis.route('/test/<string:id>')
@ -40,8 +64,7 @@ def _test(id:str=None):
if not test: if not test:
flash('Invalid exam.', 'error') flash('Invalid exam.', 'error')
return redirect(url_for('analysis._analysis')) return redirect(url_for('analysis._analysis'))
return jsonify(analyse(test)) 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()))
return render_template('/analysis/analysis.html', analysis=None, text='Exam')
@analysis.route('/dataset/<string:id>') @analysis.route('/dataset/<string:id>')
@analysis.route('/dataset/') @analysis.route('/dataset/')
@ -59,4 +82,4 @@ def _dataset(id:str=None):
if not dataset: if not dataset:
flash('Invalid dataset.', 'error') flash('Invalid dataset.', 'error')
return redirect(url_for('analysis._analysis')) return redirect(url_for('analysis._analysis'))
return jsonify(analyse(dataset)) return render_template('/analysis/analysis.html', analysis=analyse(dataset), subject=dataset.get_name(), type='dataset', dataset=dataset, questions=parse_questions(dataset.get_data()))

View File

@ -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
@ -116,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()

View File

@ -117,7 +117,6 @@ def analyse(subject:Union[Dataset,Test]) -> dict:
} }
} }
scores_raw = [] scores_raw = []
dataset = subject if isinstance(subject, Dataset) else subject.dataset
if isinstance(subject, Test): if isinstance(subject, Test):
for entry in subject.entries: for entry in subject.entries:
if entry.answers: if entry.answers:
@ -131,7 +130,6 @@ def analyse(subject:Union[Dataset,Test]) -> dict:
scores_raw.append(int(entry.result['score'])) scores_raw.append(int(entry.result['score']))
else: else:
for test in subject.tests: for test in subject.tests:
output['entries'] += len(test.entries)
for entry in test.entries: for entry in test.entries:
if entry.answers: if entry.answers:
for question, answer in entry.answers.items(): for question, answer in entry.answers.items():
@ -145,4 +143,26 @@ def analyse(subject:Union[Dataset,Test]) -> dict:
output['scores']['mean'] = mean(scores_raw) output['scores']['mean'] = mean(scores_raw)
output['scores']['median'] = median(scores_raw) output['scores']['median'] = median(scores_raw)
output['scores']['stdev'] = stdev(scores_raw, output['scores']['mean']) if len(scores_raw) > 1 else None 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 return output

View File

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

View File

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

View File

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