13 Commits

13 changed files with 60 additions and 51 deletions

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

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
@ -258,6 +258,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

@ -51,8 +51,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

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

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)