diff --git a/ref-test/app/__init__.py b/ref-test/app/__init__.py index e69de29..09bb477 100644 --- a/ref-test/app/__init__.py +++ b/ref-test/app/__init__.py @@ -0,0 +1,60 @@ +from .config import Development as Config +from .install import install_app +from .models import Entry, Dataset, Test, User +from .extensions import bootstrap, csrf, db, login_manager, mail +from .tools.data import save +from .tools.logs import write + +from flask import flash, Flask, render_template, request +from flask.helpers import url_for +from flask.json import jsonify +from flask_wtf.csrf import CSRFError +from werkzeug.middleware.proxy_fix import ProxyFix + +from cryptography.fernet import Fernet +from datetime import datetime + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config()) + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto= 1, x_host= 1) + + bootstrap.init_app(app) + csrf.init_app(app) + db.init_app(app) + login_manager.init_app(app) + mail.init_app(app) + + login_manager.login_view = 'admin._login' + @login_manager.user_loader + def _load_user(id): + return User.query.filter_by(id=id).first() + + @app.before_request + def _check_cookie_consent(): + if request.cookies.get('cookie_consent'): + return + if any([ request.path.startswith(x) for x in [ '/admin/static/', '/root/', '/quiz/static', '/cookies/' ] ]): + return + flash(f'Cookie Consent: This web site only stores minimal, functional cookies. It does not store any tracking information. By using this site, you consent to this use of cookies. For more information, see our privacy policy.', 'cookie_alert') + + @app.errorhandler(404) + def _404_handler(error): return render_template('404.html') + @app.errorhandler(CSRFError) + def _csrf_handler(): return jsonify({'error':'Could not validate a secure connection.'}), 403 + @app.context_processor + def _now(): return {'now': datetime.now()} + + from .admin.views import admin + from .api.views import api + from .quiz.views import quiz + from .views import views + + app.register_blueprint(admin, url_prefix='/admin') + app.register_blueprint(api, url_prefix='/api') + app.register_blueprint(views) + app.register_blueprint(quiz) + + install_app(app) + + return app \ No newline at end of file diff --git a/ref-test/app/admin/views.py b/ref-test/app/admin/views.py index cfea08f..c2adfcb 100644 --- a/ref-test/app/admin/views.py +++ b/ref-test/app/admin/views.py @@ -1,7 +1,7 @@ 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.forms import get_dataset_choices, get_time_options, send_errors_to_client from ..tools.data import check_is_json, validate_json from ..tools.test import answer_options, get_correct_answers @@ -61,8 +61,7 @@ def _login(): return jsonify({'success': f'Successfully logged in.'}), 200 return jsonify({'error': f'The password you entered is incorrect.'}), 401 return jsonify({'error': f'The username you entered does not exist.'}), 401 - errors = [*form.username.errors, *form.password.errors] - return jsonify({ 'error': errors}), 400 + return send_errors_to_client(form=form) if 'remembered_username' in session: form.username.data = session.pop('remembered_username') next = request.args.get('next') return render_template('/admin/auth/login.html', form=form, next=next) @@ -90,8 +89,7 @@ def _register(): return jsonify({'success': message}), 200 flash(message=message, category='error') return jsonify({'error': message}), 401 - errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors] - return jsonify({ 'error': errors}), 400 + return send_errors_to_client(form=form) return render_template('admin/auth/register.html', form=form) @admin.route('/reset/', methods=['GET','POST']) @@ -108,8 +106,7 @@ def _reset(): if not user: return jsonify({'error': 'The user account does not exist.'}), 400 if not user.get_email() == request.form.get('email'): return jsonify({'error': 'The email address does not match the user account.'}), 400 return user.reset_password() - errors = [*form.username.errors, *form.email.errors] - return jsonify({ 'error': errors}), 400 + return send_errors_to_client(form=form) token = request.args.get('token') if token: @@ -134,8 +131,7 @@ def _update_password(): session['remembered_username'] = user.get_username() flash('Your password has been reset.', 'success') return jsonify({'success':'Your password has been reset'}), 200 - errors = [*form.password.errors, *form.password_reenter.errors] - return jsonify({ 'error': errors}), 401 + return send_errors_to_client(form=form) @admin.route('/settings/users/', methods=['GET', 'POST']) @login_required @@ -152,8 +148,7 @@ def _users(): success, message = new_user.register(notify=request.form.get('notify'), password=password) if success: return jsonify({'success': message}), 200 return jsonify({'error': message}), 401 - errors = [*form.username.errors, *form.email.errors, *form.password.errors] - return jsonify({ 'error': errors}), 401 + return send_errors_to_client(form=form) return render_template('/admin/settings/users.html', form = form, users = users) @admin.route('/settings/users/delete/', methods=['GET', 'POST']) @@ -170,8 +165,7 @@ def _delete_user(id:str): 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 + return send_errors_to_client(form=form) if id == current_user.id: flash('Cannot delete your own user account.', 'error') @@ -199,8 +193,7 @@ def _update_user(id:str): flash(message, '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 + return send_errors_to_client(form=form) if not user: flash('User not found.', 'error') return redirect(url_for('admin._users')) @@ -222,8 +215,7 @@ def _questions(): ) if success: return jsonify({'success': message}), 200 return jsonify({'error': message}), 400 - errors = form.data_file.errors - return jsonify({ 'error': errors}), 400 + return send_errors_to_client(form=form) data = Dataset.query.all() return render_template('/admin/settings/questions.html', form=form, data=data) @@ -299,9 +291,7 @@ def _create_test(): 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 + return send_errors_to_client(form=form) @admin.route('/tests/edit/', methods=['POST']) @login_required @@ -385,7 +375,7 @@ def _view_entry(id:str=None): @admin.route('/certificate/',methods=['POST']) @login_required def _generate_certificate(): - from main import db + from ..extensions 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 diff --git a/ref-test/app/config.py b/ref-test/app/config.py index e62ead5..48aa4f1 100644 --- a/ref-test/app/config.py +++ b/ref-test/app/config.py @@ -13,7 +13,7 @@ class Config(object): SECRET_KEY = os.getenv('SECRET_KEY') SERVER_NAME = os.getenv('SERVER_NAME') SESSION_COOKIE_SECURE = True - SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(DATA)}/database.db' + SQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(os.path.abspath(f"{DATA}/database.db"))}' SQLALCHEMY_TRACK_MODIFICATIONS = False MAIL_SERVER = os.getenv('MAIL_SERVER') @@ -34,6 +34,7 @@ class Production(Config): class Development(Config): APP_HOST = '127.0.0.1' DEBUG = True + SERVER_NAME = '127.0.0.1:5000' SESSION_COOKIE_SECURE = False MAIL_SERVER = 'localhost' MAIL_DEBUG = True diff --git a/ref-test/app/install.py b/ref-test/app/install.py new file mode 100644 index 0000000..3ab1fa4 --- /dev/null +++ b/ref-test/app/install.py @@ -0,0 +1,33 @@ +from .extensions import db +from .tools.data import save +from .tools.logs import write + +from sqlalchemy_utils import create_database, database_exists + +from cryptography.fernet import Fernet +from os import mkdir, path +from pathlib import Path + +def install_app(app): + with app.app_context(): + data = Path(app.config.get('DATA')) + database_uri = app.config.get('SQLALCHEMY_DATABASE_URI') + 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'): + 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.isdir(f'./{data}/logs'): mkdir(f'./{data}/logs') + if not path.isfile(f'./{data}/logs/users.log'): write('users.log', 'Log file created.') + if not path.isfile(f'./{data}/logs/system.log'): write('system.log', 'Log file created.') + if not path.isfile(f'./{data}/logs/tests.log'): write('tests.log', 'Log file created.') + if not database_exists(database_uri): + create_database(database_uri) + write('system.log', 'No database found. Creating a new database.') + db.create_all() + write('system.log', 'Creating database schema.') + if not path.isfile(f'./{data}/.encryption.key'): + write('system.log', 'No encryption key found. Generating new encryption key.') + with open(f'./{data}/.encryption.key', 'wb') as key_file: + key = Fernet.generate_key() + key_file.write(key) \ No newline at end of file diff --git a/ref-test/app/models/user.py b/ref-test/app/models/user.py index a1cec1d..cd945c2 100644 --- a/ref-test/app/models/user.py +++ b/ref-test/app/models/user.py @@ -189,7 +189,10 @@ class User(UserMixin, db.Model): if not password and not email: return False, 'There were no changes requested.' if password: self.set_password(password) old_email = self.get_email() - if email: self.set_email(email) + if email: + for entry in User.query.all(): + if entry.get_email() == email and not entry == self: return False, f'The email address {email} is already in use.' + self.set_email(email) db.session.commit() write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.') if notify: diff --git a/ref-test/app/quiz/views.py b/ref-test/app/quiz/views.py index 8c8000c..c6c437b 100644 --- a/ref-test/app/quiz/views.py +++ b/ref-test/app/quiz/views.py @@ -1,5 +1,6 @@ from ..forms.quiz import StartQuiz from ..models import Entry, Test +from ..tools.forms import send_errors_to_client from ..tools.test import redirect_if_started from flask import abort, Blueprint, jsonify, redirect, render_template, request, session @@ -52,8 +53,7 @@ def _start(): 'id': entry.id }), 200 return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400 - errors = [*form.test_code.errors, *form.user_code.errors, *form.first_name.errors, *form.surname.errors, *form.email.errors, *form.club.errors] - return jsonify({ 'error': errors}), 400 + return send_errors_to_client(form=form) return render_template('/quiz/start_quiz.html', form = form) @quiz.route('/quiz/') diff --git a/ref-test/app/tools/forms.py b/ref-test/app/tools/forms.py index cef280d..eeea160 100644 --- a/ref-test/app/tools/forms.py +++ b/ref-test/app/tools/forms.py @@ -1,6 +1,7 @@ from ..extensions import db +from flask import jsonify from wtforms.validators import ValidationError import json @@ -53,4 +54,8 @@ def get_dataset_choices(): label = f'{label} (Default)' if dataset.default else label choice = (dataset.id, label) dataset_choices.append(choice) - return dataset_choices \ No newline at end of file + return dataset_choices + +def send_errors_to_client(form): + errors = [*form.errors] + return jsonify({ 'error': errors}), 400 \ No newline at end of file diff --git a/ref-test/main.py b/ref-test/main.py index f9f4838..317219e 100644 --- a/ref-test/main.py +++ b/ref-test/main.py @@ -1,86 +1,3 @@ -from app.config import Development as Config -from app.models import Entry, Dataset, Test, User -from app.extensions import bootstrap, csrf, db, login_manager, mail -from app.tools.data import save -from app.tools.logs import write - -from flask import flash, Flask, render_template, request -from flask.helpers import url_for -from flask.json import jsonify -from flask_wtf.csrf import CSRFError -from sqlalchemy_utils import database_exists, create_database -from werkzeug.middleware.proxy_fix import ProxyFix - -from cryptography.fernet import Fernet -from datetime import datetime -from os import mkdir, path -from pathlib import Path - -def create_app(): - app = Flask(__name__) - app.config.from_object(Config()) - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto= 1, x_host= 1) - - bootstrap.init_app(app) - csrf.init_app(app) - db.init_app(app) - login_manager.init_app(app) - mail.init_app(app) - - login_manager.login_view = 'admin._login' - @login_manager.user_loader - def _load_user(id): - return User.query.filter_by(id=id).first() - - @app.before_request - def _check_cookie_consent(): - if request.cookies.get('cookie_consent'): - return - if any([ request.path.startswith(x) for x in [ '/admin/static/', '/root/', '/quiz/static', '/admin/editor/static/', '/cookies/' ] ]): - return - flash(f'Cookie Consent: This web site only stores minimal, functional cookies. It does not store any tracking information. By using this site, you consent to this use of cookies. For more information, see our privacy policy.', 'cookie_alert') - - @app.errorhandler(404) - def _404_handler(error): return render_template('404.html') - @app.errorhandler(CSRFError) - def _csrf_handler(): return jsonify({'error':'Could not validate a secure connection.'}), 403 - @app.context_processor - def _now(): return {'now': datetime.now()} - - from app.admin.views import admin - from app.api.views import api - from app.quiz.views import quiz - from app.views import views - from app.editor.views import editor - - app.register_blueprint(admin, url_prefix='/admin') - app.register_blueprint(api, url_prefix='/api') - app.register_blueprint(views) - app.register_blueprint(quiz) - app.register_blueprint(editor, url_prefix='/admin/editor') - - data = Path(app.config.get('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'): - 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.isdir(f'./{data}/logs'): mkdir(f'./{data}/logs') - if not path.isfile(f'./{data}/logs/users.log'): write('users.log', 'Log file created.') - if not path.isfile(f'./{data}/logs/system.log'): write('system.log', 'Log file created.') - if not path.isfile(f'./{data}/logs/tests.log'): write('tests.log', 'Log file created.') - if not database_exists(Config.SQLALCHEMY_DATABASE_URI): - create_database(Config.SQLALCHEMY_DATABASE_URI) - write('system.log', 'No database found. Creating a new database.') - with app.app_context(): db.create_all() - write('system.log', 'Creating database schema.') - if not path.isfile(f'./{data}/.encryption.key'): - write('system.log', 'No encryption key found. Generating new encryption key.') - with open(f'./{data}/.encryption.key', 'wb') as key_file: - key = Fernet.generate_key() - key_file.write(key) - return app - +from app import create_app app = create_app() - if __name__ == '__main__': app.run() \ No newline at end of file