diff --git a/BookingSystem/__init__.py b/BookingSystem/__init__.py index 4632dea..1688a4f 100644 --- a/BookingSystem/__init__.py +++ b/BookingSystem/__init__.py @@ -18,6 +18,11 @@ API_TOKEN = os.getenv('API_TOKEN') REGEX_ITEM = r'^(?:(?![\s])[ÆØÅæøåa-zA-Z0-9_\s\-]*[ÆØÅæøåa-zA-Z0-9_\-]+)$' +MIN_DAYS = int(os.getenv('MIN_DAYS', 1)) +MAX_DAYS = int(os.getenv('MAX_DAYS', 14)) +MIN_LABELS = int(os.getenv('MIN_LABELS', 0)) +MAX_LABELS = int(os.getenv('MAX_LABELS', 10)) + # Logger setup logger = Logger(__name__) if os.getenv('DEBUG') == 'True': diff --git a/BookingSystem/api.py b/BookingSystem/api.py index 16ce6b4..c3906ad 100644 --- a/BookingSystem/api.py +++ b/BookingSystem/api.py @@ -10,13 +10,12 @@ import flask import requests -import groups import inventory import mail import user -from __init__ import DATABASE, LABEL_SERVER +from __init__ import DATABASE, LABEL_SERVER, MIN_DAYS, MAX_DAYS, MIN_LABELS, MAX_LABELS from inventory import Item -from sanitizer import VALIDATORS, MINMAX, sanitize, handle_api_exception +from sanitizer import VALIDATORS, MINMAX, sanitize, handle_api_exception, APIException from utils import login_required, next_july api = flask.Blueprint('api', __name__) @@ -112,6 +111,7 @@ def delete_item(item_id: str) -> flask.Response: @api.route('/items//label//preview', methods=['GET']) @login_required(admin_only=True) +@handle_api_exception def get_label_preview(item_id: str, variant: str = 'qr') -> flask.Response: """Get a label preview for an item.""" item = inventory.get(item_id) @@ -126,7 +126,7 @@ def print_label(item_id: str) -> flask.Response: # START: Validation validation_map = { 'print_label_count': VALIDATORS.INT, - 'print_label_count_minmax': MINMAX(0, 9), + 'print_label_count_minmax': MINMAX(MIN_LABELS, MAX_LABELS), 'print_label_type': VALIDATORS.LABEL_TYPE, } form = sanitize(validation_map, flask.request.form) @@ -152,7 +152,7 @@ def book_equipment() -> flask.Response: validation_map = { 'user': VALIDATORS.UNIQUE_ID, 'days': VALIDATORS.INT, - 'days_minmax': MINMAX(1, 90), + 'days_minmax': MINMAX(MIN_DAYS, MAX_DAYS), 'equipment': VALIDATORS.ITEM_LIST_EXISTS, } form = sanitize(validation_map, flask.request.form) @@ -179,21 +179,19 @@ def return_equipment(item_id: str) -> flask.Response: def get_user(userid: str) -> flask.Response: """Get user as JSON.""" u = user.get(userid) - if not u: - return flask.abort(404) return flask.jsonify(u) @api.route('/users', methods=['POST']) @login_required() +@handle_api_exception def register_student() -> flask.Response: """Add/update a class in the database.""" con = sqlite3.connect(DATABASE) cur = con.cursor() selected_classroom = flask.request.form.get('classroom') - if selected_classroom not in groups.get_all(): - return flask.abort(418) # I'm a teapot + sanitize({'classroom': VALIDATORS.GROUP}, {'classroom': selected_classroom}) data = { 'name': flask.session.get('user').name, @@ -212,68 +210,84 @@ def register_student() -> flask.Response: return flask.redirect(flask.request.referrer) -@api.route('/groups', methods=['POST']) +@api.route('/groups', methods=['PUT']) @login_required(admin_only=True) +@handle_api_exception def update_groups() -> flask.Response: """Update a class in the database.""" + new_groups = list() + for group in flask.request.form.get('groups').split('\n'): + if not group.strip(): + continue + sanitize({'group': VALIDATORS.GROUP_NAME}, {'group': group.strip()}) + new_groups.append(group.strip()) + con = sqlite3.connect(DATABASE) cur = con.cursor() # noinspection SqlWithoutWhere cur.execute('DELETE FROM groups') con.commit() - - for group in flask.request.form.get('groups').split('\n'): - if not group.strip(): - continue - cur.execute('INSERT INTO groups (classroom) VALUES (?)', (group.strip(),)) + for group in new_groups: + cur.execute('INSERT INTO groups (classroom) VALUES (?)', (group,)) con.commit() con.close() - return flask.redirect(flask.request.referrer) + return flask.Response('Gruppene ble oppdatert.', status=200) -@api.route('/categories', methods=['POST']) +@api.route('/categories', methods=['PUT']) @login_required(admin_only=True) +@handle_api_exception def update_categories() -> flask.Response: """Update every category in the database.""" + new_categories = list() + for category in flask.request.form.get('categories').split('\n'): + if not category.strip(): + continue + sanitize({'category': VALIDATORS.CATEGORY_NAME}, {'category': category.strip()}) + new_categories.append(category.strip()) + con = sqlite3.connect(DATABASE) cur = con.cursor() # noinspection SqlWithoutWhere cur.execute('DELETE FROM categories') con.commit() - - for category in flask.request.form.get('categories').split('\n'): - if not category.strip(): - continue - cur.execute('INSERT INTO categories (name) VALUES (?)', (category.strip(),)) + for category in new_categories: + cur.execute('INSERT INTO categories (name) VALUES (?)', (category,)) con.commit() con.close() - return flask.redirect(flask.request.referrer) + return flask.Response('Kategoriene ble oppdatert.', status=200) -@api.route('/emails', methods=['POST']) +@api.route('/emails', methods=['PUT']) @login_required(admin_only=True) +@handle_api_exception def update_emails() -> flask.Response: """Update every email in the database.""" + new_emails = list() + for email in flask.request.form.get('emails').split('\n'): + if not email.strip(): + continue + sanitize({'email': VALIDATORS.EMAIL}, {'email': email.strip()}) + new_emails.append(email.strip()) + con = sqlite3.connect(DATABASE) cur = con.cursor() # noinspection SqlWithoutWhere cur.execute('DELETE FROM emails') con.commit() - - for email in flask.request.form.get('emails').split('\n'): - if not email.strip(): - continue - cur.execute('INSERT INTO emails (email) VALUES (?)', (email.strip(),)) + for email in new_emails: + cur.execute('INSERT INTO emails (email) VALUES (?)', (email,)) con.commit() con.close() - return flask.redirect(flask.request.referrer) + return flask.Response('E-postene ble oppdatert.', status=200) @api.route('/email/report', methods=['POST']) @login_required(admin_only=True, api=True) +@handle_api_exception def email_report() -> flask.Response: """Emails a report to all users in the emails table. @@ -282,13 +296,18 @@ def email_report() -> flask.Response: If the interval parameter is set, the report will only be sent if the last report was sent more than interval days ago. + + You can only send an email once every hour by default. (Handled by mail.py) """ interval = flask.request.args.get('interval') + if interval: - last_sent = datetime.fromtimestamp(float(mail.get_last_sent())).date() + sanitize({'interval': VALIDATORS.INT}, flask.request.args) current_date = datetime.now().date() + last_sent = datetime.fromtimestamp(float(mail.get_last_sent())).date() if last_sent and (current_date - last_sent).days < int(interval): - return flask.Response(f'Ikke sendt - mindre enn {interval} dager siden forrige rapport.', status=200) + raise APIException(f'Ikke sendt - mindre enn {interval} dager siden forrige rapport.', 200) + return mail.send_report() @@ -300,6 +319,7 @@ def prune_inactive_users() -> flask.Response: Example cronjob: 0 1 1 7 * curl -X POST "http://localhost:5000/users/prune_inactive?token=" """ + # TODO: run this every day as a background task here instead of an external cronjob? user.prune_inactive() return flask.Response('Inaktive brukere ble fjernet.', status=200) diff --git a/BookingSystem/app.py b/BookingSystem/app.py index 6aa7425..ba31893 100644 --- a/BookingSystem/app.py +++ b/BookingSystem/app.py @@ -13,7 +13,7 @@ import mail import routes import user -from __init__ import logger, REGEX_ITEM +from __init__ import logger, REGEX_ITEM, MIN_DAYS, MAX_DAYS, MIN_LABELS, MAX_LABELS from db import init_db from flask_session import Session @@ -60,7 +60,11 @@ def context_processor() -> dict: used_ids=inventory.get_all_ids(), users=user.get_all_active_users(), unavailable_items=inventory.get_all_unavailable(), - overdue_items=inventory.get_all_overdue()) + overdue_items=inventory.get_all_overdue(), + MIN_DAYS=MIN_DAYS, + MAX_DAYS=MAX_DAYS, + MIN_LABELS=MIN_LABELS, + MAX_LABELS=MAX_LABELS, ) @app.errorhandler(401) def unauthorized(_) -> flask.Response: diff --git a/BookingSystem/inventory.py b/BookingSystem/inventory.py index 550466b..eeb717b 100644 --- a/BookingSystem/inventory.py +++ b/BookingSystem/inventory.py @@ -56,6 +56,9 @@ def api_repr(self) -> dict: 'last_seen': self.last_seen } + def mail_repr(self) -> str: + return f'{self.id} {self.name} ({self.category})' + def __str__(self) -> str: if self.order_due_date: return f'{self.lender_name}: {self.id} - {self.name} ({self.category}, {parser.parse(self.order_due_date):%d.%m.%Y})' @@ -79,6 +82,12 @@ def lender_association(self) -> str: return 'Sjekk historikk' return self.user.get('classroom') or 'Lærer' + @property + def lender_association_mail(self) -> str: + if not self.user.get('name'): + return 'Slettet bruker [Sjekk historikk]' + return self.user.get('classroom') or 'Ansatt' + @property def classroom(self) -> str: if '(' in self.lender_association: @@ -161,7 +170,7 @@ def delete(item_id: str) -> None: logger.info(f'Slettet utstyr {item_id}.') except sqlite3.IntegrityError: logger.error(f'{item_id} eksisterer ikke.') - raise APIException(f'{item_id} eksisterer ikke.') + raise APIException(f'{item_id} eksisterer ikke.', status_code=404) finally: con.close() audits.audit('ITEM_REM', f'{item_id} ble slettet.') @@ -170,12 +179,12 @@ def delete(item_id: str) -> None: def get(item_id: str) -> Item: """Return a JSON object of the item with the given ID.""" con = sqlite3.connect(DATABASE) - item = Item(*con.execute(read_sql_query('get_item.sql'), {'id': item_id}).fetchone()) + item = con.execute(read_sql_query('get_item.sql'), {'id': item_id}).fetchone() con.close() if not item: logger.error(f'{item_id} eksisterer ikke.') raise APIException(f'{item_id} eksisterer ikke.') - return item + return Item(*item) def get_all() -> list[Item]: diff --git a/BookingSystem/mail.py b/BookingSystem/mail.py index 457879c..83ed5ff 100644 --- a/BookingSystem/mail.py +++ b/BookingSystem/mail.py @@ -7,6 +7,7 @@ import inventory from __init__ import DATABASE, logger +from sanitizer import APIException SMTP_SERVER = os.getenv('SMTP_SERVER') SMTP_PORT = int(os.getenv('SMTP_PORT')) if os.getenv('SMTP_PORT') else 587 @@ -66,9 +67,9 @@ def formatted_overdue_items() -> str:

