Added functionality for default datasets.

Incorporated dataset selector into test creation.
This commit is contained in:
Vivek Santayana 2021-11-28 17:28:14 +00:00
parent 059dca4a40
commit 9906d82261
11 changed files with 350 additions and 85 deletions

View File

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

View File

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

View File

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

View File

@ -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,11 +437,11 @@ $('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}`,
type: 'GET', type: 'GET',
@ -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();
}) })

View File

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

View File

@ -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">
@ -21,3 +103,23 @@
</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 %}

View File

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

View File

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

View File

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

View File

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

View File

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