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 c7252d0f7b
commit 3797adfc95
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) )
time_limit = SelectField('Time Limit', choices=time_options)
dataset = SelectField('Question Dataset')
class UploadDataForm(FlaskForm):
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 flask import flash, jsonify
import secrets
import os
from json import dump, loads
from main import db
from main import app, db
from common.security import encrypt
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.start_date = start_date
self.expiry_date = expiry_date
self.time_limit = None if time_limit == 'none' or time_limit == '' else time_limit
self.creator = creator
self.dataset = dataset
def create(self):
test = {
@ -23,9 +26,16 @@ class Test:
'expiry_date': self.expiry_date,
'time_limit': self.time_limit,
'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):
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')
return jsonify({'success': test}), 200
return jsonify({'error': f'Could not create exam. An error occurred.'}), 400
@ -54,7 +64,15 @@ class Test:
return test_code.replace('', '')
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}):
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.'
flash(message, 'alert')
return jsonify({'success': message}), 200

View File

@ -132,16 +132,11 @@ table.dataTable {
width: 100%;
}
.user-table-row {
.table-row {
vertical-align: middle;
}
.user-row-actions {
text-align: center;
white-space: nowrap;
}
.test-row-actions {
.row-actions {
text-align: center;
white-space: nowrap;
}
@ -153,8 +148,8 @@ table.dataTable {
text-align:center;
}
.user-row-actions button {
margin: 0px 10px;
.row-actions button, .row-actions a {
margin: 0px 5px;
}
#cookie-alert {
@ -214,6 +209,11 @@ table.dataTable {
font-size: 20px;
}
.form-upload {
margin: 2rem 0;
font-size: 14pt;
}
/* Fallback for Edge
-------------------------------------------------- */
@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 data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -62,7 +62,7 @@ $('form[name=form-login]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -104,7 +104,7 @@ $('form[name=form-reset]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -146,7 +146,7 @@ $('form[name=form-update-password]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
console.log(data)
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -188,7 +188,7 @@ $('form[name=form-create-user]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -230,7 +230,7 @@ $('form[name=form-delete-user]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -272,7 +272,7 @@ $('form[name=form-update-user]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -314,7 +314,7 @@ $('form[name=form-update-account]').submit(function(event) {
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -355,7 +355,7 @@ $('form[name=form-create-test]').submit(function(event) {
var $form = $(this);
var alert = document.getElementById('alert-box');
var data = $form.serialize();
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -398,7 +398,7 @@ $('form[name=form-upload-questions]').submit(function(event) {
var data = new FormData($form[0]);
var file = $('input[name=data_file]')[0].files[0]
data.append('file', file)
alert.innerHTML = ''
alert.innerHTML = '';
$.ajax({
url: window.location.pathname,
@ -407,25 +407,7 @@ $('form[name=form-upload-questions]').submit(function(event) {
processData: false,
contentType: false,
success: function(response) {
if (typeof response.success === 'string' || response.success instanceof String) {
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>
`;
}
}
window.location.reload();
},
error: function(response) {
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
$('.delete-test').click(function(event) {
_id = $(this).data('_id')
$.ajax({
url: `/admin/tests/delete/${_id}`,
type: 'GET',
@ -492,6 +474,89 @@ $('.delete-test').click(function(event) {
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').click(function(event){
@ -503,13 +568,13 @@ $('#dismiss-cookie-alert').click(function(event){
},
dataType: 'json',
success: function(response){
console.log(response)
console.log(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>
</li>
<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 class="nav-item dropdown" id="nav-settings">
<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 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">
<form name="form-upload-questions" method="post" action="#" class="form-signin" enctype="multipart/form-data">
<h2 class="form-signin-heading">Upload Question Dataset</h2>
{{ form.hidden_tag() }}
<div class="form-upload">
{{ 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="row">
<div class="col text-center">
@ -21,3 +103,23 @@
</form>
</div>
{% 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>
<tbody>
{% for user in users %}
<tr class="user-table-row">
<tr class="table-row">
<td>
{% if user._id == get_id_from_cookie() %}
<div class="text-success" title="Current User">
@ -37,7 +37,7 @@
<td>
{{ user.email }}
</td>
<td class="user-row-actions">
<td class="row-actions">
<a
href="
{% if not user._id == get_id_from_cookie() %}

View File

@ -30,7 +30,7 @@
</thead>
<tbody>
{% for test in tests %}
<tr class="user-table-row">
<tr class="table-row">
<td>
{{ test.start_date.strftime('%d %b %Y') }}
</td>
@ -56,7 +56,7 @@
<td>
{{ test.attempts|length }}
</td>
<td class="test-row-actions">
<td class="row-actions">
<a
href="#"
class="btn btn-primary edit-test"
@ -101,6 +101,10 @@
{{ form.time_limit(placeholder="Select Time Limit") }}
{{ form.time_limit.label }}
</div>
<div class="form-select-input">
{{ form.dataset(placeholder="Select Question Dataset") }}
{{ form.dataset.label }}
</div>
{% include "admin/components/client-alerts.html" %}
<div class="container form-submission-button">
<div class="row">

View File

@ -3,16 +3,20 @@ from flask.helpers import url_for
from functools import wraps
from datetime import datetime
import os
from glob import glob
from json import loads
from werkzeug.security import check_password_hash
from common.security.database import decrypt_find, decrypt_find_one
from .models.users import User
from flask_mail import Message
from main import db
from main import app, db
from uuid import uuid4
import secrets
from main import mail
from datetime import datetime, date, timedelta
from .models.tests import Test
from common.data_tools import get_default_dataset
views = Blueprint(
'admin_views',
@ -65,6 +69,18 @@ def disable_if_logged_in(function):
return function(*args, **kwargs)
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('/home/')
@views.route('/dashboard/')
@ -236,41 +252,96 @@ def update_user(_id:str):
@admin_account_required
@login_required
def questions():
from main import app
from .models.forms import UploadDataForm
from common.data_tools import check_json_format, validate_json_contents, store_data_file
form = UploadDataForm()
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 form.validate_on_submit():
upload = form.data_file.data
default = True if request.form.get('default') else False
if not check_json_format(upload):
return jsonify({ 'error': 'Invalid file selected. Please upload a JSON file.'}), 400
if not validate_json_contents(upload):
return jsonify({'error': 'The data in the file is invalid.'}), 400
store_data_file(upload)
return jsonify({ 'success': 'File uploaded.'}), 200
filename = store_data_file(upload, default=default)
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]
return jsonify({ 'error': errors}), 400
@views.route('/settings/questions/upload/')
@views.route('/settings/questions/delete/<filename>')
@admin_account_required
@login_required
def upload_questions():
return render_template('/admin/settings/upload-questions.html')
def delete_questions(filename):
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/', methods=['GET'])
@admin_account_required
@login_required
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']:
return abort(404)
if filter == 'create':
from .models.forms import CreateTest
form = CreateTest()
form.dataset.choices=available_datasets()
form.time_limit.default='none'
form.dataset.default=get_default_dataset()
form.process()
display_title = ''
error_none = ''
@ -300,13 +371,13 @@ def tests(filter=''):
def _tests():
from .models.forms import CreateTest
form = CreateTest()
form.time_limit.default='none'
form.process()
form.dataset.choices = available_datasets()
if form.validate_on_submit():
start_date = request.form.get('start_date')
start_date = datetime.strptime(start_date, '%Y-%m-%d')
expiry_date = request.form.get('expiry_date')
expiry_date = datetime.strptime(expiry_date, '%Y-%m-%d')
dataset = request.form.get('dataset')
errors = []
if start_date.date() < date.today():
errors.append('The start date cannot be in the past.')
@ -323,7 +394,8 @@ def _tests():
start_date = start_date,
expiry_date = expiry_date,
time_limit = request.form.get('time_limit'),
creator = creator
creator = creator,
dataset = dataset
)
test.create()
return jsonify({'success': 'New exam created.'}), 200

View File

@ -1,5 +1,4 @@
import os
from shutil import rmtree
import pathlib
from json import dump, loads
from datetime import datetime
@ -11,16 +10,16 @@ def check_data_folder_exists():
if not os.path.exists(app.config['DATA_FILE_DIRECTORY']):
pathlib.Path(app.config['DATA_FILE_DIRECTORY']).mkdir(parents='True', exist_ok='True')
def check_current_indicator():
if not os.path.isfile(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt')):
open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'),'w').close()
def check_default_indicator():
if not os.path.isfile(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')):
open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt'),'w').close()
def make_temp_dir(file):
if not os.path.isdir('tmp'):
os.mkdir('tmp')
if os.path.isfile(f'tmp/{file.filename}'):
os.remove(f'tmp/{file.filename}')
file.save(f'tmp/{file.filename}')
def get_default_dataset():
check_default_indicator()
default_file_path = os.path.join(app.config['DATA_FILE_DIRECTORY'], '.default.txt')
with open(default_file_path, 'r') as default_file:
default = default_file.read()
return default
def check_json_format(file):
if not '.' in file.filename:
@ -42,9 +41,9 @@ def validate_json_contents(file):
return False
return True
def store_data_file(file):
def store_data_file(file, default:bool=None):
from admin.views import get_id_from_cookie
check_current_indicator()
check_default_indicator()
timestamp = datetime.utcnow()
filename = '.'.join([timestamp.strftime('%Y%m%d%H%M%S'),'json'])
filename = secure_filename(filename)
@ -53,7 +52,10 @@ def store_data_file(file):
data = loads(file.read())
data['meta']['timestamp'] = timestamp.strftime('%Y-%m-%d %H%M%S')
data['meta']['author'] = get_id_from_cookie()
data['meta']['tests'] = []
with open(file_path, 'w') as _file:
dump(data, _file, indent=4)
with open(os.path.join(app.config['DATA_FILE_DIRECTORY'], '.current.txt'), 'w') as _file:
dump(data, _file, indent=2)
if default:
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
if not db.tests.find_one({'test_code': test_code}):
return jsonify({'error': 'The exam code you entered is invalid.'}), 400
attempt = {
entry = {
'_id': uuid4().hex,
'name': encrypt(name),
'email': encrypt(email),
@ -47,8 +47,8 @@ def start():
'start_time': datetime.utcnow(),
'status': 'started'
}
if db.results.insert(attempt):
return jsonify({ 'success': f'Exam started at started {attempt["start_time"].strftime("%H:%M:%S")}.' })
if db.entries.insert(entry):
return jsonify({ 'success': f'Exam started at started {entry["start_time"].strftime("%H:%M:%S")}.' })
else:
errors = [*form.errors]
return jsonify({ 'error': errors}), 400