""" items = [item for item in inventory.get_all_unavailable() if item.overdue] - pairs = {item.lender_association: [item2 - for item2 in items - if item2.lender_association == item.lender_association] + pairs = {item.lender_association_mail: [item2.mail_repr() + for item2 in items + if item2.lender_association_mail == item.lender_association_mail] for item in items} sorted_pairs = {key: pairs[key] for key in sorted(pairs) if pairs[key]} w = '' @@ -84,11 +85,19 @@ def formatted_overdue_items() -> str: def send_report() -> flask.Response: - """Send an e-mail to all emails in the database.""" + """Send an e-mail to all emails in the database. + + Force is for debugging only. + """ + # If the last sent email was sent within the past hour, raise an exception + if get_last_sent(): + if datetime.now().timestamp() - float(get_last_sent()) < 3600: + raise APIException('Rapport ble ikke sendt: forrige rapport ble sendt for under en time siden.', 400) + items = [item for item in inventory.get_all_unavailable() if item.overdue] if not items: update_last_sent() - return flask.Response('Ikke sendt (intet å rapportere!)', status=400) + raise APIException('Rapport ble ikke sendt: finner ikke overskredet utstyr.', 400) title = f'[UtstyrServer] Rapport for overskredet utstyr {datetime.now().strftime("%d.%m.%Y")}' recipients = get_all_emails() @@ -120,8 +129,9 @@ def send_report() -> flask.Response:
{SMTP_FROM}

""".encode('utf-8') server.sendmail(SMTP_USERNAME, recipients, message) + # TODO: Handle exceptions properly except Exception as e: logger.warning(f'Failed to send email: {e}') - return flask.Response('Klarte ikke sende rapport, prøv igjen eller kontakt en administrator.', status=500) + raise APIException('Klarte ikke sende rapport, prøv igjen eller kontakt en administrator.', 500) update_last_sent() return flask.Response('Rapport sendt!', status=200) diff --git a/BookingSystem/routes.py b/BookingSystem/routes.py index 7f06f0d..fdb9b7c 100644 --- a/BookingSystem/routes.py +++ b/BookingSystem/routes.py @@ -4,6 +4,7 @@ import mail from __init__ import KIOSK_FQDN, LABEL_SERVER from db import add_admin +from sanitizer import handle_api_exception from utils import login_required app = flask.blueprints.Blueprint('app', __name__) @@ -72,12 +73,14 @@ def inventar_add() -> str: @app.route('/inventar/edit/') @login_required(admin_only=True) +@handle_api_exception def edit_item(item_id: str) -> str: return flask.render_template('inventar_edit.html', item=inventory.get(item_id)) @app.route('/inventar/print/') @login_required(admin_only=True) +@handle_api_exception def print_item(item_id: str) -> str: return flask.render_template('inventar_print.html', item=inventory.get(item_id)) diff --git a/BookingSystem/sanitizer.py b/BookingSystem/sanitizer.py index 53bfda7..f142363 100644 --- a/BookingSystem/sanitizer.py +++ b/BookingSystem/sanitizer.py @@ -3,8 +3,8 @@ from functools import wraps import flask -from werkzeug.datastructures import ImmutableMultiDict +import groups import inventory from __init__ import REGEX_ITEM, logger @@ -15,9 +15,13 @@ class VALIDATORS(Enum): UNIQUE_OR_SAME_ID = auto() NAME = auto() CATEGORY = auto() + CATEGORY_NAME = auto() INT = auto() LABEL_TYPE = auto() ITEM_LIST_EXISTS = auto() + EMAIL = auto() + GROUP = auto() + GROUP_NAME = auto() class APIException(Exception): @@ -25,6 +29,7 @@ def __init__(self, message: str, status_code: int = 400) -> None: super().__init__(message) self.message = message self.status_code = status_code + logger.debug(f'APIException: {message}') class MINMAX: @@ -52,53 +57,65 @@ def unique(fkey: str) -> bool: l_val, l_ids = form.get(fkey).lower(), [i.lower() for i in inventory.get_all_ids()] return l_val not in l_ids + def email(text: str) -> bool: + # Check if the email is valid + r = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}$") + return bool(r.match(text)) + + def categoryname(text: str) -> bool: + # Check if the category is valid + r = re.compile(r'^[a-zæøåA-ZÆØÅ0-9\- ]+$') + return bool(r.match(text)) + + def groupname(text: str) -> bool: + # Check if the group is valid + r = re.compile(r'^([a-zæøåA-ZÆØÅ0-9\- ]+) \(([a-zæøåA-ZÆØÅ0-9\-& ]+)\)$') + return bool(r.match(text)) + for key, sanitizer in sanitization_map.items(): match sanitizer: case VALIDATORS.ID | VALIDATORS.NAME: # Check if the ID/name is valid if not item_pattern(key): - logger.debug(f'Invalid item pattern for {key} ({form.get(key)})') raise APIException(f'Ugyldig ID ({form.get(key)})') case VALIDATORS.UNIQUE_ID: # Check if the ID is unique if not unique(key): - logger.debug(f'Invalid unique id for {key} ({form.get(key)})') raise APIException(f'{form.get(key)} er allerede i bruk.') # Check if the ID is valid if not item_pattern(key): - logger.debug(f'Invalid unique id for {key} ({form.get(key)})') raise APIException(f'Ugyldig ID ({form.get(key)})') case VALIDATORS.UNIQUE_OR_SAME_ID: # Check if the ID is unique or the same as the current ID same_id = form.get(key).lower() == data.get(key).lower() if not unique(key) and not same_id: - logger.debug(f'Invalid unique or same id for {key} ({form.get(key)})') raise APIException(f'{form.get(key)} er allerede i bruk.') # Check if the ID is valid if not item_pattern(key): - logger.debug(f'Invalid unique or same id for {key} ({form.get(key)})') raise APIException(f'Ugyldig ID ({form.get(key)})') case VALIDATORS.CATEGORY: # Check if the category is valid if form.get(key) not in inventory.all_categories(): - logger.debug(f'Invalid category for {key} ({form.get(key)})') raise APIException(f'Ugyldig kategori ({form.get(key)})') + case VALIDATORS.CATEGORY_NAME: + # Check if the category name is valid + if not categoryname(form.get(key)): + raise APIException(f'Ugyldig kategorinavn ({form.get(key)})') + case VALIDATORS.INT: # Check if the value is an int try: int(form.get(key)) except (ValueError, TypeError): - logger.debug(f'Invalid int for {key} ({form.get(key)})') raise APIException(f'Ugyldig tallverdi ({form.get(key)})') case VALIDATORS.LABEL_TYPE: # Check if the label type is valid if form.get(key) not in ['barcode', 'qr']: - logger.debug(f'Invalid label type for {key} ({form.get(key)})') raise APIException(f'Ugyldig etikett-type ({form.get(key)})') case VALIDATORS.ITEM_LIST_EXISTS: @@ -106,9 +123,23 @@ def unique(fkey: str) -> bool: ids = form.getlist(key) all_ids = [i for i in inventory.get_all_ids()] if not all(i in all_ids for i in ids): - logger.debug(f'Invalid item list for {key} ({form.getlist(key)})') raise APIException(f'En eller flere gjenstander finnes ikke ({form.getlist(key)})') + case VALIDATORS.EMAIL: + # Check if the email is valid + if not email(form.get(key)): + raise APIException(f'Ugyldig e-post ({form.get(key)})') + + case VALIDATORS.GROUP: + # Check if the group is valid (done by students, send to 418 ;)) + if form.get(key) not in groups.get_all(): + raise APIException(f'Ugyldig gruppe ({form.get(key)}), godt forsøk ;)', 418) + + case VALIDATORS.GROUP_NAME: + # Check if the group name is valid + if not groupname(form.get(key)): + raise APIException(f'Ugyldig gruppenavn ({form.get(key)}), må være "Klasserom (Lærer)"') + if key.endswith('_minmax'): # Check if the value is between the min and max mn, mx = sanitizer @@ -122,7 +153,7 @@ def unique(fkey: str) -> bool: def sanitize(validation_map: dict[any: VALIDATORS | MINMAX], - form: ImmutableMultiDict, + form: dict, data: dict = dict) -> dict[str: any]: """ Validate a form based on a validation map, diff --git a/BookingSystem/templates/404.html b/BookingSystem/templates/404.html index 5bfb8bb..0eec001 100644 --- a/BookingSystem/templates/404.html +++ b/BookingSystem/templates/404.html @@ -2,7 +2,13 @@ {% block content %}
-

