Added a whole lot of views.
Finished quiz API views Finished question generator and answer eval
This commit is contained in:
		@@ -1,6 +1,7 @@
 | 
				
			|||||||
from ..forms.admin import CreateUser, Login, Register, ResetPassword, UpdatePassword
 | 
					from ..forms.admin import CreateUser, DeleteUser, Login, Register, ResetPassword, UpdatePassword, UpdateUser, UploadData
 | 
				
			||||||
from ..models import User
 | 
					from ..models import Dataset, 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.data import check_is_json, validate_json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from flask import Blueprint, flash, jsonify, render_template, redirect, request, session
 | 
					from flask import Blueprint, flash, jsonify, render_template, redirect, request, session
 | 
				
			||||||
from flask.helpers import url_for
 | 
					from flask.helpers import url_for
 | 
				
			||||||
@@ -18,12 +19,16 @@ admin = Blueprint(
 | 
				
			|||||||
@admin.route('/')
 | 
					@admin.route('/')
 | 
				
			||||||
@admin.route('/home/')
 | 
					@admin.route('/home/')
 | 
				
			||||||
@admin.route('/dashboard/')
 | 
					@admin.route('/dashboard/')
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
def _home():
 | 
					def _home():
 | 
				
			||||||
    return 'Home Page'
 | 
					    return 'Home Page' # TODO Dashboard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.route('/settings/')
 | 
					@admin.route('/settings/')
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
def _settings():
 | 
					def _settings():
 | 
				
			||||||
    return 'Settings Page'
 | 
					    users = User.query.all()
 | 
				
			||||||
 | 
					    datasets = Dataset.query.all()
 | 
				
			||||||
 | 
					    return render_template('/admin/settings/index.html', users=users, datasets=datasets)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.route('/login/', methods=['GET','POST'])
 | 
					@admin.route('/login/', methods=['GET','POST'])
 | 
				
			||||||
@disable_if_logged_in
 | 
					@disable_if_logged_in
 | 
				
			||||||
@@ -64,7 +69,6 @@ def _register():
 | 
				
			|||||||
    if request.method == 'POST':
 | 
					    if request.method == 'POST':
 | 
				
			||||||
        if form.validate_on_submit():
 | 
					        if form.validate_on_submit():
 | 
				
			||||||
            new_user = User()
 | 
					            new_user = User()
 | 
				
			||||||
            new_user.generate_id()
 | 
					 | 
				
			||||||
            new_user.set_username = request.form.get('username').lower()
 | 
					            new_user.set_username = request.form.get('username').lower()
 | 
				
			||||||
            new_user.set_email = request.form.get('email').lower()
 | 
					            new_user.set_email = request.form.get('email').lower()
 | 
				
			||||||
            new_user.set_password = request.form.get('password').lower()
 | 
					            new_user.set_password = request.form.get('password').lower()
 | 
				
			||||||
@@ -140,3 +144,86 @@ def _users():
 | 
				
			|||||||
        errors = [*form.username.errors, *form.email.errors, *form.password.errors]
 | 
					        errors = [*form.username.errors, *form.email.errors, *form.password.errors]
 | 
				
			||||||
        return jsonify({ 'error': errors}), 401
 | 
					        return jsonify({ 'error': errors}), 401
 | 
				
			||||||
    return render_template('/admin/settings/users.html', form = form, users = users)
 | 
					    return render_template('/admin/settings/users.html', form = form, users = users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.route('/settings/users/delete/<string:id>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def _delete_user(id:str):
 | 
				
			||||||
 | 
					    user = User.query.filter_by(id=id).first()
 | 
				
			||||||
 | 
					    form = DeleteUser()
 | 
				
			||||||
 | 
					    if request.method == 'POST': 
 | 
				
			||||||
 | 
					        if not user: return jsonify({'error': 'User does not exist.'}), 400
 | 
				
			||||||
 | 
					        if id == current_user.id: return jsonify({'error': 'Cannot delete your own account.'}), 400
 | 
				
			||||||
 | 
					        if form.validate_on_submit():
 | 
				
			||||||
 | 
					            password = request.form.get('password')
 | 
				
			||||||
 | 
					            if not current_user.verify_password(password): return jsonify({'error': 'The password you entered is incorrect.'}), 401
 | 
				
			||||||
 | 
					            success, message = user.delete(notify=request.form.get('notify'))
 | 
				
			||||||
 | 
					            if success: return jsonify({'success': message}), 200
 | 
				
			||||||
 | 
					            return jsonify({'error': message}), 400
 | 
				
			||||||
 | 
					        errors = form.password.errors
 | 
				
			||||||
 | 
					        return jsonify({ 'error': errors}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if id == current_user.id:
 | 
				
			||||||
 | 
					        flash('Cannot delete your own user account.', 'error')
 | 
				
			||||||
 | 
					        return redirect(url_for('admin._users'))
 | 
				
			||||||
 | 
					    if not user:
 | 
				
			||||||
 | 
					        flash('User not found.', 'error')
 | 
				
			||||||
 | 
					        return redirect(url_for('admin._users'))
 | 
				
			||||||
 | 
					    return render_template('/admin/settings/delete_user.html', form=form, id = id, user = user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.route('/settings/users/update/<string:id>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def _update_user(id:str):
 | 
				
			||||||
 | 
					    user = User.query.filter_by(id=id).first()
 | 
				
			||||||
 | 
					    form = UpdateUser()
 | 
				
			||||||
 | 
					    if request.method == 'POST': 
 | 
				
			||||||
 | 
					        if not user: return jsonify({'error': 'User does not exist.'}), 400
 | 
				
			||||||
 | 
					        if form.validate_on_submit():
 | 
				
			||||||
 | 
					            success, message = user.update(
 | 
				
			||||||
 | 
					                password = request.form.get('password'),
 | 
				
			||||||
 | 
					                email = request.form.get('email'),
 | 
				
			||||||
 | 
					                notify = request.form.get('notify')
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if success: return jsonify({'success': message}), 200
 | 
				
			||||||
 | 
					            return jsonify({'error': message}), 400
 | 
				
			||||||
 | 
					        errors = [*form.confirm_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
 | 
				
			||||||
 | 
					        return jsonify({ 'error': errors}), 400
 | 
				
			||||||
 | 
					    if not user:
 | 
				
			||||||
 | 
					        flash('User not found.', 'error')
 | 
				
			||||||
 | 
					        return redirect(url_for('admin._users'))
 | 
				
			||||||
 | 
					    return render_template('/admin/settings/delete_user.html', form=form, id = id, user = user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.route('/settings/questions/', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def _quesitons():
 | 
				
			||||||
 | 
					    form = UploadData()
 | 
				
			||||||
 | 
					    if request.method == 'POST':
 | 
				
			||||||
 | 
					        if form.validate_on_submit():
 | 
				
			||||||
 | 
					            upload = form.data_file.data
 | 
				
			||||||
 | 
					            if not check_is_json(upload): return jsonify({'error': 'Invalid file. Please upload a JSON file.'}), 400
 | 
				
			||||||
 | 
					            if not validate_json(upload): return jsonify({'error': 'The data in the file is invalid.'}), 400 # TODO Perhaps make a more complex validation script
 | 
				
			||||||
 | 
					            new_dataset = Dataset()
 | 
				
			||||||
 | 
					            success, message = new_dataset.create(
 | 
				
			||||||
 | 
					                upload = upload,
 | 
				
			||||||
 | 
					                default = request.form.get('default')
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if success: return jsonify({'success': message}), 200
 | 
				
			||||||
 | 
					            return jsonify({'error': message}), 400
 | 
				
			||||||
 | 
					        errors = form.data_file.errors
 | 
				
			||||||
 | 
					        return jsonify({ 'error': errors}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data = Dataset.query.all()
 | 
				
			||||||
 | 
					    return render_template('/admin/settings/questions.html', data=data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.route('/settings/questions/edit/', methods=['POST'])
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def _delete_questions():
 | 
				
			||||||
 | 
					    id = request.get_json()['id']
 | 
				
			||||||
 | 
					    action = request.get_json()['action']
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
@@ -1,4 +1,10 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from ..models import Dataset, Entry
 | 
				
			||||||
 | 
					from ..tools.test import evaluate_answers, generate_questions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Blueprint, jsonify, request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					from json import loads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api = Blueprint(
 | 
					api = Blueprint(
 | 
				
			||||||
    name='api',
 | 
					    name='api',
 | 
				
			||||||
@@ -7,8 +13,54 @@ api = Blueprint(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@api.route('/questions/', methods=['POST'])
 | 
					@api.route('/questions/', methods=['POST'])
 | 
				
			||||||
def _fetch_questions():
 | 
					def _fetch_questions():
 | 
				
			||||||
    return 'Fetch Questions'
 | 
					    id = request.get_json()['id']
 | 
				
			||||||
 | 
					    entry = Entry.query.filter_by(id=id).first()
 | 
				
			||||||
 | 
					    if not entry: return jsonify({'error': 'Invalid entry ID.'}), 400
 | 
				
			||||||
 | 
					    test = entry['test']
 | 
				
			||||||
 | 
					    user_code = entry['user_code']
 | 
				
			||||||
 | 
					    time_limit = test['time_limit']
 | 
				
			||||||
 | 
					    time_adjustment = 0
 | 
				
			||||||
 | 
					    if time_limit:
 | 
				
			||||||
 | 
					        _time_limit = int(time_limit)
 | 
				
			||||||
 | 
					        if user_code:
 | 
				
			||||||
 | 
					            time_adjustment = test['time_adjustments'][user_code]
 | 
				
			||||||
 | 
					            _time_limit += time_adjustment
 | 
				
			||||||
 | 
					        end_delta = timedelta(minutes=_time_limit)
 | 
				
			||||||
 | 
					        end_time = datetime.utcnow() + end_delta
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        end_time = None
 | 
				
			||||||
 | 
					    entry.start()
 | 
				
			||||||
 | 
					    dataset = test['dataset']
 | 
				
			||||||
 | 
					    success, message = dataset.check_file()
 | 
				
			||||||
 | 
					    if not success: return jsonify({'error': message}), 500
 | 
				
			||||||
 | 
					    data_path = dataset.get_file()
 | 
				
			||||||
 | 
					    with open(data_path, 'r') as data_file:
 | 
				
			||||||
 | 
					        data = loads(data_file.read())
 | 
				
			||||||
 | 
					    questions = generate_questions(data)
 | 
				
			||||||
 | 
					    return jsonify({
 | 
				
			||||||
 | 
					        'time_limit': end_time,
 | 
				
			||||||
 | 
					        'questions': questions,
 | 
				
			||||||
 | 
					        'start_time': entry['start_time'],
 | 
				
			||||||
 | 
					        'time_adjustment': time_adjustment
 | 
				
			||||||
 | 
					    }), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api.route('/submit/', methods=['POST'])
 | 
					@api.route('/submit/', methods=['POST'])
 | 
				
			||||||
def _submit_quiz():
 | 
					def _submit_quiz():
 | 
				
			||||||
    return 'Submit Quiz'
 | 
					    id = request.get_json()['id']
 | 
				
			||||||
 | 
					    answers = request.get_json()['answers']
 | 
				
			||||||
 | 
					    entry = Entry.query.filter_by(id=id).first()
 | 
				
			||||||
 | 
					    if not entry: return jsonify({'error': 'Unrecognised ID.'}), 400
 | 
				
			||||||
 | 
					    test = entry['test']
 | 
				
			||||||
 | 
					    dataset = test['dataset']
 | 
				
			||||||
 | 
					    success, message = dataset.check_file()
 | 
				
			||||||
 | 
					    if not success: return jsonify({'error': message}), 500
 | 
				
			||||||
 | 
					    data_path = dataset.get_file()
 | 
				
			||||||
 | 
					    with open(data_path, 'r') as data_file:
 | 
				
			||||||
 | 
					        data = loads(data_file.read())
 | 
				
			||||||
 | 
					    result = evaluate_answers(answers=answers, key=data)
 | 
				
			||||||
 | 
					    entry.complete(answers=answers, result=result)
 | 
				
			||||||
 | 
					    return jsonify({
 | 
				
			||||||
 | 
					        'success': 'Your submission has been processed. Redirecting you to receive your results.',
 | 
				
			||||||
 | 
					        'id': id
 | 
				
			||||||
 | 
					    }), 200
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
@@ -11,6 +11,7 @@ from sqlalchemy_utils import database_exists, create_database
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def install_scripts():
 | 
					def install_scripts():
 | 
				
			||||||
    if not path.isdir(f'./{data}'): mkdir(f'./{data}')
 | 
					    if not path.isdir(f'./{data}'): mkdir(f'./{data}')
 | 
				
			||||||
 | 
					    if not path.isdir(f'./{data}/questions'): mkdir(f'./{data}/questions')
 | 
				
			||||||
    if not path.isfile(f'./{data}/.gitignore'):
 | 
					    if not path.isfile(f'./{data}/.gitignore'):
 | 
				
			||||||
        with open(f'./{data}/.gitignore', 'a+') as file: file.write(f'*')
 | 
					        with open(f'./{data}/.gitignore', 'a+') as file: file.write(f'*')
 | 
				
			||||||
    if not path.isfile(f'./{data}/config.json'): save({}, 'config.json')
 | 
					    if not path.isfile(f'./{data}/config.json'): save({}, 'config.json')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
from .entry import Entry
 | 
					from .entry import Entry
 | 
				
			||||||
from .test import Test
 | 
					from .test import Test
 | 
				
			||||||
from .user import User
 | 
					from .user import User
 | 
				
			||||||
 | 
					from .dataset import Dataset
 | 
				
			||||||
							
								
								
									
										82
									
								
								ref-test/app/models/dataset.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								ref-test/app/models/dataset.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					from ..data import data
 | 
				
			||||||
 | 
					from ..modules import db
 | 
				
			||||||
 | 
					from ..tools.logs import write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import flash, jsonify
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from werkzeug.utils import secure_filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from json import dump, loads
 | 
				
			||||||
 | 
					from os import path, remove
 | 
				
			||||||
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Dataset(db.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    id = db.Column(db.String(36), primary_key=True)
 | 
				
			||||||
 | 
					    tests = db.relationship('Test', backref='dataset')
 | 
				
			||||||
 | 
					    creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
 | 
				
			||||||
 | 
					    date = db.Column(db.DateTime, nullable=False)
 | 
				
			||||||
 | 
					    default = db.Column(db.Boolean, default=False, nullable=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
					        return f'<Dataset {self.id}> was added.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generate_id.setter
 | 
				
			||||||
 | 
					    def generate_id(self): self.id = uuid4.hex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def make_default(self):
 | 
				
			||||||
 | 
					        for dataset in Dataset.query.all():
 | 
				
			||||||
 | 
					            dataset.default = False
 | 
				
			||||||
 | 
					        self.default = True
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        write('system.log', f'Dataset {self.id} set as default by {current_user.get_username()}.')
 | 
				
			||||||
 | 
					        flash(message='Dataset set as default.', category='success')
 | 
				
			||||||
 | 
					        return True, f'Dataset set as default.'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def delete(self):
 | 
				
			||||||
 | 
					        if self.default:
 | 
				
			||||||
 | 
					            message = 'Cannot delete the default dataset.'
 | 
				
			||||||
 | 
					            flash(message, 'error')
 | 
				
			||||||
 | 
					            return False, jsonify({'error': message})
 | 
				
			||||||
 | 
					        if Dataset.query.all().count() == 1:
 | 
				
			||||||
 | 
					            message = 'Cannot delete the only dataset.'
 | 
				
			||||||
 | 
					            flash(message, 'error')
 | 
				
			||||||
 | 
					            return False, jsonify({'error': 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)
 | 
				
			||||||
 | 
					        remove(file_path)
 | 
				
			||||||
 | 
					        db.session.delete(self)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return True, 'Dataset deleted.'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def create(self, upload, default:bool=False):
 | 
				
			||||||
 | 
					        self.generate_id()
 | 
				
			||||||
 | 
					        timestamp = datetime.now()
 | 
				
			||||||
 | 
					        filename = secure_filename('.'.join([self.id,'json']))
 | 
				
			||||||
 | 
					        file_path = path.join(data, 'questions', filename)
 | 
				
			||||||
 | 
					        upload.stream.seek(0)
 | 
				
			||||||
 | 
					        questions = loads(upload.read())
 | 
				
			||||||
 | 
					        with open(file_path, 'w') as file:
 | 
				
			||||||
 | 
					            dump(questions, file, indent=2)
 | 
				
			||||||
 | 
					        self.date = timestamp
 | 
				
			||||||
 | 
					        self.creator = current_user
 | 
				
			||||||
 | 
					        if default: self.make_default()
 | 
				
			||||||
 | 
					        write('system.log', f'New dataset {self.id} added by {current_user.get_username()}.')
 | 
				
			||||||
 | 
					        db.session.add(self)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return True, 'Dataset uploaded.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_file(self):
 | 
				
			||||||
 | 
					        filename = secure_filename('.'.join([self.id,'json']))
 | 
				
			||||||
 | 
					        file_path = path.join(data, 'questions', filename)
 | 
				
			||||||
 | 
					        if not path.isfile(file_path): return False, 'Data file is missing.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_file(self):
 | 
				
			||||||
 | 
					        filename = secure_filename('.'.join([self.id,'json']))
 | 
				
			||||||
 | 
					        file_path = path.join(data, 'questions', filename)
 | 
				
			||||||
 | 
					        return file_path
 | 
				
			||||||
@@ -8,6 +8,7 @@ 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
 | 
				
			||||||
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Entry(db.Model):
 | 
					class Entry(db.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,6 +26,15 @@ class Entry(db.Model):
 | 
				
			|||||||
    answers = db.Column(JsonEncodedDict, nullable=True)
 | 
					    answers = db.Column(JsonEncodedDict, nullable=True)
 | 
				
			||||||
    result = db.Column(JsonEncodedDict, nullable=True)
 | 
					    result = db.Column(JsonEncodedDict, nullable=True)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
					        return f'<New entry by {self.first_name} {self.surname}> was added with <id {self.id}>.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generate_id.setter
 | 
				
			||||||
 | 
					    def generate_id(self): self.id = uuid4.hex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def set_first_name(self): raise AttributeError('set_first_name is not a readable attribute.')
 | 
					    def set_first_name(self): raise AttributeError('set_first_name is not a readable attribute.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,10 +73,11 @@ class Entry(db.Model):
 | 
				
			|||||||
        write('tests.log', f'New test started by {self.get_first_name()} {self.get_surname()}.')
 | 
					        write('tests.log', f'New test started by {self.get_first_name()} {self.get_surname()}.')
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def complete(self):
 | 
					    def complete(self, answers:dict=None, result:dict=None):
 | 
				
			||||||
        self.end_time = datetime.now()
 | 
					        self.end_time = datetime.now()
 | 
				
			||||||
 | 
					        self.answers = answers
 | 
				
			||||||
        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)
 | 
					        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 = 'finished'
 | 
				
			||||||
            self.valid = True
 | 
					            self.valid = True
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ from datetime import datetime
 | 
				
			|||||||
from json import dump, loads
 | 
					from json import dump, loads
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import secrets
 | 
					import secrets
 | 
				
			||||||
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Test(db.Model):
 | 
					class Test(db.Model):
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -21,18 +21,33 @@ class Test(db.Model):
 | 
				
			|||||||
    end_date = db.Column(db.DateTime, nullable=True)
 | 
					    end_date = db.Column(db.DateTime, nullable=True)
 | 
				
			||||||
    time_limit = db.Column(db.Integer, nullable=True)
 | 
					    time_limit = db.Column(db.Integer, nullable=True)
 | 
				
			||||||
    creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
 | 
					    creator_id = db.Column(db.String(36), db.ForeignKey('user.id'))
 | 
				
			||||||
    data = db.Column(db.String(36), nullable=False)
 | 
					    dataset_id = db.Column(db.String(36), db.ForeignKey('dataset.id'))
 | 
				
			||||||
    adjustments = db.Column(JsonEncodedDict, nullable=True)
 | 
					    adjustments = db.Column(JsonEncodedDict, nullable=True)
 | 
				
			||||||
    entries = db.relationship('Entry', backref='test')
 | 
					    entries = db.relationship('Entry', backref='test')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
        return f'<test with code {self.code} was created by {current_user.get_username()}.>'
 | 
					        return f'<test with code {self.code} was created by {current_user.get_username()}.>'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def generate_id(self): raise AttributeError('generate_id is not a readable attribute.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generate_id.setter
 | 
				
			||||||
 | 
					    def generate_id(self): self.id = uuid4.hex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def generate_code(self): raise AttributeError('generate_code is not a readable attribute.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    generate_code.setter
 | 
				
			||||||
 | 
					    def generate_code(self): self.code = secrets.token_hex(6).lower()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_code(self):
 | 
					    def get_code(self):
 | 
				
			||||||
        code = self.code.upper()
 | 
					        code = self.code.upper()
 | 
				
			||||||
        return '—'.join([code[:4], code[4:8], code[8:]])
 | 
					        return '—'.join([code[:4], code[4:8], code[8:]])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self):
 | 
					    def create(self):
 | 
				
			||||||
 | 
					        self.generate_id()
 | 
				
			||||||
 | 
					        self.generate_code()
 | 
				
			||||||
 | 
					        self.creator = current_user
 | 
				
			||||||
        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.code} created by {current_user.get_username()}.')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ class User(UserMixin, db.Model):
 | 
				
			|||||||
    reset_token = db.Column(db.String(20), nullable=True)
 | 
					    reset_token = db.Column(db.String(20), nullable=True)
 | 
				
			||||||
    verification_token = db.Column(db.String(20), nullable=True)
 | 
					    verification_token = db.Column(db.String(20), nullable=True)
 | 
				
			||||||
    tests = db.relationship('Test', backref='creator')
 | 
					    tests = db.relationship('Test', backref='creator')
 | 
				
			||||||
 | 
					    datasets = db.relationship('Dataset', backref='creator')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
        return f'<user {self.username}> was added with <id {self.id}>.'
 | 
					        return f'<user {self.username}> was added with <id {self.id}>.'
 | 
				
			||||||
@@ -52,6 +53,7 @@ class User(UserMixin, db.Model):
 | 
				
			|||||||
    def get_email(self): return decrypt(self.email)
 | 
					    def get_email(self): return decrypt(self.email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def register(self, notify:bool=False):
 | 
					    def register(self, notify:bool=False):
 | 
				
			||||||
 | 
					        self.generate_id()
 | 
				
			||||||
        users = User.query.all()
 | 
					        users = User.query.all()
 | 
				
			||||||
        for user in users:
 | 
					        for user in users:
 | 
				
			||||||
            if user.get_username() == self.get_username(): return False, f'Username {self.get_username()} already in use.'
 | 
					            if user.get_username() == self.get_username(): return False, f'Username {self.get_username()} already in use.'
 | 
				
			||||||
@@ -88,17 +90,19 @@ class User(UserMixin, db.Model):
 | 
				
			|||||||
        self.reset_token = self.verification_token = None
 | 
					        self.reset_token = self.verification_token = None
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def delete(self):
 | 
					    def delete(self, notify:bool=False):
 | 
				
			||||||
        username = self.get_username()
 | 
					        username = self.get_username()
 | 
				
			||||||
        db.session.delete(self)
 | 
					        db.session.delete(self)
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
        write('users.log', f'User \'{username}\' was deleted by \'{current_user.get_username()}\'.')
 | 
					        message = f'User \'{username}\' was deleted by \'{current_user.get_username()}\'.'
 | 
				
			||||||
 | 
					        write('users.log', message)
 | 
				
			||||||
 | 
					        return True, message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, password:str=None, email:str=None):
 | 
					    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, jsonify({'error': '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()
 | 
				
			||||||
        message = f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.'
 | 
					        message = f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.'
 | 
				
			||||||
        write('system.log', message)
 | 
					        write('system.log', message)
 | 
				
			||||||
        return True, jsonify({'success': message})
 | 
					        return True, message
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								ref-test/app/templates/privacy.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ref-test/app/templates/privacy.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<h1>Privacy Policy</h1>
 | 
				
			||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
from ..data import data as data_dir
 | 
					from ..data import data as data_dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 | 
					from random import shuffle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load(filename:str):
 | 
					def load(filename:str):
 | 
				
			||||||
    with open(f'./{data_dir}/{filename}') as file:
 | 
					    with open(f'./{data_dir}/{filename}') as file:
 | 
				
			||||||
@@ -7,4 +9,26 @@ def load(filename:str):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def save(data:dict, filename:str):
 | 
					def save(data:dict, filename:str):
 | 
				
			||||||
    with open(f'./{data_dir}/{filename}', 'w') as file:
 | 
					    with open(f'./{data_dir}/{filename}', 'w') as file:
 | 
				
			||||||
        json.dump(data, file, indent=4)
 | 
					        json.dump(data, file, indent=4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_is_json(file):
 | 
				
			||||||
 | 
					    if not '.' in file.filename or not file.filename.rsplit('.',1)[-1] == 'json': return False
 | 
				
			||||||
 | 
					    return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def validate_json(file):
 | 
				
			||||||
 | 
					    file.stream.seek(0)
 | 
				
			||||||
 | 
					    data = json.loads(file.read())
 | 
				
			||||||
 | 
					    if not type(data) is list: return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def randomise_list(list:list):
 | 
				
			||||||
 | 
					    _list = list.copy()
 | 
				
			||||||
 | 
					    shuffle(_list)
 | 
				
			||||||
 | 
					    return(_list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_tag_list(dataset:list):
 | 
				
			||||||
 | 
					    output = []
 | 
				
			||||||
 | 
					    for block in dataset:
 | 
				
			||||||
 | 
					        if block['type'] == 'question': output = list(set(output) | set(block['tags']))
 | 
				
			||||||
 | 
					        if block['type'] == 'block':
 | 
				
			||||||
 | 
					            for question in block['questions']: output = list(set(output) | set(question['tags']))
 | 
				
			||||||
 | 
					    return output
 | 
				
			||||||
@@ -33,4 +33,13 @@ def value(min:int=0, max:int=None):
 | 
				
			|||||||
        value = field.data or 0
 | 
					        value = field.data or 0
 | 
				
			||||||
        if value < min or max != None and value > max:
 | 
					        if value < min or max != None and value > max:
 | 
				
			||||||
            raise ValidationError(message)
 | 
					            raise ValidationError(message)
 | 
				
			||||||
    return length
 | 
					    return length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_time_options():
 | 
				
			||||||
 | 
					    time_options = [
 | 
				
			||||||
 | 
					        ('none', 'None'),
 | 
				
			||||||
 | 
					        ('60', '1 hour'),
 | 
				
			||||||
 | 
					        ('90', '1 hour 30 minutes'),
 | 
				
			||||||
 | 
					        ('120', '2 hours')
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    return time_options
 | 
				
			||||||
@@ -1,2 +1,104 @@
 | 
				
			|||||||
 | 
					from .data import randomise_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_test_code(code):
 | 
					def parse_test_code(code):
 | 
				
			||||||
        return code.replace('—', '').lower()
 | 
					    return code.replace('—', '').lower()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_questions(dataset:list):
 | 
				
			||||||
 | 
					    output = []
 | 
				
			||||||
 | 
					    for block in randomise_list(dataset):
 | 
				
			||||||
 | 
					        if block['type'] == 'question':
 | 
				
			||||||
 | 
					            question = {
 | 
				
			||||||
 | 
					                'type': 'question',
 | 
				
			||||||
 | 
					                'q_no': block['q_no'],
 | 
				
			||||||
 | 
					                'question_header': '',
 | 
				
			||||||
 | 
					                'text': block['text']
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if block['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(block['options'])])
 | 
				
			||||||
 | 
					            else: question['options'] = block['options'].copy()
 | 
				
			||||||
 | 
					            output.append(question)
 | 
				
			||||||
 | 
					        elif block['type'] == 'block':
 | 
				
			||||||
 | 
					            for key, _question in enumerate(randomise_list(block['questions'])):
 | 
				
			||||||
 | 
					                question = {
 | 
				
			||||||
 | 
					                    'type': 'block',
 | 
				
			||||||
 | 
					                    'q_no': _question['q_no'],
 | 
				
			||||||
 | 
					                    'question_header': block['question_header'] if 'question_header' in block else '',
 | 
				
			||||||
 | 
					                    'block_length': len(block['questions']),
 | 
				
			||||||
 | 
					                    'block_q_no': key,
 | 
				
			||||||
 | 
					                    'text': _question['text']
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if _question['q_type'] == 'Multiple Choice': question['options'] = randomise_list([*enumerate(_question['options'])])
 | 
				
			||||||
 | 
					                else: question['options'] = _question['options'].copy()
 | 
				
			||||||
 | 
					                output.append(question)
 | 
				
			||||||
 | 
					    return output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def evaluate_answers(answers:dict, key:list):
 | 
				
			||||||
 | 
					    score = 0
 | 
				
			||||||
 | 
					    max = 0
 | 
				
			||||||
 | 
					    tags = {}
 | 
				
			||||||
 | 
					    for block in key:
 | 
				
			||||||
 | 
					        if block['type'] == 'question':
 | 
				
			||||||
 | 
					            max += 1
 | 
				
			||||||
 | 
					            q_no = block['q_no']
 | 
				
			||||||
 | 
					            if str(q_no) in answers:
 | 
				
			||||||
 | 
					                submitted_answer = int(answers[str(q_no)])
 | 
				
			||||||
 | 
					                if submitted_answer == block['correct']:
 | 
				
			||||||
 | 
					                    score += 1
 | 
				
			||||||
 | 
					                    for tag in block['tags']:
 | 
				
			||||||
 | 
					                        if tag not in tags:
 | 
				
			||||||
 | 
					                            tags[tag] = {
 | 
				
			||||||
 | 
					                                'scored': 1,
 | 
				
			||||||
 | 
					                                'max': 1
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        else:
 | 
				
			||||||
 | 
					                            tags[tag]['scored'] += 1
 | 
				
			||||||
 | 
					                            tags[tag]['max'] += 1
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    for tag in block['tags']:
 | 
				
			||||||
 | 
					                        if tag not in tags:
 | 
				
			||||||
 | 
					                            tags[tag] = {
 | 
				
			||||||
 | 
					                                'scored': 0,
 | 
				
			||||||
 | 
					                                'max': 1
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        else: tags[tag]['max'] += 1
 | 
				
			||||||
 | 
					        elif block['type'] == 'block':
 | 
				
			||||||
 | 
					            for question in block['questions']:
 | 
				
			||||||
 | 
					                max += 1
 | 
				
			||||||
 | 
					                q_no = question['q_no']
 | 
				
			||||||
 | 
					                if str(q_no) in answers:
 | 
				
			||||||
 | 
					                    submitted_answer = int(answers[str(q_no)])
 | 
				
			||||||
 | 
					                    if submitted_answer == question['correct']:
 | 
				
			||||||
 | 
					                        score += 1
 | 
				
			||||||
 | 
					                        for tag in question['tags']:
 | 
				
			||||||
 | 
					                            if tag not in tags:
 | 
				
			||||||
 | 
					                                tags[tag] = {
 | 
				
			||||||
 | 
					                                    'scored': 1,
 | 
				
			||||||
 | 
					                                    'max': 1
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            else:
 | 
				
			||||||
 | 
					                                tags[tag]['scored'] += 1
 | 
				
			||||||
 | 
					                                tags[tag]['max'] += 1
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        for tag in question['tags']:
 | 
				
			||||||
 | 
					                            if tag not in tags:
 | 
				
			||||||
 | 
					                                tags[tag] = {
 | 
				
			||||||
 | 
					                                    'scored': 0,
 | 
				
			||||||
 | 
					                                    'max': 1
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            else: tags[tag]['max'] += 1
 | 
				
			||||||
 | 
					    grade = 'merit' if score/max >= .85 else 'pass' if score/max >= .70 else 'fail'
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'grade': grade,
 | 
				
			||||||
 | 
					        'tags': tags,
 | 
				
			||||||
 | 
					        'score': score,
 | 
				
			||||||
 | 
					        'max': max
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_correct_answers(dataset:list):
 | 
				
			||||||
 | 
					    output = {}
 | 
				
			||||||
 | 
					    for block in dataset:
 | 
				
			||||||
 | 
					        if block['type'] == 'question':
 | 
				
			||||||
 | 
					            output[str(block['q_no'])] = block['options'][block['correct']]
 | 
				
			||||||
 | 
					        if block['type'] == 'block':
 | 
				
			||||||
 | 
					            for question in block['questions']:
 | 
				
			||||||
 | 
					                output[str(question['q_no'])] = question['options'][question['correct']]
 | 
				
			||||||
 | 
					    return output
 | 
				
			||||||
							
								
								
									
										12
									
								
								ref-test/app/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ref-test/app/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					from flask import Blueprint, render_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					views = Blueprint(
 | 
				
			||||||
 | 
					    name='common',
 | 
				
			||||||
 | 
					    import_name=__name__,
 | 
				
			||||||
 | 
					    template_folder='templates',
 | 
				
			||||||
 | 
					    static_folder='static'
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@views.route('/privacy/')
 | 
				
			||||||
 | 
					def _privacy():
 | 
				
			||||||
 | 
					    return render_template('privacy.html')
 | 
				
			||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
views = Blueprint(
 | 
					 | 
				
			||||||
    name='common',
 | 
					 | 
				
			||||||
    import_name=__name__,
 | 
					 | 
				
			||||||
    template_folder='templates',
 | 
					 | 
				
			||||||
    static_folder='static'
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import privacy
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
from . import views
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@views.route('/privacy/')
 | 
					 | 
				
			||||||
def _privacy():
 | 
					 | 
				
			||||||
    return 'Privacy Policy'
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user