31 Commits

Author SHA1 Message Date
294f1e42f7 Added timezone env variable 2022-08-11 17:31:20 +01:00
070ce19fcc Added instructions on updating 2022-08-11 17:19:05 +01:00
615e59fc6d Updated form error handling 2022-08-11 16:58:00 +01:00
68314a4ed2 Add handling of anonymous user when updating account 2022-08-11 16:28:47 +01:00
b90761fd2c Simplify variabe nale 2022-08-11 16:28:13 +01:00
af03193217 Change user variable name 2022-08-11 16:13:31 +01:00
730a75c44d Bugfix: reset password 2022-08-11 16:05:28 +01:00
70883db5ad Changed dockerignore stricture 2022-08-11 13:09:41 +01:00
7cefb487da Bugfix: reset password 2022-08-11 13:09:34 +01:00
2e1b01ec9b Bugfix: reset password 2022-08-11 13:02:55 +01:00
a7a5a03991 Consistency in paths for templates 2022-08-11 13:02:41 +01:00
b36c6bfd18 Bugfix: reset password 2022-08-11 12:51:17 +01:00
a613b0006b Bugfix: password reset 2022-08-11 12:44:42 +01:00
d4db8692e7 Remove debug line 2022-08-11 12:13:33 +01:00
37ad36da31 Add debug for email reset 2022-08-11 11:42:50 +01:00
d140f93d25 Bugfix: hude club field when empty 2022-08-11 11:41:57 +01:00
26a6248a61 Tidied up nnecessary imports 2022-08-11 11:39:53 +01:00
9f8ea16974 Bugfix: button display 2022-08-11 11:01:29 +01:00
bc5ec44145 Bugfix default datetime 2022-08-11 10:59:12 +01:00
ff5b19fa0b Editing text: remove repetition 2022-08-11 10:24:15 +01:00
6c50be49c6 Bugfix: default time for exam creation 2022-08-11 10:23:40 +01:00
8bfe028e2c Make certbot silent 2022-06-22 15:05:56 +01:00
519394a656 Store data in docker volume instead of project dir 2022-06-22 15:05:44 +01:00
9e1c9caec6 Updated config to have defaults for keys
Removed abstraction of data location for image build
2022-06-22 11:56:36 +01:00
ea850c9ae2 Added defaults for config keys to avoid exceptions 2022-06-22 11:45:37 +01:00
591b868920 Separated install script to avoid launch errors 2022-06-22 11:20:30 +01:00
91dc93758a Added nginx static serving editor files 2022-06-22 11:18:53 +01:00
5d27baee08 Editor flash message bugfix 2022-06-22 10:46:43 +01:00
1254cf3698 Bugfix install script dhparam 2022-06-22 09:55:59 +01:00
efab086057 Gitignore bugfix 2022-06-22 09:31:16 +01:00
06db47c597 Push production version to Master 2022-06-22 02:01:34 +01:00
24 changed files with 137 additions and 68 deletions

View File

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

3
.gitignore vendored
View File

@ -150,3 +150,6 @@ database/data/
# Ignore Encryption Keyfile # Ignore Encryption Keyfile
.encryption.key .encryption.key
# Ignore Data Dir
**/data/*

View File

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

View File

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

View File

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

View File

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

View File

@ -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" ]

View File

@ -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
@ -54,6 +53,4 @@ def create_app():
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.'

View File

@ -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 '&lt;same as old&gt;'}</strong></p> <p>Email: {email} <br/> Password: <strong>{password if password else '&lt;same as old&gt;'}</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>

View File

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

View File

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

View File

@ -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%}

View File

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

View File

@ -1 +0,0 @@
*

34
ref-test/install.py Executable file
View 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)