Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

48 changed files with 13654 additions and 19120 deletions

1
.gitignore vendored
View File

@ -10,4 +10,3 @@ caromserver/local_settings.py
/staticfiles/ /staticfiles/
.venv/ .venv/
update.sh update.sh
Pipfile.lock

View File

@ -1,200 +0,0 @@
# Preparing
- [ ] Install and configure Mailsystem (postfix) so it is possible to send mails
- [ ] Install python ```apt install python3 python3-pip python3-venv python3-virtualenv```
- [ ] Install uwsgi ```apt install uwsgi uwsgi-plugin-python3```
- [ ] Install and configure mariadb-server ```mysql_secure_installation```
- [ ] Install and configure nginx und let's encrypt or similar
- [ ] Install git ```apt install git```
# Installation
- [ ] Create databases for carom and carom-int
```
-- carom
CREATE DATABASE carom DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
CREATE USER 'carom'@'localhost' IDENTIFIED BY 'xxx';
GRANT ALL PRIVILEGES ON carom.* TO 'carom'@'localhost';
CREATE DATABASE `carom-int` DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
CREATE USER 'carom-int'@'localhost' IDENTIFIED BY 'xxx';
GRANT ALL PRIVILEGES ON `carom-int`.* TO 'carom-int'@'localhost';
FLUSH PRIVILEGES;
```
Passphrases should be replaced by useful characters
- [ ] Create systemd unit file for uwsgi (/etc/systemd/system/uwsgi.service):
```
[Unit]
Description=uWSGI Emperor service
[Service]
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown www-data:www-data /run/uwsgi'
ExecStart=/usr/bin/uwsgi --emperor /etc/uwsgi/apps-enabled
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target
```
- [ ] Reread systemd configs for uwsgi
```
systemctl daemon-reload
systemctl enable uwsgi.service
systemctl restart uwsgi.service
```
- [ ] Checkout carom
```
cd /srv
git clone http://git.einsle.de/carom/carom-server.git carom
git clone http://git.einsle.de/carom/carom-server.git carom-int
cd carom-int
git checkout develop
git pull
cd ..
```
- [ ] Install pipenv
```
pip3 install upgrade pipenv
```
- [ ] Install mysqlclient
```
apt install libmariadbclient-dev
```
In environments carom run
```
pipenv install mysqlclient
```
```
git checkout -- Pipfile Pipfile.lock
git status
```
- [ ] Create caromserver/local_settings.py for both environments:
```
cd caromserver
cp local_settings_example.py local_settings.py
vi local_settings.py
ALLOWED_HOSTS, ADMINS, DEBUG should be filled
SECRET_KEY use pwgen 50 1 to create content for
DATABASES settings
cd ..
mkdir .venv
pipenv install
pipenv run python manage.py migrate
pipenv run python manage.py collectstatic
```
Do it for /srv/carom and /srv/carom-int
- [ ] Create Superuser Accounts using:
```
pipenv run python manage.py createsuperuser
```
- [ ] Create config File for uwsgi/carom
```
# carom...ini file
[uwsgi]
plugin = python3
chdir = /srv/carom/
module = caromserver.wsgi:application
home = /srv/carom/.venv/
master = true
processes = 5
vacuum = true
uid = www-data
gid = www-data
workers = 2
socket = /run/uwsgi/carom.socket
chmod-socket = 660
log-date = true
```
Create it for /etc/uwsgi/apps-available/carom.ini and carom-int.ini and link it
to /etc/uwsgi/apps-enabled/
```
systemctl restart uwsgi.service
```
Show at syslog for errors and fix it.
- [ ] Create Config File for nginx/carom
```
upstream socket_carom {
server unix:///run/uwsgi/carom.socket;
}
server {
listen 80;
listen [::]:80;
server_name carom...;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name carom...;
ssl_certificate /etc/ssl/certs/xxx;
ssl_certificate_key /etc/ssl/private/xxx;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
charset utf-8;
client_max_body_size 75M; # adjust to taste
location /media {
alias /srv/carom/media;
}
location /static {
alias /srv/carom/staticfiles;
}
location / {
uwsgi_pass socket_carom;
include /etc/nginx/uwsgi_params;
}
}
```
Create it for /etc/ngin/sites-available/carom... and carom-int... and link it
to /etc/ngin/sites-enabled/
Path to certificates must be modified.
```
systemctl restart nginx
```
- [ ] Create update.sh in carom and carom-int root dir
```
pushd /srv/carom/
git pull
pipenv update
pipenv run python manage.py migrate
pipenv run python manage.py collectstatic --noinput
touch /etc/uwsgi/apps-enabled/carom.ini
popd
```
Path to uwsgi config file (in apps-enabled) musst be matching.

