diff --git a/.gitignore b/.gitignore index 4f97b84..005984e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,22 @@ -/config/* -/static/admin -/application/**/migrations/* -/archive/* -/logs/* -/packages/* -/profilepictures/* -/temp -/tmp +/data/* +/data/logs/* +/data/tls/* +/data/static/* +/data/profilepictures/* +/data/archive/* +!/data/logs/ +!/data/logs/.gitkeep +!/data/tls/ +!/data/tls/.gitkeep +!/data/profilepictures/ +!/data/profilepictures/default.svg +!/data/archive/ +!/data/archive/.gitkeep +!/data/Caddyfile +!/data/*.example.* + +/venv + __pycache__ .vscode *.pem -!/config/config.sample.sh -!/config/Caddyfile -!/config/tls/ -!/profilepictures/default.svg -!.gitkeep diff --git a/LICENSE b/LICENSE index f910113..051bcde 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Julian Müller (W13R) +Copyright (c) 2021 Julian Müller (ChaoticByte) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 27df4cf..490c76a 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,77 @@ -# Drinks Manager (season 2) +# Drinks Manager (Season 3) Note: This software is tailored to my own needs. I probably won't accept feature requests, and don't recommend you to use this software if this isn't exactly what you are looking for. Can't keep track of the number of drinks your guests drink? -Now you have a web interface that *really tries* to make things less complicated- for -you and your guests. +Now you have a web interface that *really tries* to make things +less complicated- for you and your guests. -This (exaggeration intended) most incredible piece of software is written in Python, -HTML, CSS, JS, Bash and uses Django and PostgreSQL. +This (exaggeration intended) most incredible piece of software is +written in Python, HTML, CSS, JS, Bash and uses Django and PostgreSQL. You have to bring your own PostgreSQL Database though. +# Getting started -## Setup, Installation, Updating and Dependencies +## System Requirements -You can find the latest releases [here](https://gitlab.com/W13R/drinks-manager/-/releases), but you should consider using Git to easily switch between versions. -For more information see [Setup](docs/Setup.md). +Beneath a `PostgreSQL` DBMS, you need the following things: +- `pg_config` (Ubuntu: `libpq-dev`, RHEL: `libpq-devel`) +- `Caddy` 2.4.3+ (HTTP Reverse Proxy & Static File Server) +- `gcc` +- `gettext` (for development only) +- `Python` 3.9+ + - `venv` + - `pip` +- `Python` header files (RHEL: `python3-devel`, Ubuntu: `python3-dev`) + +## Database + +This project is using PostgreSQL. After creating a +user and database for this application, make shure to +```sql +revoke all on schema public from PUBLIC; +``` +and revoke/grant other privileges accordingly to secure the +database against public access. ## Configuration -see [Configuration](docs/Configuration.md) +Create the configuration file by copying `./data/config.example.yml` +to `./data/config.yml`, and modify it for your needs. +## Create Environment & Install dependencies -## Usage +Run the following from the main directory: +``` +./scripts/setup-env.sh +``` -After setup, run ```./run.sh help``` to see a help text. -Start the production server with ```./run.sh server```. You can ignore the error message about the "lifespan error". -For more commands, see [Commands](docs/Commands.md). +## Create admin account +``` +./scripts/create-admin.sh +``` +This also runs all necessary migrations. + +# Activate venv + +**On every new session**, before running commands with +manage.py, running special scripts, or developing, +you have to activate the virtual environment: +``` +source ./venv/bin/activate +``` +If you see `(venv)` before your command prompt, it worked! + +# Usage + +To start the Application and Webserver, run +``` +./start.sh +``` +or +``` +./start.sh --devel +``` diff --git a/application/app/__init__.py b/app/__init__.py similarity index 100% rename from application/app/__init__.py rename to app/__init__.py diff --git a/application/app/admin.py b/app/admin.py similarity index 98% rename from application/app/admin.py rename to app/admin.py index 4234240..0d37821 100644 --- a/application/app/admin.py +++ b/app/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin +from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from .models import User @@ -15,6 +16,7 @@ from .forms import CustomDrinkForm from .forms import CustomGlobalForm from .forms import CustomRegisterTransactionForm + # Admin Site class CustomAdminSite(admin.AdminSite): @@ -22,9 +24,8 @@ class CustomAdminSite(admin.AdminSite): site_header = "Drinks Administration" site_title = "Drinks Administration" - @never_cache + @method_decorator(never_cache) def index(self, request, extra_context=None): - return super().index(request, extra_context={ "admin_info": Global.objects.get(name="admin_info").value_string, **(extra_context or {}) @@ -100,7 +101,6 @@ class CustomRegisterAdmin(admin.ModelAdmin): self.message_user(request, f"Revoked {queryset.count()} supplies.") delete_selected_new.short_description = "Revoke selected transactions" - adminSite.register(Register, CustomRegisterAdmin) diff --git a/app/apps.py b/app/apps.py new file mode 100644 index 0000000..bcfe39b --- /dev/null +++ b/app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "app" diff --git a/application/app/context_processors.py b/app/context_processors.py similarity index 100% rename from application/app/context_processors.py rename to app/context_processors.py index f3e345f..68a9e69 100644 --- a/application/app/context_processors.py +++ b/app/context_processors.py @@ -2,8 +2,8 @@ from django.conf import settings from .models import Global -def app_version(request): +def app_version(request): try: global_message = Global.objects.get(pk="global_message").value_string except Global.DoesNotExist: diff --git a/app/db_queries.py b/app/db_queries.py new file mode 100644 index 0000000..93a2727 --- /dev/null +++ b/app/db_queries.py @@ -0,0 +1,140 @@ +#from datetime import datetime + +from django.conf import settings +from django.db import connection + + +COMBINE_ALPHABET = "abcdefghijklmnopqrstuvwxyz" + + +def _db_select(sql_select:str): + result = None + with connection.cursor() as cursor: + cursor.execute(sql_select) + result = cursor.fetchall() + return result + + +def _combine_results(results:list) -> dict: + ''' + e.g. + input: [ + [("x", 12), ("y", 13)], + [("y", 10), ("z", 42)] + ] + output: { + "x": {"a": 12}, + "y": {"a": 13, "b": 10}, + "z": {"b": 42} + } + ''' + result = {} + for i, d in enumerate(results): + a = COMBINE_ALPHABET[i] + for r in d: + r_0 = r[0] + if r_0 not in result: + result[r_0] = {} + result[r_0][a] = r[1] + return result + + +def select_history(user, language_code="en") -> list: + # select order history and deposits + user_id = user.pk + result = _db_select(f""" + select + concat( + product_name, ' (', + content_litres::real, -- converting to real removes trailing zeros + 'l) x ', amount, ' - ', price_sum, '{settings.CURRENCY_SUFFIX}') as "text", + datetime + from app_order + where user_id = {user_id} + + union + + select + concat('Deposit: +', transaction_sum, '{settings.CURRENCY_SUFFIX}') as "text", + datetime + from app_userdeposits_view + where user_id = {user_id} + + order by datetime desc + fetch first 30 rows only; + """) + result = [list(row) for row in result] + if language_code == "de": # reformat for german translation + for row in result: + row[0] = row[0].replace(".", ",") + return result + + +def orders_per_month(user) -> list: + # number of orders per month (last 12 months) + result_user = _db_select(f""" + select + to_char(date_trunc('month', datetime), 'YYYY-MM') as "month", + sum(amount) as "count" + from app_order + where user_id = {user.pk} + and date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval) + group by "month" + order by "month" desc; + """) + result_all = _db_select(f""" + select + to_char(date_trunc('month', datetime), 'YYYY-MM') as "month", + sum(amount) as "count" + from app_order + where date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval) + group by "month" + order by "month" desc; + """) + return _combine_results([result_user, result_all]) + + +def orders_per_weekday(user) -> list: + # number of orders per weekday (all time) + result_user = _db_select(f""" + select + to_char(datetime, 'Day') as "day", + sum(amount) as "count" + from app_order + where user_id = {user.pk} + group by "day" + order by "count" desc; + """) + result_all = _db_select(f""" + select + to_char(datetime, 'Day') as "day", + sum(amount) as "count" + from app_order + group by "day" + order by "count" desc; + """) + return _combine_results([result_user, result_all]) + + +def orders_per_drink(user) -> list: + # number of orders per drink (all time) + result_user = _db_select(f""" + select + d.product_name as "label", + sum(o.amount) as "data" + from app_drink d + join app_order o on (d.id = o.drink_id) + where o.user_id = {user.pk} + group by d.product_name + order by "data" desc; + """) + result_all = _db_select(f""" + select + d.product_name as "label", + sum(o.amount) as "data" + from app_drink d + join app_order o on (d.id = o.drink_id) + group by d.product_name + order by "data" desc; + """) + return _combine_results([result_user, result_all]) diff --git a/application/app/forms.py b/app/forms.py similarity index 100% rename from application/app/forms.py rename to app/forms.py diff --git a/app/locales/de/LC_MESSAGES/django.mo b/app/locales/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..76e345b Binary files /dev/null and b/app/locales/de/LC_MESSAGES/django.mo differ diff --git a/app/locales/de/LC_MESSAGES/django.po b/app/locales/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..c7b1011 --- /dev/null +++ b/app/locales/de/LC_MESSAGES/django.po @@ -0,0 +1,251 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-17 22:11+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Julian Müller (ChaoticByte)\n" +"Language: DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: app/templates/admin/base_site.html:7 +msgid "Django site admin" +msgstr "Django Administrator" + +#: app/templates/admin/base_site.html:15 +msgid "Django administration" +msgstr "Django Administration" + +#: app/templates/baselayout.html:26 +msgid "An error occured. Please log out and log in again." +msgstr "Ein Fehler ist aufgetreten. Bitte ab- und wieder anmelden." + +#: app/templates/deposit.html:6 +msgid "Drinks - Deposit" +msgstr "Getränke - Einzahlen" + +#: app/templates/deposit.html:17 app/templates/userpanel.html:23 +msgid "Deposit" +msgstr "Einzahlen" + +#: app/templates/deposit.html:19 +msgid "Amount" +msgstr "Summe" + +#: app/templates/deposit.html:30 app/templates/order.html:54 +#: app/templates/registration/login.html:28 app/templates/supply.html:29 +msgid "cancel" +msgstr "Abbrechen" + +#: app/templates/deposit.html:31 +msgid "confirm" +msgstr "Bestätigen" + +#: app/templates/history.html:6 +msgid "Drinks - History" +msgstr "Getränke - Verlauf" + +#: app/templates/history.html:10 app/templates/userpanel.html:30 +msgid "History" +msgstr "Verlauf" + +#: app/templates/history.html:14 +msgid "last 30 actions" +msgstr "letzte 30 Vorgänge" + +#: app/templates/history.html:25 +msgid "No history." +msgstr "Kein Verlauf verfügbar." + +#: app/templates/index.html:6 +msgid "Drinks - Home" +msgstr "Getränke - Home" + +#: app/templates/index.html:10 +msgid "Available Drinks" +msgstr "Verfügbare Getränke" + +#: app/templates/index.html:18 app/templates/index.html:25 +msgid "available" +msgstr "verfügbar" + +#: app/templates/index.html:32 +msgid "No drinks available." +msgstr "Es sind gerade keine Getränke verfügbar." + +#: app/templates/order.html:7 +msgid "Drinks - Order" +msgstr "Getränke - Bestellen" + +#: app/templates/order.html:16 +msgid "Order" +msgstr "Bestellung" + +#: app/templates/order.html:18 +msgid "Drink" +msgstr "Getränk" + +#: app/templates/order.html:22 +msgid "Price per Item" +msgstr "Preis pro Getränk" + +#: app/templates/order.html:29 +msgid "Available" +msgstr "Verfügbar" + +#: app/templates/order.html:34 +msgid "Sum" +msgstr "Summe" + +#: app/templates/order.html:38 +msgid "Count" +msgstr "Anzahl" + +#: app/templates/order.html:55 +msgid "order" +msgstr "Bestellen" + +#: app/templates/order.html:62 +msgid "Your balance is too low to order a drink." +msgstr "Dein Saldo ist zu niedrig um Getränke zu bestellen." + +#: app/templates/order.html:63 app/templates/order.html:69 +#: app/templates/supply.html:38 +msgid "back" +msgstr "zurück" + +#: app/templates/order.html:68 +msgid "This drink is not available." +msgstr "Dieses Getränk ist gerade nicht verfügbar." + +#: app/templates/registration/logged_out.html:6 +msgid "Drinks - Logged Out" +msgstr "Getränke - Abgemeldet" + +#: app/templates/registration/logged_out.html:15 +msgid "Logged out! You will be redirected shortly." +msgstr "Du wurdest abgemeldet und wirst in Kürze weitergeleitet." + +#: app/templates/registration/logged_out.html:16 +msgid "Click here if automatic redirection does not work." +msgstr "" +"Bitte klicke hier, wenn die automatische Weiterleitung nicht funktioniert." + +#: app/templates/registration/login.html:8 +msgid "Drinks - Login" +msgstr "Getränke - Anmeldung" + +#: app/templates/registration/login.html:22 +msgid "Log in" +msgstr "Anmelden" + +#: app/templates/registration/login.html:26 +msgid "Password/PIN" +msgstr "Passwort/PIN" + +#: app/templates/registration/login.html:29 +msgid "login" +msgstr "Anmelden" + +#: app/templates/registration/login.html:40 +msgid "Choose your account" +msgstr "Wähle deinen Account" + +#: app/templates/statistics.html:6 +msgid "Drinks - Statistics" +msgstr "Getränke - Statistiken" + +#: app/templates/statistics.html:10 app/templates/userpanel.html:31 +msgid "Statistics" +msgstr "Statistiken" + +#: app/templates/statistics.html:13 +msgid "Orders per drink" +msgstr "Bestellungen pro Getränk" + +#: app/templates/statistics.html:16 +msgid "drink" +msgstr "Getränk" + +#: app/templates/statistics.html:17 app/templates/statistics.html:34 +#: app/templates/statistics.html:51 +msgid "you" +msgstr "Du" + +#: app/templates/statistics.html:18 app/templates/statistics.html:35 +#: app/templates/statistics.html:52 +msgid "all" +msgstr "Alle" + +#: app/templates/statistics.html:30 +msgid "Orders per month (last 12 months)" +msgstr "Bestellungen pro Monat (letzte 12 Monate)" + +#: app/templates/statistics.html:33 +msgid "month" +msgstr "Monat" + +#: app/templates/statistics.html:47 +msgid "Orders per weekday" +msgstr "Bestellungen pro Wochentag" + +#: app/templates/statistics.html:50 +msgid "day" +msgstr "Tag" + +#: app/templates/supply.html:7 +msgid "Drinks - Supply" +msgstr "Getränke - Beschaffung" + +#: app/templates/supply.html:14 app/templates/userpanel.html:36 +msgid "Supply" +msgstr "Beschaffung" + +#: app/templates/supply.html:16 +msgid "Description" +msgstr "Beschreibung" + +#: app/templates/supply.html:22 +msgid "Price" +msgstr "Preis" + +#: app/templates/supply.html:30 +msgid "submit" +msgstr "Senden" + +#: app/templates/supply.html:37 +msgid "You are not allowed to view this site." +msgstr "Dir fehlt die Berechtigung, diese Seite anzuzeigen." + +#: app/templates/userpanel.html:9 app/templates/userpanel.html:11 +msgid "User" +msgstr "Benutzer" + +#: app/templates/userpanel.html:15 app/templates/userpanel.html:17 +msgid "Balance" +msgstr "Saldo" + +#: app/templates/userpanel.html:24 +msgid "Logout" +msgstr "Abmelden" + +#: app/templates/userpanel.html:27 +msgid "Account" +msgstr "Account" + +#: app/templates/userpanel.html:38 +msgid "Change Password" +msgstr "Passwort ändern" + +#: app/views.py:42 +msgid "Invalid username or password." +msgstr "Benutzername oder Passwort ungültig." diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py new file mode 100644 index 0000000..71ead05 --- /dev/null +++ b/app/migrations/0001_initial.py @@ -0,0 +1,267 @@ +# Generated by Django 4.1.6 on 2023-02-11 15:24 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "balance", + models.DecimalField(decimal_places=2, default=0.0, max_digits=8), + ), + ( + "allow_order_with_negative_balance", + models.BooleanField(default=False), + ), + ( + "profile_picture_filename", + models.CharField(default="default.svg", max_length=25), + ), + ("allowed_to_supply", models.BooleanField(default=False)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="Drink", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("product_name", models.CharField(max_length=64)), + ( + "content_litres", + models.DecimalField(decimal_places=3, default=0.5, max_digits=6), + ), + ( + "price", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ("available", models.PositiveIntegerField(default=0)), + ("deleted", models.BooleanField(default=False)), + ("do_not_count", models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name="Global", + fields=[ + ( + "name", + models.CharField( + max_length=42, primary_key=True, serialize=False, unique=True + ), + ), + ("comment", models.TextField()), + ("value_float", models.FloatField(default=0.0)), + ("value_string", models.TextField()), + ], + ), + migrations.CreateModel( + name="RegisterTransaction", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "transaction_sum", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ( + "old_transaction_sum", + models.DecimalField(decimal_places=2, default=0.0, max_digits=6), + ), + ("datetime", models.DateTimeField(default=django.utils.timezone.now)), + ("is_user_deposit", models.BooleanField(default=False)), + ("comment", models.TextField(default=" ")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "transaction", + "verbose_name_plural": "transactions", + }, + ), + migrations.CreateModel( + name="Order", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("datetime", models.DateTimeField(default=django.utils.timezone.now)), + ("amount", models.PositiveIntegerField(default=1, editable=False)), + ("product_name", models.CharField(editable=False, max_length=64)), + ( + "price_sum", + models.DecimalField( + decimal_places=2, default=0, editable=False, max_digits=6 + ), + ), + ( + "content_litres", + models.DecimalField( + decimal_places=3, default=0, editable=False, max_digits=6 + ), + ), + ( + "drink", + models.ForeignKey( + limit_choices_to=models.Q(("available__gt", 0)), + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="app.drink", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/app/migrations/0002_setup.py b/app/migrations/0002_setup.py new file mode 100644 index 0000000..fdb8962 --- /dev/null +++ b/app/migrations/0002_setup.py @@ -0,0 +1,34 @@ +# GlobalValues Data migration #1 + +from django.db import migrations + + +def create_globals(apps, schema_editor): + Global = apps.get_model("app", "Global") + Global( + name="global_message", + comment="Here you can set a global message that will be shown to every user", + value_float=0.0, + value_string="").save() + Global( + name="admin_info", + comment="Here you can set am infotext that will be displayed on the admin panel", + value_float=0.0, + value_string="").save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + # create globals + migrations.RunPython(create_globals), + # create view for userdeposits + migrations.RunSQL(""" + create or replace view app_userdeposits_view as + select * from app_registertransaction + where is_user_deposit = true;""") + ] diff --git a/application/drinks_manager/__init__.py b/app/migrations/__init__.py similarity index 100% rename from application/drinks_manager/__init__.py rename to app/migrations/__init__.py diff --git a/application/app/models.py b/app/models.py similarity index 95% rename from application/app/models.py rename to app/models.py index a3bb11c..ddfd302 100644 --- a/application/app/models.py +++ b/app/models.py @@ -2,12 +2,10 @@ from django.db import models from django.conf import settings from django.contrib.auth.models import AbstractUser -from django_currentuser.db.models import CurrentUserField from django.forms import ValidationError from django.utils import timezone - # Custom user model class User(AbstractUser): @@ -25,8 +23,6 @@ class User(AbstractUser): self.email = "" super().save() -# - class Drink(models.Model): @@ -46,7 +42,8 @@ class Drink(models.Model): self.deleted = True super().save() - def __str__(self): return f"{self.product_name} ({float(self.content_litres):.2f}l) - {self.price}{settings.CURRENCY_SUFFIX}" + def __str__(self): + return f"{self.product_name} ({float(self.content_litres):.2f}l) - {self.price}{settings.CURRENCY_SUFFIX}" class RegisterTransaction(models.Model): @@ -62,7 +59,7 @@ class RegisterTransaction(models.Model): datetime = models.DateTimeField(default=timezone.now) is_user_deposit = models.BooleanField(default=False) comment = models.TextField(default=" ") - user = CurrentUserField() + user = models.ForeignKey(User, on_delete=models.CASCADE) def save(self, *args, **kwargs): if self._state.adding: @@ -100,7 +97,7 @@ class Order(models.Model): null=True, limit_choices_to=models.Q(available__gt=0) # Query only those drinks with a availability greater than (gt) 0 ) - user = CurrentUserField() + user = models.ForeignKey(User, on_delete=models.CASCADE) datetime = models.DateTimeField(default=timezone.now) amount = models.PositiveIntegerField(default=1, editable=False) diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 100644 index 0000000..afdd6c6 --- /dev/null +++ b/app/static/css/main.css @@ -0,0 +1,507 @@ +/* Variables */ + +:root { + --font-family: 'Liberation Sans', sans-serif; + --color: #fafafa; + --color-error: #ff682c; + --bg-page-color: #222222; + --bg-color: #4e4e4e; + --bg-hover-color: #636363; + --bg-color2: #383838; + --bg-hover-color2: #4a4a4a; + --border-color: #808080; + --bg-globalmessage: #161616; + --border-radius: .5rem; +} + +/* General */ + +body { + margin: 0; + padding: 0; + width: 100vw; + min-height: 100vh; + font-family: var(--font-family); + background: var(--bg-page-color); + color: var(--color); + overflow-x: hidden; +} + +a { + color: var(--color); +} + +h1 { + font-size: 1.8rem; +} + +h1, h2, h3, h4 { + text-align: center; +} + +input[type="number"] { + width: 8rem; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +input[type="number"]::-webkit-inner-spin-button { + display: none; +} + +input[type="text"], input[type="password"], input[type="number"] { + padding: .6rem .8rem; + text-align: center; + font-size: 1rem; + color: var(--color); + border: none; + outline: none; + border-bottom: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--bg-color); +} + +table { + border-collapse: collapse; + border-spacing: 0; + text-align: left; + border-radius: var(--border-radius); +} + +tr { + background: var(--bg-color); +} + +tr:nth-child(2n+2) { + background: var(--bg-color2); +} + +/* +Rounded corners on table cells apparently don't work with +Firefox, so Firefox users won't have rounded corners +on tables. Can't fix that by myself. +*/ + +table tr:first-child th:first-child { + border-top-left-radius: var(--border-radius); +} + +table tr:first-child th:last-child { + border-top-right-radius: var(--border-radius); +} + +table tr:last-child td:first-child { + border-bottom-left-radius: var(--border-radius); +} + +table tr:last-child td:last-child { + border-bottom-right-radius: var(--border-radius); +} + +td, th { + padding: .5rem .8rem; +} + +th { + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +/* Basic Layout */ + +.baselayout { + justify-content: start; + align-items: center; + min-height: 100vh; + width: 100vw; + max-width: 100vw; +} + +.globalmessage { + width: 100vw; + z-index: 999; + background: var(--bg-globalmessage); + padding: .3rem 0; +} + +.globalmessage > div { + width: 96%; + text-align: center; + word-break: keep-all; + word-wrap: break-word; + box-sizing: border-box; +} + +.userpanel { + flex-direction: row; + margin-top: 1rem; + width: 94%; + gap: 1rem; +} + +.userinfo { + text-align: center; +} + +.userinfo > span { + vertical-align: middle; +} + +.userinfo > img { + vertical-align: middle; + width: 1.8rem; + height: 1.8rem; + margin: .5rem; +} + +.userpanel-buttons { + gap: .5rem; +} + +.userbalancewarn { + color: var(--color-error); + font-weight: bold; +} + +main { + justify-content: flex-start; + align-items: center; + flex-grow: 1; + width: 100%; +} + +.content { + justify-content: start; + align-items: center; + flex-grow: 1; + padding: 2rem 0; +} + +.footer-container { + z-index: 900; + margin-top: auto; + pointer-events: none; +} + +.footer { + margin-top: 1.5rem; + padding-bottom: .3rem; + text-align: center; + pointer-events: initial; +} + +.footer > div { + font-size: .95rem; + margin-top: .15rem; + margin-bottom: .15rem; +} + +.footer > div::after { + margin-left: .5rem; + content: "-"; + margin-right: .5rem; +} + +.footer > div:last-child::after { + content: none; + margin-left: 0; + margin-right: 0; +} + +/* Common */ + +.flex { + display: flex; +} + +.flex-row { + flex-direction: row; +} + +.flex-column { + flex-direction: column; +} + +.flex-center { + justify-content: center; + align-items: center; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.gap-1rem { + gap: 1rem; +} + +.fill { + height: 100%; + width: 100%; +} + +.fill-vertical { + height: 100%; +} + +.buttons { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + gap: 1rem; +} + +.button, button { + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-family); + text-decoration: none; + text-align: center !important; + background: var(--bg-color); + color: var(--color); + font-size: 1rem; + padding: .6rem .8rem; + outline: none; + border: none; + border-bottom: 1px solid var(--border-color); + border-radius: var(--border-radius); + cursor: pointer; + user-select: none; + box-sizing: content-box; + width: fit-content; +} + +.button:hover, button:hover, .button:active, button:active { + background: var(--bg-hover-color); +} + +.button:disabled, button:disabled { + opacity: 40%; +} + +.appform > .forminfo { + width: 100%; + text-align: left; + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 2rem; +} + +.forminfo > span:last-child { + float: right; +} + +.appform > .forminput { + width: 100%; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.appform > .statusinfo { + margin-top: .5rem; +} + +.dropdownmenu { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + border-radius: var(--border-radius); +} + +.dropdownbutton { + width: fit-content; + z-index: 190; + text-align: center; + justify-content: center; +} + +.dropdownlist { + position: absolute; + display: flex; + flex-direction: column; + pointer-events: none; + border-radius: var(--border-radius) !important; + z-index: 200; + margin-top: 3.2rem; + opacity: 0%; + transition: opacity 100ms; +} + +.dropdownchoice { + border-radius: 0 !important; + margin: 0; + text-align: center; + justify-content: center; + background: var(--bg-color2) !important; + backdrop-filter: none !important; + width: initial; +} + +.dropdownchoice:hover { + background: var(--bg-hover-color2) !important; +} + +.dropdownlist :first-child { + border-top-left-radius: var(--border-radius) !important; + border-top-right-radius: var(--border-radius) !important; +} + +.dropdownlist :last-child { + border-bottom-left-radius: var(--border-radius) !important; + border-bottom-right-radius: var(--border-radius) !important; +} + +.dropdownvisible .dropdownlist { + opacity: 100%; + visibility: visible; + pointer-events: visible; +} + +.customnumberinput { + height: 2.2rem; +} + +.customnumberinput button { + min-width: 2.5rem !important; + width: 2.5rem !important; + padding: 0; + margin: 0; + height: 100%; +} + +.customnumberinput-minus { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + z-index: 10; +} + +.customnumberinput-plus { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + z-index: 10; +} + +.customnumberinput input[type="number"] { + height: 100%; + width: 4rem; + padding: 0; + margin: 0; + background: var(--bg-color2); + border-radius: 0 !important; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +.errortext { + color: var(--color-error); +} + +.nodisplay { + display: none !important; +} + +/* Login */ + +.userlist { + width: 60%; + list-style: none; + margin: 0; + padding: 1rem; + gap: 1rem; +} + +.userlist > li { + margin-bottom: .5rem; + padding: 0 .5rem; +} + +.userlist > li > img { + margin-right: auto; + margin-left: 0; + height: 2rem; + width: 2rem; +} + +.userlist > li > div { + flex-grow: 1; + text-align: center; + padding: .8rem 1.1rem; +} + +.loginform { + gap: 1rem; + flex-direction: row; +} + +.loginform > .buttons { + margin-top: 0; +} + +/* Drinks List */ + +.drinks-list { + justify-content: center; + align-items: start; + padding: 0; + width: 60%; +} + +.drinks-list > li { + flex-grow: 1; +} + +.drinks-list > li > .button { + width: 100%; + justify-content: space-between; + padding: .8rem 1.1rem; +} + +/* Responsive */ + +@media only screen and (max-width: 1200px) { + .userlist { + width: 75%; + } + .drinks-list { + width: 70%; + } +} + +@media only screen and (max-width: 1000px) { + .userlist { + width: 90%; + } + .drinks-list { + width: 80%; + } +} + +@media only screen and (max-width: 700px) { + .userpanel { + flex-direction: column; + } + .userlist { + gap: 0.25rem; + } + .userlist > li { + width: 100%; + } + .userlist > li > div { + margin-right: 2rem; + } + .loginform { + flex-direction: column; + } + .drinks-list { + width: 90%; + } +} diff --git a/app/static/css/simple-keyboard.css b/app/static/css/simple-keyboard.css new file mode 100644 index 0000000..7b9e413 --- /dev/null +++ b/app/static/css/simple-keyboard.css @@ -0,0 +1,11 @@ +/*! + * + * simple-keyboard v3.5.22 + * https://github.com/hodgef/simple-keyboard + * + * Copyright (c) Francisco Hodge (https://github.com/hodgef) and project contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */.hg-theme-default{background-color:#ececec;border-radius:5px;box-sizing:border-box;font-family:HelveticaNeue-Light,Helvetica Neue Light,Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;overflow:hidden;padding:5px;touch-action:manipulation;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.hg-theme-default .hg-button span{pointer-events:none}.hg-theme-default button.hg-button{border-width:0;font-size:inherit;outline:0}.hg-theme-default .hg-button{display:inline-block;flex-grow:1}.hg-theme-default .hg-row{display:flex}.hg-theme-default .hg-row:not(:last-child){margin-bottom:5px}.hg-theme-default .hg-row .hg-button-container,.hg-theme-default .hg-row .hg-button:not(:last-child){margin-right:5px}.hg-theme-default .hg-row>div:last-child{margin-right:0}.hg-theme-default .hg-row .hg-button-container{display:flex}.hg-theme-default .hg-button{-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;background:#fff;border-bottom:1px solid #b5b5b5;border-radius:5px;box-shadow:0 0 3px -1px rgba(0,0,0,.3);box-sizing:border-box;cursor:pointer;display:flex;height:40px;justify-content:center;padding:5px}.hg-theme-default .hg-button.hg-standardBtn{width:20px}.hg-theme-default .hg-button.hg-activeButton{background:#efefef}.hg-theme-default.hg-layout-numeric .hg-button{align-items:center;display:flex;height:60px;justify-content:center;width:33.3%}.hg-theme-default .hg-button.hg-button-numpadadd,.hg-theme-default .hg-button.hg-button-numpadenter{height:85px}.hg-theme-default .hg-button.hg-button-numpad0{width:105px}.hg-theme-default .hg-button.hg-button-com{max-width:85px}.hg-theme-default .hg-button.hg-standardBtn.hg-button-at{max-width:45px}.hg-theme-default .hg-button.hg-selectedButton{background:rgba(5,25,70,.53);color:#fff}.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=".com"]{max-width:82px}.hg-theme-default .hg-button.hg-standardBtn[data-skbtn="@"]{max-width:60px}.hg-candidate-box{background:#ececec;border-bottom:2px solid #b5b5b5;border-radius:5px;display:inline-flex;margin-top:-10px;max-width:272px;position:absolute;transform:translateY(-100%);-webkit-user-select:none;-moz-user-select:none;user-select:none}ul.hg-candidate-box-list{display:flex;flex:1;list-style:none;margin:0;padding:0}li.hg-candidate-box-list-item{align-items:center;display:flex;height:40px;justify-content:center;width:40px}li.hg-candidate-box-list-item:hover{background:rgba(0,0,0,.03);cursor:pointer}li.hg-candidate-box-list-item:active{background:rgba(0,0,0,.1)}.hg-candidate-box-prev:before{content:"◄"}.hg-candidate-box-next:before{content:"►"}.hg-candidate-box-next,.hg-candidate-box-prev{align-items:center;background:#d0d0d0;color:#969696;cursor:pointer;display:flex;padding:0 10px}.hg-candidate-box-next{border-bottom-right-radius:5px;border-top-right-radius:5px}.hg-candidate-box-prev{border-bottom-left-radius:5px;border-top-left-radius:5px}.hg-candidate-box-btn-active{color:#444} \ No newline at end of file diff --git a/app/static/css/simple-keyboard_dark.css b/app/static/css/simple-keyboard_dark.css new file mode 100644 index 0000000..16bfec5 --- /dev/null +++ b/app/static/css/simple-keyboard_dark.css @@ -0,0 +1,23 @@ +.simple-keyboard.darkTheme.numeric { + width: 13rem; +} +.simple-keyboard.darkTheme { + width: 50rem; + max-width: 100%; + background: transparent; +} +.simple-keyboard.darkTheme .hg-button { + height: 50px; + display: flex; + justify-content: center; + align-items: center; + background: var(--bg-color); + color: white; + border: none; + border-bottom: 1px solid var(--border-color); +} +.simple-keyboard.darkTheme .hg-button:active, +.simple-keyboard.darkTheme .hg-button:hover { + color: white; + background: var(--bg-hover-color); +} diff --git a/static/favicon.ico b/app/static/favicon.ico similarity index 100% rename from static/favicon.ico rename to app/static/favicon.ico diff --git a/static/favicon.png b/app/static/favicon.png similarity index 100% rename from static/favicon.png rename to app/static/favicon.png diff --git a/static/js/autoreload.js b/app/static/js/autoreload.js similarity index 51% rename from static/js/autoreload.js rename to app/static/js/autoreload.js index 0c30078..a86e6ff 100644 --- a/static/js/autoreload.js +++ b/app/static/js/autoreload.js @@ -1,3 +1,3 @@ setInterval(() => { location.reload(); -}, 1000*60*2); // reload after 2 minutes \ No newline at end of file +}, 1000*60*2); // reload after 2 minutes diff --git a/static/js/custom_number_input.js b/app/static/js/custom_number_input.js similarity index 79% rename from static/js/custom_number_input.js rename to app/static/js/custom_number_input.js index 299e02b..f39d100 100644 --- a/static/js/custom_number_input.js +++ b/app/static/js/custom_number_input.js @@ -1,5 +1,4 @@ -{ - +(() => { document.addEventListener("DOMContentLoaded", () => { // get all customnumberinput Elements let customNumberInputElements = document.getElementsByClassName("customnumberinput"); @@ -8,16 +7,11 @@ // number input let numberFieldElement = element.getElementsByClassName("customnumberinput-field")[0]; // minus button - element.getElementsByClassName("customnumberinput-minus")[0].addEventListener("click", () => { - alterCustomNumberField(numberFieldElement, -1) - }); + element.getElementsByClassName("customnumberinput-minus")[0].addEventListener("click", () => alterCustomNumberField(numberFieldElement, -1)); // plus button - element.getElementsByClassName("customnumberinput-plus")[0].addEventListener("click", () => { - alterCustomNumberField(numberFieldElement, +1) - }); + element.getElementsByClassName("customnumberinput-plus")[0].addEventListener("click", () => alterCustomNumberField(numberFieldElement, +1)); }) }) - function alterCustomNumberField(numberFieldElement, n) { numberFieldElement.value = Math.min( Math.max( @@ -26,5 +20,4 @@ numberFieldElement.max || Number.MAX_VALUE ); } - -} \ No newline at end of file +})(); diff --git a/static/js/deposit.js b/app/static/js/deposit.js similarity index 98% rename from static/js/deposit.js rename to app/static/js/deposit.js index 4e8905d..c2b6785 100644 --- a/static/js/deposit.js +++ b/app/static/js/deposit.js @@ -1,28 +1,18 @@ document.addEventListener("DOMContentLoaded", () => { - // elements - let depositForm = document.getElementById("depositform"); let statusInfo = document.getElementById("statusinfo"); let depositSubmitButton = document.getElementById("depositsubmitbtn"); - // event listener for deposit form // this implements a custom submit method - depositForm.addEventListener("submit", (event) => { - depositSubmitButton.disabled = true; - event.preventDefault(); // Don't do the default submit action! - let xhr = new XMLHttpRequest(); let formData = new FormData(depositForm); - xhr.addEventListener("load", (event) => { - status_ = event.target.status; response_ = event.target.responseText; - if (status_ == 200 && response_ == "success") { statusInfo.innerText = "Success. Redirecting soon."; window.location.replace("/"); @@ -32,18 +22,13 @@ document.addEventListener("DOMContentLoaded", () => { statusInfo.innerText = "An error occured. Redirecting in 5 seconds..."; window.setTimeout(() => { window.location.replace("/") }, 5000); } - }) - xhr.addEventListener("error", (event) => { statusInfo.classList.add("errortext"); statusInfo.innerText = "An error occured. Redirecting in 5 seconds..."; window.setTimeout(() => { window.location.replace("/") }, 5000); }) - xhr.open("POST", "/api/deposit"); xhr.send(formData); - }); - -}) \ No newline at end of file +}); diff --git a/static/js/logged_out.js b/app/static/js/logged_out.js similarity index 100% rename from static/js/logged_out.js rename to app/static/js/logged_out.js diff --git a/static/js/login.js b/app/static/js/login.js similarity index 70% rename from static/js/login.js rename to app/static/js/login.js index 47c2b9f..0c43a72 100644 --- a/static/js/login.js +++ b/app/static/js/login.js @@ -1,7 +1,5 @@ (() => { - // Define variables - let usernameInputElement; let passwordInputElement; let submitButton; @@ -10,78 +8,37 @@ let userlistButtons; let pinpadButtons; let userlistContainerElement; - - // Add event listeners after DOM Content loaded - document.addEventListener("DOMContentLoaded", () => { - // elements - usernameInputElement = document.getElementById("id_username"); passwordInputElement = document.getElementById("id_password"); submitButton = document.getElementById("submit_login"); - passwordOverlayElement = document.getElementById("passwordoverlaycontainer"); + passwordOverlayElement = document.getElementById("passwordoverlay-container"); pwOverlayCancelButton = document.getElementById("pwocancel"); userlistContainerElement = document.getElementById("userlistcontainer"); - userlistButtons = document.getElementsByClassName("userlistbutton"); - pinpadButtons = document.getElementsByClassName("pinpadbtn"); - // event listeners - // [...] converts an html collection to an array - [...userlistButtons].forEach(element => { element.addEventListener("click", () => { set_username(element.dataset.username); show_password_overlay(); }) }); - - [...pinpadButtons].forEach(element => { - element.addEventListener("click", () => { - pinpad_press(element.dataset.btn); - }) - }) - pwOverlayCancelButton.addEventListener("click", () => { hide_password_overlay(); }); - }) - - function set_username(username) { usernameInputElement.value = username; } - function show_password_overlay() { - window.scrollTo(0, 0); passwordOverlayElement.classList.remove("nodisplay"); - userlistContainerElement.classList.add("nodisplay"); - } - function hide_password_overlay() { - passwordOverlayElement.classList.add("nodisplay"); - userlistContainerElement.classList.remove("nodisplay"); passwordInputElement.value = ""; - } - - function pinpad_press(key) { - if (key == "enter") { - submitButton.click(); - } - else if (key == "x") { - passwordInputElement.value = ""; - } - else { - passwordInputElement.value += key; - } - } - -})() \ No newline at end of file +})(); diff --git a/static/js/main.js b/app/static/js/main.js similarity index 98% rename from static/js/main.js rename to app/static/js/main.js index 58b2373..711aed8 100644 --- a/static/js/main.js +++ b/app/static/js/main.js @@ -1,21 +1,14 @@ document.addEventListener("DOMContentLoaded", () => { - let dropdownmenuElement = document.getElementById("dropdownmenu"); let dropdownmenuButtonElement = document.getElementById("dropdownmenu-button"); - if (dropdownmenuButtonElement != null) { - dropdownmenuButtonElement.addEventListener("click", () => { - if (dropdownmenuElement.classList.contains("dropdownvisible")) { dropdownmenuElement.classList.remove("dropdownvisible"); } else { dropdownmenuElement.classList.add("dropdownvisible"); } - }) - } - -}) \ No newline at end of file +}); diff --git a/static/js/order.js b/app/static/js/order.js similarity index 98% rename from static/js/order.js rename to app/static/js/order.js index 46f88b6..801e167 100644 --- a/static/js/order.js +++ b/app/static/js/order.js @@ -1,61 +1,39 @@ document.addEventListener("DOMContentLoaded", () => { - // elements - let orderNumberofdrinksInput = document.getElementById("numberofdrinks"); let orderNumberofdrinksBtnA = document.getElementById("numberofdrinks-btn-minus"); let orderNumberofdrinksBtnB = document.getElementById("numberofdrinks-btn-plus"); let orderSumElement = document.getElementById("ordercalculatedsum"); - let orderFormElement = document.getElementById("orderform"); let statusInfoElement = document.getElementById("statusinfo"); let orderSubmitButton = document.getElementById("ordersubmitbtn"); - - // calculate & display sum - let orderPricePerDrink = parseFloat(document.getElementById("priceperdrink").dataset.drinkPrice); - function calculateAndDisplaySum() { - setTimeout(() => { - let numberOfDrinks = parseFloat(orderNumberofdrinksInput.value); if (isNaN(numberOfDrinks)) { numberOfDrinks = 1; } let calculated_sum = orderPricePerDrink * numberOfDrinks; orderSumElement.innerText = new Intl.NumberFormat(undefined, {minimumFractionDigits: 2}).format(calculated_sum); - }, 25); - } - orderNumberofdrinksInput.addEventListener("input", calculateAndDisplaySum); orderNumberofdrinksBtnA.addEventListener("click", calculateAndDisplaySum); orderNumberofdrinksBtnB.addEventListener("click", calculateAndDisplaySum); - - // custom submit method - orderFormElement.addEventListener("submit", (event) => { - orderSubmitButton.disabled = true; - event.preventDefault(); // Don't do the default submit action! - if (isNaN(parseFloat(orderNumberofdrinksInput.value))) { orderNumberofdrinksInput.value = 1; } - let xhr = new XMLHttpRequest(); let formData = new FormData(orderFormElement); - xhr.addEventListener("load", (event) => { - status_ = event.target.status; response_ = event.target.responseText; - if (status_ == 200 && response_ == "success") { statusInfoElement.innerText = "Success."; window.location.replace("/"); @@ -65,18 +43,13 @@ document.addEventListener("DOMContentLoaded", () => { statusInfoElement.innerText = "An error occured."; window.setTimeout(() => { window.location.reload() }, 5000); } - }) - xhr.addEventListener("error", (event) => { statusInfoElement.classList.add("errortext"); statusInfoElement.innerText = "An error occured."; window.setTimeout(() => { window.location.reload() }, 5000); }) - xhr.open("POST", "/api/order-drink"); xhr.send(formData); - }); - -}) \ No newline at end of file +}); diff --git a/app/static/js/simple-keyboard.js b/app/static/js/simple-keyboard.js new file mode 100644 index 0000000..da44a98 --- /dev/null +++ b/app/static/js/simple-keyboard.js @@ -0,0 +1,12 @@ +/*! + * + * simple-keyboard v3.5.22 + * https://github.com/hodgef/simple-keyboard + * + * Copyright (c) Francisco Hodge (https://github.com/hodgef) and project contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SimpleKeyboard=e():t.SimpleKeyboard=e()}(this,(function(){return function(){var t={9662:function(t,e,n){var r=n(614),o=n(6330),i=TypeError;t.exports=function(t){if(r(t))return t;throw i(o(t)+" is not a function")}},9483:function(t,e,n){var r=n(4411),o=n(6330),i=TypeError;t.exports=function(t){if(r(t))return t;throw i(o(t)+" is not a constructor")}},6077:function(t,e,n){var r=n(614),o=String,i=TypeError;t.exports=function(t){if("object"==typeof t||r(t))return t;throw i("Can't set "+o(t)+" as a prototype")}},1223:function(t,e,n){var r=n(5112),o=n(30),i=n(3070).f,a=r("unscopables"),s=Array.prototype;null==s[a]&&i(s,a,{configurable:!0,value:o(null)}),t.exports=function(t){s[a][t]=!0}},1530:function(t,e,n){"use strict";var r=n(8710).charAt;t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},9670:function(t,e,n){var r=n(111),o=String,i=TypeError;t.exports=function(t){if(r(t))return t;throw i(o(t)+" is not an object")}},8533:function(t,e,n){"use strict";var r=n(2092).forEach,o=n(9341)("forEach");t.exports=o?[].forEach:function(t){return r(this,t,arguments.length>1?arguments[1]:void 0)}},8457:function(t,e,n){"use strict";var r=n(9974),o=n(6916),i=n(7908),a=n(3411),s=n(7659),u=n(4411),c=n(6244),l=n(6135),f=n(4121),d=n(1246),p=Array;t.exports=function(t){var e=i(t),n=u(this),h=arguments.length,v=h>1?arguments[1]:void 0,y=void 0!==v;y&&(v=r(v,h>2?arguments[2]:void 0));var g,m,b,x,w,E,S=d(e),O=0;if(!S||this===p&&s(S))for(g=c(e),m=n?new this(g):p(g);g>O;O++)E=y?v(e[O],O):e[O],l(m,O,E);else for(w=(x=f(e,S)).next,m=n?new this:[];!(b=o(w,x)).done;O++)E=y?a(x,v,[b.value,O],!0):b.value,l(m,O,E);return m.length=O,m}},1318:function(t,e,n){var r=n(5656),o=n(1400),i=n(6244),a=function(t){return function(e,n,a){var s,u=r(e),c=i(u),l=o(a,c);if(t&&n!=n){for(;c>l;)if((s=u[l++])!=s)return!0}else for(;c>l;l++)if((t||l in u)&&u[l]===n)return t||l||0;return!t&&-1}};t.exports={includes:a(!0),indexOf:a(!1)}},2092:function(t,e,n){var r=n(9974),o=n(1702),i=n(8361),a=n(7908),s=n(6244),u=n(5417),c=o([].push),l=function(t){var e=1==t,n=2==t,o=3==t,l=4==t,f=6==t,d=7==t,p=5==t||f;return function(h,v,y,g){for(var m,b,x=a(h),w=i(x),E=r(v,y),S=s(w),O=0,k=g||u,I=e?k(h,S):n||d?k(h,0):void 0;S>O;O++)if((p||O in w)&&(b=E(m=w[O],O,x),t))if(e)I[O]=b;else if(b)switch(t){case 3:return!0;case 5:return m;case 6:return O;case 2:c(I,m)}else switch(t){case 4:return!1;case 7:c(I,m)}return f?-1:o||l?l:I}};t.exports={forEach:l(0),map:l(1),filter:l(2),some:l(3),every:l(4),find:l(5),findIndex:l(6),filterReject:l(7)}},1194:function(t,e,n){var r=n(7293),o=n(5112),i=n(7392),a=o("species");t.exports=function(t){return i>=51||!r((function(){var e=[];return(e.constructor={})[a]=function(){return{foo:1}},1!==e[t](Boolean).foo}))}},9341:function(t,e,n){"use strict";var r=n(7293);t.exports=function(t,e){var n=[][t];return!!n&&r((function(){n.call(null,e||function(){return 1},1)}))}},3671:function(t,e,n){var r=n(9662),o=n(7908),i=n(8361),a=n(6244),s=TypeError,u=function(t){return function(e,n,u,c){r(n);var l=o(e),f=i(l),d=a(l),p=t?d-1:0,h=t?-1:1;if(u<2)for(;;){if(p in f){c=f[p],p+=h;break}if(p+=h,t?p<0:d<=p)throw s("Reduce of empty array with no initial value")}for(;t?p>=0:d>p;p+=h)p in f&&(c=n(c,f[p],p,l));return c}};t.exports={left:u(!1),right:u(!0)}},3658:function(t,e,n){"use strict";var r=n(9781),o=n(3157),i=TypeError,a=Object.getOwnPropertyDescriptor,s=r&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(t){return t instanceof TypeError}}();t.exports=s?function(t,e){if(o(t)&&!a(t,"length").writable)throw i("Cannot set read only .length");return t.length=e}:function(t,e){return t.length=e}},1589:function(t,e,n){var r=n(1400),o=n(6244),i=n(6135),a=Array,s=Math.max;t.exports=function(t,e,n){for(var u=o(t),c=r(e,u),l=r(void 0===n?u:n,u),f=a(s(l-c,0)),d=0;c0;)t[r]=t[--r];r!==i++&&(t[r]=n)}return t},s=function(t,e,n,r){for(var o=e.length,i=n.length,a=0,s=0;a9007199254740991)throw e("Maximum allowed index exceeded");return t}},8324:function(t){t.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},8509:function(t,e,n){var r=n(317)("span").classList,o=r&&r.constructor&&r.constructor.prototype;t.exports=o===Object.prototype?void 0:o},8886:function(t,e,n){var r=n(8113).match(/firefox\/(\d+)/i);t.exports=!!r&&+r[1]},256:function(t,e,n){var r=n(8113);t.exports=/MSIE|Trident/.test(r)},5268:function(t,e,n){var r=n(4326);t.exports="undefined"!=typeof process&&"process"==r(process)},8113:function(t){t.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},7392:function(t,e,n){var r,o,i=n(7854),a=n(8113),s=i.process,u=i.Deno,c=s&&s.versions||u&&u.version,l=c&&c.v8;l&&(o=(r=l.split("."))[0]>0&&r[0]<4?1:+(r[0]+r[1])),!o&&a&&(!(r=a.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/))&&(o=+r[1]),t.exports=o},8008:function(t,e,n){var r=n(8113).match(/AppleWebKit\/(\d+)\./);t.exports=!!r&&+r[1]},748:function(t){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:function(t,e,n){var r=n(7854),o=n(1236).f,i=n(8880),a=n(8052),s=n(3072),u=n(9920),c=n(4705);t.exports=function(t,e){var n,l,f,d,p,h=t.target,v=t.global,y=t.stat;if(n=v?r:y?r[h]||s(h,{}):(r[h]||{}).prototype)for(l in e){if(d=e[l],f=t.dontCallGetSet?(p=o(n,l))&&p.value:n[l],!c(v?l:h+(y?".":"#")+l,t.forced)&&void 0!==f){if(typeof d==typeof f)continue;u(d,f)}(t.sham||f&&f.sham)&&i(d,"sham",!0),a(n,l,d,t)}}},7293:function(t){t.exports=function(t){try{return!!t()}catch(t){return!0}}},7007:function(t,e,n){"use strict";n(4916);var r=n(1470),o=n(8052),i=n(2261),a=n(7293),s=n(5112),u=n(8880),c=s("species"),l=RegExp.prototype;t.exports=function(t,e,n,f){var d=s(t),p=!a((function(){var e={};return e[d]=function(){return 7},7!=""[t](e)})),h=p&&!a((function(){var e=!1,n=/a/;return"split"===t&&((n={}).constructor={},n.constructor[c]=function(){return n},n.flags="",n[d]=/./[d]),n.exec=function(){return e=!0,null},n[d](""),!e}));if(!p||!h||n){var v=r(/./[d]),y=e(d,""[t],(function(t,e,n,o,a){var s=r(t),u=e.exec;return u===i||u===l.exec?p&&!a?{done:!0,value:v(e,n,o)}:{done:!0,value:s(n,e,o)}:{done:!1}}));o(String.prototype,t,y[0]),o(l,d,y[1])}f&&u(l[d],"sham",!0)}},2104:function(t,e,n){var r=n(4374),o=Function.prototype,i=o.apply,a=o.call;t.exports="object"==typeof Reflect&&Reflect.apply||(r?a.bind(i):function(){return a.apply(i,arguments)})},9974:function(t,e,n){var r=n(1470),o=n(9662),i=n(4374),a=r(r.bind);t.exports=function(t,e){return o(t),void 0===e?t:i?a(t,e):function(){return t.apply(e,arguments)}}},4374:function(t,e,n){var r=n(7293);t.exports=!r((function(){var t=function(){}.bind();return"function"!=typeof t||t.hasOwnProperty("prototype")}))},6916:function(t,e,n){var r=n(4374),o=Function.prototype.call;t.exports=r?o.bind(o):function(){return o.apply(o,arguments)}},6530:function(t,e,n){var r=n(9781),o=n(2597),i=Function.prototype,a=r&&Object.getOwnPropertyDescriptor,s=o(i,"name"),u=s&&"something"===function(){}.name,c=s&&(!r||r&&a(i,"name").configurable);t.exports={EXISTS:s,PROPER:u,CONFIGURABLE:c}},1470:function(t,e,n){var r=n(4326),o=n(1702);t.exports=function(t){if("Function"===r(t))return o(t)}},1702:function(t,e,n){var r=n(4374),o=Function.prototype,i=o.call,a=r&&o.bind.bind(i,i);t.exports=r?a:function(t){return function(){return i.apply(t,arguments)}}},5005:function(t,e,n){var r=n(7854),o=n(614),i=function(t){return o(t)?t:void 0};t.exports=function(t,e){return arguments.length<2?i(r[t]):r[t]&&r[t][e]}},1246:function(t,e,n){var r=n(648),o=n(8173),i=n(8554),a=n(7497),s=n(5112)("iterator");t.exports=function(t){if(!i(t))return o(t,s)||o(t,"@@iterator")||a[r(t)]}},4121:function(t,e,n){var r=n(6916),o=n(9662),i=n(9670),a=n(6330),s=n(1246),u=TypeError;t.exports=function(t,e){var n=arguments.length<2?s(t):e;if(o(n))return i(r(n,t));throw u(a(t)+" is not iterable")}},8173:function(t,e,n){var r=n(9662),o=n(8554);t.exports=function(t,e){var n=t[e];return o(n)?void 0:r(n)}},647:function(t,e,n){var r=n(1702),o=n(7908),i=Math.floor,a=r("".charAt),s=r("".replace),u=r("".slice),c=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,l=/\$([$&'`]|\d{1,2})/g;t.exports=function(t,e,n,r,f,d){var p=n+t.length,h=r.length,v=l;return void 0!==f&&(f=o(f),v=c),s(d,v,(function(o,s){var c;switch(a(s,0)){case"$":return"$";case"&":return t;case"`":return u(e,0,n);case"'":return u(e,p);case"<":c=f[u(s,1,-1)];break;default:var l=+s;if(0===l)return o;if(l>h){var d=i(l/10);return 0===d?o:d<=h?void 0===r[d-1]?a(s,1):r[d-1]+a(s,1):o}c=r[l-1]}return void 0===c?"":c}))}},7854:function(t,e,n){var r=function(t){return t&&t.Math==Math&&t};t.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},2597:function(t,e,n){var r=n(1702),o=n(7908),i=r({}.hasOwnProperty);t.exports=Object.hasOwn||function(t,e){return i(o(t),e)}},3501:function(t){t.exports={}},490:function(t,e,n){var r=n(5005);t.exports=r("document","documentElement")},4664:function(t,e,n){var r=n(9781),o=n(7293),i=n(317);t.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},8361:function(t,e,n){var r=n(1702),o=n(7293),i=n(4326),a=Object,s=r("".split);t.exports=o((function(){return!a("z").propertyIsEnumerable(0)}))?function(t){return"String"==i(t)?s(t,""):a(t)}:a},9587:function(t,e,n){var r=n(614),o=n(111),i=n(7674);t.exports=function(t,e,n){var a,s;return i&&r(a=e.constructor)&&a!==n&&o(s=a.prototype)&&s!==n.prototype&&i(t,s),t}},2788:function(t,e,n){var r=n(1702),o=n(614),i=n(5465),a=r(Function.toString);o(i.inspectSource)||(i.inspectSource=function(t){return a(t)}),t.exports=i.inspectSource},9909:function(t,e,n){var r,o,i,a=n(4811),s=n(7854),u=n(111),c=n(8880),l=n(2597),f=n(5465),d=n(6200),p=n(3501),h="Object already initialized",v=s.TypeError,y=s.WeakMap;if(a||f.state){var g=f.state||(f.state=new y);g.get=g.get,g.has=g.has,g.set=g.set,r=function(t,e){if(g.has(t))throw v(h);return e.facade=t,g.set(t,e),e},o=function(t){return g.get(t)||{}},i=function(t){return g.has(t)}}else{var m=d("state");p[m]=!0,r=function(t,e){if(l(t,m))throw v(h);return e.facade=t,c(t,m,e),e},o=function(t){return l(t,m)?t[m]:{}},i=function(t){return l(t,m)}}t.exports={set:r,get:o,has:i,enforce:function(t){return i(t)?o(t):r(t,{})},getterFor:function(t){return function(e){var n;if(!u(e)||(n=o(e)).type!==t)throw v("Incompatible receiver, "+t+" required");return n}}}},7659:function(t,e,n){var r=n(5112),o=n(7497),i=r("iterator"),a=Array.prototype;t.exports=function(t){return void 0!==t&&(o.Array===t||a[i]===t)}},3157:function(t,e,n){var r=n(4326);t.exports=Array.isArray||function(t){return"Array"==r(t)}},614:function(t,e,n){var r=n(4154),o=r.all;t.exports=r.IS_HTMLDDA?function(t){return"function"==typeof t||t===o}:function(t){return"function"==typeof t}},4411:function(t,e,n){var r=n(1702),o=n(7293),i=n(614),a=n(648),s=n(5005),u=n(2788),c=function(){},l=[],f=s("Reflect","construct"),d=/^\s*(?:class|function)\b/,p=r(d.exec),h=!d.exec(c),v=function(t){if(!i(t))return!1;try{return f(c,l,t),!0}catch(t){return!1}},y=function(t){if(!i(t))return!1;switch(a(t)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return h||!!p(d,u(t))}catch(t){return!0}};y.sham=!0,t.exports=!f||o((function(){var t;return v(v.call)||!v(Object)||!v((function(){t=!0}))||t}))?y:v},4705:function(t,e,n){var r=n(7293),o=n(614),i=/#|\.prototype\./,a=function(t,e){var n=u[s(t)];return n==l||n!=c&&(o(e)?r(e):!!e)},s=a.normalize=function(t){return String(t).replace(i,".").toLowerCase()},u=a.data={},c=a.NATIVE="N",l=a.POLYFILL="P";t.exports=a},5988:function(t,e,n){var r=n(111),o=Math.floor;t.exports=Number.isInteger||function(t){return!r(t)&&isFinite(t)&&o(t)===t}},8554:function(t){t.exports=function(t){return null==t}},111:function(t,e,n){var r=n(614),o=n(4154),i=o.all;t.exports=o.IS_HTMLDDA?function(t){return"object"==typeof t?null!==t:r(t)||t===i}:function(t){return"object"==typeof t?null!==t:r(t)}},1913:function(t){t.exports=!1},7850:function(t,e,n){var r=n(111),o=n(4326),i=n(5112)("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[i])?!!e:"RegExp"==o(t))}},2190:function(t,e,n){var r=n(5005),o=n(614),i=n(7976),a=n(3307),s=Object;t.exports=a?function(t){return"symbol"==typeof t}:function(t){var e=r("Symbol");return o(e)&&i(e.prototype,s(t))}},9212:function(t,e,n){var r=n(6916),o=n(9670),i=n(8173);t.exports=function(t,e,n){var a,s;o(t);try{if(!(a=i(t,"return"))){if("throw"===e)throw n;return n}a=r(a,t)}catch(t){s=!0,a=t}if("throw"===e)throw n;if(s)throw a;return o(a),n}},3061:function(t,e,n){"use strict";var r=n(3383).IteratorPrototype,o=n(30),i=n(9114),a=n(8003),s=n(7497),u=function(){return this};t.exports=function(t,e,n,c){var l=e+" Iterator";return t.prototype=o(r,{next:i(+!c,n)}),a(t,l,!1,!0),s[l]=u,t}},1656:function(t,e,n){"use strict";var r=n(2109),o=n(6916),i=n(1913),a=n(6530),s=n(614),u=n(3061),c=n(9518),l=n(7674),f=n(8003),d=n(8880),p=n(8052),h=n(5112),v=n(7497),y=n(3383),g=a.PROPER,m=a.CONFIGURABLE,b=y.IteratorPrototype,x=y.BUGGY_SAFARI_ITERATORS,w=h("iterator"),E="keys",S="values",O="entries",k=function(){return this};t.exports=function(t,e,n,a,h,y,I){u(n,e,a);var P,C,A,M=function(t){if(t===h&&R)return R;if(!x&&t in j)return j[t];switch(t){case E:case S:case O:return function(){return new n(this,t)}}return function(){return new n(this)}},T=e+" Iterator",D=!1,j=t.prototype,N=j[w]||j["@@iterator"]||h&&j[h],R=!x&&N||M(h),L="Array"==e&&j.entries||N;if(L&&(P=c(L.call(new t)))!==Object.prototype&&P.next&&(i||c(P)===b||(l?l(P,b):s(P[w])||p(P,w,k)),f(P,T,!0,!0),i&&(v[T]=k)),g&&h==S&&N&&N.name!==S&&(!i&&m?d(j,"name",S):(D=!0,R=function(){return o(N,this)})),h)if(C={values:M(S),keys:y?R:M(E),entries:M(O)},I)for(A in C)(x||D||!(A in j))&&p(j,A,C[A]);else r({target:e,proto:!0,forced:x||D},C);return i&&!I||j[w]===R||p(j,w,R,{name:h}),v[e]=R,C}},3383:function(t,e,n){"use strict";var r,o,i,a=n(7293),s=n(614),u=n(111),c=n(30),l=n(9518),f=n(8052),d=n(5112),p=n(1913),h=d("iterator"),v=!1;[].keys&&("next"in(i=[].keys())?(o=l(l(i)))!==Object.prototype&&(r=o):v=!0),!u(r)||a((function(){var t={};return r[h].call(t)!==t}))?r={}:p&&(r=c(r)),s(r[h])||f(r,h,(function(){return this})),t.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:v}},7497:function(t){t.exports={}},6244:function(t,e,n){var r=n(7466);t.exports=function(t){return r(t.length)}},6339:function(t,e,n){var r=n(1702),o=n(7293),i=n(614),a=n(2597),s=n(9781),u=n(6530).CONFIGURABLE,c=n(2788),l=n(9909),f=l.enforce,d=l.get,p=String,h=Object.defineProperty,v=r("".slice),y=r("".replace),g=r([].join),m=s&&!o((function(){return 8!==h((function(){}),"length",{value:8}).length})),b=String(String).split("String"),x=t.exports=function(t,e,n){"Symbol("===v(p(e),0,7)&&(e="["+y(p(e),/^Symbol\(([^)]*)\)/,"$1")+"]"),n&&n.getter&&(e="get "+e),n&&n.setter&&(e="set "+e),(!a(t,"name")||u&&t.name!==e)&&(s?h(t,"name",{value:e,configurable:!0}):t.name=e),m&&n&&a(n,"arity")&&t.length!==n.arity&&h(t,"length",{value:n.arity});try{n&&a(n,"constructor")&&n.constructor?s&&h(t,"prototype",{writable:!1}):t.prototype&&(t.prototype=void 0)}catch(t){}var r=f(t);return a(r,"source")||(r.source=g(b,"string"==typeof e?e:"")),t};Function.prototype.toString=x((function(){return i(this)&&d(this).source||c(this)}),"toString")},4758:function(t){var e=Math.ceil,n=Math.floor;t.exports=Math.trunc||function(t){var r=+t;return(r>0?n:e)(r)}},3929:function(t,e,n){var r=n(7850),o=TypeError;t.exports=function(t){if(r(t))throw o("The method doesn't accept regular expressions");return t}},1574:function(t,e,n){"use strict";var r=n(9781),o=n(1702),i=n(6916),a=n(7293),s=n(1956),u=n(5181),c=n(5296),l=n(7908),f=n(8361),d=Object.assign,p=Object.defineProperty,h=o([].concat);t.exports=!d||a((function(){if(r&&1!==d({b:1},d(p({},"a",{enumerable:!0,get:function(){p(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var t={},e={},n=Symbol(),o="abcdefghijklmnopqrst";return t[n]=7,o.split("").forEach((function(t){e[t]=t})),7!=d({},t)[n]||s(d({},e)).join("")!=o}))?function(t,e){for(var n=l(t),o=arguments.length,a=1,d=u.f,p=c.f;o>a;)for(var v,y=f(arguments[a++]),g=d?h(s(y),d(y)):s(y),m=g.length,b=0;m>b;)v=g[b++],r&&!i(p,y,v)||(n[v]=y[v]);return n}:d},30:function(t,e,n){var r,o=n(9670),i=n(6048),a=n(748),s=n(3501),u=n(490),c=n(317),l=n(6200),f=l("IE_PROTO"),d=function(){},p=function(t){return" - \ No newline at end of file diff --git a/app/templates/deposit.html b/app/templates/deposit.html new file mode 100644 index 0000000..9334b3c --- /dev/null +++ b/app/templates/deposit.html @@ -0,0 +1,36 @@ +{% extends "baselayout.html" %} + +{% load i18n %} + +{% block title %} +{% translate "Drinks - Deposit" %} +{% endblock %} + +{% block headAdditional %} + + +{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

{% translate "Deposit" %}

+
+ {% translate "Amount" %} {{ currency_suffix }}: + + + +
+
+ +
+ + + +
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..89d58c6 --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,7 @@ +{% load i18n %} +
+ +
\ No newline at end of file diff --git a/application/app/templates/globalmessage.html b/app/templates/globalmessage.html similarity index 56% rename from application/app/templates/globalmessage.html rename to app/templates/globalmessage.html index 83fd733..9b33850 100644 --- a/application/app/templates/globalmessage.html +++ b/app/templates/globalmessage.html @@ -1,5 +1,5 @@ {% if global_message != "" %} -
+
{{ global_message }}
{% endif %} \ No newline at end of file diff --git a/app/templates/history.html b/app/templates/history.html new file mode 100644 index 0000000..89d689a --- /dev/null +++ b/app/templates/history.html @@ -0,0 +1,28 @@ +{% extends "baselayout.html" %} + +{% load i18n %} + +{% block title %} +{% translate "Drinks - History" %} +{% endblock %} + +{% block content %} +

{% translate "History" %}

+{% if history %} + + + + + + {% for h in history %} + + + + + {% endfor %} +
{% translate "last 30 actions" %}
{{ h.0 }}{{ h.1 }}
+{% else %} +{% translate "No history." %} +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..076af03 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,35 @@ +{% extends "baselayout.html" %} + +{% load i18n %} + +{% block title %} +{% translate "Drinks - Home" %} +{% endblock %} + +{% block content %} +

{% translate "Available Drinks" %}

+{% if available_drinks %} + +{% else %} +{% translate "No drinks available." %} +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/order.html b/app/templates/order.html new file mode 100644 index 0000000..2cae204 --- /dev/null +++ b/app/templates/order.html @@ -0,0 +1,74 @@ +{% extends "baselayout.html" %} + +{% load i18n %} +{% load l10n %} + +{% block title %} +{% translate "Drinks - Order" %} +{% endblock %} + +{% block content %} +
+{% if drink and drink.available > 0 and not drink.deleted %} +{% if user.balance > 0 or user.allow_order_with_negative_balance %} +
+ {% csrf_token %} +

{% translate "Order" %}

+
+ {% translate "Drink" %}: + {{ drink.product_name }} +
+
+ {% translate "Price per Item" %} ({{ currency_suffix }}): + + {{ drink.price }} + +
+ {% if not drink.do_not_count %} +
+ {% translate "Available" %}: + {{ drink.available }} +
+ {% endif %} +
+ {% translate "Sum" %} ({{ currency_suffix }}): + {{ drink.price }} +
+
+ {% translate "Count" %}: + + + {% if drink.do_not_count %} + + {% else %} + + {% endif %} + + +
+
+ + +
+ + +{% else %} +
+

{% translate "Your balance is too low to order a drink." %}

+ {% translate "back" %} +
+{% endif %} +{% else %} +
+

{% translate "This drink is not available." %}

+ {% translate "back" %} +
+{% endif %} + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/registration/logged_out.html b/app/templates/registration/logged_out.html new file mode 100644 index 0000000..c4d69fc --- /dev/null +++ b/app/templates/registration/logged_out.html @@ -0,0 +1,19 @@ +{% extends "baselayout.html" %} + +{% load i18n %} + +{% block title %} +{% translate "Drinks - Logged Out" %} +{% endblock %} + +{% block headAdditional %} + +{% endblock %} + +{% block content %} +
+ {% translate "Logged out! You will be redirected shortly." %} + {% translate "Click here if automatic redirection does not work." %} +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/registration/login.html b/app/templates/registration/login.html new file mode 100644 index 0000000..837dc70 --- /dev/null +++ b/app/templates/registration/login.html @@ -0,0 +1,60 @@ + +{% extends "baselayout.html" %} + +{% load i18n %} +{% load static %} + +{% block title %} +{% translate "Drinks - Login" %} +{% endblock %} + +{% block headAdditional %} + + +{% endblock %} + +{% block content %} +{% if error_message %} +

{{ error_message }}

+{% endif %} +
+
+

{% translate "Log in" %}

+
+ {% csrf_token %} + + +
+ + +
+
+
+ + {% get_current_language as LANGUAGE_CODE %} +
+ + +
+
+

{% translate "Choose your account" %}

+
    + {% for user_ in user_list %} +
  • + +
    + {% if user_.first_name %} + {% if user_.last_name %} + {{ user_.last_name }}, + {% endif %} + {{ user_.first_name }} + {% else %} + {{ user_.username }} + {% endif %} +
    +
  • + {% endfor %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/statistics.html b/app/templates/statistics.html new file mode 100644 index 0000000..4f938b9 --- /dev/null +++ b/app/templates/statistics.html @@ -0,0 +1,64 @@ +{% extends "baselayout.html" %} + +{% load i18n %} + +{% block title %} +{% translate "Drinks - Statistics" %} +{% endblock %} + +{% block content %} +

{% translate "Statistics" %}

+
+
+

{% translate "Orders per drink" %}

+ + + + + + + {% for key, values in orders_per_drink.items %} + + + + + + {% endfor %} +
{% translate "drink" %}{% translate "you" %}{% translate "all" %}
{{ key }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
+
+
+

{% translate "Orders per month (last 12 months)" %}

+ + + + + + + {% for key, values in orders_per_month.items %} + + + + + + {% endfor %} +
{% translate "month" %}{% translate "you" %}{% translate "all" %}
{{ key }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
+
+
+

{% translate "Orders per weekday" %}

+ + + + + + + {% for key, values in orders_per_weekday.items %} + + + + + + {% endfor %} +
{% translate "day" %}{% translate "you" %}{% translate "all" %}
{{ key }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/supply.html b/app/templates/supply.html new file mode 100644 index 0000000..f907f2d --- /dev/null +++ b/app/templates/supply.html @@ -0,0 +1,42 @@ +{% extends "baselayout.html" %} + +{% load i18n %} +{% load l10n %} + +{% block title %} +{% translate "Drinks - Supply" %} +{% endblock %} + +{% block content %} +{% if user.is_superuser or user.allowed_to_supply %} +
+ {% csrf_token %} +

{% translate "Supply" %}

+
+ {% translate "Description" %}: + + + +
+
+ {% translate "Price" %} ({{ currency_suffix }}): + + + +
+
+ +
+ + +{% else %} +
+

{% translate "You are not allowed to view this site." %}

+ {% translate "back" %} +
+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/application/app/templates/userpanel.html b/app/templates/userpanel.html similarity index 63% rename from application/app/templates/userpanel.html rename to app/templates/userpanel.html index 7a15a36..159dcf5 100644 --- a/application/app/templates/userpanel.html +++ b/app/templates/userpanel.html @@ -1,24 +1,24 @@ {% load i18n %} {% load static %} -
+
- + {% if user.first_name != "" %} - {% translate "User" %}: {{ user.first_name }} {{ user.last_name }} ({{ user.username }}) + {% translate "User" %}: {{ user.first_name }} {{ user.last_name }} ({{ user.username }}) {% else %} - {% translate "User" %}: {{ user.username }} + {% translate "User" %}: {{ user.username }} {% endif %}  -  {% if user.balance < 0.01 %} - {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} + {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} {% else %} - {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} + {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} {% endif %}
-
+
Home {% translate "Deposit" %} {% translate "Logout" %} @@ -30,13 +30,13 @@ {% translate "History" %} {% translate "Statistics" %} {% if user.is_superuser or user.is_staff %} - Admin Panel + Admin Panel {% endif %} {% if user.is_superuser or user.allowed_to_supply %} - {% translate "Supply" %} + {% translate "Supply" %} {% endif %} {% translate "Change Password" %}
-
+
\ No newline at end of file diff --git a/application/app/urls.py b/app/urls.py similarity index 91% rename from application/app/urls.py rename to app/urls.py index 70feadd..f6f8cc4 100644 --- a/application/app/urls.py +++ b/app/urls.py @@ -16,10 +16,8 @@ urlpatterns = [ path('accounts/password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'), path('accounts/password_change_done/', views.redirect_home, name='password_change_done'), path('admin/', adminSite.urls), - # custom-handled resources - path('profilepictures', views.profile_pictures), # API # path('api/order-drink', views.api_order_drink), path('api/deposit', views.api_deposit), path('api/supply', views.api_supply) -] \ No newline at end of file +] diff --git a/application/app/views.py b/app/views.py similarity index 81% rename from application/app/views.py rename to app/views.py index 33e4b49..7f3ea30 100644 --- a/application/app/views.py +++ b/app/views.py @@ -15,34 +15,22 @@ from django.http.response import HttpResponse from django.shortcuts import render from django.utils.translation import gettext as _ - from django.utils.formats import decimal -from . import sql_queries +from . import db_queries from .models import Drink from .models import Order from .models import RegisterTransaction -# - -profile_pictures_path = Path(settings.PROFILE_PICTURES).resolve() - -# login view - def login_page(request): - userlist = get_user_model().objects.filter(is_superuser=False).filter(is_active=True).order_by("username") - if request.method == "POST": - form = AuthenticationForm(request.POST) username = request.POST['username'] password = request.POST['password'] - user = authenticate(username=username,password=password) - if user: if user.is_active: login(request, user) @@ -53,22 +41,16 @@ def login_page(request): "user_list": userlist, "error_message": _("Invalid username or password.") }) - else: - if request.user.is_authenticated: return HttpResponseRedirect("/") - form = AuthenticationForm() - return render(request,'registration/login.html', { "form": form, "user_list": userlist }) -# actual application - @login_required def index(request): context = { @@ -76,91 +58,67 @@ def index(request): } return render(request, "index.html", context) + @login_required def history(request): context = { - "history": sql_queries.select_history(request.user, language_code=request.LANGUAGE_CODE), + "history": db_queries.select_history(request.user, language_code=request.LANGUAGE_CODE), } return render(request, "history.html", context) + @login_required def order(request, drinkid): try: drink_ = Drink.objects.get(pk=drinkid) - context = { - "drink": drink_ - } + context = {"drink": drink_} return render(request, "order.html", context) except Drink.DoesNotExist: return HttpResponseRedirect("/") + @login_required def deposit(request): return render(request, "deposit.html", {}) + @login_required def statistics(request): context = { - "yopml12m": sql_queries.select_yopml12m(request.user), - "aopml12m": sql_queries.select_aopml12m(), - "yopwd": sql_queries.select_yopwd(request.user), - "aopwd": sql_queries.select_aopwd(), - "noyopd": sql_queries.select_noyopd(request.user), - "noaopd": sql_queries.select_noaopd() + "orders_per_month": db_queries.orders_per_month(request.user), + "orders_per_weekday": db_queries.orders_per_weekday(request.user), + "orders_per_drink": db_queries.orders_per_drink(request.user), } return render(request, "statistics.html", context) + @login_required def supply(request): return render(request, "supply.html") + @login_required def redirect_home(request): return HttpResponseRedirect("/") -# Custom-Handled Resources - -def profile_pictures(request): - if not "name" in request.GET: - return HttpResponse(b"", status=400) - ppic_filepath = Path(profile_pictures_path / request.GET["name"]).resolve() - try: - ppic_filepath.relative_to(profile_pictures_path) - except: - return HttpResponse("No.", status=403) - if ppic_filepath.is_file(): - return FileResponse(ppic_filepath.open('rb')) - else: - return FileResponse(b"", status=404) - - # API for XHR requests # @login_required def api_order_drink(request): - # check request -> make order - user = request.user - try: - if user.allow_order_with_negative_balance or user.balance > 0: - drinkid = int(request.POST["drinkid"]) amount = int(request.POST["numberofdrinks"]) - drink = Drink.objects.get(pk=drinkid) - if ((drink.do_not_count and drink.available > 0) or (drink.available >= amount)) and not drink.deleted: Order.objects.create(drink=drink, user=user, amount=amount) return HttpResponse("success", status=200) else: return HttpResponse("notAvailable", status=400) - else: raise Exception("Unexpected input or missing privileges.") - except Exception as e: print(f"An exception occured while processing an order: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) @@ -168,43 +126,31 @@ def api_order_drink(request): @login_required def api_deposit(request): - # check request -> deposit - user = request.user - try: - amount = decimal.Decimal(request.POST["depositamount"]) - if 0.00 < amount < 9999.99: # create transaction RegisterTransaction.objects.create( transaction_sum=amount, comment=f"User deposit by user {user.username}", is_user_deposit=True, - user=user - ) - # + user=user) return HttpResponse("success", status=200) else: raise Exception("Deposit amount too big or small.") - except Exception as e: print(f"An exception occured while processing a transaction: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) + @login_required def api_supply(request): - # check request -> supply - user = request.user - try: - price = decimal.Decimal(request.POST["supplyprice"]) description = str(request.POST["supplydescription"]) - if 0.00 < price < 9999.99 and (user.allowed_to_supply or user.is_superuser): # create transaction RegisterTransaction.objects.create( @@ -213,10 +159,8 @@ def api_supply(request): is_user_deposit=False, user=user ) - # return HttpResponse("success", status=200) else: raise Exception("Unexpected input or missing privileges.") - except Exception as e: print(f"An exception occured while processing a supply transaction: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) diff --git a/application/app/apps.py b/application/app/apps.py deleted file mode 100644 index e61ab8c..0000000 --- a/application/app/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.contrib.admin.apps import AdminConfig - - -class DAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'app' diff --git a/application/app/middleware.py b/application/app/middleware.py deleted file mode 100644 index 43acf6c..0000000 --- a/application/app/middleware.py +++ /dev/null @@ -1,11 +0,0 @@ - -# Define CSP middleware: - -def csp_middleware(get_response): - - def middleware(request): - response = get_response(request) - response["content-security-policy"] = "default-src 'self'" - return response - - return middleware diff --git a/application/app/sql_queries.py b/application/app/sql_queries.py deleted file mode 100644 index 0448708..0000000 --- a/application/app/sql_queries.py +++ /dev/null @@ -1,137 +0,0 @@ -#from datetime import datetime - -from django.conf import settings -from django.db import connection - - -def _select_from_db(sql_select:str): - result = None - with connection.cursor() as cursor: - cursor.execute(sql_select) - result = cursor.fetchall() - return result - - -def select_history(user, language_code="en") -> list: - # select order history and deposits - user_id = user.pk - result = _select_from_db(f""" - select - concat( - product_name, ' (', - content_litres::real, -- converting to real removes trailing zeros - 'l) x ', amount, ' - ', price_sum, '{settings.CURRENCY_SUFFIX}') as "text", - datetime - from app_order - where user_id = {user_id} - - union - - select - concat('Deposit: +', transaction_sum, '{settings.CURRENCY_SUFFIX}') as "text", - datetime - from app_userdeposits_view - where user_id = {user_id} - - order by datetime desc - fetch first 30 rows only; - """) - result = [list(row) for row in result] - if language_code == "de": # reformat for german translation - for row in result: - row[0] = row[0].replace(".", ",") - return result - - -def select_yopml12m(user) -> list: - # number of orders per month (last 12 months) - # only for the specified user - user_id = user.pk - result = _select_from_db(f""" - -- select the count of the orders per month (last 12 days) - select - to_char(date_trunc('month', datetime), 'YYYY-MM') as "month", - sum(amount) as "count" - from app_order - where user_id = {user_id} - and date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval) - group by "month" - order by "month" desc; - """) - return [list(row) for row in result] - -def select_aopml12m() -> list: - # number of orders per month (last 12 months) - result = _select_from_db(f""" - -- select the count of the orders per month (last 12 days) - select - to_char(date_trunc('month', datetime), 'YYYY-MM') as "month", - sum(amount) as "count" - from app_order - where date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval) - group by "month" - order by "month" desc; - """) - return [list(row) for row in result] - - -def select_yopwd(user) -> list: - # number of orders per weekday (all time) - # only for the specified user - user_id = user.pk - result = _select_from_db(f""" - -- select the count of the orders per weekday (all time) - select - to_char(datetime, 'Day') as "day", - sum(amount) as "count" - from app_order - where user_id = {user_id} - group by "day" - order by "count" desc; - """) - return [list(row) for row in result] - return [] - -def select_aopwd() -> list: - # number of orders per weekday (all time) - result = _select_from_db(f""" - -- select the count of the orders per weekday (all time) - select - to_char(datetime, 'Day') as "day", - sum(amount) as "count" - from app_order - group by "day" - order by "count" desc; - """) - return [list(row) for row in result] - return [] - - -def select_noyopd(user) -> list: - # number of orders per drink (all time) - # only for specified user - user_id = user.pk - result = _select_from_db(f""" - select - d.product_name as "label", - sum(o.amount) as "data" - from app_drink d - join app_order o on (d.id = o.drink_id) - where o.user_id = {user_id} - group by d.product_name - order by "data" desc; - """) - return [list(row) for row in result] - -def select_noaopd() -> list: - # number of orders per drink (all time) - result = _select_from_db(f""" - select - d.product_name as "label", - sum(o.amount) as "data" - from app_drink d - join app_order o on (d.id = o.drink_id) - group by d.product_name - order by "data" desc; - """) - return [list(row) for row in result] \ No newline at end of file diff --git a/application/app/templates/deposit.html b/application/app/templates/deposit.html deleted file mode 100644 index cec4ef2..0000000 --- a/application/app/templates/deposit.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} - {% translate "Drinks - Deposit" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - - -{% block content %} - -
- {% csrf_token %} - -

{% translate "Deposit" %}

- -
- {% translate "Amount" %} {{ currency_suffix }}: - - - -
- -
- - - -
- - - - -{% endblock %} diff --git a/application/app/templates/footer.html b/application/app/templates/footer.html deleted file mode 100644 index fbfe674..0000000 --- a/application/app/templates/footer.html +++ /dev/null @@ -1,6 +0,0 @@ -{% load i18n %} - - diff --git a/application/app/templates/history.html b/application/app/templates/history.html deleted file mode 100644 index 7abd1f3..0000000 --- a/application/app/templates/history.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} - {% translate "Drinks - History" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - - -{% block content %} - -

{% translate "History" %}

- - {% if history %} - - - - - - {% for h in history %} - - - - - {% endfor %} -
{% translate "last 30 actions" %}
{{ h.0 }}{{ h.1 }}
- {% else %} - {% translate "No history." %} - {% endif %} - - - -{% endblock %} diff --git a/application/app/templates/index.html b/application/app/templates/index.html deleted file mode 100644 index 3756b0b..0000000 --- a/application/app/templates/index.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} - {% translate "Drinks - Home" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - -{% block content %} - -

{% translate "Available Drinks" %}

- - {% if available_drinks %} - - - - {% else %} - - {% translate "No drinks available." %} - - {% endif %} - - - -{% endblock %} diff --git a/application/app/templates/order.html b/application/app/templates/order.html deleted file mode 100644 index d720197..0000000 --- a/application/app/templates/order.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} -{% load l10n %} - -{% block title %} -{% translate "Drinks - Order" %} -{% endblock %} - -{% block headAdditional %} - - -{% endblock %} - - -{% block content %} - - {% if drink and drink.available > 0 and not drink.deleted %} - - {% if user.balance > 0 or user.allow_order_with_negative_balance %} - -
- {% csrf_token %} - -

{% translate "Order" %}

- -
- {% translate "Drink" %}: - {{ drink.product_name }} -
-
- {% translate "Price per Item" %} ({{ currency_suffix }}): - - {{ drink.price }} - -
- - {% if not drink.do_not_count %} -
- {% translate "Available" %}: - {{ drink.available }} -
- {% endif %} - -
- {% translate "Sum" %} ({{ currency_suffix }}): - {{ drink.price }} -
- -
- {% translate "Count" %}: - - - {% if drink.do_not_count %} - - {% else %} - - {% endif %} - - -
- -
- - - - - -
- - - - - - {% else %} - -
-

{% translate "Your balance is too low to order a drink." %}

- {% translate "back" %} -
- - {% endif %} - - {% else %} - -
-

{% translate "This drink is not available." %}

- {% translate "back" %} -
- - {% endif %} - - - -{% endblock %} diff --git a/application/app/templates/registration/logged_out.html b/application/app/templates/registration/logged_out.html deleted file mode 100644 index 38bb024..0000000 --- a/application/app/templates/registration/logged_out.html +++ /dev/null @@ -1,24 +0,0 @@ - -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} - {% translate "Drinks - Logged Out" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - -{% block content %} - -
- {% translate "Logged out! You will be redirected shortly." %} -

- {% translate "Click here if automatic redirection does not work." %} -
- - - -{% endblock %} diff --git a/application/app/templates/registration/login.html b/application/app/templates/registration/login.html deleted file mode 100644 index 4323c9e..0000000 --- a/application/app/templates/registration/login.html +++ /dev/null @@ -1,93 +0,0 @@ - -{% extends "baselayout.html" %} - -{% load i18n %} -{% load static %} - -{% block title %} - {% translate "Drinks - Login" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - -{% block content %} - - {% if error_message %} -

{{ error_message }}

- {% endif %} - -
- -
- -
- {% csrf_token %} -

{% translate "Log in" %}

- - - -
- - - - - - - - - - - - - - - - - - - - - -
-
- -
- - -
- -
- -
-
- -

{% translate "Choose your account" %}

- -
-
    - {% for user_ in user_list %} -
  • - -
    - {% if user_.first_name %} - - {% if user_.last_name %} - {{ user_.last_name }}, - {% endif %} - - {{ user_.first_name }} - - {% else %} - {{ user_.username }} - {% endif %} -
    -
  • - {% endfor %} -
-
- - - -{% endblock %} diff --git a/application/app/templates/statistics.html b/application/app/templates/statistics.html deleted file mode 100644 index 662e7d4..0000000 --- a/application/app/templates/statistics.html +++ /dev/null @@ -1,148 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} - {% translate "Drinks - Statistics" %} -{% endblock %} - -{% block headAdditional %} - -{% endblock %} - - -{% block content %} - -

{% translate "Statistics" %}

- -
- -
- -
-

{% translate "Your orders per drink" %}

- {% if noyopd %} - - - - - - {% for row in noyopd %} - - - - - {% endfor %} -
{% translate "drink" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
-

{% translate "All orders per drink" %}

- {% if noaopd %} - - - - - - {% for row in noaopd %} - - - - - {% endfor %} -
{% translate "drink" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
-

{% translate "Your orders per month (last 12 months)" %}

- {% if yopml12m %} - - - - - - {% for row in yopml12m %} - - - - - {% endfor %} -
{% translate "month" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
-

{% translate "All orders per month (last 12 months)" %}

- {% if aopml12m %} - - - - - - {% for row in aopml12m %} - - - - - {% endfor %} -
{% translate "month" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
-

{% translate "Your orders per weekday" %}

- {% if yopwd %} - - - - - - {% for row in yopwd %} - - - - - {% endfor %} -
{% translate "day" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
-

{% translate "All orders per weekday" %}

- {% if aopwd %} - - - - - - {% for row in aopwd %} - - - - - {% endfor %} -
{% translate "day" %}{% translate "count" %}
{{ row.0 }}{{ row.1 }}
- {% else %} -
{% translate "No history." %}
- {% endif %} -
- -
- -
- - - -{% endblock %} diff --git a/application/app/templates/supply.html b/application/app/templates/supply.html deleted file mode 100644 index fabb224..0000000 --- a/application/app/templates/supply.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} -{% load l10n %} - -{% block title %} -{% translate "Drinks - Supply" %} -{% endblock %} - -{% block headAdditional %} - - -{% endblock %} - - -{% block content %} - - {% if user.is_superuser or user.allowed_to_supply %} - -
- {% csrf_token %} - -

{% translate "Supply" %}

- -
- {% translate "Description" %}: - - - -
- -
- {% translate "Price" %} ({{ currency_suffix }}): - - - -
- -
- - - -
- - - - - {% else %} - -
-

{% translate "You are not allowed to view this site." %}

- {% translate "back" %} -
- - {% endif %} - - - -{% endblock %} diff --git a/application/app/tests.py b/application/app/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/application/app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/application/drinks_manager/settings.py b/application/drinks_manager/settings.py deleted file mode 100644 index d01503f..0000000 --- a/application/drinks_manager/settings.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -Django settings for drinks_manager project. - -Generated by 'django-admin startproject' using Django 3.2.5. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - -import os - -from pathlib import Path - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key secret! -django_secret_key_absolute_fp = os.environ["DJANGO_SK_ABS_FP"] -with open(django_secret_key_absolute_fp) as secret_key_file: - SECRET_KEY = secret_key_file.read().strip() - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = (os.environ["DJANGO_DEBUG"].lower() == "true") - - -ALLOWED_HOSTS = [ - "*" -] - - -### ----------------- ### - - -# Application definition - -INSTALLED_APPS = [ - "app.apps.DAppConfig", - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - "django.middleware.locale.LocaleMiddleware", - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - "django_currentuser.middleware.ThreadLocalUserMiddleware", - "app.middleware.csp_middleware" -] - -ROOT_URLCONF = 'drinks_manager.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - "app.context_processors.app_version" - ], - }, - }, -] - -WSGI_APPLICATION = 'drinks_manager.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': os.environ["PGDB_DB"], - 'USER': os.environ["PGDB_USER"], - 'PASSWORD': os.environ["PGDB_PASSWORD"], - 'HOST': os.environ["PGDB_HOST"], - 'PORT': str(os.environ["PGDB_PORT"]) - } -} - -CONN_MAX_AGE = 20 # keep database connections alive for n seconds - - -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - -if os.environ["DJANGO_ENABLE_PASSWORD_VALIDATION"].lower() == "true": - AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, - ] -else: - AUTH_PASSWORD_VALIDATORS = [] - - -AUTH_USER_MODEL = "app.User" - -# user will be logged out after x seconds -SESSION_COOKIE_AGE = int(os.environ["DJANGO_SESSION_COOKIE_AGE"]) - - -# more security settings - -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_SECURE = True - - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ - -LANGUAGE_CODE = os.environ["DJANGO_LANGUAGE_CODE"] # this is the default and fallback language (currently only de-de and en-us supported) - -TIME_ZONE = os.environ["DJANGO_TIME_ZONE"] - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -LOCALE_PATHS = [ - BASE_DIR / "locale" -] - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.environ["STATIC_FILES"] - - -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# - -APP_VERSION = os.environ["APP_VERSION"] - -try: - CURRENCY_SUFFIX = os.environ["CURRENCY_SUFFIX"] -except KeyError: - CURRENCY_SUFFIX = "$" - -PROFILE_PICTURES = os.environ["PROFILE_PICTURES"] diff --git a/application/locale/de/LC_MESSAGES/django.mo b/application/locale/de/LC_MESSAGES/django.mo deleted file mode 100644 index 1f4683e..0000000 Binary files a/application/locale/de/LC_MESSAGES/django.mo and /dev/null differ diff --git a/application/locale/de/LC_MESSAGES/django.po b/application/locale/de/LC_MESSAGES/django.po deleted file mode 100644 index efc8657..0000000 --- a/application/locale/de/LC_MESSAGES/django.po +++ /dev/null @@ -1,282 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-15 19:20+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Julian Müller (W13R)\n" -"Language: DE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: application/app/templates/admin/base_site.html:7 -msgid "Django site admin" -msgstr "Django Administrator" - -#: application/app/templates/admin/base_site.html:15 -msgid "Django administration" -msgstr "Django Administration" - -#: application/app/templates/baselayout.html:41 -msgid "An error occured. Please log out and log in again." -msgstr "Ein Fehler ist aufgetreten. Bitte ab- und wieder anmelden." - -#: application/app/templates/deposit.html:6 -msgid "Drinks - Deposit" -msgstr "Getränke - Einzahlen" - -#: application/app/templates/deposit.html:14 -#: application/app/templates/userpanel.html:23 -msgid "Deposit" -msgstr "Einzahlen" - -#: application/app/templates/deposit.html:23 -msgid "Amount" -msgstr "Summe" - -#: application/app/templates/deposit.html:31 -#: application/app/templates/order.html:72 -#: application/app/templates/registration/login.html:57 -#: application/app/templates/supply.html:41 -msgid "cancel" -msgstr "Abbrechen" - -#: application/app/templates/deposit.html:32 -msgid "confirm" -msgstr "Bestätigen" - -#: application/app/templates/history.html:6 -msgid "Drinks - History" -msgstr "Getränke - Verlauf" - -#: application/app/templates/history.html:14 -#: application/app/templates/userpanel.html:30 -msgid "History" -msgstr "Verlauf" - -#: application/app/templates/history.html:22 -msgid "last 30 actions" -msgstr "letzte 30 Vorgänge" - -#: application/app/templates/history.html:33 -#: application/app/templates/statistics.html:41 -#: application/app/templates/statistics.html:61 -#: application/app/templates/statistics.html:81 -#: application/app/templates/statistics.html:101 -#: application/app/templates/statistics.html:121 -#: application/app/templates/statistics.html:141 -msgid "No history." -msgstr "Kein Verlauf verfügbar." - -#: application/app/templates/index.html:6 -msgid "Drinks - Home" -msgstr "Getränke - Home" - -#: application/app/templates/index.html:14 -msgid "Available Drinks" -msgstr "Verfügbare Getränke" - -#: application/app/templates/index.html:27 -#: application/app/templates/index.html:34 -msgid "available" -msgstr "verfügbar" - -#: application/app/templates/index.html:43 -msgid "No drinks available." -msgstr "Es sind gerade keine Getränke verfügbar." - -#: application/app/templates/order.html:7 -msgid "Drinks - Order" -msgstr "Getränke - Bestellen" - -#: application/app/templates/order.html:16 -#: packages/django/forms/formsets.py:405 packages/django/forms/formsets.py:412 -msgid "Order" -msgstr "Bestellung" - -#: application/app/templates/order.html:29 -msgid "Drink" -msgstr "Getränk" - -#: application/app/templates/order.html:34 -msgid "Price per Item" -msgstr "Preis pro Getränk" - -#: application/app/templates/order.html:40 -msgid "Available" -msgstr "Verfügbar" - -#: application/app/templates/order.html:46 -msgid "Count" -msgstr "Anzahl" - -#: application/app/templates/order.html:63 -msgid "Sum" -msgstr "Summe" - -#: application/app/templates/order.html:73 -msgid "order" -msgstr "Bestellen" - -#: application/app/templates/order.html:85 -msgid "Your balance is too low to order a drink." -msgstr "Dein Saldo ist zu niedrig um Getränke zu bestellen." - -#: application/app/templates/order.html:86 -#: application/app/templates/order.html:95 -#: application/app/templates/supply.html:54 -msgid "back" -msgstr "zurück" - -#: application/app/templates/order.html:94 -msgid "This drink is not available." -msgstr "Dieses Getränk ist gerade nicht verfügbar." - -#: application/app/templates/registration/logged_out.html:7 -msgid "Drinks - Logged Out" -msgstr "Getränke - Abgemeldet" - -#: application/app/templates/registration/logged_out.html:17 -msgid "Logged out! You will be redirected shortly." -msgstr "Du wurdest abgemeldet und wirst in Kürze weitergeleitet." - -#: application/app/templates/registration/logged_out.html:19 -msgid "Click here if automatic redirection does not work." -msgstr "" -"Bitte klicke hier, wenn die automatische Weiterleitung nicht funktioniert." - -#: application/app/templates/registration/login.html:8 -msgid "Drinks - Login" -msgstr "Getränke - Anmeldung" - -#: application/app/templates/registration/login.html:27 -msgid "Log in" -msgstr "Anmelden" - -#: application/app/templates/registration/login.html:29 -msgid "Password/PIN" -msgstr "Passwort/PIN" - -#: application/app/templates/registration/login.html:58 -msgid "login" -msgstr "Anmelden" - -#: application/app/templates/registration/login.html:66 -msgid "Choose your account" -msgstr "Wähle deinen Account" - -#: application/app/templates/statistics.html:6 -msgid "Drinks - Statistics" -msgstr "Getränke - Statistiken" - -#: application/app/templates/statistics.html:15 -#: application/app/templates/userpanel.html:31 -msgid "Statistics" -msgstr "Statistiken" - -#: application/app/templates/statistics.html:26 -msgid "Your orders per drink" -msgstr "Deine Bestellungen pro Getränk" - -#: application/app/templates/statistics.html:30 -#: application/app/templates/statistics.html:50 -msgid "drink" -msgstr "Getränk" - -#: application/app/templates/statistics.html:31 -#: application/app/templates/statistics.html:51 -#: application/app/templates/statistics.html:71 -#: application/app/templates/statistics.html:91 -#: application/app/templates/statistics.html:111 -#: application/app/templates/statistics.html:131 -msgid "count" -msgstr "Anzahl" - -#: application/app/templates/statistics.html:46 -msgid "All orders per drink" -msgstr "Alle Bestellungen pro Getränk" - -#: application/app/templates/statistics.html:66 -msgid "Your orders per month (last 12 months)" -msgstr "Deine Bestellungen pro Monat (letzte 12 Monate)" - -#: application/app/templates/statistics.html:70 -#: application/app/templates/statistics.html:90 -msgid "month" -msgstr "Monat" - -#: application/app/templates/statistics.html:86 -msgid "All orders per month (last 12 months)" -msgstr "Alle Bestellungen pro Monat (letzte 12 Monate)" - -#: application/app/templates/statistics.html:106 -msgid "Your orders per weekday" -msgstr "Deine Bestellungen pro Wochentag" - -#: application/app/templates/statistics.html:110 -#: application/app/templates/statistics.html:130 -msgid "day" -msgstr "Tag" - -#: application/app/templates/statistics.html:126 -msgid "All orders per weekday" -msgstr "Alle Bestellungen pro Wochentag" - -#: application/app/templates/supply.html:7 -msgid "Drinks - Supply" -msgstr "Getränke - Beschaffung" - -#: application/app/templates/supply.html:16 -#: application/app/templates/userpanel.html:36 -msgid "Supply" -msgstr "Beschaffung" - -#: application/app/templates/supply.html:27 -msgid "Description" -msgstr "Beschreibung" - -#: application/app/templates/supply.html:32 -msgid "Price" -msgstr "Preis" - -#: application/app/templates/supply.html:42 -msgid "submit" -msgstr "Senden" - -#: application/app/templates/supply.html:53 -msgid "You are not allowed to view this site." -msgstr "Dir fehlt die Berechtigung, diese Seite anzuzeigen." - -#: application/app/templates/userpanel.html:9 -#: application/app/templates/userpanel.html:11 -msgid "User" -msgstr "Benutzer" - -#: application/app/templates/userpanel.html:15 -#: application/app/templates/userpanel.html:17 -msgid "Balance" -msgstr "Saldo" - -#: application/app/templates/userpanel.html:24 -msgid "Logout" -msgstr "Abmelden" - -#: application/app/templates/userpanel.html:27 -msgid "Account" -msgstr "Account" - -#: application/app/templates/userpanel.html:38 -msgid "Change Password" -msgstr "Passwort ändern" - -#: application/app/views.py:47 -msgid "Invalid username or password." -msgstr "Benutzername oder Passwort ungültig." diff --git a/config/Caddyfile b/config/Caddyfile deleted file mode 100644 index e5fe817..0000000 --- a/config/Caddyfile +++ /dev/null @@ -1,39 +0,0 @@ -{ - # disable admin backend - admin off - # define the ports by the environment variables - http_port {$HTTP_PORT} - https_port {$HTTPS_PORT} -} - -https:// { - # the tls certificates - tls ./config/tls/server.pem ./config/tls/server-key.pem - route { - # static files - file_server /static/* { - root {$STATIC_FILES}/.. - } - # favicon - redir /favicon.ico /static/favicon.ico - # reverse proxy to the (django) application - reverse_proxy localhost:{$DJANGO_PORT} - } - # use compression - encode gzip - # logging - log { - output file {$CADDY_ACCESS_LOG} - format filter { - wrap console - fields { - common_log delete - request>headers delete - request>tls delete - user_id delete - resp_headers delete - } - } - level INFO - } -} diff --git a/config/config.sample.sh b/config/config.sample.sh deleted file mode 100644 index 51aa8f0..0000000 --- a/config/config.sample.sh +++ /dev/null @@ -1,31 +0,0 @@ -# environment variables - -export HTTP_PORT=80 # required by caddy, will be redirected to https -export HTTPS_PORT=443 # actual port for the webinterface - -export DJANGO_PORT=8001 # caddy's http port (should be blocked by the firewall) - -export DJANGO_SESSION_COOKIE_AGE=600 # auto-logout, in seconds -export SESSION_CLEAR_INTERVAL=120 # interval for automatic session clearing, in minutes - -export DJANGO_LANGUAGE_CODE="en" # the default and fallback language, currently only de and en are supported. -export DJANGO_TIME_ZONE="CET" # your timezone - -export CURRENCY_SUFFIX="$" # if you have another currency symbol, you can specify it here - -# Do you want to enable password validation? -# (numeric PINs as Password will not be seen as valid) -export DJANGO_ENABLE_PASSWORD_VALIDATION="true" - -# database connection (postgresql) -export PGDB_DB="" # The name of the database -export PGDB_USER="" # The database user -export PGDB_PASSWORD='' # The password for the database user -export PGDB_HOST="127.0.0.1" # The hostname of your database -export PGDB_PORT=5432 # The port your database is listening on - -# log files -# only change if you know what you are doing -export CADDY_ACCESS_LOG="$(pwd)/logs/http-access.log" -export CADDY_LOG="$(pwd)/logs/caddy.log" -export APPLICATION_LOG="$(pwd)/logs/application.log" diff --git a/data/Caddyfile b/data/Caddyfile new file mode 100644 index 0000000..c5bbc58 --- /dev/null +++ b/data/Caddyfile @@ -0,0 +1,49 @@ +{ + # disable unwanted stuff + admin off + skip_install_trust + # define the ports by the environment variables + http_port {$HTTP_PORT} + https_port {$HTTPS_PORT} +} + +{$CADDY_HOSTS} { + # the tls certificates + tls {$DATADIR}/tls/server.pem {$DATADIR}/tls/server-key.pem + route { + # profile pictures + file_server /profilepictures/* { + root {$DATADIR}/profilepictures/.. + } + # static files + file_server /static/* { + root {$DATADIR}/static/.. + } + # favicon + redir /favicon.ico /static/favicon.ico + # reverse proxy to the (django) application + reverse_proxy localhost:{$APPLICATION_PORT} + # set additional security headers + header Content-Security-Policy "default-src 'self'" + } + # use compression + encode gzip + # logging + log { + output file {$ACCESS_LOG} + format filter { + wrap json { + time_format rfc3339 + } + fields { + request>headers delete + request>tls delete + request>remote_ip hash + request>remote_port delete + user_id delete + resp_headers delete + } + } + level INFO + } +} diff --git a/archive/.gitkeep b/data/archive/.gitkeep similarity index 100% rename from archive/.gitkeep rename to data/archive/.gitkeep diff --git a/data/config.example.yml b/data/config.example.yml new file mode 100644 index 0000000..0586bcf --- /dev/null +++ b/data/config.example.yml @@ -0,0 +1,40 @@ +--- +app: + # The secret key, used for security protections + # This MUST be a secret, very long (40+ characters), random string + secret_key: "!!!insert random data!!!" + # The port for the asgi application + # This should be blocked by the firewall + application_port: 8001 + # Used for auto-logout, in seconds + session_cookie_age: 600 + # Interval for automatic session clearing, in minutes + session_clear_interval: 120 + # The default and fallback language, currently only de and en are supported. + language_code: "en" + # Your timezone + timezone: "CET" + # Specify the suffix for your currency + currency_suffix: "$" + # Enable/Disable password validation + # (numeric PINs are NOT valid when this is set to true) + password_validation: true +db: + # Database configuration + database: "drinks" + user: "drinks" + password: "insert password" + host: "127.0.0.1" + port: 5432 +caddy: + # Webserver settings + hosts: + - "localhost" + - "127.0.0.1" + http_port: 80 + https_port: 443 +logs: + # Logfile paths + caddy: "./data/logs/caddy.log" + http_access: "./data/logs/http-access.log" + application: "./data/logs/application.log" diff --git a/config/tls/.gitkeep b/data/logs/.gitkeep similarity index 100% rename from config/tls/.gitkeep rename to data/logs/.gitkeep diff --git a/profilepictures/default.svg b/data/profilepictures/default.svg similarity index 100% rename from profilepictures/default.svg rename to data/profilepictures/default.svg diff --git a/logs/.gitkeep b/data/tls/.gitkeep similarity index 100% rename from logs/.gitkeep rename to data/tls/.gitkeep diff --git a/docs/Commands.md b/docs/Commands.md deleted file mode 100644 index b47bbc3..0000000 --- a/docs/Commands.md +++ /dev/null @@ -1,81 +0,0 @@ -# Commands - -You run a command with - -``` -./run.sh -``` - -## Available Commands - ---- - -### `server` -This starts the application (a caddy instance, uvicorn with the Django application and a scheduler that automatically removes expired session data). -Log files will be written. - ---- - -### `setup` -This sets up some database tables, views, and more, generates a secret key for the application and lets you create an admin user. - ---- - -### `create-admin` -Lets you create an admin user - ---- - -### `generate-secret-key` -Generate a new random secret key for Django. -This will overwrite the old one. -Warning: After running this, current sessions will be invalid, and the users have to relogin. Don't run this command while the server is running. - ---- - -### `clear-sessions` -manually remove all expired sessions from the database - ---- - -### `force-db-upgrade` -force a database migration and -upgrade. -This is mainly used in development. - ---- - -### `archive-tables` -archive (copy & delete) all entries in app_order and app_registertransaction. -Use this to archive old orders or transactions (e.g. when the database gets too big). - ---- - -### `development-server` -This starts a caddy instance, the Django development server with DEBUGGING enabled and the session-clear-scheduler. -Only the HTTP-Access-Log will be written to its logfile, other logs will be written to the console. - ---- - -### `shell` - -Start a Django shell. - ---- - -### `help` -Show a help text - ---- - - -## Examples - -Run the production server: -``` -./run.sh server -``` - -Create a new admin: -``` -./run.sh create-admin -``` \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md deleted file mode 100644 index 00d5933..0000000 --- a/docs/Configuration.md +++ /dev/null @@ -1,14 +0,0 @@ -# Configuration - -## Main Configuration - -`config/config.sh` - -There is a sample configuration with explanations: [/config/config.sample.sh](/config/config.sample.sh) - - -## Caddy (Reverse Proxy & Static File Server) - -[config/Caddyfile](/config/Caddyfile) - -The default configuration should work out of the box, don't edit this file unless you know what you're doing. diff --git a/docs/Setup.md b/docs/Setup.md deleted file mode 100644 index bf3d0c4..0000000 --- a/docs/Setup.md +++ /dev/null @@ -1,110 +0,0 @@ -# Setup - -## I. Dependencies - -Before the actual setup, you have to satisfy the following dependencies: - - -### System - -- `pg_config` - - Ubuntu: `libpq-dev` - - Fedora/RHEL: `libpq-devel` -- `Caddy` 2.4.3+ (HTTP Reverse Proxy & Static File Server) -- `gcc`, `gettext` -- `Python` 3.9+ with pip - - `Python` header files - - Fedora/RHEL: `python3-devel` - - Ubuntu: `python3-dev` - - -### Python Packages (pip) - -All required python packages are listed in [requirements.txt](/requirements.txt) - -You can install the required python packages with -```bash -./install-pip-dependencies.sh -``` - -## II.A Installation - -You can get the latest version with git: - -``` -git clone --branch release-x https://gitlab.com/W13R/drinks-manager.git -``` -(replace x with the latest version) - -Alternatively, you can download the [latest release](https://gitlab.com/W13R/drinks-manager/-/releases) and extract the files to your prefered destination. - -**Warning:** - -Make shure that you set the correct file permissions, especially for the config files !! - -The following should be sufficient: - -```bash -chmod -R u+rw,g+r,g-w,o-rwx -``` - - -## II.B Update - -If you installed the application with git, you can run the following in the drinks-manager directory to update to the new version: - -``` -git fetch -git checkout x -``` -(replace x with the new version) - -If you downloaded the application from the releases page, you can download the new release in the same manner, and overwrite the old files with the new ones. - -You have to restart the application server to apply the changes. -WARNING: The auto-upgrade mechanism may expect you to input information. Therefore, you should start the application from the command-line the first time after an update. - -Further upgrading-instructions may be provided in the Release Notes on the Releases Page of this Project (Deployments -> Releases). - - -## III. Database - -This project is using PostgreSQL. You have to set up a database: - -```sql -create user drinksmanager password ''; -create database drinksmgr owner drinksmanager; -``` - -After creating the user, you have to edit your `pg_hba.conf` (see https://www.postgresql.org/docs/current/auth-pg-hba-conf.html). -Add the following line: -``` -host drinksmgr drinksmanager 127.0.0.1/32 md5 -``` - -Now you can configure your database connection in `config/config.sh`. - - -## IV. HTTPS & TLS Certificates - -A TLS/SSL certificate and key is required. -Filepaths: - -- `config/tls/server.pem` for the certificate -- `config/tls/server-key.pem` for the key - -You can set another filepath for those files in your caddy configuration at [config/Caddyfile](/config/Caddyfile). - - -## V. Configuration - -see [Configuration](Configuration.md) - - -## VI. Run Setup Command - -run `./run.sh setup` - -This will automatically set up database tables, views and entries, set up Django and let you create a admin user. - -After this, start the server with `./run.sh server` and navigate to `https://your.ip.add.ress:port/admin/`. diff --git a/install-pip-dependencies.sh b/install-pip-dependencies.sh deleted file mode 100755 index 6c02848..0000000 --- a/install-pip-dependencies.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# install the required python packages - -wd=$(dirname $0) - -pip3 install -r "$wd/requirements.txt" -t "$wd/packages" diff --git a/lib/activate-devel-env.sh b/lib/activate-devel-env.sh deleted file mode 100755 index 3467721..0000000 --- a/lib/activate-devel-env.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source ./lib/env.sh -source ./config/config.sh -export DJANGO_DEBUG=true -export PYTHONPATH="./packages" diff --git a/lib/archive-tables.py b/lib/archive-tables.py deleted file mode 100644 index a1901cc..0000000 --- a/lib/archive-tables.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import os, sys - -from datetime import datetime -from pathlib import Path - -from psycopg2 import connect - - -# archive (copy & delete) all entries in app_order and app_registertransaction - -timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") -archive_folder = Path("./archive") -orders_archive_path = archive_folder / ("orders-archive-" + timestamp + ".csv") -transactions_archive_path = archive_folder / ("transactions-archive-" + timestamp + ".csv") - - -if __name__ == "__main__": - exit_code = 1 - connection = connect( - user = os.environ["PGDB_USER"], - password = os.environ["PGDB_PASSWORD"], - host = os.environ["PGDB_HOST"], - port = os.environ["PGDB_PORT"], - database = os.environ["PGDB_DB"] - ) - cur = connection.cursor() - try: - print(f"Starting archiving to {orders_archive_path.__str__()} and {transactions_archive_path.__str__()}...") - # # # # # - # copy - with orders_archive_path.open("w") as of: - cur.copy_expert( - "copy (select * from app_order) to STDOUT with csv delimiter ';'", - of - ) - with transactions_archive_path.open("w") as tf: - cur.copy_expert( - "copy (select * from app_registertransaction) to STDOUT with csv delimiter ';'", - tf - ) - # delete - cur.execute("delete from app_order;") - cur.execute("delete from app_registertransaction;") - connection.commit() - # # # # # - exit_code = 0 - print("done.") - except (Error, Exception) as err: - connection.rollback() - print(f"An error occured while upgrading the database at {os.environ['PGDB_HOST']}:\n{err}") - exit_code = 1 - finally: - cur.close() - connection.close() - exit(exit_code) diff --git a/lib/auto-upgrade-db.sh b/lib/auto-upgrade-db.sh deleted file mode 100644 index beadb59..0000000 --- a/lib/auto-upgrade-db.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - - -echo -e "Checking if database needs an upgrade..." - -if python3 $(pwd)/lib/verify-db-app-version.py; then - - echo -e "No database upgrade needed." - -else - - echo -e "Starting automatic database upgrade..." - source "$(pwd)/lib/db-migrations.sh" - python3 $(pwd)/lib/upgrade-db.py - -fi diff --git a/lib/bootstrap.py b/lib/bootstrap.py deleted file mode 100644 index 23f0d34..0000000 --- a/lib/bootstrap.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 - - -from os import environ -from os import getcwd -from shlex import quote -from signal import SIGINT -from subprocess import run -from subprocess import Popen -from sys import argv -from sys import stdout -from sys import stderr - - -# some vars -devel = False -caddy_process = None -scs_process = None -app_process = None - - -def stop(): - print("\n\nStopping services.\n\n") - caddy_process.send_signal(SIGINT) - scs_process.send_signal(SIGINT) - app_process.send_signal(SIGINT) - print(f"Caddy stopped with exit code {caddy_process.wait()}.") - print(f"session-clear-scheduler stopped with exit code {scs_process.wait()}.") - if devel: - print(f"Django stopped with exit code {app_process.wait()}.") - else: - print(f"Django/Uvicorn stopped with exit code {app_process.wait()}.") - if caddy_process.returncode != 0 or scs_process.returncode != 0 or app_process.returncode !=0: - exit(1) - else: - exit(0) - - -if __name__ == "__main__": - # development or production environment? - try: - if argv[1] == "devel": - devel = True - except IndexError: - pass - # vars - pwd = getcwd() - APPLICATION_LOG = environ["APPLICATION_LOG"] - CADDY_ACCESS_LOG = environ["CADDY_ACCESS_LOG"] - CADDY_LOG = environ["CADDY_LOG"] - DJANGO_PORT = environ["DJANGO_PORT"] - HTTPS_PORT = environ["HTTPS_PORT"] - if devel: - environ["DJANGO_DEBUG"] = "true" - else: - environ["DJANGO_DEBUG"] = "false" - # info - print(f"\n\nStarting server on port {HTTPS_PORT}...\nYou should be able to access the application locally at https://127.0.0.1:{HTTPS_PORT}/\n\nPress Ctrl+C to stop all services.\n\n") - if not devel: - print(f"All further messages will be written to {APPLICATION_LOG} and {CADDY_LOG}") - print(f"HTTP Access Log will be written to {CADDY_ACCESS_LOG}") - try: - # start django/uvicorn - if devel: - run( - ["python3", f"{pwd}/application/manage.py", "collectstatic", "--noinput"], - stdout=stdout, - stderr=stderr, - env=environ - ) - app_process = Popen( - ["python3", f"{pwd}/application/manage.py", "runserver", f"127.0.0.1:{DJANGO_PORT}"], - stdout=stdout, - stderr=stderr, - env=environ - ) - else: - application_log_file = open(APPLICATION_LOG, "a") - run( - ["python3", f"{pwd}/application/manage.py", "collectstatic", "--noinput"], - stdout=application_log_file, - stderr=application_log_file, - env=environ - ) - app_process = Popen( - [ - "python3", "-m", "uvicorn", - "--host", "127.0.0.1", - "--port", quote(DJANGO_PORT), - "drinks_manager.asgi:application" - ], - stdout=application_log_file, - stderr=application_log_file, - cwd=f"{pwd}/application/", - env=environ - ) - # start caddy - if devel: - caddy_log_file = stdout - caddy_log_file_stderr = stderr - else: - caddy_log_file = caddy_log_file_stderr = open(CADDY_LOG, "a") - caddy_process = Popen( - ["caddy", "run", "--config", f"{pwd}/config/Caddyfile"], - stdout=caddy_log_file, - stderr=caddy_log_file_stderr, - env=environ - ) - # start session-clear-scheduler - if devel: - clear_sched_log_file = stdout - clear_sched_log_file_stderr = stderr - else: - clear_sched_log_file = clear_sched_log_file_stderr = open(APPLICATION_LOG, "a") - scs_process = Popen( - ["python3", f"{pwd}/lib/session-clear-scheduler.py"], - stdout=clear_sched_log_file, - stderr=clear_sched_log_file_stderr - ) - caddy_process.wait() - scs_process.wait() - app_process.wait() - except KeyboardInterrupt: - stop() diff --git a/lib/clear-expired-sessions.sh b/lib/clear-expired-sessions.sh deleted file mode 100644 index c10b5ce..0000000 --- a/lib/clear-expired-sessions.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py clearsessions \ No newline at end of file diff --git a/lib/create-admin.sh b/lib/create-admin.sh deleted file mode 100644 index ee46fd4..0000000 --- a/lib/create-admin.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py createsuperuser - -echo -e "done." \ No newline at end of file diff --git a/lib/db-migrations.sh b/lib/db-migrations.sh deleted file mode 100644 index 7c17843..0000000 --- a/lib/db-migrations.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -# make migrations & migrate -python3 $(pwd)/application/manage.py makemigrations -python3 $(pwd)/application/manage.py makemigrations app -python3 $(pwd)/application/manage.py migrate - -echo -e "done with db migration." \ No newline at end of file diff --git a/lib/env.sh b/lib/env.sh deleted file mode 100644 index 5863525..0000000 --- a/lib/env.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -export DJANGO_SK_ABS_FP="$(pwd)/config/secret_key.txt" -export PROFILE_PICTURES="$(pwd)/profilepictures/" -export STATIC_FILES="$(pwd)/static/" -export APP_VERSION="13" -export PYTHONPATH="$(pwd)/packages/" diff --git a/lib/generate-secret-key.py b/lib/generate-secret-key.py deleted file mode 100644 index 7f98866..0000000 --- a/lib/generate-secret-key.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from pathlib import Path -from secrets import token_bytes -from base64 import b85encode - -# - -override = False -if len(sys.argv) > 1: - if sys.argv[1] == "--override": - override = True - -random_token_length = 128 - -secret_key_fp = Path("config/secret_key.txt") - -# - -if secret_key_fp.exists() and not override: - print(f"Warning: secret_key.txt already exists in directory {secret_key_fp.absolute()}. Won't override.", file=sys.stderr) - exit(1) -else: - print("Generating random secret key...") - random_key = b85encode(token_bytes(random_token_length)) - with secret_key_fp.open("wb") as secret_key_f: - secret_key_f.write(random_key) - print("done.") \ No newline at end of file diff --git a/lib/session-clear-scheduler.py b/lib/session-clear-scheduler.py deleted file mode 100644 index 6096d72..0000000 --- a/lib/session-clear-scheduler.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 - -# This script clears expired sessions in a regular interval -# The interval is defined (in minutes) by config.sh (SESSION_CLEAR_INTERVAL) - -import os - -from pathlib import Path -from subprocess import run -from time import sleep -from datetime import datetime - -try: - - exiting = False - clear_running = False - - print("[session-clear-scheduler] Starting session-clear-scheduler.") - - session_clear_script_fp = Path("lib/clear-expired-sessions.sh") - clear_interval_seconds = int(os.environ["SESSION_CLEAR_INTERVAL"]) * 60 - - sleep(10) # wait some seconds before the first session clean-up - - while True: - - clear_running = True - run(["/bin/sh", session_clear_script_fp.absolute()]) - clear_running = False - - print(f"[session-clear-scheduler: {datetime.now()}] Cleared expired sessions.") - - if exiting: - break - - sleep(clear_interval_seconds) - -except KeyboardInterrupt: - - exiting = True - - if clear_running: - print(f"[session-clear-scheduler: {datetime.now()}] Received SIGINT. Waiting for current clear process to finish.") - sleep(20) # wait some time - - print(f"[session-clear-scheduler: {datetime.now()}] Exiting") - exit(0) diff --git a/lib/setup-application.sh b/lib/setup-application.sh deleted file mode 100644 index 56ae66d..0000000 --- a/lib/setup-application.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - - -# enable debugging for this command -export DJANGO_DEBUG="true" - -python3 "$(pwd)/lib/generate-secret-key.py" - -source "$(pwd)/lib/db-migrations.sh" - -python3 $(pwd)/lib/upgrade-db.py - -echo -e "\nCreate admin account. Email is optional.\n" -source "$(pwd)/lib/create-admin.sh" - -python3 $(pwd)/application/manage.py collectstatic --noinput diff --git a/lib/start-django-shell.sh b/lib/start-django-shell.sh deleted file mode 100644 index a696310..0000000 --- a/lib/start-django-shell.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# start a django shell - -export DJANGO_DEBUG="true" - -oldcwd="$(pwd)" -echo "Starting a django shell..." -echo -e "--------------------------------------------------------------------------------\n" -"$(pwd)/application/manage.py" shell -echo -e "\n--------------------------------------------------------------------------------" -cd "$oldcwd" \ No newline at end of file diff --git a/lib/upgrade-db.py b/lib/upgrade-db.py deleted file mode 100644 index d07da87..0000000 --- a/lib/upgrade-db.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -import os, sys - -from pathlib import Path - -from psycopg2 import connect -from psycopg2._psycopg import cursor as _cursor -from psycopg2._psycopg import connection as _connection -from psycopg2 import Error -from psycopg2 import IntegrityError -from psycopg2 import errorcodes - - -# setup or upgrade the database - -def log(s, error=False): - if error: - print(f"{s}", file=sys.stderr) - else: - print(f"{s}", file=sys.stdout) - - -def execute_sql_statement(cursor:_cursor, connection:_connection, sql_statement): - try: - cursor.execute(sql_statement) - connection.commit() - except IntegrityError as ie: - if ie.pgcode == errorcodes.UNIQUE_VIOLATION: - log("Skipping one row that already exists.") - connection.rollback() - else: - log(f"An integrity error occured:\n{ie}\nRolling back...", error=True) - connection.rollback() - except Error as e: - log(f"An SQL statement failed while upgrading the database at {os.environ['PGDB_HOST']}:\n{e}", error=True) - connection.rollback() - - -if __name__ == "__main__": - exit_code = 1 - conn = connect( - user = os.environ["PGDB_USER"], - password = os.environ["PGDB_PASSWORD"], - host = os.environ["PGDB_HOST"], - port = os.environ["PGDB_PORT"], - database = os.environ["PGDB_DB"] - ) - cur = conn.cursor() - try: - log("\nSetting up/upgrading database...") - # # # # # - log("Not deleting register_balance. You can delete it via the Admin Panel (Globals -> register_balance), as it is no more used.") - execute_sql_statement(cur, conn, """ - insert into app_global - values ('global_message', 'Here you can set a global message that will be shown to every user.', 0.0, ''); - """) - execute_sql_statement(cur, conn, """ - insert into app_global - values ('admin_info', 'Here you can set am infotext that will be displayed on the admin panel.', 0.0, ''); - """) - execute_sql_statement(cur, conn, """ - create or replace view app_userdeposits_view as - select * from app_registertransaction - where is_user_deposit = true; - """) - # # # # # - # set app_version in file and database - # database - try: - cur.execute(""" - select value from application_info - where key = 'app_version'; - """) - result = cur.fetchone() - if result == None: - cur.execute(f""" - insert into application_info values ('app_version', '{os.environ['APP_VERSION']}'); - """) - conn.commit() - else: - cur.execute(f""" - update application_info set value = '{os.environ['APP_VERSION']}' where key = 'app_version'; - """) - conn.commit() - except Error as err: - if err.pgcode == errorcodes.UNDEFINED_TABLE: - try: - conn.rollback() - cur.execute(""" - create table application_info ( - key varchar(32) primary key, - value text - ); - """) - cur.execute(f""" - insert into application_info values ('app_version', '{os.environ['APP_VERSION']}'); - """) - conn.commit() - except Error as err2: - log(f"An error occurred while setting app_version in table application_info: {err}", error=True) - exit_code = 1 - else: - log(f"An error occurred while setting app_version in table application_info: {err}", error=True) - exit_code = 1 - # file - Path("./config/db_app_version.txt").write_text(os.environ["APP_VERSION"]) - # done - exit_code = 0 - log("done with db setup/upgrade.") - except (Error, Exception) as err: - log(f"An error occured while upgrading the database at {os.environ['PGDB_HOST']}:\n{err}", error=True) - exit_code = 1 - finally: - cur.close() - conn.close() - exit(exit_code) diff --git a/lib/verify-db-app-version.py b/lib/verify-db-app-version.py deleted file mode 100644 index 3fb10f9..0000000 --- a/lib/verify-db-app-version.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 - -from os import environ -from pathlib import Path - -from psycopg2 import connect -from psycopg2._psycopg import cursor -from psycopg2 import Error -from psycopg2 import errorcodes - - -# verify if the installation -# exit code 0 -> no database update is necessary -# exit code 1 -> database update is necessary - - -def check_file(): - db_app_version_file = Path("./config/db_app_version.txt") - if not db_app_version_file.exists(): - exit(1) - if not db_app_version_file.is_file(): - exit(1) - if not db_app_version_file.read_text().strip(" ").strip("\n") == environ["APP_VERSION"]: - exit(1) - - -def check_database(): - connection = connect( - user = environ["PGDB_USER"], - password = environ["PGDB_PASSWORD"], - host = environ["PGDB_HOST"], - port = environ["PGDB_PORT"], - database = environ["PGDB_DB"] - ) - cur = connection.cursor() - try: - # check application version in db - cur.execute(""" - select value from application_info - where key = 'app_version'; - """) - appinfo_result = list(cur.fetchone())[0] - if appinfo_result == None: - cur.close() - connection.close() - exit(1) - if appinfo_result != environ["APP_VERSION"]: - cur.close() - connection.close() - exit(1) - # check rows in app_global - required_rows = [ - "global_message", - "admin_info" - ] - cur.execute(""" - select name from app_global; - """) - table_global_result = list(cur.fetchall()) - cur.close() - connection.close() - existing_rows = [list(row)[0] for row in table_global_result] - for r in required_rows: - if not r in existing_rows: - exit(1) - except Error: - cur.close() - connection.close() - exit(1) - except Exception as e: - print(f"An exception occured: {e}") - cur.close() - connection.close() - exit(1) - - -if __name__ == "__main__": - check_file() - check_database() - exit(0) diff --git a/application/manage.py b/manage.py similarity index 80% rename from application/manage.py rename to manage.py index dce56eb..5481674 100755 --- a/application/manage.py +++ b/manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!./venv/bin/python3 """Django's command-line utility for administrative tasks.""" import os import sys @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/misc/drinks-manager.service b/misc/drinks-manager.service index 9dbc463..e5d23e8 100644 --- a/misc/drinks-manager.service +++ b/misc/drinks-manager.service @@ -6,15 +6,15 @@ Requires=network-online.target Description=Drinks Manager [Service] -User=drinks-manager -Group=drinks-manager +User=drinks +Group=drinks WorkingDirectory=/srv/drinks-manager/ # start the server: -ExecStart=/usr/bin/bash -c "/srv/drinks-manager/run.sh server" +ExecStart=/usr/bin/bash -c "/srv/drinks-manager/start.sh" # stop the process with a SIGINT: -ExecStop=/usr/bin/bash -c "/bin/kill -2 $MAINPID; /usr/bin/sleep 10" +ExecStop=/usr/bin/bash -c "/bin/kill -2 $MAINPID;" Restart=on-failure -TimeoutStopSec=40s +TimeoutStopSec=15s LimitNPROC=512 LimitNOFILE=1048576 AmbientCapabilities=CAP_NET_BIND_SERVICE @@ -22,4 +22,5 @@ PrivateTmp=true ProtectSystem=full [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target + diff --git a/misc/icons/drinksmanager-icon.src.svg b/misc/icons/drinksmanager-icon.src.svg index f231677..f18b107 100644 --- a/misc/icons/drinksmanager-icon.src.svg +++ b/misc/icons/drinksmanager-icon.src.svg @@ -104,7 +104,7 @@ rdf:about=""> - Julian Müller (W13R) + Julian Müller (ChaoticByte) diff --git a/packages/.gitkeep b/project/__init__.py similarity index 100% rename from packages/.gitkeep rename to project/__init__.py diff --git a/application/drinks_manager/asgi.py b/project/asgi.py similarity index 56% rename from application/drinks_manager/asgi.py rename to project/asgi.py index dcfe5ee..486c89b 100644 --- a/application/drinks_manager/asgi.py +++ b/project/asgi.py @@ -1,16 +1,16 @@ """ -ASGI config for drinks_manager project. +ASGI config for project project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_asgi_application() diff --git a/project/settings.py b/project/settings.py new file mode 100644 index 0000000..a5362ca --- /dev/null +++ b/project/settings.py @@ -0,0 +1,167 @@ +""" +Django settings for project project. + +Generated by 'django-admin startproject' using Django 4.1.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +import os +from pathlib import Path +from yaml import safe_load + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Load configuration file +with Path(BASE_DIR / "data" / "config.yml").open("r") as f: + config = safe_load(f) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config["app"]["secret_key"] +if SECRET_KEY == "!!!replace this with random data!!!" or len(SECRET_KEY) < 40: + print( + "WARNING: You didn't provide a secure secret_key in the configuration file!", + "This is a security risk!!!") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True +if "APP_PROD" in os.environ: + DEBUG = not os.environ["APP_PROD"] + +# ALLOWED_HOSTS can be wildcarded, +# because caddy already handles requests +ALLOWED_HOSTS = ["*"] + +# Application definition + +INSTALLED_APPS = [ + "app.apps.AppConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "project.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "app.context_processors.app_version" + ], + }, + }, +] + +WSGI_APPLICATION = "project.wsgi.application" + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": 'django.db.backends.postgresql', + "NAME": config["db"]["database"], + "USER": config["db"]["user"], + "PASSWORD": config["db"]["password"], + "HOST": config["db"]["host"], + "PORT": str(config["db"]["port"]), + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +if config["app"]["password_validation"]: + AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, + ] +else: + AUTH_PASSWORD_VALIDATORS = [] + +# Security settings + +AUTH_USER_MODEL = "app.User" +SESSION_COOKIE_AGE = int(config["app"]["session_cookie_age"]) +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +CSRF_TRUSTED_ORIGINS = [] + +for host in config['caddy']['hosts']: + CSRF_TRUSTED_ORIGINS.append(f"http://{host}") + CSRF_TRUSTED_ORIGINS.append(f"https://{host}") + CSRF_TRUSTED_ORIGINS.append(f"http://{host}:{config['caddy']['https_port']}") + CSRF_TRUSTED_ORIGINS.append(f"https://{host}:{config['caddy']['https_port']}") + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = config["app"]["language_code"] +TIME_ZONE = config["app"]["timezone"] +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LOCALE_PATHS = [ + BASE_DIR / "app" / "locales" +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "data" / "static" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Additional settings + +if "APP_VERSION" in os.environ: + APP_VERSION = os.environ["APP_VERSION"] +else: + APP_VERSION = "unknown" + +CURRENCY_SUFFIX = config["app"]["currency_suffix"] diff --git a/application/drinks_manager/urls.py b/project/urls.py similarity index 86% rename from application/drinks_manager/urls.py rename to project/urls.py index 5bf5958..77d62fb 100644 --- a/application/drinks_manager/urls.py +++ b/project/urls.py @@ -1,7 +1,7 @@ -"""drinks_manager URL Configuration +"""project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.2/topics/http/urls/ + https://docs.djangoproject.com/en/4.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views @@ -13,9 +13,8 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ - from django.urls import path, include urlpatterns = [ path('', include("app.urls")) -] \ No newline at end of file +] diff --git a/application/drinks_manager/wsgi.py b/project/wsgi.py similarity index 56% rename from application/drinks_manager/wsgi.py rename to project/wsgi.py index b42c9aa..b5da491 100644 --- a/application/drinks_manager/wsgi.py +++ b/project/wsgi.py @@ -1,16 +1,16 @@ """ -WSGI config for drinks_manager project. +WSGI config for project project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drinks_manager.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt index da50c9d..2be275b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django~=3.2.7 -django-currentuser==0.5.3 -psycopg2~=2.9.1 -uvicorn~=0.17.6 +Django~=4.1 +psycopg2~=2.9.5 +uvicorn~=0.20.0 +PyYAML~=6.0 diff --git a/run.sh b/run.sh deleted file mode 100755 index a1664c5..0000000 --- a/run.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - - -function show_dm_help { # $1 = exit code - - echo -e "Usage:\t./run.sh \n" - echo -e "\nCommands:\n" - echo -e " server\t\tstart server" - echo -e " setup\t\t\tset up the application" - echo -e " create-admin\t\tcreate an admin account" - echo -e " generate-secret-key\tgenerate a new random secret key for Django" - echo -e " clear-sessions\tmanually remove all expired sessions from the database" - echo -e " force-db-upgrade\tforce a database migration & upgrade" - echo -e " archive-tables\tarchive (copy & delete) all entries in app_order and app_registertransaction" - echo -e " development-server\tstart Django development server and enable debugging" - echo -e " shell\t\t\tstart a Django shell" - echo -e " help\t\t\tShow this help text\n" - echo -e "\nExamples:\n" - echo -e " ./run.sh server" - echo -e " ./run.sh create-admin" - echo "" - - exit $1 - -} - -# set current working directory -cd $(dirname "$0") - -source "$(pwd)/lib/env.sh" - -echo -e "\n## Drinks Manager" -echo -e "## version $APP_VERSION\n" - - -if [ -z $1 ]; then - - show_dm_help 1 - -else - - source "$(pwd)/config/config.sh" - - if [ $1 = 'server' ]; then - - source "$(pwd)/lib/auto-upgrade-db.sh" - python3 "$(pwd)/lib/bootstrap.py" - - elif [ $1 = 'development-server' ]; then - - source "$(pwd)/lib/auto-upgrade-db.sh" - python3 "$(pwd)/lib/bootstrap.py" devel - - elif [ $1 = 'setup' ]; then - - source "$(pwd)/lib/setup-application.sh" - - elif [ $1 = 'generate-secret-key' ]; then - - python3 "$(pwd)/lib/generate-secret-key.py" --override - - elif [ $1 = 'force-db-upgrade' ]; then - - source "$(pwd)/lib/db-migrations.sh" - python3 "$(pwd)/lib/upgrade-db.py" - - elif [ $1 = 'create-admin' ]; then - - source "$(pwd)/lib/create-admin.sh" - - elif [ $1 = 'clear-sessions' ]; then - - source "$(pwd)/lib/clear-expired-sessions.sh" - echo -e "done." - - elif [ $1 = 'archive-tables' ]; then - - python3 "$(pwd)/lib/archive-tables.py" - - elif [ $1 = 'shell' ]; then - - source "$(pwd)/lib/start-django-shell.sh" - - elif [ $1 = 'help' ]; then - - show_dm_help 0 - - else - - show_dm_help 1 - - fi - -fi \ No newline at end of file diff --git a/scripts/_bootstrap.py b/scripts/_bootstrap.py new file mode 100755 index 0000000..9533953 --- /dev/null +++ b/scripts/_bootstrap.py @@ -0,0 +1,175 @@ +#!./venv/bin/python3 +# Copyright 2023 Julian Müller (ChaoticByte) + +import os + +from argparse import ArgumentParser +from atexit import register as register_exithandler +from pathlib import Path +from signal import SIGINT +from subprocess import Popen +from sys import stdout, stderr +from time import sleep + +from yaml import safe_load + + +base_directory = Path(__file__).parent.parent +data_directory = base_directory / "data" +logfile_directory = data_directory / "logs" +configuration_file = data_directory / "config.yml" +caddyfile = data_directory / "Caddyfile" +logfile_caddy = logfile_directory / "caddy.log" +logfile_app = logfile_directory / "app.log" + + +class MonitoredSubprocess: + def __init__( + self, + name: str, + commandline: list, + logfile: Path, + environment: dict = os.environ, + max_tries: int = 5, + ): + self.name = name + self.commandline = commandline + self.logfile = logfile + self.environment = environment + self.max_tries = max_tries + self.s = None # the subprocess object + self._tries = 0 + self._stopped = False + + def try_start(self): + if self._tries < self.max_tries: + self._tries += 1 + print(f"Starting {self.name}...") + if self.logfile is None: + self.s = Popen( + self.commandline, + stdout=stdout.buffer, + stderr=stderr.buffer, + env=self.environment) + else: + with self.logfile.open("ab") as l: + self.s = Popen( + self.commandline, + stdout=l, + stderr=l, + env=self.environment) + return True + else: + print(f"Max. tries exceeded ({self.name})!") + # the process must already be stopped at this + # point, so we can set the variable accordingly + self._stopped = True + return False + + def stop(self): + if not self._stopped: + print(f"Stopping {self.name}...") + self.s.terminate() + self._stopped = True + + +def cleanup_procs(processes): + for p in processes: + p.stop() + + +def start_and_monitor(monitored_subprocesses: list): + # start processes + for p in monitored_subprocesses: + p.try_start() + register_exithandler(cleanup_procs, monitored_subprocesses) + # monitor processes + try: + while True: + sleep(1) + for p in monitored_subprocesses: + returncode = p.s.poll() + if returncode is None: + continue + else: + print(f"{p.name} stopped with exit code {returncode}.") + if p.try_start() is False: + # stop everything if the process + # has exceeded max. tries + exit() + except KeyboardInterrupt: + print("Received KeyboardInterrupt, exiting...") + exit() + + +if __name__ == "__main__": + argp = ArgumentParser() + argp.add_argument("--devel", help="Start development server", action="store_true") + args = argp.parse_args() + # Load configuration + with configuration_file.open("r") as f: + config = safe_load(f) + # Prepare + os.chdir(str(base_directory)) + Popen( + ["./venv/bin/python3", "./manage.py", "collectstatic", "--noinput"], env=os.environ).wait() + Popen( + ["./venv/bin/python3", "./manage.py", "migrate", "--noinput"], env=os.environ).wait() + # Caddy configuration via env + environment_caddy = os.environ + environment_caddy["DATADIR"] = str(data_directory.absolute()) + environment_caddy["CADDY_HOSTS"] = ", ".join(config["caddy"]["hosts"]) + environment_caddy["HTTP_PORT"] = str(config["caddy"]["http_port"]) + environment_caddy["HTTPS_PORT"] = str(config["caddy"]["https_port"]) + environment_caddy["APPLICATION_PORT"] = str(config["app"]["application_port"]) + environment_caddy["ACCESS_LOG"] = config["logs"]["http_access"] + # Start + if args.devel: + procs = [ + MonitoredSubprocess( + "Caddy Webserver", + ["caddy", "run", "--config", str(caddyfile)], + None, + environment=environment_caddy), + MonitoredSubprocess( + "Django Development Server", + ["./venv/bin/python3", "./manage.py", "runserver", str(config["app"]["application_port"])], + None), + MonitoredSubprocess( + "Session Autocleaner", + ["./scripts/_session-autocleaner.py", str(config["app"]["session_clear_interval"])], + None) + ] + start_and_monitor(procs) + else: + # Application configuration via env + environment_app = os.environ + environment_app["APP_PROD"] = "1" + print("\nRunning in production mode.\n") + # define processes + procs = [ + MonitoredSubprocess( + "Caddy Webserver", + ["caddy", "run", "--config", str(caddyfile)], + logfile_caddy, + environment=environment_caddy), + MonitoredSubprocess( + "Drinks-Manager", + [ + "./venv/bin/python3", + "-m", + "uvicorn", + "--host", + "127.0.0.1", + "--port", + str(config["app"]["application_port"]), + "project.asgi:application", + ], + logfile_app, + environment=environment_app), + MonitoredSubprocess( + "Session Autocleaner", + ["./scripts/_session-autocleaner.py", str(config["app"]["session_clear_interval"])], + logfile_app) + ] + start_and_monitor(procs) diff --git a/scripts/_session-autocleaner.py b/scripts/_session-autocleaner.py new file mode 100755 index 0000000..bb81089 --- /dev/null +++ b/scripts/_session-autocleaner.py @@ -0,0 +1,55 @@ +#!./venv/bin/python3 + +# This script clears expired sessions in a regular interval + +import os + +from argparse import ArgumentParser +from atexit import register as register_exithandler +from pathlib import Path +from subprocess import Popen +from time import sleep +from datetime import datetime + + +current_proc = None + + +def exithandler(): + if current_proc is not None: + seconds_waited = 0 + while current_proc.poll() is None: + # wait for 10 seconds to quit session cleaner + if seconds_waited >= 10: + current_proc.terminate() + break + # is still running + sleep(1) + seconds_waited += 1 + print("Stopped session-autocleaner.") + + +if __name__ == "__main__": + try: + argp = ArgumentParser() + argp.add_argument("interval", help="The interval in minutes", type=int) + args = argp.parse_args() + os.chdir(str(Path(__file__).parent.parent)) + print(f"Started session-autocleaner with an interval of {args.interval} minute(s)") + interval = args.interval * 60 + # register exithandler that cleans up stuff + register_exithandler(exithandler) + # main loop + while True: + if current_proc is not None: + # wait for last iteration + while current_proc.poll() is None: + # is still running + print("Last cleanup is still running, waiting before clearing sessions...") + sleep(1) + print(f"Clearing expired sessions at {datetime.now()}...") + current_proc = Popen( + ["./manage.py", "clearsessions"]) + sleep(interval) + except KeyboardInterrupt: + exit() diff --git a/scripts/archive.py b/scripts/archive.py new file mode 100755 index 0000000..7128fb8 --- /dev/null +++ b/scripts/archive.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +from datetime import datetime +from pathlib import Path + +from psycopg2 import connect +from yaml import safe_load + + +base_directory = Path(__file__).parent.parent +data_directory = base_directory / "data" +configuration_file = data_directory / "config.yml" +archive_directory = data_directory / "archive" + + +if __name__ == "__main__": + exit_code = 0 + try: + # read config + with configuration_file.open("r") as f: + config = safe_load(f) + # connect to database + connection = connect( + user = config["db"]["user"], + password = config["db"]["password"], + host = config["db"]["host"], + port = config["db"]["port"], + database = config["db"]["database"] + ) + cur = connection.cursor() + # copy data from database + timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") + orders_archive_path = archive_directory / f"orders-archive-{timestamp}.csv" + transactions_archive_path = archive_directory / f"transactions-archive-{timestamp}.csv" + print(f"Copying data...") + with orders_archive_path.open("w") as of: + cur.copy_expert( + "copy (select * from app_order) to STDOUT with csv delimiter ';'", of) + print(str(orders_archive_path)) + with transactions_archive_path.open("w") as tf: + cur.copy_expert( + "copy (select * from app_registertransaction) to STDOUT with csv delimiter ';'", tf) + print(str(transactions_archive_path)) + # delete data from database + print("Deleting data from database...") + cur.execute("delete from app_order;") + cur.execute("delete from app_registertransaction;") + connection.commit() + print("done.") + except (Error, Exception) as err: + connection.rollback() + print(f"An error occured while upgrading the database at {os.environ['PGDB_HOST']}:\n{err}") + exit_code = 1 + finally: + cur.close() + connection.close() + exit(exit_code) diff --git a/scripts/create-admin.sh b/scripts/create-admin.sh new file mode 100755 index 0000000..783e509 --- /dev/null +++ b/scripts/create-admin.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Copyright 2023 Julian Müller (ChaoticByte) + +# change to correct directory, if necessary +script_absolute=$(realpath "$0") +script_directory=$(dirname "$script_absolute") +desired_directory=$(realpath "$script_directory"/..) +if [ "$PWD" != "$desired_directory" ]; then + echo "Changing to project directory..." + cd "$desired_directory" +fi + +echo "Activating venv..." +source ./venv/bin/activate + +echo "Applying migrations..." +./manage.py migrate + +./manage.py createsuperuser diff --git a/scripts/setup-env.sh b/scripts/setup-env.sh new file mode 100755 index 0000000..f700819 --- /dev/null +++ b/scripts/setup-env.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright 2023 Julian Müller (ChaoticByte) + +# change to correct directory, if necessary +script_absolute=$(realpath "$0") +script_directory=$(dirname "$script_absolute") +desired_directory=$(realpath "$script_directory"/..) +if [ "$PWD" != "$desired_directory" ]; then + echo "Changing to project directory..." + cd "$desired_directory" +fi + +echo "Creating venv..." +python3 -m venv ./venv + +echo "Activating venv..." +source ./venv/bin/activate + +echo "Installing dependencies..." +python3 -m pip install -r requirements.txt diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..fdca992 --- /dev/null +++ b/start.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +basedir=$(dirname "$0") +basedir=$(realpath $basedir) +cd "$basedir" + +# Set file permissions +chmod -c -R g-w,o-rwx * +chmod -c -R g-w,o-rwx .git/ +chmod -c -R g-w,o-rwx .gitignore + +export PYTHONPATH="$basedir" +export DJANGO_SETTINGS_MODULE="project.settings" +export APP_VERSION="revamp-pre" + +exec ./scripts/_bootstrap.py "$@" diff --git a/static/css/appform.css b/static/css/appform.css deleted file mode 100644 index 5c727b5..0000000 --- a/static/css/appform.css +++ /dev/null @@ -1,61 +0,0 @@ -.appform { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: max-content; - font-size: 1.1rem; -} -.appform > .forminfo { - width: 100%; - text-align: left; - margin: .4rem 0; -} -.forminfo > span:first-child { - margin-right: 1rem; -} -.forminfo > span:last-child { - float: right; -} -.appform > .forminput { - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - margin: .8rem 0; - gap: 1rem; -} -.appform > .statusinfo { - margin-top: .5rem; -} -.appform > .formbuttons { - border-top: 1px solid var(--glass-border-color); - padding-top: 1rem; - margin-top: 1rem; - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 1rem; -} -.formbuttons button, .formbuttons .button { - box-sizing: content-box; - font-size: 1rem; - width: fit-content; -} -.formheading { - text-align: left; - width: 100%; - margin-top: 0; -} -@media only screen and (max-width: 700px) { - .appform > .forminput { - flex-direction: column; - gap: .5rem; - } - .formheading { - text-align: center; - } -} \ No newline at end of file diff --git a/static/css/custom_number_input.css b/static/css/custom_number_input.css deleted file mode 100644 index 375f0de..0000000 --- a/static/css/custom_number_input.css +++ /dev/null @@ -1,39 +0,0 @@ -/* custom number input */ -.customnumberinput { - display: flex; - flex-direction: row; - height: 2.2rem; -} -.customnumberinput button { - min-width: 2.5rem !important; - width: 2.5rem !important; - padding: 0; - margin: 0; - height: 100%; -} -.customnumberinput-minus { - border-bottom-right-radius: 0; - border-top-right-radius: 0; - z-index: 10; -} -.customnumberinput-plus { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - z-index: 10; -} -.customnumberinput input[type="number"] { - max-height: 100%; - width: 4rem; - padding: 0; - margin: 0; - font-size: .9rem; - color: var(--color); - text-align: center; - background: var(--glass-bg-color2); - outline: none; - border: none; - border-radius: 0 !important; - -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; -} \ No newline at end of file diff --git a/static/css/history.css b/static/css/history.css deleted file mode 100644 index 6c87d89..0000000 --- a/static/css/history.css +++ /dev/null @@ -1,23 +0,0 @@ -.history { - margin: 0; - padding: 0; - width: 40%; - min-width: 30rem; -} -.history td { - padding-top: .4rem !important; - padding-bottom: .4rem !important; - font-size: .95rem; -} -.history .historydate { - margin-left: auto; - text-align: right; - font-size: .8rem !important; -} -/* mobile devices */ -@media only screen and (max-width: 700px) { - .history { - width: 90%; - min-width: 90%; - } -} \ No newline at end of file diff --git a/static/css/index.css b/static/css/index.css deleted file mode 100644 index a89df5d..0000000 --- a/static/css/index.css +++ /dev/null @@ -1,46 +0,0 @@ -.availabledrinkslist { - width: 50%; - max-width: 45rem; - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; -} -.availabledrinkslist li { - display: flex; - width: 100%; - height: fit-content; - margin-bottom: .6rem; -} -.availabledrinkslist li a { - display: flex; - width: 100%; - align-items: center; - justify-content: start; - color: var(--color); - padding: .8rem 1.1rem; - text-decoration: none; - font-size: 1rem; -} -.availabledrinkslist li a span:first-child { - margin-right: 1rem !important; - text-align: left; -} -.availabledrinkslist li a span:last-child { - margin-left: auto; - text-align: right; - font-size: 1rem; -} -/* mobile devices */ -@media only screen and (max-width: 700px) { - .availabledrinkslist { - width: 95%; - } - .availabledrinkslist li a { - width: calc(100vw - (2 * .8rem)) !important; - padding: .8rem !important; - } -} \ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css deleted file mode 100644 index 13776e9..0000000 --- a/static/css/login.css +++ /dev/null @@ -1,131 +0,0 @@ -/* login page */ -main { - margin-top: 2vh; -} -main > h1 { - display: none; -} -.userlistcontainer { - display: flex; - flex-direction: column; - align-items: center; - justify-content: start; -} -.userlist { - width: 50vw; - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} -.userlist > li { - display: flex; - width: 100%; - align-items: center; - justify-content: center; - margin-bottom: .5rem; - padding: 0 .5rem; -} -.userlist > li > img { - margin-right: auto; - margin-left: 0; - height: 2rem; - width: 2rem; -} -.userlist > li > div { - display: flex; - flex-grow: 1; - align-items: center; - justify-content: center; - text-align: center; - padding: .8rem 1.1rem; -} -.userlistbutton { - font-size: 1.1rem; -} -.passwordoverlaycontainer { - position: absolute; - top: 0; - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: start; - background: var(--page-background); - z-index: 40; -} -.passwordoverlay { - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; -} -.passwordoverlay > form { - min-width: unset; - width: fit-content; -} -.passwordoverlay > form > h1 { - margin-top: 2rem; - margin-bottom: 2rem; -} -/* loginform */ -.loginform { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} -.loginform input[type="password"], form input[type="text"] { - width: 94%; - padding-top: .5rem; - padding-bottom: .5rem; - font-size: 1rem; - margin: .1rem 0; -} -.loginform .horizontalbuttonlist { - margin-top: 1.5rem; -} -.horizontalbuttonlist .button, .horizontalbuttonlist button { - font-size: 1rem; -} -/***/ -.pinpad { - margin-top: 1.5rem; - margin-bottom: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - max-width: 30vw; -} -.pinpad table { - box-shadow: none !important; -} -.pinpad table tr, .pinpad td { - padding: unset; - background: unset; -} -.pinpad tr td button { - height: 4.0rem; - width: 4.1rem; - font-size: 1.16rem; - margin: .2rem !important; -} -@media only screen and (max-width: 700px) { - .userlistcontainer { - width: 95vw; - } - .userlist { - width: 100%; - } - .pinpad table tr td button { - height: 4.2rem; - width: 4.2rem; - font-size: 1.16rem; - margin: .2rem; - } -} \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css deleted file mode 100644 index 8b999b7..0000000 --- a/static/css/main.css +++ /dev/null @@ -1,341 +0,0 @@ -/* VARIABLES */ -:root { - /** FONT **/ - --font-family: 'Liberation Sans', sans-serif; - /** colors **/ - --color: #fafafa; - --color-error: rgb(255, 70, 70); - /** glass **/ - --glass-bg-dropdown: #3a3b44ef; - --glass-bg-dropdown-hover: #55565efa; - --glass-bg-color1: #ffffff31; - --glass-bg-color2: #ffffff1a; - --glass-bg-hover-color1: #ffffff46; - --glass-bg-hover-color2: #ffffff1a; - --glass-blur: none; - --glass-border-color: #ffffff77; - --glass-bg: linear-gradient(var(--glass-bg-color1), var(--glass-bg-color2)); - --glass-bg-hover: linear-gradient(var(--glass-bg-hover-color1), var(--glass-bg-hover-color2)); - --glass-corner-radius: .5rem; - /** page background **/ - --page-background-color1: #131d25; - --page-background-color2: #311d30; - --page-background: linear-gradient(-190deg, var(--page-background-color1), var(--page-background-color2)); - /** global message banner **/ - --bg-globalmessage: linear-gradient(135deg, #4b351c, #411d52, #1c404b); -} -@supports(backdrop-filter: blur(10px)) { - :root { - --glass-bg-dropdown: #ffffff1a; - --glass-bg-dropdown-hover: #ffffff46; - --glass-blur: blur(18px); - } -} -/* BASE LAYOUT */ -body { - margin: 0; - padding: 0; - width: 100vw; - min-height: 100vh; - font-family: var(--font-family); - background: var(--page-background); - color: var(--color); - overflow-x: hidden; -} -.baselayout { - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; - min-height: 100vh; - width: 100vw; - max-width: 100vw; -} -main { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - flex-grow: 1; - width: 100%; - margin-top: 5vh; -} -.userpanel { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - margin-top: 1rem; - font-size: 1rem; - width: 94%; -} -.userinfo > span { - font-size: 1.1rem; - vertical-align: middle; -} -.userinfo > img { - vertical-align: middle; - width: 1.8rem; - height: 1.8rem; - margin: .5rem; -} -.userpanel > .horizontalbuttonlist { - margin-left: auto; - margin-right: 0; -} -.userbalancewarn { - color: var(--color-error); - font-weight: bold; -} -.content { - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; - width: 100%; - flex-grow: 1; -} -.globalmessage { - width: 100vw; - z-index: 999; - display: flex; - justify-content: center; - align-items: center; - background: var(--bg-globalmessage); - padding: .3rem 0; -} -.globalmessage div { - width: 96%; - text-align: center; - word-break: keep-all; - word-wrap: break-word; - box-sizing: border-box; -} -/* DROP DOWN MENUS */ -.dropdownmenu { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - border-radius: var(--glass-corner-radius); -} -.dropdownbutton { - width: fit-content; - z-index: 190; - box-shadow: none; - text-align: center; - justify-content: center; -} -.dropdownbutton, .dropdownchoice { - font-size: 1rem; -} -.dropdownlist { - position: absolute; - display: flex; - flex-direction: column; - pointer-events: none; - border-radius: var(--glass-corner-radius) !important; - backdrop-filter: var(--glass-blur); - z-index: 200; - margin-top: 3.2rem; - opacity: 0%; - transition: opacity 100ms; -} -.dropdownchoice { - box-shadow: none; - border-radius: 0 !important; - margin: 0; - margin-top: -1px; - text-align: center; - justify-content: center; - background: var(--glass-bg-dropdown) !important; - backdrop-filter: none !important; -} -.dropdownchoice:hover { - background: var(--glass-bg-dropdown-hover) !important; -} -.dropdownlist :first-child { - border-top-left-radius: var(--glass-corner-radius) !important; - border-top-right-radius: var(--glass-corner-radius) !important; -} -.dropdownlist :last-child { - border-bottom-left-radius: var(--glass-corner-radius) !important; - border-bottom-right-radius: var(--glass-corner-radius) !important; -} -.dropdownvisible .dropdownlist { - opacity: 100%; - visibility: visible; - pointer-events: visible; -} -/* FOOTER */ -.footer { - z-index: 900; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - flex-wrap: wrap; - margin-top: auto; - padding-top: 3rem; - padding-bottom: .3rem; - text-align: center; -} -.footer div { - font-size: .95rem; - margin-top: .15rem; - margin-bottom: .15rem; -} -.footer div::after { - margin-left: .5rem; - content: "-"; - margin-right: .5rem; -} -.footer div:last-child::after { - content: none; - margin-left: 0; - margin-right: 0; -} -/* TABLES */ -table { - border-collapse: collapse; - border-spacing: 0; - text-align: left; - border-radius: var(--glass-corner-radius); - backdrop-filter: var(--glass-blur); -} -tr { - background: var(--glass-bg-color1); -} -tr:nth-child(2n+2) { - background: var(--glass-bg-color2); -} -/* -Rounded corners on table cells apparently don't work with -Firefox, so Firefox users won't have rounded corners -on tables. Can't fix that by myself. -*/ -table tr:first-child th:first-child { - border-top-left-radius: var(--glass-corner-radius); -} -table tr:first-child th:last-child { - border-top-right-radius: var(--glass-corner-radius); -} -table tr:last-child td:first-child { - border-bottom-left-radius: var(--glass-corner-radius); -} -table tr:last-child td:last-child { - border-bottom-right-radius: var(--glass-corner-radius); -} -/* - */ -td, th { - padding: .5rem .8rem; -} -th { - text-align: left; - border-bottom: 1px solid var(--color); -} -/* BUTTONS & OTHER INPUT ELEMENTS */ -.button, button { - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-family); - text-decoration: none; - text-align: center !important; - background: var(--glass-bg); - color: var(--color); - padding: .6rem .8rem; - outline: none; - border: 1px solid var(--glass-border-color); - border-radius: var(--glass-corner-radius); - /*backdrop-filter: var(--glass-blur); disabled for performance reasons*/ - cursor: pointer; - user-select: none; -} -.button:hover, button:hover, .button:active, button:active { - background: var(--glass-bg-hover); -} -.button:disabled, button:disabled { - opacity: 40%; -} -a { - color: var(--color); -} -input[type="number"] { - -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; -} -input[type="number"]::-webkit-inner-spin-button { - display: none; -} -input[type="text"], input[type="password"], input[type="number"] { - background: var(--glass-bg-color2); - outline: none; - padding: .4rem .6rem; - font-size: .9rem; - color: var(--color); - text-align: center; - border: none; - border-radius: var(--glass-corner-radius); -} -/**** OTHER CLASSES ****/ -.centeringflex { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - padding: 2rem 1rem; -} -.horizontalbuttonlist { - display: flex; - flex-direction: row; - align-items: flex-end; - justify-content: space-between; -} -.horizontalbuttonlist > .button, .horizontalbuttonlist > button, .horizontalbuttonlist > div { - margin: 0 .5rem; -} -.errortext { - margin-top: 1rem; - color: var(--color-error); -} -.nodisplay { - display: none !important; -} -.heading { - margin-top: 0; -} -/* MISC / GENERAL */ -h1 { - text-align: center; - font-size: 1.8rem; -} -/* MOBILE OPTIMIZATIONS */ -@media only screen and (max-width: 700px) { - main { - margin-top: 2rem; - } - .globalmessage span { - width: 90%; - } - .userpanel { - flex-direction: column; - justify-content: start; - align-items: center; - } - .userpanel > .horizontalbuttonlist { - margin-right: 0; - margin-left: 0; - margin-top: .5rem; - justify-content: center; - flex-wrap: wrap; - } - .userpanel > .horizontalbuttonlist > .button, - .userpanel > .horizontalbuttonlist > .dropdownmenu { - margin: 0.25rem; - } -} \ No newline at end of file diff --git a/static/css/statistics.css b/static/css/statistics.css deleted file mode 100644 index 5abc145..0000000 --- a/static/css/statistics.css +++ /dev/null @@ -1,53 +0,0 @@ -.maincontainer { - min-width: 70vw; - display: flex; - flex-direction: column; - align-items: center; - justify-content: start; -} -.tablescontainer { - display: flex; - flex-direction: column; - align-items: center; - justify-content: start; - width: 95%; - margin-top: 2rem; -} -.statisticstable { - margin-bottom: 2rem; - padding-bottom: 1rem; - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; - text-align: center; -} -.statisticstable h1 { - margin-top: 0; - font-size: 1.2rem; - text-align: left; - min-width: 10rem; - text-align: center; -} -.statisticstable table { - min-width: 20vw; - width: fit-content; -} -.statisticstable th:last-child { - text-align: right; -} -.statisticstable td:last-child { - text-align: right; -} -@media only screen and (max-width: 700px) { - .statisticstable h1 { - min-width: 90vw; - } - .statisticstable table { - min-width: 80vw; - } - .statisticstable { - margin-bottom: 2rem; - padding-bottom: 1rem; - } -} \ No newline at end of file