19 Commits

Author SHA1 Message Date
1ef34465c2 Debug install script 2022-06-20 12:53:40 +01:00
d9962f18ed Production ready v.0.2.1 2022-06-20 12:28:00 +01:00
d8044a7c76 Make config production ready 2022-06-20 12:27:32 +01:00
2663d5e3b7 Tidied up unused imports 2022-06-20 12:15:28 +01:00
6a09559b70 Database URI absolute path fix 2022-06-20 12:10:52 +01:00
26227a66c5 App Factory pattern 2022-06-20 12:10:37 +01:00
d6836915bb Prevent edit user from duplicating email address 2022-06-20 12:09:31 +01:00
49a7fb1007 More elegant error handling 2022-06-20 11:27:05 +01:00
90bc30757a Added local server for development 2022-06-20 11:26:44 +01:00
12207d1159 Changed modules to extensions 2022-06-19 13:22:24 +01:00
ac02f4dee1 Changed structure of referencing data 2022-06-19 13:22:05 +01:00
76fa1e1dd9 Removed todo tags 2022-06-19 11:17:21 +01:00
6d5f74bd62 Tidied up code 2022-06-19 11:17:00 +01:00
2e00d503c8 Added detailed data validation 2022-06-19 11:13:47 +01:00
516c2cdf81 Buxfix: static folders bypass cookie consent 2022-06-18 09:26:05 +01:00
8f9b78ac32 Merge branch 'editor' 2022-06-18 02:18:45 +01:00
17b985d238 Bugfix: 404 errors with request.endpoint
Fixed static folder 404 errors
2022-06-18 02:18:07 +01:00
69a0791a6d Bug fixes to main branch 2022-06-18 02:11:29 +01:00
4414d1720e Typo 2022-06-17 13:16:40 +01:00
26 changed files with 289 additions and 163 deletions

View File

@ -117,7 +117,8 @@ server {
listen 80;
listen [::]:80;
# Redirect to non-www
return 301 $scheme://domain_name$request_uri; ...
return 301 $scheme://domain_name$request_uri;
...
}
server {

View File

@ -2,7 +2,12 @@
# Source https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71
if ! [ -x "$(command -v docker compose)" ]; then
if ! [ -x "$(command -v docker)" ]; then
echo 'Error: docker is not installed.' >&2
exit 1
fi
if ! [ -x "$(command -v compose)" ]; then
echo 'Error: docker compose is not installed.' >&2
exit 1
fi

View File

@ -0,0 +1,57 @@
from .config import Production as Config
from .install import install_app
from .models import User
from .extensions import bootstrap, csrf, db, login_manager, mail
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 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

@ -77,7 +77,7 @@ $('.test-action').click(function(event) {
$.ajax({
url: `/admin/tests/edit/`,
type: 'POST',
data: JSON.stringify({'id': id, 'action': action}), // TODO Change how CRUD operations work
data: JSON.stringify({'id': id, 'action': action}),
contentType: 'application/json',
success: function(response) {
window.location.href = '/admin/tests/';

View File

@ -2,16 +2,17 @@
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **request.view_args) }}" />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">

View File

@ -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/<string:id>', 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'))
@ -214,7 +207,7 @@ def _questions():
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
if not validate_json(upload): return jsonify({'error': 'The data in the file is invalid.'}), 400
new_dataset = Dataset()
success, message = new_dataset.create(
upload = upload,
@ -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

View File

@ -1,9 +1,7 @@
import os
from pathlib import Path
if not os.getenv('DATA'):
from dotenv import load_dotenv
load_dotenv('../.env')
from dotenv import load_dotenv
load_dotenv('../.env')
class Config(object):
APP_HOST = '0.0.0.0'
@ -13,7 +11,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')
@ -28,18 +26,19 @@ class Config(object):
MAIL_SUPPRESS_SEND = False
MAIL_ASCII_ATTACHMENTS = bool(os.getenv('MAIL_ASCII_ATTACHMENTS'))
class ProductionConfig(Config):
class Production(Config):
pass
class DevelopmentConfig(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
MAIL_SUPPRESS_SEND = False
class TestingConfig(DevelopmentConfig):
class Testing(Development):
TESTING = True
SESSION_COOKIE_SECURE = False
MAIL_SERVER = os.getenv('MAIL_SERVER')

View File

@ -1,5 +0,0 @@
from config import Config
from os import path
from pathlib import Path
data = Path(Config.DATA)

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

@ -0,0 +1,34 @@
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.')
from .models import Entry, Dataset, Test, User
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

@ -1,14 +1,15 @@
from ..data import data
from ..modules import db
from ..extensions import db
from ..tools.logs import write
from flask import flash
from flask import current_app as app
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 pathlib import Path
from uuid import uuid4
class Dataset(db.Model):
@ -48,6 +49,7 @@ class Dataset(db.Model):
return False, message
write('system.log', f'Dataset {self.id} deleted by {current_user.get_username()}.')
filename = secure_filename('.'.join([self.id,'json']))
data = Path(app.config.get('DATA'))
file_path = path.join(data, 'questions', filename)
remove(file_path)
db.session.delete(self)
@ -58,6 +60,7 @@ class Dataset(db.Model):
self.generate_id()
timestamp = datetime.now()
filename = secure_filename('.'.join([self.id,'json']))
data = Path(app.config.get('DATA'))
file_path = path.join(data, 'questions', filename)
upload.stream.seek(0)
questions = loads(upload.read())
@ -73,11 +76,13 @@ class Dataset(db.Model):
def check_file(self):
filename = secure_filename('.'.join([self.id,'json']))
data = Path(app.config.get('DATA'))
file_path = path.join(data, 'questions', filename)
if not path.isfile(file_path): return False, 'Data file is missing.'
return True, 'Data file found.'
def get_file(self):
filename = secure_filename('.'.join([self.id,'json']))
data = Path(app.config.get('DATA'))
file_path = path.join(data, 'questions', filename)
return file_path

View File

@ -1,4 +1,4 @@
from ..modules import db, mail
from ..extensions import db, mail
from ..tools.forms import JsonEncodedDict
from ..tools.encryption import decrypt, encrypt
from ..tools.logs import write

View File

@ -1,4 +1,4 @@
from ..modules import db
from ..extensions import db
from ..tools.encryption import decrypt, encrypt
from ..tools.forms import JsonEncodedDict
from ..tools.logs import write

View File

@ -1,4 +1,4 @@
from ..modules import db, mail
from ..extensions import db, mail
from ..tools.encryption import decrypt, encrypt
from ..tools.logs import write
@ -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:

View File

@ -2,16 +2,17 @@
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **request.view_args) }}" />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('.static', filename='favicon.ico') }}">

