Merge branch 'master' into editor

This commit is contained in:
Vivek Santayana 2022-06-20 12:13:09 +01:00
commit d83999aa43
8 changed files with 119 additions and 110 deletions

View File

@ -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'<strong>Cookie Consent</strong>: 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 <a href="{url_for("views._privacy")}">privacy policy</a>.', '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

View File

@ -1,7 +1,7 @@
from ..forms.admin import AddTimeAdjustment, CreateTest, 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, Entry, Test, 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.forms import get_dataset_choices, get_time_options, send_errors_to_client
from ..tools.data import check_is_json, validate_json from ..tools.data import check_is_json, validate_json
from ..tools.test import answer_options, get_correct_answers 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({'success': f'Successfully logged in.'}), 200
return jsonify({'error': f'The password you entered is incorrect.'}), 401 return jsonify({'error': f'The password you entered is incorrect.'}), 401
return jsonify({'error': f'The username you entered does not exist.'}), 401 return jsonify({'error': f'The username you entered does not exist.'}), 401
errors = [*form.username.errors, *form.password.errors] return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
if 'remembered_username' in session: form.username.data = session.pop('remembered_username') if 'remembered_username' in session: form.username.data = session.pop('remembered_username')
next = request.args.get('next') next = request.args.get('next')
return render_template('/admin/auth/login.html', form=form, next=next) return render_template('/admin/auth/login.html', form=form, next=next)
@ -90,8 +89,7 @@ def _register():
return jsonify({'success': message}), 200 return jsonify({'success': message}), 200
flash(message=message, category='error') flash(message=message, category='error')
return jsonify({'error': message}), 401 return jsonify({'error': message}), 401
errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors] return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
return render_template('admin/auth/register.html', form=form) return render_template('admin/auth/register.html', form=form)
@admin.route('/reset/', methods=['GET','POST']) @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: 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 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() return user.reset_password()
errors = [*form.username.errors, *form.email.errors] return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
token = request.args.get('token') token = request.args.get('token')
if token: if token:
@ -134,8 +131,7 @@ def _update_password():
session['remembered_username'] = user.get_username() session['remembered_username'] = user.get_username()
flash('Your password has been reset.', 'success') flash('Your password has been reset.', 'success')
return jsonify({'success':'Your password has been reset'}), 200 return jsonify({'success':'Your password has been reset'}), 200
errors = [*form.password.errors, *form.password_reenter.errors] return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 401
@admin.route('/settings/users/', methods=['GET', 'POST']) @admin.route('/settings/users/', methods=['GET', 'POST'])
@login_required @login_required
@ -152,8 +148,7 @@ def _users():
success, message = new_user.register(notify=request.form.get('notify'), password=password) success, message = new_user.register(notify=request.form.get('notify'), password=password)
if success: return jsonify({'success': message}), 200 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 401 return jsonify({'error': message}), 401
errors = [*form.username.errors, *form.email.errors, *form.password.errors] return send_errors_to_client(form=form)
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']) @admin.route('/settings/users/delete/<string:id>', methods=['GET', 'POST'])
@ -170,8 +165,7 @@ def _delete_user(id:str):
success, message = user.delete(notify=request.form.get('notify')) success, message = user.delete(notify=request.form.get('notify'))
if success: return jsonify({'success': message}), 200 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
errors = form.password.errors return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
if id == current_user.id: if id == current_user.id:
flash('Cannot delete your own user account.', 'error') flash('Cannot delete your own user account.', 'error')
@ -199,8 +193,7 @@ def _update_user(id:str):
flash(message, 'success') flash(message, 'success')
return jsonify({'success': message}), 200 return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
errors = [*form.confirm_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors] return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
if not user: if not user:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('admin._users')) return redirect(url_for('admin._users'))
@ -222,8 +215,7 @@ def _questions():
) )
if success: return jsonify({'success': message}), 200 if success: return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
errors = form.data_file.errors return send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
data = Dataset.query.all() data = Dataset.query.all()
return render_template('/admin/settings/questions.html', form=form, data=data) return render_template('/admin/settings/questions.html', form=form, data=data)
@ -299,9 +291,7 @@ def _create_test():
flash(message=message, category='success') flash(message=message, category='success')
return jsonify({'success': message}), 200 return jsonify({'success': message}), 200
return jsonify({'error': message}), 400 return jsonify({'error': message}), 400
else: return send_errors_to_client(form=form)
errors = [*form.start_date.errors, *form.expiry_date.errors, *form.time_limit.errors]
return jsonify({ 'error': errors}), 400
@admin.route('/tests/edit/', methods=['POST']) @admin.route('/tests/edit/', methods=['POST'])
@login_required @login_required
@ -385,7 +375,7 @@ def _view_entry(id:str=None):
@admin.route('/certificate/',methods=['POST']) @admin.route('/certificate/',methods=['POST'])
@login_required @login_required
def _generate_certificate(): def _generate_certificate():
from main import db from ..extensions import db
id = request.get_json()['id'] id = request.get_json()['id']
entry = Entry.query.filter_by(id=id).first() entry = Entry.query.filter_by(id=id).first()
if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404 if not entry: return jsonify({'error': 'Invalid entry ID.'}), 404

