Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
294f1e42f7 | |||
070ce19fcc | |||
615e59fc6d | |||
68314a4ed2 | |||
b90761fd2c | |||
af03193217 | |||
730a75c44d | |||
70883db5ad | |||
7cefb487da | |||
2e1b01ec9b | |||
a7a5a03991 | |||
b36c6bfd18 | |||
a613b0006b | |||
d4db8692e7 | |||
37ad36da31 | |||
d140f93d25 | |||
26a6248a61 | |||
9f8ea16974 | |||
bc5ec44145 | |||
ff5b19fa0b | |||
6c50be49c6 | |||
8bfe028e2c | |||
519394a656 | |||
9e1c9caec6 | |||
ea850c9ae2 | |||
591b868920 | |||
91dc93758a | |||
5d27baee08 | |||
1254cf3698 | |||
efab086057 | |||
06db47c597 |
@ -1,5 +1,7 @@
|
|||||||
SERVER_NAME= # URL where this will be hosted.
|
SERVER_NAME= # URL where this will be hosted.
|
||||||
|
|
||||||
|
TZ=Europe/London # Time Zone
|
||||||
|
|
||||||
## Flask Configuration
|
## Flask Configuration
|
||||||
SECRET_KEY= # Long, secure, secret string.
|
SECRET_KEY= # Long, secure, secret string.
|
||||||
DATA=./data/
|
DATA=./data/
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -149,4 +149,7 @@ ref-test/testing.py
|
|||||||
database/data/
|
database/data/
|
||||||
|
|
||||||
# Ignore Encryption Keyfile
|
# Ignore Encryption Keyfile
|
||||||
.encryption.key
|
.encryption.key
|
||||||
|
|
||||||
|
# Ignore Data Dir
|
||||||
|
**/data/*
|
56
README.md
56
README.md
@ -166,3 +166,59 @@ The app uses [OpenDyslexic](https://opendyslexic.org/), which is available on-li
|
|||||||
It also has the option of rendering in other system fonts, but this can vary depending on your operating system.
|
It also has the option of rendering in other system fonts, but this can vary depending on your operating system.
|
||||||
Because these are proprietary fonts, they cannot be made available on-line in the same way as open source ones should your system not have them.
|
Because these are proprietary fonts, they cannot be made available on-line in the same way as open source ones should your system not have them.
|
||||||
Some fonts may not display correctly as a result.
|
Some fonts may not display correctly as a result.
|
||||||
|
|
||||||
|
## Updating the Installation
|
||||||
|
|
||||||
|
If the app is updated, you can update the version on your installation using the following method:
|
||||||
|
|
||||||
|
### Navigate to the root folder
|
||||||
|
|
||||||
|
This will be the root folder into which you cloned the git repository when you set the app up.
|
||||||
|
|
||||||
|
### Stash your local changes
|
||||||
|
|
||||||
|
When you update the code, there is a risk the changes you made to your configuration will be overwritten.
|
||||||
|
To avoid this, use the following command:
|
||||||
|
|
||||||
|
```git stash```
|
||||||
|
|
||||||
|
This will stash the changes you made, and we can re-apply the changes once the new code has been downloaded.
|
||||||
|
If you do not have any other changes stashed, the index number of these changes should be `0` in a later step.
|
||||||
|
If there are other changes, make sure to note what the correct index number for the stashed changes is.
|
||||||
|
|
||||||
|
### Take down the Docker containers
|
||||||
|
|
||||||
|
We will need to stop the current containers with the following command:
|
||||||
|
|
||||||
|
```sudo docker compose down```
|
||||||
|
|
||||||
|
This may take a few seconds.
|
||||||
|
|
||||||
|
### Pull the updated code
|
||||||
|
|
||||||
|
Download the updated code from the Git repository:
|
||||||
|
|
||||||
|
```git pull```
|
||||||
|
|
||||||
|
This step might fail if you have any un-stashed local changed.
|
||||||
|
|
||||||
|
### Re-Apply your local configurations
|
||||||
|
|
||||||
|
Because we stashed our local configurations, we can re-apply them once again:
|
||||||
|
|
||||||
|
```git stash pop 0```
|
||||||
|
|
||||||
|
The index number (`0`) is assuming there were no other changes saved on your git repository.
|
||||||
|
If you have a different index number for the relevant changes from the above step, change this accordingly.
|
||||||
|
|
||||||
|
### Re-build the docker image
|
||||||
|
|
||||||
|
Now that we have the base code downloaded, we just need to update the docker image:
|
||||||
|
|
||||||
|
```sudo docker compose build app```
|
||||||
|
|
||||||
|
### Re-build the containers
|
||||||
|
|
||||||
|
This is the same last step as running the containers in the last step of the installation:
|
||||||
|
|
||||||
|
```sudo docker compose up -d```
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
version: '3.9'
|
version: '3.9'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
container_name: reftest_server
|
container_name: reftest_server
|
||||||
@ -8,6 +11,7 @@ services:
|
|||||||
- ./certbot:/etc/letsencrypt:ro
|
- ./certbot:/etc/letsencrypt:ro
|
||||||
- ./nginx:/etc/nginx
|
- ./nginx:/etc/nginx
|
||||||
- ./src/html:/usr/share/nginx/html/
|
- ./src/html:/usr/share/nginx/html/
|
||||||
|
- ./ref-test/app/editor/static:/usr/share/nginx/html/admin/editor/static
|
||||||
- ./ref-test/app/admin/static:/usr/share/nginx/html/admin/static
|
- ./ref-test/app/admin/static:/usr/share/nginx/html/admin/static
|
||||||
- ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static
|
- ./ref-test/app/quiz/static:/usr/share/nginx/html/quiz/static
|
||||||
- ./ref-test/app/root:/usr/share/nginx/html/root
|
- ./ref-test/app/root:/usr/share/nginx/html/root
|
||||||
@ -30,7 +34,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5000
|
- 5000
|
||||||
volumes:
|
volumes:
|
||||||
- ./ref-test/data:/ref-test/data
|
- data:/ref-test/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- frontend
|
- frontend
|
||||||
|
@ -46,8 +46,6 @@ if [ ! -e "$data_path/lets-encrypt-x3-cross-signed.pem" ]; then
|
|||||||
echo "### Downloading lets-encrypt-x3-cross-signed.pem ..."
|
echo "### Downloading lets-encrypt-x3-cross-signed.pem ..."
|
||||||
wget -O $data_path/lets-encrypt-x3-cross-signed.pem \
|
wget -O $data_path/lets-encrypt-x3-cross-signed.pem \
|
||||||
"https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
|
"https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
|
||||||
docker compose run --rm --entrypoint "\
|
|
||||||
openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 4096" certbot
|
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -79,7 +77,7 @@ esac
|
|||||||
if [ $staging != "0" ]; then staging_arg="--staging"; fi
|
if [ $staging != "0" ]; then staging_arg="--staging"; fi
|
||||||
|
|
||||||
docker compose run --rm --entrypoint "\
|
docker compose run --rm --entrypoint "\
|
||||||
certbot certonly --webroot -w /var/www/html \
|
certbot certonly --non-interactive --webroot -w /var/www/html \
|
||||||
$staging_arg \
|
$staging_arg \
|
||||||
$email_arg \
|
$email_arg \
|
||||||
$domain_args \
|
$domain_args \
|
||||||
|
@ -29,6 +29,11 @@ server {
|
|||||||
alias /usr/share/nginx/html/admin/static/;
|
alias /usr/share/nginx/html/admin/static/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ^~ /admin/editor/static/ {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
alias /usr/share/nginx/html/admin/editor/static/;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
include /etc/nginx/conf.d/proxy_headers.conf;
|
include /etc/nginx/conf.d/proxy_headers.conf;
|
||||||
proxy_pass http://reftest;
|
proxy_pass http://reftest;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
ARG DATA=./data/
|
||||||
|
ENV DATA=$DATA
|
||||||
WORKDIR /ref-test
|
WORKDIR /ref-test
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pip install --upgrade pip && pip install -r requirements.txt
|
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||||
|
RUN chmod +x install.py && ./install.py
|
||||||
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "-w", "5", "wsgi:app" ]
|
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "-w", "5", "wsgi:app" ]
|
@ -1,5 +1,4 @@
|
|||||||
from .config import Development as Config
|
from .config import Production as Config
|
||||||
from .install import install_app
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .extensions import bootstrap, csrf, db, login_manager, mail
|
from .extensions import bootstrap, csrf, db, login_manager, mail
|
||||||
|
|
||||||
@ -53,7 +52,5 @@ def create_app():
|
|||||||
app.register_blueprint(views)
|
app.register_blueprint(views)
|
||||||
app.register_blueprint(quiz)
|
app.register_blueprint(quiz)
|
||||||
app.register_blueprint(editor, url_prefix='/admin/editor')
|
app.register_blueprint(editor, url_prefix='/admin/editor')
|
||||||
|
|
||||||
install_app(app)
|
|
||||||
|
|
||||||
return app
|
return app
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<form name="form-update-password" class="form-display form-post" action="{{ url_for(request.endpoint, **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
|
<form name="form-update-password" class="form-display form-post" action="{{ url_for('admin._update_password', **request.view_args) }}" data-rel-success="{{ url_for('admin._login') }}">
|
||||||
{% include "admin/components/server-alerts.html" %}
|
{% include "admin/components/server-alerts.html" %}
|
||||||
<h2 class="form-heading">Update Password</h2>
|
<h2 class="form-heading">Update Password</h2>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ entry.get_email() }}
|
{{ entry.get_email() }}
|
||||||
</li>
|
</li>
|
||||||
{% if entry.club %}
|
{% if entry.get_club() %}
|
||||||
<li class="list-group-item list-group-item-action">
|
<li class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h5 class="mb-1">Club</h5>
|
<h5 class="mb-1">Club</h5>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ entry.get_email() }}
|
{{ entry.get_email() }}
|
||||||
</li>
|
</li>
|
||||||
{% if entry.club %}
|
{% if entry.get_club() %}
|
||||||
<li class="list-group-item list-group-item-action">
|
<li class="list-group-item list-group-item-action">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h5 class="mb-1">Club</h5>
|
<h5 class="mb-1">Club</h5>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
{{ entry.get_surname() }}, {{ entry.get_first_name() }}
|
{{ entry.get_surname() }}, {{ entry.get_first_name() }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if entry.club %}
|
{% if entry.get_club() %}
|
||||||
{{ entry.get_club() }}
|
{{ entry.get_club() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -9,7 +9,7 @@ from flask import abort, Blueprint, jsonify, render_template, redirect, request,
|
|||||||
from flask.helpers import flash, url_for
|
from flask.helpers import flash, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
from json import loads
|
from json import loads
|
||||||
from os import path
|
from os import path
|
||||||
import secrets
|
import secrets
|
||||||
@ -91,7 +91,7 @@ def _register():
|
|||||||
flash(message=message, category='error')
|
flash(message=message, category='error')
|
||||||
return jsonify({'error': message}), 401
|
return jsonify({'error': message}), 401
|
||||||
return send_errors_to_client(form=form)
|
return send_errors_to_client(form=form)
|
||||||
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'])
|
||||||
def _reset():
|
def _reset():
|
||||||
@ -117,7 +117,8 @@ def _reset():
|
|||||||
user.clear_reset_tokens()
|
user.clear_reset_tokens()
|
||||||
if request.args.get('verification') == verification_token:
|
if request.args.get('verification') == verification_token:
|
||||||
form = UpdatePassword()
|
form = UpdatePassword()
|
||||||
return render_template('/auth/update_password.html', form=form, user=user.id)
|
session['user'] = user.id
|
||||||
|
return render_template('/admin/auth/update-password.html', form=form)
|
||||||
flash('The verification of your password reset request failed and the token has been invalidated. Please make a new reset password request.', 'error')
|
flash('The verification of your password reset request failed and the token has been invalidated. Please make a new reset password request.', 'error')
|
||||||
|
|
||||||
return render_template('/admin/auth/reset.html', form=form)
|
return render_template('/admin/auth/reset.html', form=form)
|
||||||
@ -126,7 +127,7 @@ def _reset():
|
|||||||
def _update_password():
|
def _update_password():
|
||||||
form = UpdatePassword()
|
form = UpdatePassword()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = request.form.get('user')
|
user = session.pop('user')
|
||||||
user = User.query.filter_by(id=user).first()
|
user = User.query.filter_by(id=user).first()
|
||||||
user.update(password=request.form.get('password'))
|
user.update(password=request.form.get('password'))
|
||||||
session['remembered_username'] = user.get_username()
|
session['remembered_username'] = user.get_username()
|
||||||
@ -258,6 +259,8 @@ def _tests(filter:str=None):
|
|||||||
if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active'))
|
if filter not in ['create','active','scheduled','expired','all']: return redirect(url_for('admin._tests', filter='active'))
|
||||||
if filter == 'create':
|
if filter == 'create':
|
||||||
form = CreateTest()
|
form = CreateTest()
|
||||||
|
form.start_date.default = datetime.now()
|
||||||
|
form.expiry_date.default = date.today() + timedelta(days=1)
|
||||||
form.time_limit.choices = get_time_options()
|
form.time_limit.choices = get_time_options()
|
||||||
form.dataset.choices = get_dataset_choices()
|
form.dataset.choices = get_dataset_choices()
|
||||||
form.time_limit.default='none'
|
form.time_limit.default='none'
|
||||||
|
@ -5,7 +5,7 @@ load_dotenv('../.env')
|
|||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
APP_HOST = '0.0.0.0'
|
APP_HOST = '0.0.0.0'
|
||||||
DATA = os.getenv('DATA')
|
DATA = './data/'
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TESTING = False
|
TESTING = False
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
@ -15,16 +15,16 @@ class Config(object):
|
|||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
MAIL_SERVER = os.getenv('MAIL_SERVER')
|
||||||
MAIL_PORT = int(os.getenv('MAIL_PORT'))
|
MAIL_PORT = int(os.getenv('MAIL_PORT') or 25)
|
||||||
MAIL_USE_TLS = False
|
MAIL_USE_TLS = False
|
||||||
MAIL_USE_SSL = False
|
MAIL_USE_SSL = False
|
||||||
MAIL_DEBUG = False
|
MAIL_DEBUG = False
|
||||||
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
|
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
|
||||||
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
||||||
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
|
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
|
||||||
MAIL_MAX_EMAILS = int(os.getenv('MAIL_MAX_EMAILS'))
|
MAIL_MAX_EMAILS = int(os.getenv('MAIL_MAX_EMAILS') or 25)
|
||||||
MAIL_SUPPRESS_SEND = False
|
MAIL_SUPPRESS_SEND = False
|
||||||
MAIL_ASCII_ATTACHMENTS = bool(os.getenv('MAIL_ASCII_ATTACHMENTS'))
|
MAIL_ASCII_ATTACHMENTS = bool(os.getenv('MAIL_ASCII_ATTACHMENTS') or True)
|
||||||
|
|
||||||
class Production(Config):
|
class Production(Config):
|
||||||
pass
|
pass
|
||||||
|
@ -6,8 +6,6 @@ from wtforms import BooleanField, IntegerField, PasswordField, SelectField, Stri
|
|||||||
from wtforms.fields import DateTimeLocalField
|
from wtforms.fields import DateTimeLocalField
|
||||||
from wtforms.validators import InputRequired, Email, EqualTo, Length, Optional
|
from wtforms.validators import InputRequired, Email, EqualTo, Length, Optional
|
||||||
|
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
|
|
||||||
class Login(FlaskForm):
|
class Login(FlaskForm):
|
||||||
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
||||||
password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
@ -51,8 +49,8 @@ class UpdateAccount(FlaskForm):
|
|||||||
password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')])
|
password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')])
|
||||||
|
|
||||||
class CreateTest(FlaskForm):
|
class CreateTest(FlaskForm):
|
||||||
start_date = DateTimeLocalField('Start Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()], default = datetime.now() )
|
start_date = DateTimeLocalField('Start Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()] )
|
||||||
expiry_date = DateTimeLocalField('Expiry Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()], default = date.today() + timedelta(days=1) )
|
expiry_date = DateTimeLocalField('Expiry Date', format='%Y-%m-%dT%H:%M', validators=[InputRequired()] )
|
||||||
time_limit = SelectField('Time Limit')
|
time_limit = SelectField('Time Limit')
|
||||||
dataset = SelectField('Question Dataset')
|
dataset = SelectField('Question Dataset')
|
||||||
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
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)
|
|
@ -102,7 +102,7 @@ class Dataset(db.Model):
|
|||||||
with open(file_path, 'w') as file:
|
with open(file_path, 'w') as file:
|
||||||
dump(data, file, indent=2)
|
dump(data, file, indent=2)
|
||||||
write('system.log', f'Dataset {self.id} edited by {current_user.get_username()}.')
|
write('system.log', f'Dataset {self.id} edited by {current_user.get_username()}.')
|
||||||
flash(f'Dataset {self.name} successfully edited.', 'success')
|
flash(f'Dataset {self.get_name()} successfully edited.', 'success')
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True, 'Dataset successfully edited.'
|
return True, 'Dataset successfully edited.'
|
@ -194,7 +194,8 @@ class User(UserMixin, db.Model):
|
|||||||
if entry.get_email() == email and not entry == self: return False, f'The email address {email} is already in use.'
|
if entry.get_email() == email and not entry == self: return False, f'The email address {email} is already in use.'
|
||||||
self.set_email(email)
|
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()}.')
|
_current_user = current_user.get_username() if current_user.is_authenticated else 'anonymous'
|
||||||
|
write('system.log', f'Information for user {self.get_username()} has been updated by {_current_user}.')
|
||||||
if notify:
|
if notify:
|
||||||
message = Message(
|
message = Message(
|
||||||
subject='RefTest | Account Update',
|
subject='RefTest | Account Update',
|
||||||
@ -202,7 +203,7 @@ class User(UserMixin, db.Model):
|
|||||||
bcc=[old_email,current_user.get_email()],
|
bcc=[old_email,current_user.get_email()],
|
||||||
body=f"""
|
body=f"""
|
||||||
Hello {self.get_username()},\n\n
|
Hello {self.get_username()},\n\n
|
||||||
Your administrator account for the SKA RefTest App has been updated by {current_user.get_username()}.\n\n
|
Your administrator account for the SKA RefTest App has been updated by {_current_user}.\n\n
|
||||||
Your new account details are as follows:\n\n
|
Your new account details are as follows:\n\n
|
||||||
Email: {email}\n
|
Email: {email}\n
|
||||||
Password: {password if password else '<same as old>'}\n\n
|
Password: {password if password else '<same as old>'}\n\n
|
||||||
@ -213,7 +214,7 @@ class User(UserMixin, db.Model):
|
|||||||
""",
|
""",
|
||||||
html=f"""
|
html=f"""
|
||||||
<p>Hello {self.get_username()},</p>
|
<p>Hello {self.get_username()},</p>
|
||||||
<p>Your administrator account for the SKA RefTest App has been updated by {current_user.get_username()}.</p>
|
<p>Your administrator account for the SKA RefTest App has been updated by {_current_user}.</p>
|
||||||
<p>Your new account details are as follows:</p>
|
<p>Your new account details are as follows:</p>
|
||||||
<p>Email: {email} <br/> Password: <strong>{password if password else '<same as old>'}</strong></p>
|
<p>Email: {email} <br/> Password: <strong>{password if password else '<same as old>'}</strong></p>
|
||||||
<p>You can update your email address and password by logging in to the admin console using the following URL:</p>
|
<p>You can update your email address and password by logging in to the admin console using the following URL:</p>
|
||||||
|
@ -146,7 +146,7 @@ $("#btn-start-quiz").click(function(event){
|
|||||||
data: JSON.stringify({'id': id}),
|
data: JSON.stringify({'id': id}),
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
$(this).fadeOut();
|
$("#btn-start-quiz").fadeOut();
|
||||||
$(".btn-quiz-return").fadeIn();
|
$(".btn-quiz-return").fadeIn();
|
||||||
$(".quiz-console").fadeIn();
|
$(".quiz-console").fadeIn();
|
||||||
$("#quiz-settings").fadeOut();
|
$("#quiz-settings").fadeOut();
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
<div class="container question-container quiz-start-text">
|
<div class="container question-container quiz-start-text">
|
||||||
<h4 class="question-title">Sample Question</h4>
|
<h4 class="question-title">Sample Question</h4>
|
||||||
<p class="question-header">
|
<p class="question-header">
|
||||||
Korfball is a mixed-sex, controlled-contact, indoor, invasion ball sport. The sport originated in the Netherlands. It is a mixed-sex team sport. Its governing body is the International Korball Federation. There are numerous korfball leagues and associations around the world. A korfball match is officiated by a referee.
|
Korfball is a mixed-sex, controlled-contact, indoor, invasion, team ball sport. The sport originated in the Netherlands. Its governing body is the International Korball Federation. There are numerous korfball leagues and associations around the world. A korfball match is officiated by a referee.
|
||||||
</p>
|
</p>
|
||||||
<p class="question-text">
|
<p class="question-text">
|
||||||
In order to be a referee, what do you need to know?
|
In order to be a referee, what do you need to know?
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<strong class="results-details">Email Address</strong>: {{ entry.get_email() }} <br />
|
<strong class="results-details">Email Address</strong>: {{ entry.get_email() }} <br />
|
||||||
|
|
||||||
{% if entry.club %}
|
{% if entry.get_club() %}
|
||||||
<strong class="results-details">Club</strong>: {{ entry.get_club() }} <br />
|
<strong class="results-details">Club</strong>: {{ entry.get_club() }} <br />
|
||||||
{% endif%}
|
{% endif%}
|
||||||
|
|
||||||
|
@ -57,5 +57,5 @@ def get_dataset_choices():
|
|||||||
return dataset_choices
|
return dataset_choices
|
||||||
|
|
||||||
def send_errors_to_client(form):
|
def send_errors_to_client(form):
|
||||||
errors = [*form.errors]
|
errors = [*form.errors.values()]
|
||||||
return jsonify({ 'error': errors}), 400
|
return jsonify({ 'error': errors}), 400
|
1
ref-test/data/.gitignore
vendored
1
ref-test/data/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
34
ref-test/install.py
Executable file
34
ref-test/install.py
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from main import app
|
||||||
|
from app.extensions import db
|
||||||
|
from app.tools.data import save
|
||||||
|
from app.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
|
||||||
|
|
||||||
|
data = Path(app.config.get('DATA'))
|
||||||
|
database_uri = app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
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', 'w') 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 app.models import *
|
||||||
|
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)
|
Reference in New Issue
Block a user