Added functionality for default datasets.
Incorporated dataset selector into test creation.
This commit is contained in:
		@@ -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,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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
@@ -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() }}
 | 
				
			||||||
 | 
					            <div class="form-upload">
 | 
				
			||||||
                {{ form.data_file() }}
 | 
					                {{ form.data_file() }}
 | 
				
			||||||
            {% include "admin/components/client-alerts.html" %}
 | 
					            </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 %}
 | 
				
			||||||
@@ -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:
 | 
				
			||||||
 | 
					        with open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt'), 'w') as _file:
 | 
				
			||||||
            _file.write(filename)
 | 
					            _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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user