Completed admin views

Corrected model method return values
This commit is contained in:
Vivek Santayana 2022-06-15 11:23:38 +01:00
parent 126bf9203c
commit a1bee61679
6 changed files with 226 additions and 43 deletions

View File

@ -1,12 +1,16 @@
from ..forms.admin import CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData
from ..models import Dataset, User from ..models import Dataset, Entry, Test, User
from ..tools.auth import disable_if_logged_in, require_account_creation from ..tools.auth import disable_if_logged_in, require_account_creation
from ..tools.forms import get_dataset_choices, get_time_options
from ..tools.data import check_is_json, validate_json from ..tools.data import check_is_json, validate_json
from ..tools.test import get_correct_answers
from flask import Blueprint, flash, jsonify, render_template, redirect, request, session from flask import Blueprint, jsonify, render_template, redirect, request, session
from flask.helpers import url_for from flask.helpers import flash, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from datetime import date, datetime
from json import loads
import secrets import secrets
admin = Blueprint( admin = Blueprint(
@ -21,7 +25,17 @@ admin = Blueprint(
@admin.route('/dashboard/') @admin.route('/dashboard/')
@login_required @login_required
def _home(): def _home():
return 'Home Page' # TODO Dashboard tests = Test.query.all()
results = Entry.query.all()
current_tests = [ test for test in tests if test['expiry_date'].date() >= datetime.now().date() and test['start_date'].date() <= date.today() ]
current_tests.sort(key= lambda x: x['expiry_date'], reverse=True)
upcoming_tests = [ test for test in tests if test['start_date'].date() > datetime.now().date()]
upcoming_tests.sort(key= lambda x: x['start_date'])
recent_results = [result for result in results if not result['status'] == 'started' ]
recent_results.sort(key= lambda x: x['end_time'], reverse=True)
for result in recent_results:
result['percent'] = round(100*result['result']['score']/result['result']['max'])
return render_template('/admin/index.html', current_tests = current_tests, upcomimg_tests = upcoming_tests, recent_results = recent_results)
@admin.route('/settings/') @admin.route('/settings/')
@login_required @login_required
@ -216,14 +230,159 @@ def _quesitons():
@admin.route('/settings/questions/edit/', methods=['POST']) @admin.route('/settings/questions/edit/', methods=['POST'])
@login_required @login_required
def _delete_questions(): def _edit_questions():
id = request.get_json()['id'] id = request.get_json()['id']
action = request.get_json()['action'] action = request.get_json()['action']
if action not in ['defailt', 'delete']: return jsonify({'error': 'Invalid action.'}), 400
dataset = Dataset.query.filter_by(id=id).first() dataset = Dataset.query.filter_by(id=id).first()
if action == 'delete': success, message = dataset.delete() if action == 'delete': success, message = dataset.delete()
elif action == 'default': success, message = dataset.make_default() elif action == 'default': success, message = dataset.make_default()
if success: return jsonify({'success': message}), 200 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
# TODO Test views @admin.route('/tests/<string:filter>/', methods=['GET'])
# TODO Result views @admin.route('/tests/', methods=['GET'])
@login_required
def _tests(filter:str=None):
datasets = Dataset.query.all()
tests = None
_tests = Test.query.all()
form = None
if not 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._questions'))
if filter not in [None, '', 'create','active','scheduled','expired','all']: return redirect(url_for('admin._tests'))
if filter == 'create':
form = CreateTest()
form.time_limit.choices = get_time_options()
form.dataset.choices = get_dataset_choices
form.time_limit.default='none'
form.process()
display_title = ''
error_none = ''
if filter in [None, '', 'active']:
tests = [ test for test in _tests if test['expiry_date'].date() >= date.today() and test['start_date'].date() <= date.today() ]
display_title = 'Active Exams'
error_none = 'There are no exams that are currently active. You can create one using the Creat Exam form.'
if filter == 'expired':
tests = [ test for test in _tests if test['expiry_date'].date() < date.today() ]
display_title = 'Expired Exams'
error_none = 'There are no expired exams. Exams will appear in this category after their expiration date has passed.'
if filter == 'scheduled':
tests = [ test for test in _tests if test['start_date'].date() > date.today()]
display_title = 'Scheduled Exams'
error_none = 'There are no scheduled exams pending. You can schedule an exam for the future using the Create Exam form.'
if filter == 'all':
tests = _tests
display_title = 'All Exams'
error_none = 'There are no exams set up. You can create one using the Create Exam form.'
return render_template('/admin/tests.html', form = form, tests=tests, display_title=display_title, error_none=error_none, filter=filter)
@admin.route('/tests/create/', methods=['POST'])
@login_required
def _create_test():
form = CreateTest()
form.dataset.choices = get_dataset_choices()
form.time_limit.choices = get_time_options()
if form.validate_on_submit():
new_test = Test()
new_test.start_date = request.form.get('start_date')
new_test.start_date = datetime.strptime(new_test.start_date, '%Y-%m-%d')
new_test.end_date = request.form.get('expiry_date')
new_test.end_date = datetime.strptime(new_test.end_date, '%Y-%m-%d')
dataset = request.form.get('dataset')
new_test.dataset = Dataset.query.filter_by(id=dataset)
success, message = new_test.create()
if success:
flash(message=message, category='success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
else:
errors = [*form.start_date.errors, *form.expiry_date.errors, *form.time_limit.errors]
return jsonify({ 'error': errors}), 400
@admin.route('/tests/edit/', methods=['POST'])
@login_required
def _edit_test():
id = request.get_json()['id']
action = request.get_json()['action']
if action not in ['start', 'delete', 'end']: return jsonify({'error': 'Invalid action.'}), 400
test = Test.query.filter_by(id=id).first()
if not test: return jsonify({'error': 'Could not find the corresponding test to delete.'}), 404
if action == 'delete': success, message = test.delete()
if action == 'start': success, message = test.start()
if action == 'end': success, message = test.end()
if success:
flash(message=message, category='success')
return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
@admin.route('/test/<string:id>/', methods=['GET','POST'])
@login_required
def _view_test(id:str=None):
form = AddTimeAdjustment()
test = Test.query.filter_by(id=id).first()
if request.method == 'POST':
if not test: return jsonify({'error': 'Invalid test ID.'}), 404
if form.validate_on_submit():
time = int(request.form.get('time'))
success, message = test.add_adjustment(time)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
return jsonify({'error': form.time.errors }), 400
if not test:
flash('Invalid test ID.', 'error')
return redirect(url_for('admin._tests'))
return render_template('/admin/test.html', test = test, form = form)
@admin.route('/test/<string:id>/delete-adjustment/', methods=['POST'])
@login_required
def _delete_adjustment(id:str=None):
test = Test.query.filter_by(id=id).first()
if not test: return jsonify({'error': 'Invalid test ID.'}), 404
user_code = request.get_json()['user_code'].lower()
success, message = test.remove_adjustment(user_code)
if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400
@admin.route('/results/')
@login_required
def _view_entries():
entries = Entry.query.all()
return render_template('/admin/results.html', entries = entries)
@admin.route('/results/<string:id>/', methods = ['GET', 'POST'])
@login_required
def _view_entry(id:str=None):
entry = Entry.query.filter_by(id=id)
if request.method == 'POST':
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
action = request.get_json()['action']
if action not in ['validate', 'delete']: return jsonify({'error': 'Invalid action.'}), 400
if action == 'validate':
success, message = entry.validate()
if action == 'delete':
success, message = entry.delete()
if success:
flash(message, 'success')
return jsonify({'success': message}), 200
return jsonify({'error': message}),400
if not entry:
flash('Invalid entry ID.', 'error')
return redirect(url_for('admin._view_entries'))
test = entry['test']
dataset = test['dataset']
dataset_path = dataset.get_file()
with open(dataset_path, 'r') as _dataset:
data = loads(_dataset.read())
correct = get_correct_answers(dataset=data)
return render_template('/admin/result-detail.html', entry = entry, correct = correct)
@admin.route('/certificate/',methods=['POST'])
@login_required
def _generate_certificate():
from main import db
id = request.get_json()['id']
entry = Entry.query.filter_by(id=id).first()
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404
return render_template('/admin/components/certificate.html', entry = entry)

View File

@ -2,7 +2,7 @@ from ..data import data
from ..modules import db from ..modules import db
from ..tools.logs import write from ..tools.logs import write
from flask import flash, jsonify from flask import flash
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -41,11 +41,11 @@ class Dataset(db.Model):
if self.default: if self.default:
message = 'Cannot delete the default dataset.' message = 'Cannot delete the default dataset.'
flash(message, 'error') flash(message, 'error')
return False, jsonify({'error': message}) return False, message
if Dataset.query.all().count() == 1: if Dataset.query.all().count() == 1:
message = 'Cannot delete the only dataset.' message = 'Cannot delete the only dataset.'
flash(message, 'error') flash(message, 'error')
return False, jsonify({'error': message}) return False, message
write('system.log', f'Dataset {self.id} deleted by {current_user.get_username()}.') write('system.log', f'Dataset {self.id} deleted by {current_user.get_username()}.')
filename = secure_filename('.'.join([self.id,'json'])) filename = secure_filename('.'.join([self.id,'json']))
file_path = path.join(data, 'questions', filename) file_path = path.join(data, 'questions', filename)

View File

@ -4,7 +4,6 @@ from ..tools.encryption import decrypt, encrypt
from ..tools.logs import write from ..tools.logs import write
from .test import Test from .test import Test
from flask import jsonify
from flask_login import current_user from flask_login import current_user
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -79,7 +78,7 @@ class Entry(db.Model):
write('tests.log', f'Test completed by {self.get_first_name()} {self.get_surname()}.') write('tests.log', f'Test completed by {self.get_first_name()} {self.get_surname()}.')
delta = timedelta(minutes=self.test.time_limit+1) delta = timedelta(minutes=self.test.time_limit+1)
if not self.test.time_limit or self.end_time <= self.start_time + delta: if not self.test.time_limit or self.end_time <= self.start_time + delta:
self.status = 'finished' self.status = 'completed'
self.valid = True self.valid = True
else: else:
self.status = 'late' self.status = 'late'
@ -87,10 +86,18 @@ class Entry(db.Model):
db.session.commit() db.session.commit()
def validate(self): def validate(self):
if self.valid: return False, jsonify({'error':f'The entry is already valid.'}) if self.valid: return False, f'The entry is already valid.'
if self.status == 'started': return False, jsonify({'error':f'The entry is still pending.'}) if self.status == 'started': return False, 'The entry is still pending.'
self.valid = True self.valid = True
self.status = 'completed' self.status = 'completed'
db.session.commit() db.session.commit()
message = f'The entry {self.id} has been validated by {current_user.get_username()}.' write('system.log', f'The entry {self.id} has been validated by {current_user.get_username()}.')
return True, jsonify({'success': message}) return True, f'The entry {self.id} has been validated.'
def delete(self):
id = self.id
name = f'{self.get_first_name()} {self.get_surname()}'
db.session.delete(self)
db.session.commit()
write('system.log', f'The entry {id} by {name} has been deleted by {current_user.get_username()}.')
return True, 'Entry deleted.'

View File

@ -3,11 +3,9 @@ from ..tools.encryption import decrypt, encrypt
from ..tools.forms import JsonEncodedDict from ..tools.forms import JsonEncodedDict
from ..tools.logs import write from ..tools.logs import write
from flask import jsonify
from flask.helpers import flash
from flask_login import current_user from flask_login import current_user
from datetime import datetime from datetime import date, datetime
from json import dump, loads from json import dump, loads
import os import os
import secrets import secrets
@ -48,35 +46,45 @@ class Test(db.Model):
self.generate_id() self.generate_id()
self.generate_code() self.generate_code()
self.creator = current_user self.creator = current_user
errors = []
if self.start_date.date() < date.today():
errors.append('The start date cannot be in the past.')
if self.end_date.date() < date.today():
errors.append('The expiry date cannot be in the past.')
if self.end_date < self.start_date:
errors.append('The expiry date cannot be before the start date.')
if errors:
return False, errors
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
write('system.log', f'Test with code {self.code} created by {current_user.get_username()}.') write('system.log', f'Test with code {self.get_code()} created by {current_user.get_username()}.')
return True, f'Test with code {self.get_code()} has been created.'
def delete(self): def delete(self):
code = self.code code = self.code
if self.entries: return False, f'Cannot delete a test with submitted entries.'
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
write('system.log', f'Test with code {code} deleted by {current_user.get_username()}.') write('system.log', f'Test with code {code} has been deleted by {current_user.get_username()}.')
return True, f'Test with code {code} has been deleted.'
def start(self): def start(self):
now = datetime.now() now = datetime.now()
if self.start_date > now: if self.start_date.date() > now.date():
self.start_date = now self.start_date = now
db.session.commit() db.session.commit()
message = f'Test with code {self.code} started by {current_user.get_username()}.' write('system.log', f'Test with code {self.code} has been started by {current_user.get_username()}.')
write('system.log', message) return True, f'Test with code {self.code} has been started.'
return True, jsonify({'success': message}) return False, f'Test with code {self.code} has already started.'
return False, jsonify({'error': f'Test with code {self.code} has already started.'})
def end(self): def end(self):
now = datetime.now() now = datetime.now()
if self.end_date > now: if self.end_date.date() > now.date():
self.end_date = now self.end_date = now
db.session.commit() db.session.commit()
message = f'Test with code {self.code} ended by {current_user.get_username()}.' write('system.log', f'Test with code {self.code} ended by {current_user.get_username()}.')
write('system.log', message) return True, f'Test with code {self.code} has been ended.'
return True, jsonify({'success': message}) return False, f'Test with code {self.code} has already ended.'
return False, jsonify({'error': f'Test with code {self.code} has already started.'})
def add_adjustment(self, time:int): def add_adjustment(self, time:int):
adjustments = self.adjustments if self.adjustments is not None else {} adjustments = self.adjustments if self.adjustments is not None else {}
@ -85,23 +93,21 @@ class Test(db.Model):
self.adjustments = adjustments self.adjustments = adjustments
db.session.commit() db.session.commit()
write('system.log', f'Time adjustment for {time} minutes with code {code} added to test {self.get_code()} by {current_user.get_username()}.') write('system.log', f'Time adjustment for {time} minutes with code {code} added to test {self.get_code()} by {current_user.get_username()}.')
return True, jsonify({'success': f'Time adjustment for {time} minutes added to test {self.get_code()}. This can be accessed using the user code {code.upper()}.'}) return True, f'Time adjustment for {time} minutes added to test {self.get_code()}. This can be accessed using the user code {code.upper()}.'
def remove_adjustment(self, code:str): def remove_adjustment(self, code:str):
if not self.adjustments: return False, jsonify({'error': f'There are no adjustments configured for test {self.get_code()}.'}) if not self.adjustments: return False, f'There are no adjustments configured for test {self.get_code()}.'
self.adjustments.pop(code) self.adjustments.pop(code)
if not self.adjustments: self.adjustments = None if not self.adjustments: self.adjustments = None
db.session.commit() db.session.commit()
message = f'Time adjustment for with code {code} removed from test {self.get_code()} by {current_user.get_username()}.' write('system.log', f'Time adjustment for with code {code} has been removed from test {self.get_code()} by {current_user.get_username()}.')
write('system.log', message) return True, f'Time adjustment for with code {code} has been removed from test {self.get_code()}.'
return True, jsonify({'success': message})
def update(self, start_date:datetime=None, end_date:datetime=None, time_limit:int=None): def update(self, start_date:datetime=None, end_date:datetime=None, time_limit:int=None):
if not start_date and not end_date and time_limit is None: return False, jsonify({'error': 'There were no changes requested.'}) if not start_date and not end_date and time_limit is None: return False, 'There were no changes requested.'
if start_date: self.start_date = start_date if start_date: self.start_date = start_date
if end_date: self.end_date = end_date if end_date: self.end_date = end_date
if time_limit is not None: self.time_limit = time_limit if time_limit is not None: self.time_limit = time_limit
db.session.commit() db.session.commit()
message = f'Test with code {self.get_code()} has been updated by user {current_user.get_username()}' write('system.log', f'Test with code {self.get_code()} has been updated by user {current_user.get_username()}.')
write('system.log', message) return True, f'Test with code {self.get_code()} has been updated by.'
return True, jsonify({'success': message})

View File

@ -99,7 +99,7 @@ class User(UserMixin, db.Model):
return True, message return True, message
def update(self, password:str=None, email:str=None, notify:bool=False): def update(self, password:str=None, email:str=None, notify:bool=False):
if not password and not email: return False, jsonify({'error': 'There were no changes requested.'}) if not password and not email: return False, 'There were no changes requested.'
if password: self.set_password(password) if password: self.set_password(password)
if email: self.set_email(email) if email: self.set_email(email)
db.session.commit() db.session.commit()

View File

@ -1,4 +1,5 @@
from ..models import Dataset
from ..modules import db from ..modules import db
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
@ -43,3 +44,13 @@ def get_time_options():
('120', '2 hours') ('120', '2 hours')
] ]
return time_options return time_options
def get_dataset_choices():
datasets = Dataset.query.all()
dataset_choices = []
for dataset in datasets:
label = dataset['date'].strftime('%Y%m%d%H%M%S')
label = f'{label} (Default)' if dataset.default else label
choice = (dataset['id'], label)
dataset_choices.append(choice)
return dataset_choices