18
Pipfile
View File

@ -6,11 +6,13 @@ verify_ssl = true
[dev-packages] [dev-packages]
[packages] [packages]
django = "==3.2.8" django = "==2.1.5"
django-crispy-forms = "==1.13.0" django-crispy-forms = "==1.7.2"
django-debug-toolbar = "==3.2.2" django-debug-toolbar = "==1.11"
django-extensions = "==3.1.3" django-extensions = "==2.1.5"
django-tables2 = "==2.4.1" django-tables2 = "==2.0.4"
djangorestframework = "==3.12.4" djangorestframework = "==3.9.1"
requests = "==2.26.0" requests = "==2.21.0"
django-simple-task = "==0.1.2"
[requires]
python_version = "3.5"

126
Pipfile.lock generated Normal file
View File

@ -0,0 +1,126 @@
{
"_meta": {
"hash": {
"sha256": "de1e65bd4b2342db22fea58996979f48314fcad56bd7b50f4c939035771ef85c"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.5"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"django": {
"hashes": [
"sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8",
"sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3"
],
"index": "pypi",
"version": "==2.1.5"
},
"django-crispy-forms": {
"hashes": [
"sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f",
"sha256:705ededc554ad8736157c666681165fe22ead2dec0d5446d65fc9dd976a5a876"
],
"index": "pypi",
"version": "==1.7.2"
},
"django-debug-toolbar": {
"hashes": [
"sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736",
"sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0"
],
"index": "pypi",
"version": "==1.11"
},
"django-extensions": {
"hashes": [
"sha256:6fcedb2ea660c9dbf9ac59441721ffdd4ab5b753fbd6159c3e28f391a65bab46",
"sha256:a607459e5fa8c579a672131b63366fa52fab80adb2a862d362f5fb48cd2d2cac"
],
"index": "pypi",
"version": "==2.1.5"
},
"django-tables2": {
"hashes": [
"sha256:a893fca1afe2e95b9739c6428cc6c9735a219f65707e24274df3920f61358525",
"sha256:b5f7b4c76160ee927005e52ebea633c86d4529cf84757c0acd5d0434d31798a1"
],
"index": "pypi",
"version": "==2.0.4"
},
"djangorestframework": {
"hashes": [
"sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66",
"sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f"
],
"index": "pypi",
"version": "==3.9.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"index": "pypi",
"version": "==2.21.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"sqlparse": {
"hashes": [
"sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec",
"sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4"
],
"version": "==0.2.4"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
}
},
"develop": {}
}

View File

@ -1 +0,0 @@
default_app_config = 'billard.apps.BillardConfig'

View File