View File

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

View File

@ -1,8 +1,82 @@
{% extends "components/base.html" %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
<link
rel="stylesheet"
href="{{ url_for('views.static', filename='css/style.css') }}"
/>
<link rel="shortcut icon" href="{{ url_for('views.static', filename='favicon.ico') }}">
{% block style %}
{% endblock %}
<title>{% block title %} SKA Referee Test Beta {% endblock %}</title>
</head>
<body class="bg-light">
{% block content %}
<h1>Page Not Found</h1>
<p>
The page you were looking for does not exist. Try going back and navigating to the desired destination correctly.
</p>
{% endblock %}
{% block navbar %}
{% include "components/navbar.html" %}
{% endblock %}
<div class="container quiz-container">
{% block top_alerts %}
{% include "components/server-alerts.html" %}
{% endblock %}
{% block content %}
<h1>Page Not Found</h1>
<p>
The page you were looking for does not exist. Try going back and navigating to the desired destination correctly.
</p>
{% endblock %}
<footer class="container site-footer">
{% include "components/footer.html" %}
</footer>
</div>
<!-- JQuery, Popper, and Bootstrap js dependencies -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous">
</script>
<script>
window.jQuery || document.write(`<script src="{{ url_for('.static', filename='js/jquery-3.6.0.min.js') }}"><\/script>`)
</script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
crossorigin="anonymous">
</script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
crossorigin="anonymous"
></script>
<!-- Custom js -->
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script
type="text/javascript"
src="{{ url_for('views.static', filename='js/script.js') }}"
></script>
{% block script %}
{% endblock %}
</body>
</html>

View File

@ -2,16 +2,17 @@
<meta property="og:locale" content="en_UK" />
<meta property="og:type" content="website" />
<meta property="og:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **request.view_args) }}" />
<meta property="og:url" content="{{ url_for(request.endpoint, _external = True, **(request.view_args or {})) }}" />
<meta property="og:site_name" content="Scottish Korfball Association Referee Theory Exam" />
<meta property="og:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta property="og:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta property="og:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:description" content="A web app for taking the Scottish Korfball Association Refereeing Theory Exam on-line." />
<meta name="twitter:image" content="{{ url_for('static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image" content="{{ url_for('.static', filename='favicon.png', _external = True) }}" />
<meta name="twitter:image:alt" content="Logo of the SKA Refereeing Exam App" />
<meta name="twitter:creator" content="@viveksantayana" />
<meta name="twitter:site" content="@viveksantayana" />
<meta name="theme-color" content="#343a40" />
<meta name="theme-color" content="#343a40" />
<link rel="shortcut icon" href="{{ url_for('views.static', filename='favicon.ico') }}">

View File

@ -1,13 +1,16 @@
from ..data import data as data_dir
from flask import current_app as app
import json
from pathlib import Path
from random import shuffle
def load(filename:str):
data_dir = Path(app.config.get('DATA'))
with open(f'./{data_dir}/{filename}') as file:
return json.load(file)
def save(data:dict, filename:str):
data_dir = Path(app.config.get('DATA'))
with open(f'./{data_dir}/{filename}', 'w') as file:
json.dump(data, file, indent=4)
@ -19,6 +22,39 @@ def validate_json(file):
file.stream.seek(0)
data = json.loads(file.read())
if not isinstance(data, list): return False
for block in data:
block_type = block.pop('type', None)
if block_type not in ['block', 'question']: return False
if block_type == 'question':
if not all (key in block for key in ['q_no', 'text', 'options', 'correct', 'q_type', 'tags']): return False
if not isinstance(block['q_no'], int): return False
if not isinstance(block['text'], str): return False
if not isinstance(block['options'], list): return False
for option in block['options']:
if not isinstance(option, str): return False
if not isinstance(block['correct'], int): return False
if not isinstance(block['q_type'], str): return False
if block['q_type'] not in ['Multiple Choice', 'Yes/No', 'List']: return False
if not isinstance(block['tags'], list): return False
for tag in block['tags']:
if not isinstance(tag, str): return False
if block_type == 'block':
if not all (key in block for key in ['question_header', 'questions']): return False
if not isinstance(block['question_header'], str): return False
if not isinstance(block['questions'], list): return False
for question in block['questions']:
if not all (key in question for key in ['q_no', 'text', 'options', 'correct', 'q_type', 'tags']): return False
if not isinstance(question['text'], str): return False
if not isinstance(question['q_no'], int): return False
if not isinstance(question['options'], list): return False
for option in question['options']:
if not isinstance(option, str): return False
if not isinstance(question['correct'], int): return False
if not isinstance(question['q_type'], str): return False
if question['q_type'] not in ['Multiple Choice', 'Yes/No', 'List']: return False
if not isinstance(question['tags'], list): return False
for tag in question['tags']:
if not isinstance(tag, str): return False
return True
def randomise_list(list:list):

View File

@ -1,7 +1,10 @@
from ..data import data
from flask import current_app as app
from cryptography.fernet import Fernet
from pathlib import Path
def load_key():
data = Path(app.config.get('DATA'))
with open(f'./{data}/.encryption.key', 'rb') as keyfile: return keyfile.read()
def decrypt(input:str):

View File

@ -1,6 +1,7 @@
from ..modules import db
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
return dataset_choices
def send_errors_to_client(form):
errors = [*form.errors]
return jsonify({ 'error': errors}), 400

View File

@ -1,10 +1,14 @@
from ..data import data
from flask import current_app as app
from datetime import datetime
from pathlib import Path
def read(filename:str):
data = Path(app.config.get('DATA'))
with open(f'./{data}/logs/{filename}') as file:
return file.readlines()
def write(filename:str, message:str):
data = Path(app.config.get('DATA'))
with open(f'./{data}/logs/{filename}', 'a+') as file:
file.write(f'{datetime.now().strftime("%Y-%m-%d-%X")}: {message}\n')

View File

@ -1,6 +1,5 @@
from .config import Config
from flask import Blueprint, redirect, request, render_template
from flask import current_app as app
from datetime import datetime, timedelta
@ -24,7 +23,7 @@ def _cookie_consent():
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else None,
path = '/',
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else None,
domain = f'{Config.SERVER_NAME}',
domain = f'{app.config.get("SERVER_NAME")}',
secure = True
)
return resp

View File

@ -1 +0,0 @@
from app.config import ProductionConfig as Config

View File

@ -1,87 +1,3 @@
from app.data import data
from app.models import Entry, Dataset, Test, User
from app.modules import bootstrap, csrf, db, login_manager, mail
from app.tools.data import save
from app.tools.logs import write
from config import Config
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
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/', '/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
app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(views)
app.register_blueprint(quiz)
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()
if __name__ == '__main__': app.run()

View File

@ -1,4 +1,2 @@
from main import app
if __name__ == '__main__':
app.run()
if __name__ == '__main__': app.run()