404 - Page not found

+

404 - Fant ikke siden

Siden du prøvde å nå finnes ikke, eller er fjernet.

+ + {% if error %} +
+ Feilmelding: {{ error }} +
+ {% endif %} {% endblock %} \ No newline at end of file diff --git a/BookingSystem/templates/admin_settings.html b/BookingSystem/templates/admin_settings.html index 4183a05..faacbba 100644 --- a/BookingSystem/templates/admin_settings.html +++ b/BookingSystem/templates/admin_settings.html @@ -9,7 +9,7 @@

Dersom du er forvirret av denne siden, så kan du trygt forlate den :)

{% if last_sent %} - + {% else %} @@ -36,8 +36,9 @@

En kategori per linje

{% include 'forms/categories.html' %} diff --git a/BookingSystem/templates/forms/emails.html b/BookingSystem/templates/forms/emails.html index 3301a92..332e462 100644 --- a/BookingSystem/templates/forms/emails.html +++ b/BookingSystem/templates/forms/emails.html @@ -1,6 +1,22 @@ -
+
+ + diff --git a/BookingSystem/templates/forms/groups.html b/BookingSystem/templates/forms/groups.html index 9bcfb63..fb4dfb2 100644 --- a/BookingSystem/templates/forms/groups.html +++ b/BookingSystem/templates/forms/groups.html @@ -1,6 +1,22 @@ -
+
+ + \ No newline at end of file diff --git a/BookingSystem/templates/forms/return.html b/BookingSystem/templates/forms/return.html index 63c05b9..e66cf4d 100644 --- a/BookingSystem/templates/forms/return.html +++ b/BookingSystem/templates/forms/return.html @@ -1,7 +1,7 @@
+ pattern="{{ regex_item }}" autocomplete="off" autofocus>
diff --git a/BookingSystem/templates/inventar_add.html b/BookingSystem/templates/inventar_add.html index 4389fa8..cb57c46 100644 --- a/BookingSystem/templates/inventar_add.html +++ b/BookingSystem/templates/inventar_add.html @@ -38,7 +38,8 @@