@ -32,14 +32,12 @@ class LocationAdmin(admin.ModelAdmin):
@admin.register(Client) @admin.register(Client)
class ClientAdmin(admin.ModelAdmin): class ClientAdmin(admin.ModelAdmin):
list_display = ('uuid', 'location', 'report_user', 'last_seen') list_display = ('uuid', 'location', 'report_user', 'last_seen')
list_filter = ('location', 'report_user')
fields = ['location', 'uuid', 'report_user', 'last_seen'] fields = ['location', 'uuid', 'report_user', 'last_seen']
@admin.register(ClientData) @admin.register(ClientData)
class ClientDataAdmin(admin.ModelAdmin): class ClientDataAdmin(admin.ModelAdmin):
list_display = ('uuid', 'last_seen') list_display = ('uuid', 'last_seen')
list_filter = ('uuid', 'last_seen')
fields = ['location', 'last_seen'] fields = ['location', 'last_seen']
@ -60,7 +58,6 @@ class LocationDataAdmin(admin.ModelAdmin):
return redirect('admin:billard_locationdata_changelist') return redirect('admin:billard_locationdata_changelist')
list_display = ('client_id', 'desk_no', 'tst', 'on_off', 'processed', 'error_msg') list_display = ('client_id', 'desk_no', 'tst', 'on_off', 'processed', 'error_msg')
list_filter = ('client_id', 'desk_no', 'processed')
fields = ['client_id', 'desk_no', 'tst', 'on_off', 'processed', 'error_msg'] fields = ['client_id', 'desk_no', 'tst', 'on_off', 'processed', 'error_msg']
@ -80,21 +77,14 @@ class DeskAdminForm(forms.ModelForm):
class DeskAdmin(admin.ModelAdmin): class DeskAdmin(admin.ModelAdmin):
form = DeskAdminForm form = DeskAdminForm
list_display = ('client', 'desk_no', 'name', 'enabled', 'prize', 'prize_hh') list_display = ('client', 'desk_no', 'name', 'enabled', 'prize', 'prize_hh')
list_filter = ('client', 'desk_no', 'enabled', 'prize', 'prize_hh')
@admin.register(Accounting) @admin.register(Accounting)
class AccountingAdmin(admin.ModelAdmin): class AccountingAdmin(admin.ModelAdmin):
list_display = ('desk', 'time_from', 'time_to', 'prize', 'billed', 'account_user', 'account_tst') list_display = ('desk', 'time_from', 'time_to', 'prize', 'billed', 'account_user', 'account_tst')
list_filter = ('desk__client__location', 'account_user', 'account_tst', 'billed') list_filter = ('desk__client__location', 'account_user', 'account_tst', 'billed')
actions = ['mark_billed']
def has_add_permission(self, request): def has_add_permission(self, request):
if request.user.username == 'reinsle': if request.user.username == 'reinsle':
return True return True
return False return False
@admin.action(description='Ausgewählte Buchhaltungseinträge als Abgerechnet markieren')
def mark_billed(self, request, queryset):
queryset.update(billed=True)

View File

@ -2,8 +2,4 @@ from django.apps import AppConfig
class BillardConfig(AppConfig): class BillardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'billard' name = 'billard'
def ready(self):
import billard.signals #noqa

View File

@ -1,9 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from billard.tasks import process_location_data
class Command(BaseCommand):
help = 'Process location data objects'
def handle(self, *args, **options):
process_location_data

View File

