Completed admin views
Corrected model method return values
This commit is contained in:
		| @@ -1,12 +1,16 @@ | ||||
| from ..forms.admin import CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData | ||||
| from ..models import Dataset, User | ||||
| from ..forms.admin import AddTimeAdjustment, CreateTest, CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData | ||||
| from ..models import Dataset, Entry, Test, User | ||||
| 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.test import get_correct_answers | ||||
|  | ||||
| from flask import Blueprint, flash, jsonify, render_template, redirect, request, session | ||||
| from flask.helpers import url_for | ||||
| from flask import Blueprint, jsonify, render_template, redirect, request, session | ||||
| from flask.helpers import flash, url_for | ||||
| from flask_login import current_user, login_required | ||||
|  | ||||
| from datetime import date, datetime | ||||
| from json import loads | ||||
| import secrets | ||||
|  | ||||
| admin = Blueprint( | ||||
| @@ -21,7 +25,17 @@ admin = Blueprint( | ||||
| @admin.route('/dashboard/') | ||||
| @login_required | ||||
| 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/') | ||||
| @login_required | ||||
| @@ -216,14 +230,159 @@ def _quesitons(): | ||||
|  | ||||
| @admin.route('/settings/questions/edit/', methods=['POST']) | ||||
| @login_required | ||||
| def _delete_questions(): | ||||
| def _edit_questions(): | ||||
|     id = request.get_json()['id'] | ||||
|     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() | ||||
|     if action == 'delete': success, message = dataset.delete() | ||||
|     elif action == 'default': success, message = dataset.make_default() | ||||
|     if success: return jsonify({'success': message}), 200 | ||||
|     return jsonify({'error': message}), 400 | ||||
|  | ||||
| # TODO Test views | ||||
| # TODO Result views | ||||
| @admin.route('/tests/<string:filter>/', methods=['GET']) | ||||
| @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) | ||||
| @@ -2,7 +2,7 @@ from ..data import data | ||||
| from ..modules import db | ||||
| from ..tools.logs import write | ||||
|  | ||||
| from flask import flash, jsonify | ||||
| from flask import flash | ||||
| from flask_login import current_user | ||||
| from werkzeug.utils import secure_filename | ||||
|  | ||||
| @@ -41,11 +41,11 @@ class Dataset(db.Model): | ||||
|         if self.default: | ||||
|             message = 'Cannot delete the default dataset.' | ||||
|             flash(message, 'error') | ||||
|             return False, jsonify({'error': message}) | ||||
|             return False, message | ||||
|         if Dataset.query.all().count() == 1: | ||||
|             message = 'Cannot delete the only dataset.' | ||||
|             flash(message, 'error') | ||||
|             return False, jsonify({'error': message}) | ||||
|             return False, message | ||||
|         write('system.log', f'Dataset {self.id} deleted by {current_user.get_username()}.') | ||||
|         filename = secure_filename('.'.join([self.id,'json'])) | ||||
|         file_path = path.join(data, 'questions', filename) | ||||
|   | ||||
| @@ -4,7 +4,6 @@ from ..tools.encryption import decrypt, encrypt | ||||
| from ..tools.logs import write | ||||
| from .test import Test | ||||
|  | ||||
| from flask import jsonify | ||||
| from flask_login import current_user | ||||
|  | ||||
| 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()}.') | ||||
|         delta = timedelta(minutes=self.test.time_limit+1) | ||||
|         if not self.test.time_limit or self.end_time <= self.start_time + delta: | ||||
|             self.status = 'finished' | ||||
|             self.status = 'completed' | ||||
|             self.valid = True | ||||
|         else: | ||||
|             self.status = 'late' | ||||
| @@ -87,10 +86,18 @@ class Entry(db.Model): | ||||
|         db.session.commit() | ||||
|  | ||||
|     def validate(self): | ||||
|         if self.valid: return False, jsonify({'error':f'The entry is already valid.'}) | ||||
|         if self.status == 'started': return False, jsonify({'error':f'The entry is still pending.'}) | ||||
|         if self.valid: return False, f'The entry is already valid.' | ||||
|         if self.status == 'started': return False, 'The entry is still pending.' | ||||
|         self.valid = True | ||||
|         self.status = 'completed' | ||||
|         db.session.commit() | ||||
|         message = f'The entry {self.id} has been validated by {current_user.get_username()}.' | ||||
|         return True, jsonify({'success': message}) | ||||
|         write('system.log', f'The entry {self.id} has been validated by {current_user.get_username()}.') | ||||
|         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.' | ||||
| @@ -3,11 +3,9 @@ from ..tools.encryption import decrypt, encrypt | ||||
| from ..tools.forms import JsonEncodedDict | ||||
| from ..tools.logs import write | ||||
|  | ||||
| from flask import jsonify | ||||
| from flask.helpers import flash | ||||
| from flask_login import current_user | ||||
|  | ||||
| from datetime import datetime | ||||
| from datetime import date, datetime | ||||
| from json import dump, loads | ||||
| import os | ||||
| import secrets | ||||
| @@ -48,35 +46,45 @@ class Test(db.Model): | ||||
|         self.generate_id() | ||||
|         self.generate_code() | ||||
|         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.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): | ||||
|         code = self.code | ||||
|         if self.entries: return False, f'Cannot delete a test with submitted entries.' | ||||
|         db.session.delete(self) | ||||
|         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): | ||||
|         now = datetime.now() | ||||
|         if self.start_date > now: | ||||
|         if self.start_date.date() > now.date(): | ||||
|             self.start_date = now | ||||
|             db.session.commit() | ||||
|             message = f'Test with code {self.code} started by {current_user.get_username()}.' | ||||
|             write('system.log', message) | ||||
|             return True, jsonify({'success': message}) | ||||
|         return False, jsonify({'error': f'Test with code {self.code} has already started.'}) | ||||
|             write('system.log', f'Test with code {self.code} has been started by {current_user.get_username()}.') | ||||
|             return True,  f'Test with code {self.code} has been started.' | ||||
|         return False, f'Test with code {self.code} has already started.' | ||||
|  | ||||
|     def end(self): | ||||
|         now = datetime.now() | ||||
|         if self.end_date > now: | ||||
|         if self.end_date.date() > now.date(): | ||||
|             self.end_date = now | ||||
|             db.session.commit() | ||||
|             message = f'Test with code {self.code} ended by {current_user.get_username()}.' | ||||
|             write('system.log', message) | ||||
|             return True, jsonify({'success': message}) | ||||
|         return False, jsonify({'error': f'Test with code {self.code} has already started.'}) | ||||
|             write('system.log', f'Test with code {self.code} ended by {current_user.get_username()}.') | ||||
|             return True, f'Test with code {self.code} has been ended.' | ||||
|         return False, f'Test with code {self.code} has already ended.' | ||||
|  | ||||
|     def add_adjustment(self, time:int): | ||||
|         adjustments = self.adjustments if self.adjustments is not None else {} | ||||
| @@ -85,23 +93,21 @@ class Test(db.Model): | ||||
|         self.adjustments = adjustments | ||||
|         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()}.') | ||||
|         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): | ||||
|         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) | ||||
|         if not self.adjustments: self.adjustments = None | ||||
|         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', message) | ||||
|         return True, jsonify({'success': message}) | ||||
|         write('system.log', f'Time adjustment for with code {code} has been removed from test {self.get_code()} by {current_user.get_username()}.') | ||||
|         return True, f'Time adjustment for with code {code} has been removed from test {self.get_code()}.' | ||||
|  | ||||
|     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 end_date: self.end_date = end_date | ||||
|         if time_limit is not None: self.time_limit = time_limit | ||||
|         db.session.commit() | ||||
|         message = f'Test with code {self.get_code()} has been updated by user {current_user.get_username()}' | ||||
|         write('system.log', message) | ||||
|         return True, jsonify({'success': message}) | ||||
|         write('system.log', f'Test with code {self.get_code()} has been updated by user {current_user.get_username()}.') | ||||
|         return True, f'Test with code {self.get_code()} has been updated by.' | ||||
| @@ -99,7 +99,7 @@ class User(UserMixin, db.Model): | ||||
|         return True, message | ||||
|  | ||||
|     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 email: self.set_email(email) | ||||
|         db.session.commit() | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
|  | ||||
| from ..models import Dataset | ||||
| from ..modules import db | ||||
|  | ||||
| from wtforms.validators import ValidationError | ||||
| @@ -43,3 +44,13 @@ def get_time_options(): | ||||
|         ('120', '2 hours') | ||||
|     ] | ||||
|     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 | ||||
		Reference in New Issue
	
	Block a user