Her kan du legge til nytt inventar. Legg gjerne til flere, så forlater du b - + @@ -67,6 +68,7 @@

Her kan du legge til nytt inventar. Legg gjerne til flere, så forlater du b url: '{{ url_for("api.add_item") }}', data: $('form').serialize(), success: function (response) { + used_ids.push($('#id').val().toLowerCase()); iziToast.success({ title: 'Suksess!', message: response, @@ -111,8 +113,9 @@

Her kan du legge til nytt inventar. Legg gjerne til flere, så forlater du b }); } + let used_ids = []; {% if used_ids %} - let used_ids = {{ used_ids | tojson | lower }}; + used_ids = {{ used_ids | tojson | lower }}; let id = $('#id') $(id).change(function () { if (used_ids.includes($(id).val().toLowerCase())) { diff --git a/BookingSystem/templates/inventar_print.html b/BookingSystem/templates/inventar_print.html index 7e01522..8af1d64 100644 --- a/BookingSystem/templates/inventar_print.html +++ b/BookingSystem/templates/inventar_print.html @@ -15,7 +15,8 @@

For {{ item.id }} ({{ item.name }})

- + diff --git a/BookingSystem/templates/templates/alerts.html b/BookingSystem/templates/templates/alerts.html index ae87057..af88e23 100644 --- a/BookingSystem/templates/templates/alerts.html +++ b/BookingSystem/templates/templates/alerts.html @@ -61,24 +61,20 @@ function customAlert(header, msg, type, fullscreen = false) { let icon; + let color; if (fullscreen) { - if (type === 'success') { - icon = 'fa fa-check-circle'; - type = 'green'; - } else if (type === 'warning') { - icon = 'fa fa-exclamation-circle'; - type = 'orange'; - } else if (type === 'error') { - icon = 'fa fa-times-circle'; - type = 'red'; - } else { - icon = 'fa fa-info-circle'; - type = 'blue'; + const typeMapping = { + success: {icon: 'fa fa-check-circle', type: 'green',}, + warning: {icon: 'fa fa-exclamation-circle', type: 'orange',}, + error: {icon: 'fa fa-times-circle', type: 'red',}, + default: {icon: 'fa fa-info-circle', type: 'blue',} } + icon = typeMapping[type].icon || typeMapping.default.icon; + color = typeMapping[type].type || typeMapping.default.type; $.alert({ title: header, content: msg, - type: type, + type: color, icon: icon, buttons: { confirm: { diff --git a/Dockerfile b/Dockerfile index ecfcf2d..4ba3c3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,12 @@ ENV TZ='Europe/Oslo' RUN apt-get update && apt-get install -y tzdata RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +# Global settings +ENV MIN_DAYS='1' +ENV MAX_DAYS='90' +ENV MIN_LABELS='0' +ENV MAX_LABELS='10' + # FEIDE OAuth2 ENV FEIDE_REDIRECT_URI='https:///login/feide/callback' ENV FEIDE_CLIENT_ID='' diff --git a/docker-compose.yml b/docker-compose.yml index 57e9cc8..72203b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,10 @@ services: # Label server FQDN (https://github.com/VaagenIM/EtikettServer) - LABEL_SERVER='https://' + # Global settings + - MAX_DAYS='14' # Maximum days equipment can be borrowed + - MAX_LABELS='10' # Maximum number of labels that can be printed at a time + # SMTP settings for email reports (overdue items) - SMTP_SERVER='' - SMTP_PORT='587' diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..04abd16 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1 @@ +# TODO: test for injections in the API