View File

@ -13,7 +13,7 @@ class Config(object):
SECRET_KEY = os.getenv('SECRET_KEY') SECRET_KEY = os.getenv('SECRET_KEY')
SERVER_NAME = os.getenv('SERVER_NAME') SERVER_NAME = os.getenv('SERVER_NAME')
SESSION_COOKIE_SECURE = True 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 SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.getenv('MAIL_SERVER') MAIL_SERVER = os.getenv('MAIL_SERVER')
@ -34,6 +34,7 @@ class Production(Config):
class Development(Config): class Development(Config):
APP_HOST = '127.0.0.1' APP_HOST = '127.0.0.1'
DEBUG = True DEBUG = True
SERVER_NAME = '127.0.0.1:5000'
SESSION_COOKIE_SECURE = False SESSION_COOKIE_SECURE = False
MAIL_SERVER = 'localhost' MAIL_SERVER = 'localhost'
MAIL_DEBUG = True MAIL_DEBUG = True

33
ref-test/app/install.py Normal file
View File

@ -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)

View File

@ -189,7 +189,10 @@ class User(UserMixin, db.Model):
if not password and not email: return False, '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)
old_email = self.get_email() 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() db.session.commit()
write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.') write('system.log', f'Information for user {self.get_username()} has been updated by {current_user.get_username()}.')
if notify: if notify:

View File

@ -1,5 +1,6 @@
from ..forms.quiz import StartQuiz from ..forms.quiz import StartQuiz
from ..models import Entry, Test from ..models import Entry, Test
from ..tools.forms import send_errors_to_client
from ..tools.test import redirect_if_started from ..tools.test import redirect_if_started
from flask import abort, Blueprint, jsonify, redirect, render_template, request, session from flask import abort, Blueprint, jsonify, redirect, render_template, request, session
@ -52,8 +53,7 @@ def _start():
'id': entry.id 'id': entry.id
}), 200 }), 200
return jsonify({'error': 'There was an error processing the user test and/or user codes.'}), 400 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 send_errors_to_client(form=form)
return jsonify({ 'error': errors}), 400
return render_template('/quiz/start_quiz.html', form = form) return render_template('/quiz/start_quiz.html', form = form)
@quiz.route('/quiz/') @quiz.route('/quiz/')

View File

@ -1,6 +1,7 @@
from ..extensions import db from ..extensions import db
from flask import jsonify
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
import json import json
@ -53,4 +54,8 @@ def get_dataset_choices():
label = f'{label} (Default)' if dataset.default else label label = f'{label} (Default)' if dataset.default else label
choice = (dataset.id, label) choice = (dataset.id, label)
dataset_choices.append(choice) dataset_choices.append(choice)
return dataset_choices return dataset_choices
def send_errors_to_client(form):
errors = [*form.errors]
return jsonify({ 'error': errors}), 400

View File

@ -1,86 +1,3 @@
from app.config import Development as Config from app import create_app
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'<strong>Cookie Consent</strong>: 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 <a href="{url_for("views._privacy")}">privacy policy</a>.', '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
app = create_app() app = create_app()
if __name__ == '__main__': app.run() if __name__ == '__main__': app.run()