Merge branch 'development' of ssh://git.vsnt.uk:2222/viveksantayana/ska-referee-test into development
This commit is contained in:
commit
8013a776a9
@ -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()
|
||||||
|
@ -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 }}
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
@ -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()
|
||||||
})
|
})
|
@ -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 ‘the next n questions are about a specific scenario’, the app uses the placeholder <code><block_remaining_questions></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 %}
|
{% 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 %}
|
@ -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>
|
@ -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"/>
|
||||||
|
@ -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 %}
|
@ -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 %}
|
@ -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():
|
||||||
|
try:
|
||||||
_tests = Test.query.all()
|
_tests = Test.query.all()
|
||||||
tests = [ test for test in _tests if test.entries ]
|
|
||||||
_datasets = Dataset.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 ]
|
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()))
|
@ -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()
|
||||||
|
@ -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():
|
||||||
@ -146,3 +144,25 @@ def analyse(subject:Union[Dataset,Test]) -> dict:
|
|||||||
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
|
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({
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user