@ -1,43 +0,0 @@
# Generated by Django 3.2.8 on 2021-10-24 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billard', '0027_clientdata'),
]
operations = [
migrations.AlterField(
model_name='accounting',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='client',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='clientdata',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='desk',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='location',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='locationdata',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,11 +0,0 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django_simple_task import defer
from billard.models import LocationData
from billard.tasks import process_location_data
@receiver(post_save, sender=LocationData)
def update_location_data(sender, instance, **kwargs):
defer(process_location_data(sender=sender, kwargs=kwargs))

View File

@ -1,2 +1,2 @@
{% load static %} {% load static from staticfiles %}
<a href="{% url 'billard:accounting_detail' record.id %}" class="btn btn-outline-danger btn-sm">Abrechnen</a> <a href="{% url 'billard:accounting_detail' record.id %}" class="btn btn-outline-danger btn-sm">Abrechnen</a>

View File

@ -1,3 +1,3 @@
{% load static %} {% load static from staticfiles %}
<a href="{% url 'billard:location_detail' record.id %}" <a href="{% url 'billard:location_detail' record.id %}"
class="btn btn-outline-primary btn-sm">{{ record.code|default_if_none:"" }}</a> class="btn btn-outline-primary btn-sm">{{ record.code|default_if_none:"" }}</a>

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta, date from datetime import datetime, timedelta
from django import template from django import template
from django.db.models import Sum from django.db.models import Sum
@ -10,14 +10,8 @@ register = template.Library()
@register.filter(is_safe=True) @register.filter(is_safe=True)
def display_daily_sale(location): def display_daily_sale(location):
date = datetime.now().replace(hour=6, minute=0, second=0, microsecond=0) start_date = datetime.now().replace(hour=5, minute=0, second=0, microsecond=0)
now = datetime.now() end_date = start_date + timedelta(days=1)
if now < date:
start_date = date - timedelta(days=1)
end_date = date
else:
start_date = date
end_date = date + timedelta(days=1)
prize__sum = Accounting.objects.filter(desk__client__location=location, prize__sum = Accounting.objects.filter(desk__client__location=location,
time_to__range=(start_date, end_date)).aggregate(Sum('prize')) time_to__range=(start_date, end_date)).aggregate(Sum('prize'))
if prize__sum['prize__sum'] is None: if prize__sum['prize__sum'] is None:

View File

@ -1,18 +0,0 @@
"""
ASGI config for caromserver 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/
"""
import os
from django.core.asgi import get_asgi_application
from django_simple_task import django_simple_task_middlware
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'caromserver.settings')
application = get_asgi_application()
application = django_simple_task_middlware(application)

View File

@ -1,9 +1,9 @@
import os
SECRET_KEY = '@-9++2z_6%^vr(f0wax0aq8-pd@0u$*))w!5l^sv#wxrn7k!v-' SECRET_KEY = '@-9++2z_6%^vr(f0wax0aq8-pd@0u$*))w!5l^sv#wxrn7k!v-'
DEBUG = False DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['carom-dev.einsle.de']
ADMINS = [ ADMINS = [('Robert Einsle', 'robert@einsle.de'),]
('Robert Einsle', 'robert@einsle.de'),
]
DATABASES = { DATABASES = {
'default': { 'default': {
@ -14,20 +14,20 @@ DATABASES = {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'OPTIONS': { 'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
}, },
} }
} }
STATIC_ROOT = "/srv/../staticfiles/" STATIC_ROOT = "/srv/carom-dev/carom-server/staticfiles/"
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'mail.server.tld' EMAIL_HOST = 'mail.einsle.de'
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = 'username' EMAIL_HOST_USER = 'relay@einsle.de'
EMAIL_HOST_PASSWORD = 'password' EMAIL_HOST_PASSWORD = 'Boaghi0thaiH'
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = "noreply@carom.de" DEFAULT_FROM_EMAIL = "webmaster@einsle.de"
URL_LOCATION_PROCESSOR = 'https://<carom-server-tld>/billard/process_locationdata' URL_LOCATION_PROCESSOR = 'https://carom-dev.einsle.de/billard/process_locationdata'
PRODUCT_INFO = 'carom' PRODUCT_INFO = 'carom-dev'

View File

@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = '@-9++2z_6%^vr(f0wax0aq8-pd@0u$*))w!5l^sv#wxrn7k!v-' SECRET_KEY = '@-9++2z_6%^vr(f0wax0aq8-pd@0u$*))w!5l^sv#wxrn7k!v-'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
@ -38,7 +38,6 @@ INSTALLED_APPS = [
# third party apps # third party apps
'crispy_forms', 'crispy_forms',
'debug_toolbar', 'debug_toolbar',
'django_simple_task',
'django_tables2', 'django_tables2',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
@ -163,15 +162,13 @@ EMAIL_PORT = 25
URL_LOCATION_PROCESSOR = 'http://127.0.0.1:8000/billard/process_locationdata' URL_LOCATION_PROCESSOR = 'http://127.0.0.1:8000/billard/process_locationdata'
PRODUCT_INFO = 'CAROM' PRODUCT_INFO = 'CAROM-DEV'
PRODUCT_VERSION = 'v 1.0.5' PRODUCT_VERSION = 'v 1.0.0'
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html' DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
try: try:
from local_settings import * from local_settings import *
except ImportError: except ImportError:

View File

@ -4,13 +4,13 @@ WSGI config for caromserver project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'caromserver.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "caromserver.settings")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,22 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
if __name__ == "__main__":
def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "caromserver.settings")
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'caromserver.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError:
raise ImportError( # The above import may fail for some other reason. Ensure that the
"Couldn't import Django. Are you sure it's installed and " # issue is really that Django is missing to avoid masking other
"available on your PYTHONPATH environment variable? Did you " # exceptions on Python 2.
"forget to activate a virtual environment?" try:
) from exc import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

15064
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

6551
static/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

326
static/js/html5shiv.js vendored
View File

@ -1,326 +0,0 @@
/**
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
;(function(window, document) {
/*jshint evil:true */
/** version */
var version = '3.7.3';
/** Preset options */
var options = window.html5 || {};
/** Used to skip problem elements */
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
/** Not all elements can be cloned in IE **/
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
/** Detect whether the browser supports default html5 styles */
var supportsHtml5Styles;
/** Name of the expando, to work with multiple documents or to re-shiv one document */
var expando = '_html5shiv';
/** The id for the the documents expando */
var expanID = 0;
/** Cached data for each document */
var expandoData = {};
/** Detect whether the browser supports unknown elements */
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
// assign a false positive if unable to shiv
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
// assign a false positive if detection fails => unable to shiv
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
/*--------------------------------------------------------------------------*/
/**
* Creates a style sheet with the given CSS text and adds it to the document.
* @private
* @param {Document} ownerDocument The document.
* @param {String} cssText The CSS text.
* @returns {StyleSheet} The style element.
*/
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
/**
* Returns the value of `html5.elements` as an array.
* @private
* @returns {Array} An array of shived element node names.
*/
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
/**
* Extends the built-in list of html5 elements
* @memberOf html5
* @param {String|Array} newElements whitespace separated list or array of new element names to shiv
* @param {Document} ownerDocument The context document.
*/
function addElements(newElements, ownerDocument) {
var elements = html5.elements;
if(typeof elements != 'string'){
elements = elements.join(' ');
}
if(typeof newElements != 'string'){
newElements = newElements.join(' ');
}
html5.elements = elements +' '+ newElements;
shivDocument(ownerDocument);
}
/**
* Returns the data associated to the given document
* @private
* @param {Document} ownerDocument The document.
* @returns {Object} An object of data.
*/
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
/**
* returns a shived element for the given nodeName and document
* @memberOf html5
* @param {String} nodeName name of the element
* @param {Document|DocumentFragment} ownerDocument The context document.
* @returns {Object} The shived element.
*/
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
// Avoid adding some elements to fragments in IE < 9 because
// * Attributes like `name` or `type` cannot be set/changed once an element
// is inserted into a document/fragment
// * Link elements with `src` attributes that are inaccessible, as with
// a 403 response, will cause the tab/window to crash
// * Script elements appended to fragments will execute when their `src`
// or `text` property is set
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
/**
* returns a shived DocumentFragment for the given document
* @memberOf html5
* @param {Document} ownerDocument The context document.
* @returns {Object} The shived DocumentFragment.
*/
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
/**
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
* @private
* @param {Document|DocumentFragment} ownerDocument The document.
* @param {Object} data of the document.
*/
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
//abort shiv
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
// unroll the `createElement` calls
getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
/*--------------------------------------------------------------------------*/
/**
* Shivs the given document.
* @memberOf html5
* @param {Document} ownerDocument The document to shiv.
* @returns {Document} The shived document.
*/
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
// corrects block display not defined in IE6/7/8/9
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
// adds styling not present in IE6/7/8/9
'mark{background:#FF0;color:#000}' +
// hides non-rendered elements
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
/*--------------------------------------------------------------------------*/
/**
* The `html5` object is exposed so that more elements can be shived and
* existing shiving can be detected on iframes.
* @type Object
* @example
*
* // options can be changed before the script is included
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
*/
var html5 = {
/**
* An array or space separated string of node names of the elements to shiv.
* @memberOf html5
* @type Array|String
*/
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
/**
* current version of html5shiv
*/
'version': version,
/**
* A flag to indicate that the HTML5 style sheet should be inserted.
* @memberOf html5
* @type Boolean
*/
'shivCSS': (options.shivCSS !== false),
/**
* Is equal to true if a browser supports creating unknown/HTML5 elements
* @memberOf html5
* @type boolean
*/
'supportsUnknownElements': supportsUnknownElements,
/**
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
* methods should be overwritten.
* @memberOf html5
* @type Boolean
*/
'shivMethods': (options.shivMethods !== false),
/**
* A string to describe the type of `html5` object ("default" or "default print").
* @memberOf html5
* @type String
*/
'type': 'default',
// shivs the document according to the specified `html5` object options
'shivDocument': shivDocument,
//creates a shived element
createElement: createElement,
//creates a shived documentFragment
createDocumentFragment: createDocumentFragment,
//extends list of elements
addElements: addElements
};
/*--------------------------------------------------------------------------*/
// expose html5
window.html5 = html5;
// shiv the document
shivDocument(document);
if(typeof module == 'object' && module.exports){
module.exports = html5;
}
}(typeof window !== "undefined" ? window : this, document));

