Added functionality for default datasets.
Incorporated dataset selector into test creation.
This commit is contained in:
parent
e37d287397
commit
f8d97314d3
@ -55,6 +55,8 @@ class CreateTest(FlaskForm):
|
|||||||
]
|
]
|
||||||
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
expiry_date = DateField('Expiry Date', format="%Y-%m-%d", validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
||||||
time_limit = SelectField('Time Limit', choices=time_options)
|
time_limit = SelectField('Time Limit', choices=time_options)
|
||||||
|
dataset = SelectField('Question Dataset')
|
||||||
|
|
||||||
class UploadDataForm(FlaskForm):
|
class UploadDataForm(FlaskForm):
|
||||||
data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])])
|
data_file = FileField('Data File', validators=[FileRequired(), FileAllowed(['json'])])
|
||||||
|
default = BooleanField('Make Default', render_kw={'checked': True})
|
@ -3,17 +3,20 @@ from datetime import datetime
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from flask import flash, jsonify
|
from flask import flash, jsonify
|
||||||
import secrets
|
import secrets
|
||||||
|
import os
|
||||||
|
from json import dump, loads
|
||||||
|
|
||||||
from main import db
|
from main import app, db
|
||||||
from common.security import encrypt
|
from common.security import encrypt
|
||||||
|
|
||||||
class Test:
|
class Test:
|
||||||
def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None):
|
def __init__(self, _id=None, start_date=None, expiry_date=None, time_limit=None, creator=None, dataset=None):
|
||||||
self._id = _id
|
self._id = _id
|
||||||
self.start_date = start_date
|
self.start_date = start_date
|
||||||
self.expiry_date = expiry_date
|
self.expiry_date = expiry_date
|
||||||
self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit
|
self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit
|
||||||
self.creator = creator
|
self.creator = creator
|
||||||
|
self.dataset = dataset
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
test = {
|
test = {
|
||||||
@ -23,9 +26,16 @@ class Test:
|
|||||||
'expiry_date': self.expiry_date,
|
'expiry_date': self.expiry_date,
|
||||||
'time_limit': self.time_limit,
|
'time_limit': self.time_limit,
|
||||||
'creator': encrypt(self.creator),
|
'creator': encrypt(self.creator),
|
||||||
'test_code': secrets.token_hex(6).upper()
|
'test_code': secrets.token_hex(6).upper(),
|
||||||
|
'dataset': self.dataset
|
||||||
}
|
}
|
||||||
if db.tests.insert_one(test):
|
if db.tests.insert_one(test):
|
||||||
|
dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset)
|
||||||
|
with open(dataset_file_path, 'r') as dataset_file:
|
||||||
|
data = loads(dataset_file.read())
|
||||||
|
data['meta']['tests'].append(self._id)
|
||||||
|
with open(dataset_file_path, 'w') as dataset_file:
|
||||||
|
dump(data, dataset_file, indent=2)
|
||||||
flash(f'Created a new exam with Exam Code <strong>{self.render_test_code(test["test_code"])}</strong>.', 'success')
|
flash(f'Created a new exam with Exam Code <strong>{self.render_test_code(test["test_code"])}</strong>.', 'success')
|
||||||
return jsonify({'success': test}), 200
|
return jsonify({'success': test}), 200
|
||||||
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
|
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
|
||||||
@ -54,7 +64,15 @@ class Test:
|
|||||||
return test_code.replace('—', '')
|
return test_code.replace('—', '')
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
if self.dataset is None:
|
||||||
|
self.dataset = db.tests.find_one({'_id': self._id})['dataset']
|
||||||
if db.tests.delete_one({'_id': self._id}):
|
if db.tests.delete_one({'_id': self._id}):
|
||||||
|
dataset_file_path = os.path.join(app.config["DATA_FILE_DIRECTORY"],self.dataset)
|
||||||
|
with open(dataset_file_path, 'r') as dataset_file:
|
||||||
|
data = loads(dataset_file.read())
|
||||||
|
data['meta']['tests'].remove(self._id)
|
||||||
|
with open(dataset_file_path, 'w') as dataset_file:
|
||||||
|
dump(data, dataset_file, indent=2)
|
||||||
message = 'Deleted exam.'
|
message = 'Deleted exam.'
|
||||||
flash(message, 'alert')
|
flash(message, 'alert')
|
||||||
return jsonify({'success': message}), 200
|
return jsonify({'success': message}), 200
|
||||||
|
@ -132,16 +132,11 @@ table.dataTable {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-table-row {
|
.table-row {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row-actions {
|
.row-actions {
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-row-actions {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@ -153,8 +148,8 @@ table.dataTable {
|
|||||||
text-align:center;
|
text-align:center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row-actions button {
|
.row-actions button, .row-actions a {
|
||||||
margin: 0px 10px;
|
margin: 0px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cookie-alert {
|
#cookie-alert {
|
||||||
@ -214,6 +209,11 @@ table.dataTable {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-upload {
|
||||||
|
margin: 2rem 0;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fallback for Edge
|
/* Fallback for Edge
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
@supports (-ms-ime-align: auto) {
|
@supports (-ms-ime-align: auto) {
|
||||||
|
@ -20,7 +20,7 @@ $('form[name=form-register]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -62,7 +62,7 @@ $('form[name=form-login]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -104,7 +104,7 @@ $('form[name=form-reset]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -146,7 +146,7 @@ $('form[name=form-update-password]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
console.log(data)
|
console.log(data)
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -188,7 +188,7 @@ $('form[name=form-create-user]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -230,7 +230,7 @@ $('form[name=form-delete-user]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -272,7 +272,7 @@ $('form[name=form-update-user]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -314,7 +314,7 @@ $('form[name=form-update-account]').submit(function(event) {
|
|||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
|
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -355,7 +355,7 @@ $('form[name=form-create-test]').submit(function(event) {
|
|||||||
var $form = $(this);
|
var $form = $(this);
|
||||||
var alert = document.getElementById('alert-box');
|
var alert = document.getElementById('alert-box');
|
||||||
var data = $form.serialize();
|
var data = $form.serialize();
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -398,7 +398,7 @@ $('form[name=form-upload-questions]').submit(function(event) {
|
|||||||
var data = new FormData($form[0]);
|
var data = new FormData($form[0]);
|
||||||
var file = $('input[name=data_file]')[0].files[0]
|
var file = $('input[name=data_file]')[0].files[0]
|
||||||
data.append('file', file)
|
data.append('file', file)
|
||||||
alert.innerHTML = ''
|
alert.innerHTML = '';
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname,
|
url: window.location.pathname,
|
||||||
@ -407,25 +407,7 @@ $('form[name=form-upload-questions]').submit(function(event) {
|
|||||||
processData: false,
|
processData: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (typeof response.success === 'string' || response.success instanceof String) {
|
window.location.reload();
|
||||||
alert.innerHTML = alert.innerHTML + `
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
|
||||||
${response.success}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (response.success instanceof Array) {
|
|
||||||
for (var i = 0; i < response.success.length; i ++) {
|
|
||||||
alert.innerHTML = alert.innerHTML + `
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
|
||||||
${response.success[i]}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: function(response) {
|
error: function(response) {
|
||||||
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
@ -455,10 +437,10 @@ $('form[name=form-upload-questions]').submit(function(event) {
|
|||||||
|
|
||||||
|
|
||||||
// Edit and Delete Test Button Handlers
|
// Edit and Delete Test Button Handlers
|
||||||
|
|
||||||
$('.delete-test').click(function(event) {
|
$('.delete-test').click(function(event) {
|
||||||
|
|
||||||
_id = $(this).data('_id')
|
_id = $(this).data('_id')
|
||||||
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `/admin/tests/delete/${_id}`,
|
url: `/admin/tests/delete/${_id}`,
|
||||||
@ -492,6 +474,89 @@ $('.delete-test').click(function(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit and Delete Dataset Button Handlers
|
||||||
|
$('.delete-question-dataset').click(function(event) {
|
||||||
|
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
alert.innerHTML = '';
|
||||||
|
|
||||||
|
var filename = $(this).data('filename');
|
||||||
|
var disabled = $(this).hasClass('disabled');
|
||||||
|
|
||||||
|
if ( !disabled ) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/settings/questions/delete/${filename}`,
|
||||||
|
type: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<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) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.edit-question-dataset').click(function(event) {
|
||||||
|
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
alert.innerHTML = '';
|
||||||
|
|
||||||
|
var filename = $(this).data('filename');
|
||||||
|
var disabled = $(this).hasClass('disabled');
|
||||||
|
|
||||||
|
if ( !disabled ) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/admin/settings/questions/default/${filename}`,
|
||||||
|
type: 'GET',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<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) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
// Dismiss Cookie Alert
|
// Dismiss Cookie Alert
|
||||||
$('#dismiss-cookie-alert').click(function(event){
|
$('#dismiss-cookie-alert').click(function(event){
|
||||||
|
|
||||||
@ -503,13 +568,13 @@ $('#dismiss-cookie-alert').click(function(event){
|
|||||||
},
|
},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(response){
|
success: function(response){
|
||||||
console.log(response)
|
console.log(response);
|
||||||
},
|
},
|
||||||
error: function(response){
|
error: function(response){
|
||||||
console.log(response)
|
console.log(response);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
|
||||||
})
|
})
|
@ -24,7 +24,7 @@
|
|||||||
<a href="{{ url_for('results._results') }}" id="link-results" class="nav-link">View Results</a>
|
<a href="{{ url_for('results._results') }}" id="link-results" class="nav-link">View Results</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" id="nav-tests">
|
<li class="nav-item" id="nav-tests">
|
||||||
<a href="{{ url_for('admin_views.tests') }}" id="link-tests" class="nav-link">Manage Tests</a>
|
<a href="{{ url_for('admin_views.tests') }}" id="link-tests" class="nav-link">Manage Exams</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown" id="nav-settings">
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
<a
|
<a
|
||||||
|
@ -1,13 +1,95 @@
|
|||||||
{% extends "admin/components/base.html" %}
|
{% extends "admin/components/datatable.html" %}
|
||||||
{% block title %} SKA Referee Test | Upload Questions {% endblock %}
|
{% block title %} SKA Referee Test | Upload Questions {% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- <h1>Upload Question Dataset</h1> -->
|
<h1>Manage Question Datasets</h1>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
{% if data %}
|
||||||
|
<table id="question-datasets-table" class="table table-striped" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th data-priority="1">
|
||||||
|
File Name
|
||||||
|
</th>
|
||||||
|
<th data-priority="2">
|
||||||
|
Uploaded
|
||||||
|
</th>
|
||||||
|
<th data-priority="3">
|
||||||
|
Author
|
||||||
|
</th>
|
||||||
|
<th data-priority="3">
|
||||||
|
Use
|
||||||
|
</th>
|
||||||
|
<th data-priority="1">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for element in data %}
|
||||||
|
<tr class="table-row">
|
||||||
|
<td>
|
||||||
|
{% if element.filename == default %}
|
||||||
|
<div class="text-success" title="Default Dataset">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ element.filename }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ element.timestamp.strftime('%d %b %Y') }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ element.author }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ element.use }}
|
||||||
|
</td>
|
||||||
|
<td class="row-actions">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-primary edit-question-dataset {% if element.filename == default %}disabled{% endif %}"
|
||||||
|
data-filename="{{ element.filename }}"
|
||||||
|
title="Make Default"
|
||||||
|
>
|
||||||
|
<i class="bi bi-file-earmark-text-fill button-icon"></i>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-danger delete-question-dataset {% if element.filename == default %}disabled{% endif %}"
|
||||||
|
data-filename="{{ element.filename }}"
|
||||||
|
title="Delete Dataset"
|
||||||
|
>
|
||||||
|
<i class="bi bi-file-earmark-excel-fill button-icon"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary alert-db-empty">
|
||||||
|
<i class="bi bi-info-circle-fill" aria-title="Alert" title="Alert"></i>
|
||||||
|
There are no question datasets uploaded. Please use the panel below to upload a new question dataset.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<form name="form-upload-questions" method="post" action="#" class="form-signin" enctype="multipart/form-data">
|
<form name="form-upload-questions" method="post" action="#" class="form-signin" enctype="multipart/form-data">
|
||||||
<h2 class="form-signin-heading">Upload Question Dataset</h2>
|
<h2 class="form-signin-heading">Upload Question Dataset</h2>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ form.data_file() }}
|
<div class="form-upload">
|
||||||
{% include "admin/components/client-alerts.html" %}
|
{{ form.data_file() }}
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.default(class_="form-check-input") }}
|
||||||
|
{{ form.default.label }}
|
||||||
|
</div>
|
||||||
<div class="container form-submission-button">
|
<div class="container form-submission-button">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
@ -20,4 +102,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% if data %}
|
||||||
|
{% block custom_data_script %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#question-datasets-table').DataTable({
|
||||||
|
'columnDefs': [
|
||||||
|
{'sortable': false, 'targets': [0,5]},
|
||||||
|
{'searchable': false, 'targets': [0,4,5]}
|
||||||
|
],
|
||||||
|
'order': [[2, 'desc'], [3, 'asc']],
|
||||||
|
'responsive': 'true',
|
||||||
|
'fixedHeader': 'true',
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
$('#question-datasets-table').show();
|
||||||
|
$(window).trigger('resize');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% endif %}
|
@ -21,7 +21,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr class="user-table-row">
|
<tr class="table-row">
|
||||||
<td>
|
<td>
|
||||||
{% if user._id == get_id_from_cookie() %}
|
{% if user._id == get_id_from_cookie() %}
|
||||||
<div class="text-success" title="Current User">
|
<div class="text-success" title="Current User">
|
||||||
@ -37,7 +37,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
</td>
|
</td>
|
||||||
<td class="user-row-actions">
|
<td class="row-actions">
|
||||||
<a
|
<a
|
||||||
href="
|
href="
|
||||||
{% if not user._id == get_id_from_cookie() %}
|
{% if not user._id == get_id_from_cookie() %}
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for test in tests %}
|
{% for test in tests %}
|
||||||
<tr class="user-table-row">
|
<tr class="table-row">
|
||||||
<td>
|
<td>
|
||||||
{{ test.start_date.strftime('%d %b %Y') }}
|
{{ test.start_date.strftime('%d %b %Y') }}
|
||||||
</td>
|
</td>
|
||||||
@ -56,7 +56,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{{ test.attempts|length }}
|
{{ test.attempts|length }}
|
||||||
</td>
|
</td>
|
||||||
<td class="test-row-actions">
|
<td class="row-actions">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-primary edit-test"
|
class="btn btn-primary edit-test"
|
||||||
@ -101,6 +101,10 @@
|
|||||||
{{ form.time_limit(placeholder="Select Time Limit") }}
|
{{ form.time_limit(placeholder="Select Time Limit") }}
|
||||||
{{ form.time_limit.label }}
|
{{ form.time_limit.label }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-select-input">
|
||||||
|
{{ form.dataset(placeholder="Select Question Dataset") }}
|
||||||
|
{{ form.dataset.label }}
|
||||||
|
</div>
|
||||||
{% include "admin/components/client-alerts.html" %}
|
{% include "admin/components/client-alerts.html" %}
|
||||||
<div class="container form-submission-button">
|
<div class="container form-submission-button">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -3,16 +3,20 @@ from flask.helpers import url_for
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import os
|
||||||
|
from glob import glob
|
||||||
|
from json import loads
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from common.security.database import decrypt_find, decrypt_find_one
|
from common.security.database import decrypt_find, decrypt_find_one
|
||||||
from .models.users import User
|
from .models.users import User
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from main import db
|
from main import app, db
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import secrets
|
import secrets
|
||||||
from main import mail
|
from main import mail
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from .models.tests import Test
|
from .models.tests import Test
|
||||||
|
from common.data_tools import get_default_dataset
|
||||||
|
|
||||||
views = Blueprint(
|
views = Blueprint(
|
||||||
'admin_views',
|
'admin_views',
|
||||||
@ -65,6 +69,18 @@ def disable_if_logged_in(function):
|
|||||||
return function(*args, **kwargs)
|
return function(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
def available_datasets():
|
||||||
|
files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
||||||
|
default = get_default_dataset()
|
||||||
|
output = []
|
||||||
|
for file in files:
|
||||||
|
filename = file.rsplit('/')[-1]
|
||||||
|
label = f'{filename[:-5]} (Default)' if filename == default else filename[:-5]
|
||||||
|
element = (filename, label)
|
||||||
|
output.append(element)
|
||||||
|
output.reverse()
|
||||||
|
return output
|
||||||
|
|
||||||
@views.route('/')
|
@views.route('/')
|
||||||
@views.route('/home/')
|
@views.route('/home/')
|
||||||
@views.route('/dashboard/')
|
@views.route('/dashboard/')
|
||||||
@ -236,41 +252,96 @@ def update_user(_id:str):
|
|||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def questions():
|
def questions():
|
||||||
from main import app
|
|
||||||
from .models.forms import UploadDataForm
|
from .models.forms import UploadDataForm
|
||||||
from common.data_tools import check_json_format, validate_json_contents, store_data_file
|
from common.data_tools import check_json_format, validate_json_contents, store_data_file
|
||||||
form = UploadDataForm()
|
form = UploadDataForm()
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_template('/admin/settings/questions.html', form=form)
|
files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
||||||
|
data = []
|
||||||
|
if files:
|
||||||
|
for file in files:
|
||||||
|
filename = file.rsplit('/')[-1]
|
||||||
|
with open(file) as _file:
|
||||||
|
load = loads(_file.read())
|
||||||
|
_author = load['meta']['author']
|
||||||
|
author = decrypt_find_one(db.users, {'_id': _author})['username']
|
||||||
|
data_element = {
|
||||||
|
'filename': filename,
|
||||||
|
'timestamp': datetime.strptime(load['meta']['timestamp'], '%Y-%m-%d %H%M%S'),
|
||||||
|
'author': author,
|
||||||
|
'use': len(load['meta']['tests'])
|
||||||
|
}
|
||||||
|
data.append(data_element)
|
||||||
|
default = get_default_dataset()
|
||||||
|
return render_template('/admin/settings/questions.html', form=form, data=data, default=default)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
upload = form.data_file.data
|
upload = form.data_file.data
|
||||||
|
default = True if request.form.get('default') else False
|
||||||
if not check_json_format(upload):
|
if not check_json_format(upload):
|
||||||
return jsonify({ 'error': 'Invalid file selected. Please upload a JSON file.'}), 400
|
return jsonify({ 'error': 'Invalid file selected. Please upload a JSON file.'}), 400
|
||||||
if not validate_json_contents(upload):
|
if not validate_json_contents(upload):
|
||||||
return jsonify({'error': 'The data in the file is invalid.'}), 400
|
return jsonify({'error': 'The data in the file is invalid.'}), 400
|
||||||
store_data_file(upload)
|
filename = store_data_file(upload, default=default)
|
||||||
return jsonify({ 'success': 'File uploaded.'}), 200
|
flash(f'Dataset {form.data_file.data.filename} has been uploaded as {filename}.', 'success')
|
||||||
|
return jsonify({ 'success': f'Dataset {form.data_file.data.filename} has been uploaded as {filename}.'}), 200
|
||||||
errors = [*form.errors]
|
errors = [*form.errors]
|
||||||
return jsonify({ 'error': errors}), 400
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
@views.route('/settings/questions/upload/')
|
@views.route('/settings/questions/delete/<filename>')
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def upload_questions():
|
def delete_questions(filename):
|
||||||
return render_template('/admin/settings/upload-questions.html')
|
data_files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
||||||
|
if any(filename in file for file in data_files):
|
||||||
|
default = get_default_dataset()
|
||||||
|
if default == filename:
|
||||||
|
return jsonify({'error': 'Cannot delete the default question dataset.'}), 400
|
||||||
|
data_file = os.path.join(app.config["DATA_FILE_DIRECTORY"],filename)
|
||||||
|
with open(data_file, 'r') as _data_file:
|
||||||
|
data = loads(_data_file.read())
|
||||||
|
if data['meta']['tests']:
|
||||||
|
return jsonify({'error': 'Cannot delete a dataset that is in use by an exam.'}), 400
|
||||||
|
if len(data_files) == 1:
|
||||||
|
return jsonify({'error': 'Cannot delete the only question dataset.'}), 400
|
||||||
|
os.remove(data_file)
|
||||||
|
flash(f'Question dataset {filename} has been deleted.', 'success')
|
||||||
|
return jsonify({'success': f'Question dataset {filename} has been deleted.'}), 200
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
@views.route('/settings/questions/default/<filename>')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def make_default_questions(filename):
|
||||||
|
data_files = glob(os.path.join(app.config["DATA_FILE_DIRECTORY"],'*.json'))
|
||||||
|
default_file_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')
|
||||||
|
if any(filename in file for file in data_files):
|
||||||
|
with open(default_file_path, 'r') as default_file:
|
||||||
|
default = default_file.read()
|
||||||
|
if default == filename:
|
||||||
|
return jsonify({'error': 'Cannot delete default question dataset.'}), 400
|
||||||
|
with open(default_file_path, 'w') as default_file:
|
||||||
|
default_file.write(filename)
|
||||||
|
flash(f'Set dataset f{filename} as the default.', 'success')
|
||||||
|
return jsonify({'success': f'Set dataset {filename} as the default.'})
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
@views.route('/tests/<filter>/', methods=['GET'])
|
@views.route('/tests/<filter>/', methods=['GET'])
|
||||||
@views.route('/tests/', methods=['GET'])
|
@views.route('/tests/', methods=['GET'])
|
||||||
@admin_account_required
|
@admin_account_required
|
||||||
@login_required
|
@login_required
|
||||||
def tests(filter=''):
|
def tests(filter=''):
|
||||||
|
if not available_datasets():
|
||||||
|
flash('There are no available question datasets. Please upload a question dataset in order to set up an exam.', 'error')
|
||||||
|
return redirect(url_for('admin_views.questions'))
|
||||||
if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']:
|
if filter not in ['', 'create', 'active', 'scheduled', 'expired', 'all']:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if filter == 'create':
|
if filter == 'create':
|
||||||
from .models.forms import CreateTest
|
from .models.forms import CreateTest
|
||||||
form = CreateTest()
|
form = CreateTest()
|
||||||
|
form.dataset.choices=available_datasets()
|
||||||
form.time_limit.default='none'
|
form.time_limit.default='none'
|
||||||
|
form.dataset.default=get_default_dataset()
|
||||||
form.process()
|
form.process()
|
||||||
display_title = ''
|
display_title = ''
|
||||||
error_none = ''
|
error_none = ''
|
||||||
@ -300,13 +371,13 @@ def tests(filter=''):
|
|||||||
def _tests():
|
def _tests():
|
||||||
from .models.forms import CreateTest
|
from .models.forms import CreateTest
|
||||||
form = CreateTest()
|
form = CreateTest()
|
||||||
form.time_limit.default='none'
|
form.dataset.choices = available_datasets()
|
||||||
form.process()
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
start_date = request.form.get('start_date')
|
start_date = request.form.get('start_date')
|
||||||
start_date = datetime.strptime(start_date, '%Y-%m-%d')
|
start_date = datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
expiry_date = request.form.get('expiry_date')
|
expiry_date = request.form.get('expiry_date')
|
||||||
expiry_date = datetime.strptime(expiry_date, '%Y-%m-%d')
|
expiry_date = datetime.strptime(expiry_date, '%Y-%m-%d')
|
||||||
|
dataset = request.form.get('dataset')
|
||||||
errors = []
|
errors = []
|
||||||
if start_date.date() < date.today():
|
if start_date.date() < date.today():
|
||||||
errors.append('The start date cannot be in the past.')
|
errors.append('The start date cannot be in the past.')
|
||||||
@ -323,7 +394,8 @@ def _tests():
|
|||||||
start_date = start_date,
|
start_date = start_date,
|
||||||
expiry_date = expiry_date,
|
expiry_date = expiry_date,
|
||||||
time_limit = request.form.get('time_limit'),
|
time_limit = request.form.get('time_limit'),
|
||||||
creator = creator
|
creator = creator,
|
||||||
|
dataset = dataset
|
||||||
)
|
)
|
||||||
test.create()
|
test.create()
|
||||||
return jsonify({'success': 'New exam created.'}), 200
|
return jsonify({'success': 'New exam created.'}), 200
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
from shutil import rmtree
|
|
||||||
import pathlib
|
import pathlib
|
||||||
from json import dump, loads
|
from json import dump, loads
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -11,16 +10,16 @@ def check_data_folder_exists():
|
|||||||
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
|
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
|
||||||
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
|
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
|
||||||
|
|
||||||
def check_current_indicator():
|
def check_default_indicator():
|
||||||
if not os.path.isfile(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt')):
|
if not os.path.isfile(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')):
|
||||||
open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'),'w').close()
|
open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt'),'w').close()
|
||||||
|
|
||||||
def make_temp_dir(file):
|
def get_default_dataset():
|
||||||
if not os.path.isdir('tmp'):
|
check_default_indicator()
|
||||||
os.mkdir('tmp')
|
default_file_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')
|
||||||
if os.path.isfile(f'tmp/{file.filename}'):
|
with open(default_file_path, 'r') as default_file:
|
||||||
os.remove(f'tmp/{file.filename}')
|
default = default_file.read()
|
||||||
file.save(f'tmp/{file.filename}')
|
return default
|
||||||
|
|
||||||
def check_json_format(file):
|
def check_json_format(file):
|
||||||
if not '.' in file.filename:
|
if not '.' in file.filename:
|
||||||
@ -42,9 +41,9 @@ def validate_json_contents(file):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def store_data_file(file):
|
def store_data_file(file, default:bool=None):
|
||||||
from admin.views import get_id_from_cookie
|
from admin.views import get_id_from_cookie
|
||||||
check_current_indicator()
|
check_default_indicator()
|
||||||
timestamp = datetime.utcnow()
|
timestamp = datetime.utcnow()
|
||||||
filename = '.'.join([timestamp.strftime('%Y%m%d%H%M%S'),'json'])
|
filename = '.'.join([timestamp.strftime('%Y%m%d%H%M%S'),'json'])
|
||||||
filename = secure_filename(filename)
|
filename = secure_filename(filename)
|
||||||
@ -53,7 +52,10 @@ def store_data_file(file):
|
|||||||
data = loads(file.read())
|
data = loads(file.read())
|
||||||
data['meta']['timestamp'] = timestamp.strftime('%Y-%m-%d %H%M%S')
|
data['meta']['timestamp'] = timestamp.strftime('%Y-%m-%d %H%M%S')
|
||||||
data['meta']['author'] = get_id_from_cookie()
|
data['meta']['author'] = get_id_from_cookie()
|
||||||
|
data['meta']['tests'] = []
|
||||||
with open(file_path, 'w') as _file:
|
with open(file_path, 'w') as _file:
|
||||||
dump(data, _file, indent=4)
|
dump(data, _file, indent=2)
|
||||||
with open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'), 'w') as _file:
|
if default:
|
||||||
_file.write(filename)
|
with open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt'), 'w') as _file:
|
||||||
|
_file.write(filename)
|
||||||
|
return filename
|
@ -37,7 +37,7 @@ def start():
|
|||||||
user_code = None if user_code == '' else user_code
|
user_code = None if user_code == '' else user_code
|
||||||
if not db.tests.find_one({'test_code': test_code}):
|
if not db.tests.find_one({'test_code': test_code}):
|
||||||
return jsonify({'error': 'The exam code you entered is invalid.'}), 400
|
return jsonify({'error': 'The exam code you entered is invalid.'}), 400
|
||||||
attempt = {
|
entry = {
|
||||||
'_id': uuid4().hex,
|
'_id': uuid4().hex,
|
||||||
'name': encrypt(name),
|
'name': encrypt(name),
|
||||||
'email': encrypt(email),
|
'email': encrypt(email),
|
||||||
@ -47,8 +47,8 @@ def start():
|
|||||||
'start_time': datetime.utcnow(),
|
'start_time': datetime.utcnow(),
|
||||||
'status': 'started'
|
'status': 'started'
|
||||||
}
|
}
|
||||||
if db.results.insert(attempt):
|
if db.entries.insert(entry):
|
||||||
return jsonify({ 'success': f'Exam started at started {attempt["start_time"].strftime("%H:%M:%S")}.' })
|
return jsonify({ 'success': f'Exam started at started {entry["start_time"].strftime("%H:%M:%S")}.' })
|
||||||
else:
|
else:
|
||||||
errors = [*form.errors]
|
errors = [*form.errors]
|
||||||
return jsonify({ 'error': errors}), 400
|
return jsonify({ 'error': errors}), 400
|
||||||
|
Loading…
Reference in New Issue
Block a user