File diff suppressed because it is too large Load Diff

2
static/js/jquery-3.3.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

2
static/js/jquery-3.3.1.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
static/js/npm.js Normal file
View File

@ -0,0 +1,13 @@
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
require('../../js/transition.js')
require('../../js/alert.js')
require('../../js/button.js')
require('../../js/carousel.js')
require('../../js/collapse.js')
require('../../js/dropdown.js')
require('../../js/modal.js')
require('../../js/tooltip.js')
require('../../js/popover.js')
require('../../js/scrollspy.js')
require('../../js/tab.js')
require('../../js/affix.js')

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,237 +0,0 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill
* Copyright 2014 Scott Jehl
* Licensed under MIT
* https://j.mp/respondjs */
/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */
/*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */
(function(w) {
"use strict";
w.matchMedia = w.matchMedia || function(doc, undefined) {
var bool, docElem = doc.documentElement, refNode = docElem.firstElementChild || docElem.firstChild, fakeBody = doc.createElement("body"), div = doc.createElement("div");
div.id = "mq-test-1";
div.style.cssText = "position:absolute;top:-100em";
fakeBody.style.background = "none";
fakeBody.appendChild(div);
return function(q) {
div.innerHTML = '&shy;<style media="' + q + '"> #mq-test-1 { width: 42px; }</style>';
docElem.insertBefore(fakeBody, refNode);
bool = div.offsetWidth === 42;
docElem.removeChild(fakeBody);
return {
matches: bool,
media: q
};
};
}(w.document);
})(this);
(function(w) {
"use strict";
var respond = {};
w.respond = respond;
respond.update = function() {};
var requestQueue = [], xmlHttp = function() {
var xmlhttpmethod = false;
try {
xmlhttpmethod = new w.XMLHttpRequest();
} catch (e) {
xmlhttpmethod = new w.ActiveXObject("Microsoft.XMLHTTP");
}
return function() {
return xmlhttpmethod;
};
}(), ajax = function(url, callback) {
var req = xmlHttp();
if (!req) {
return;
}
req.open("GET", url, true);
req.onreadystatechange = function() {
if (req.readyState !== 4 || req.status !== 200 && req.status !== 304) {
return;
}
callback(req.responseText);
};
if (req.readyState === 4) {
return;
}
req.send(null);
}, isUnsupportedMediaQuery = function(query) {
return query.replace(respond.regex.minmaxwh, "").match(respond.regex.other);
};
respond.ajax = ajax;
respond.queue = requestQueue;
respond.unsupportedmq = isUnsupportedMediaQuery;
respond.regex = {
media: /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,
keyframes: /@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,
comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,
urls: /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,
findStyles: /@media *([^\{]+)\{([\S\s]+?)$/,
only: /(only\s+)?([a-zA-Z]+)\s?/,
minw: /\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
maxw: /\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
minmaxwh: /\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,
other: /\([^\)]*\)/g
};
respond.mediaQueriesSupported = w.matchMedia && w.matchMedia("only all") !== null && w.matchMedia("only all").matches;
if (respond.mediaQueriesSupported) {
return;
}
var doc = w.document, docElem = doc.documentElement, mediastyles = [], rules = [], appendedEls = [], parsedSheets = {}, resizeThrottle = 30, head = doc.getElementsByTagName("head")[0] || docElem, base = doc.getElementsByTagName("base")[0], links = head.getElementsByTagName("link"), lastCall, resizeDefer, eminpx, getEmValue = function() {
var ret, div = doc.createElement("div"), body = doc.body, originalHTMLFontSize = docElem.style.fontSize, originalBodyFontSize = body && body.style.fontSize, fakeUsed = false;
div.style.cssText = "position:absolute;font-size:1em;width:1em";
if (!body) {
body = fakeUsed = doc.createElement("body");
body.style.background = "none";
}
docElem.style.fontSize = "100%";
body.style.fontSize = "100%";
body.appendChild(div);
if (fakeUsed) {
docElem.insertBefore(body, docElem.firstChild);
}
ret = div.offsetWidth;
if (fakeUsed) {
docElem.removeChild(body);
} else {
body.removeChild(div);
}
docElem.style.fontSize = originalHTMLFontSize;
if (originalBodyFontSize) {
body.style.fontSize = originalBodyFontSize;
}
ret = eminpx = parseFloat(ret);
return ret;
}, applyMedia = function(fromResize) {
var name = "clientWidth", docElemProp = docElem[name], currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[name] || docElemProp, styleBlocks = {}, lastLink = links[links.length - 1], now = new Date().getTime();
if (fromResize && lastCall && now - lastCall < resizeThrottle) {
w.clearTimeout(resizeDefer);
resizeDefer = w.setTimeout(applyMedia, resizeThrottle);
return;
} else {
lastCall = now;
}
for (var i in mediastyles) {
if (mediastyles.hasOwnProperty(i)) {
var thisstyle = mediastyles[i], min = thisstyle.minw, max = thisstyle.maxw, minnull = min === null, maxnull = max === null, em = "em";
if (!!min) {
min = parseFloat(min) * (min.indexOf(em) > -1 ? eminpx || getEmValue() : 1);
}
if (!!max) {
max = parseFloat(max) * (max.indexOf(em) > -1 ? eminpx || getEmValue() : 1);
}
if (!thisstyle.hasquery || (!minnull || !maxnull) && (minnull || currWidth >= min) && (maxnull || currWidth <= max)) {
if (!styleBlocks[thisstyle.media]) {
styleBlocks[thisstyle.media] = [];
}
styleBlocks[thisstyle.media].push(rules[thisstyle.rules]);
}
}
}
for (var j in appendedEls) {
if (appendedEls.hasOwnProperty(j)) {
if (appendedEls[j] && appendedEls[j].parentNode === head) {
head.removeChild(appendedEls[j]);
}
}
}
appendedEls.length = 0;
for (var k in styleBlocks) {
if (styleBlocks.hasOwnProperty(k)) {
var ss = doc.createElement("style"), css = styleBlocks[k].join("\n");
ss.type = "text/css";
ss.media = k;
head.insertBefore(ss, lastLink.nextSibling);
if (ss.styleSheet) {
ss.styleSheet.cssText = css;
} else {
ss.appendChild(doc.createTextNode(css));
}
appendedEls.push(ss);
}
}
}, translate = function(styles, href, media) {
var qs = styles.replace(respond.regex.comments, "").replace(respond.regex.keyframes, "").match(respond.regex.media), ql = qs && qs.length || 0;
href = href.substring(0, href.lastIndexOf("/"));
var repUrls = function(css) {
return css.replace(respond.regex.urls, "$1" + href + "$2$3");
}, useMedia = !ql && media;
if (href.length) {
href += "/";
}
if (useMedia) {
ql = 1;
}
for (var i = 0; i < ql; i++) {
var fullq, thisq, eachq, eql;
if (useMedia) {
fullq = media;
rules.push(repUrls(styles));
} else {
fullq = qs[i].match(respond.regex.findStyles) && RegExp.$1;
rules.push(RegExp.$2 && repUrls(RegExp.$2));
}
eachq = fullq.split(",");
eql = eachq.length;
for (var j = 0; j < eql; j++) {
thisq = eachq[j];
if (isUnsupportedMediaQuery(thisq)) {
continue;
}
mediastyles.push({
media: thisq.split("(")[0].match(respond.regex.only) && RegExp.$2 || "all",
rules: rules.length - 1,
hasquery: thisq.indexOf("(") > -1,
minw: thisq.match(respond.regex.minw) && parseFloat(RegExp.$1) + (RegExp.$2 || ""),
maxw: thisq.match(respond.regex.maxw) && parseFloat(RegExp.$1) + (RegExp.$2 || "")
});
}
}
applyMedia();
}, makeRequests = function() {
if (requestQueue.length) {
var thisRequest = requestQueue.shift();
ajax(thisRequest.href, function(styles) {
translate(styles, thisRequest.href, thisRequest.media);
parsedSheets[thisRequest.href] = true;
w.setTimeout(function() {
makeRequests();
}, 0);
});
}
}, ripCSS = function() {
for (var i = 0; i < links.length; i++) {
var sheet = links[i], href = sheet.href, media = sheet.media, isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet";
if (!!href && isCSS && !parsedSheets[href]) {
if (sheet.styleSheet && sheet.styleSheet.rawCssText) {
translate(sheet.styleSheet.rawCssText, href, media);
parsedSheets[href] = true;
} else {
if (!/^([a-zA-Z:]*\/\/)/.test(href) && !base || href.replace(RegExp.$1, "").split("/")[0] === w.location.host) {
if (href.substring(0, 2) === "//") {
href = w.location.protocol + href;
}
requestQueue.push({
href: href,
media: media
});
}
}
}
}
makeRequests();
};
ripCSS();
respond.update = ripCSS;
respond.getEmValue = getEmValue;
function callMedia() {
applyMedia(true);
}
if (w.addEventListener) {
w.addEventListener("resize", callMedia, false);
} else if (w.attachEvent) {
w.attachEvent("onresize", callMedia);
}
})(this);

View File

@ -8,7 +8,6 @@
<meta name="description" content="carom billard management"> <meta name="description" content="carom billard management">
<meta name="author" content="Robert Einsle <robert@einsle.de>"> <meta name="author" content="Robert Einsle <robert@einsle.de>">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/billiard.ico' %}">
{% block header %} {% block header %}
{% endblock %} {% endblock %}
<!-- Bootstrap --> <!-- Bootstrap -->
@ -25,43 +24,43 @@
<body> <body>
{% block body %} {% block body %}
<div class="wrapper"> <div class="wrapper">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="container"> <div class="container">
<a class="navbar-brand" href="{% url 'index' %}">{% settings_value "PRODUCT_INFO" %}</a> <a class="navbar-brand" href="{% url 'index' %}">{% settings_value "PRODUCT_INFO" %}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportetContent"> <div class="collapse navbar-collapse" id="mainMenu">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'billard:location_index' %}">Standorte</a> <a class="nav-link" href="{% url 'billard:location_index' %}">Standorte</a>
</li> </li>
{% if user.is_authenticated %} </ul>
{% if user.is_authenticated %}
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userMenu" role="button" <a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown"
data-bs-toggle="dropdown" aria-expanded="false"> aria-haspopup="true" aria-expanded="false">
{{ user.username }} {{ user.username }}
</a> </a>
<ul class="dropdown-menu" aria-labelledby="userMenu"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
<li><a class="dropdown-item" href="{% url 'billard:my_account' %}">My account</a> <a class="dropdown-item" href="{% url 'billard:my_account' %}">My account</a>
</li>
{% if user.is_superuser %} {% if user.is_superuser %}
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Administration</a> <a class="dropdown-item" href="/admin">Administration</a>
</li>
{% endif %} {% endif %}
<li><a class="dropdown-item" href="#">{% settings_value "PRODUCT_VERSION" %}</a> <a class="disabled dropdown-item"
</li> href="#">{% settings_value "PRODUCT_VERSION" %}</a>
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li> <div class="dropdown-divider"></div>
</ul> <a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
</div>
</li> </li>
{% else %} </ul>
<form class="form-inline ml-auto"> {% else %}
<a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a> <form class="form-inline ml-auto">
</form> <a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
{% endif %} </form>
</ul> {% endif %}
</div> </div>
</div> </div>
</nav> </nav>
@ -76,7 +75,7 @@
<div class="push"></div> <div class="push"></div>
</div> </div>
{% endblock body %} {% endblock body %}
<script src="{% static 'js/jquery-3.6.0.min.js' %}" type="text/javascript"></script> <script src="{% static 'js/jquery-3.3.1.min.js' %}" type="text/javascript"></script>
<script src="{% static 'js/popper.min.js' %}" type="text/javascript"></script> <script src="{% static 'js/popper.min.js' %}" type="text/javascript"></script>
<script src="{% static 'js/bootstrap.min.js' %}" type="text/javascript"></script> <script src="{% static 'js/bootstrap.min.js' %}" type="text/javascript"></script>
<script src="{% static 'js/carom.js' %}" type="text/javascript"></script> <script src="{% static 'js/carom.js' %}" type="text/javascript"></script>

View File

@ -1,5 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load static %} {% load static from staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load form_tags %} {% load form_tags %}
{% block title %}Anmelden{% endblock %} {% block title %}Anmelden{% endblock %}