28 Commits

Author SHA1 Message Date
28d3d99754 update account holder in wiref file 2025-11-17 12:17:46 +01:00
78c2860cca update etherpad to 2.5.2; fix charset and add auth method 2025-11-04 22:02:07 +01:00
2024466a48 Fix the help text 2025-11-04 18:49:03 +01:00
72570e25c2 add firstname and surname to search field 2025-11-03 18:46:24 +01:00
a3b252c9be Fix: Find the current last day; Use the correct get now time function 2025-10-31 13:30:43 +01:00
b0e686245a Merge branch 'master' of https://git.fet.at/bofh/fet2020 2025-10-31 13:04:28 +01:00
sebivh
b50c010b3b Add persistant Storage to Container Databases 2025-10-30 23:21:25 +01:00
be581675cd update homepage version to 2.2.1 2025-10-30 15:42:48 +01:00
c1519bab0f add migrations 2025-10-30 15:40:49 +01:00
585bc60676 update assets 2025-10-30 15:39:58 +01:00
b9943b7d41 add help text for status 2025-10-30 15:20:14 +01:00
65ac5ae18e fix: sending mail; add email host user and pwd for authentication 2025-10-30 15:05:36 +01:00
8ff3905657 Add total deposit calculation to Rental model 2025-10-30 15:00:20 +01:00
5d2a052c1e add internal rentals with zero deposit 2025-10-30 14:05:53 +01:00
4a7076b120 fix: third line in products in pdf file 2025-10-30 13:59:00 +01:00
2204c07deb fix: path to pdf file 2025-10-30 13:58:11 +01:00
07449db128 add migrations 2025-10-30 13:06:28 +01:00
5d9ad679de Fix: auto-split rentals with >5 items; update create_done text 2025-10-30 12:59:11 +01:00
0e1a61cefc sorted all rentalitems alphabetically 2025-10-30 12:45:01 +01:00
4d35e498c5 fix: wrong binding for checked box 2025-10-30 10:32:46 +01:00
370577493a optimize function 2025-10-29 22:36:11 +01:00
e10fa77c3a add week view 2025-10-29 22:29:10 +01:00
9cc1068e63 Sort image list alphabetically 2025-10-27 20:18:21 +01:00
9eaf3ecdd8 add migrations 2025-10-27 20:18:01 +01:00
0bb313bbed fix gitignore 2025-10-27 20:16:05 +01:00
6e57c28d4b add conf to have access to folder gallery 2025-10-27 20:02:33 +01:00
153e937bfe add conf to have access to folder assets 2025-10-27 20:00:58 +01:00
719774dcf4 delete the word 'Honorarnoten' 2025-10-26 23:57:37 +01:00
33 changed files with 1721 additions and 1362 deletions

43
.gitignore vendored
View File

@@ -1,21 +1,22 @@
.env/* .env/*
*.pyc *.pyc
*_design1 *_design1
fet2020/.env/* fet2020/.env/*
*.sqlite3 *.sqlite3
.theia/* .theia/*
.flake8 .flake8
migrate migrate
run run
*.pid *.pid
*~ *~
APIKEY.txt APIKEY.txt
tmp tmp
.ruff_cache .ruff_cache
.venv .venv
etherpad etherpad
files files
flowbite flowbite
gallery gallery/*
tailwind tailwind
whoosh_index whoosh_index
databases/django

View File

@@ -96,6 +96,8 @@ docker build -t django-nginx-image -f nginx/Dockerfile ./nginx
### Start docker container ### Start docker container
Add email password for 'Verleih' account to EMAIL_HOST_PASSWORD in the docker compose file!
Build the docker containers: Build the docker containers:
```bash ```bash
@@ -134,6 +136,10 @@ ckeditor -> django-prose-editor
## Version History ## Version History
2.2.1
* Fix rental (view, pdf file, sending mail)
2.2.0 2.2.0
* Add rental * Add rental

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,9 @@ services:
depends_on: depends_on:
- django-homepage - django-homepage
volumes: volumes:
- files-volume:/usr/src/app/files - ./files:/usr/src/app/files
- ./gallery:/usr/src/app/files/uploads/gallery
- ./assets:/usr/src/app/assets:ro
networks: networks:
- fet-network - fet-network
django-homepage: django-homepage:
@@ -23,6 +25,8 @@ services:
MYSQL_USER: "user" MYSQL_USER: "user"
MYSQL_PASSWORD: "hgu" MYSQL_PASSWORD: "hgu"
ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3" ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3"
EMAIL_HOST_USER: "verleih@fet.at"
EMAIL_HOST_PASSWORD: ""
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
@@ -32,6 +36,7 @@ services:
- ./fet2020:/usr/src/app - ./fet2020:/usr/src/app
- ./gallery:/usr/src/app/files/uploads/gallery:shared - ./gallery:/usr/src/app/files/uploads/gallery:shared
- files-volume:/usr/src/app/files - files-volume:/usr/src/app/files
- ./assets:/usr/src/app/assets:ro
networks: networks:
- fet-network - fet-network
- django-db-network - django-db-network
@@ -42,7 +47,7 @@ services:
retries: 20 retries: 20
etherpad: etherpad:
container_name: etherpad-container container_name: etherpad-container
image: etherpad/etherpad:1.8.17 image: etherpad/etherpad:2.5.2
# ports: # ports:
# - 9001:9001 # - 9001:9001
environment: environment:
@@ -52,8 +57,9 @@ services:
DB_NAME: etherpaddb DB_NAME: etherpaddb
DB_USER: user DB_USER: user
DB_PASS: "hgu" DB_PASS: "hgu"
DB_CHARSET: utf8 DB_CHARSET: "utf8mb4"
TRUST_PROXY: false TRUST_PROXY: false
AUTHENTICATION_METHOD: "apikey"
depends_on: depends_on:
etherpadsql: etherpadsql:
condition: "service_healthy" condition: "service_healthy"
@@ -79,7 +85,8 @@ services:
MYSQL_CHARSET: utf8 MYSQL_CHARSET: utf8
MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes: volumes:
- mysql-volume:/docker-entrypoint-initdb.d/ - ./inits/django:/docker-entrypoint-initdb.d/
- ./databases/django:/var/lib/mysql:Z
networks: networks:
- django-db-network - django-db-network
healthcheck: healthcheck:
@@ -97,7 +104,8 @@ services:
MYSQL_CHARSET: utf8 MYSQL_CHARSET: utf8
MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes: volumes:
- etherpad-mysql-volume:/docker-entrypoint-initdb.d/ - ./init/etherpad:/docker-entrypoint-initdb.d/
- ./databases/etherpad:/var/lib/mysql:Z
networks: networks:
- etherpad-db-network - etherpad-db-network
healthcheck: healthcheck:

View File

@@ -1,6 +1,6 @@
from django.utils.version import get_version from django.utils.version import get_version
VERSION = (2, 2, 0, "final", 0) VERSION = (2, 2, 1, "final", 0)
BUILD = 0 BUILD = 0
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

View File

@@ -18,6 +18,8 @@ env = environ.Env(
ETHERPAD_GROUP=(str, ""), ETHERPAD_GROUP=(str, ""),
GALLERY_PATH=(str, "uploads/gallery"), GALLERY_PATH=(str, "uploads/gallery"),
MC_MASTERPASSWORD=(str, ""), MC_MASTERPASSWORD=(str, ""),
EMAIL_HOST_USER=(str, ""),
EMAIL_HOST_PASSWORD=(str, ""),
) )
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -103,6 +105,8 @@ else:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "buran.htu.tuwien.ac.at" EMAIL_HOST = "buran.htu.tuwien.ac.at"
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True EMAIL_USE_TLS = True

View File

@@ -204,7 +204,12 @@ class BillAdmin(admin.ModelAdmin):
actions = ["make_cleared", "make_finished"] actions = ["make_cleared", "make_finished"]
autocomplete_fields = ["resolution"] autocomplete_fields = ["resolution"]
list_filter = ["status", "affiliation", "payer", BillPeriodeFilter] list_filter = ["status", "affiliation", "payer", BillPeriodeFilter]
search_fields = ["purpose", "bankdata__name"] search_fields = [
"purpose",
"bankdata__name",
"bill_creator__firstname",
"bill_creator__surname",
]
show_facets = admin.ShowFacets.ALWAYS show_facets = admin.ShowFacets.ALWAYS
ordering = ["-id"] ordering = ["-id"]

View File

@@ -6,6 +6,7 @@ from django import forms
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db.models import Count, Q from django.db.models import Count, Q
from django.forms import DateInput from django.forms import DateInput
from django.utils import timezone
from members.models import Member from members.models import Member
@@ -412,7 +413,7 @@ class BillAdminForm(forms.ModelForm):
self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter( self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter(
( (
Q(option=Resolution.Option.FINANCE) Q(option=Resolution.Option.FINANCE)
& Q(date__gt=datetime.datetime.now(tz=datetime.UTC).date() - relativedelta(years=2)) & Q(date__gt=timezone.now().date() - relativedelta(years=2))
) )
| Q(option=Resolution.Option.PERMANENT) | Q(option=Resolution.Option.PERMANENT)
) )

View File

@@ -1,8 +1,8 @@
import datetime
import io import io
from pathlib import Path from pathlib import Path
from django.core.files import File from django.core.files import File
from django.utils import timezone
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814 from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814
@@ -34,7 +34,7 @@ def generate_pdf(wiref):
) )
# Get budget year # Get budget year
today = datetime.datetime.now(tz=datetime.UTC).date() today = timezone.now().date()
if today.month < 7: if today.month < 7:
budget_year = f"{today.year - 1}-{today.year}" budget_year = f"{today.year - 1}-{today.year}"
else: else:

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-10-27 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_album_event_place'),
]
operations = [
migrations.AlterField(
model_name='album',
name='description',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='album',
name='photographer',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='Fotograph(en)'),
),
migrations.AlterField(
model_name='album',
name='thumbnail',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='Thumbnail'),
),
]

View File

@@ -1,58 +1,60 @@
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
from django.core.validators import get_available_image_extensions from django.core.validators import get_available_image_extensions
from PIL import Image, ImageOps from PIL import Image, ImageOps
gallery_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["path"] gallery_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["path"]
gallery_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["path"] gallery_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["path"]
gallery_thumb_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["thumb_path"] gallery_thumb_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["thumb_path"]
gallery_thumb_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["thumb_path"] gallery_thumb_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["thumb_path"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
size = (320, 320) size = (320, 320)
Image.logger.setLevel(level=logging.INFO) Image.logger.setLevel(level=logging.INFO)
def get_image_list(folder_name: str) -> list: def get_image_list(folder_name: str) -> list:
image_path = Path(gallery_path) / folder_name image_path = Path(gallery_path) / folder_name
thumb_path = Path(gallery_thumb_path) / folder_name thumb_path = Path(gallery_thumb_path) / folder_name
img_list = [] img_list = []
if not Path(image_path).exists(): if not Path(image_path).exists():
logger.info("Image path '%s' not found.", image_path) logger.info("Image path '%s' not found.", image_path)
return img_list return img_list
Path(thumb_path).mkdir(exist_ok=True) Path(thumb_path).mkdir(exist_ok=True)
for _file in os.listdir(image_path): for _file in os.listdir(image_path):
if Path(_file).suffix.lower()[1:] not in get_available_image_extensions(): if Path(_file).suffix.lower()[1:] not in get_available_image_extensions():
continue continue
thumb_file_path = Path(thumb_path) / f"thumb_{_file}" thumb_file_path = Path(thumb_path) / f"thumb_{_file}"
if not Path(thumb_file_path).exists(): if not Path(thumb_file_path).exists():
with Image.open(Path(image_path) / _file, "r") as im: with Image.open(Path(image_path) / _file, "r") as im:
if im._getexif() is not None: if im._getexif() is not None:
im = ImageOps.exif_transpose(im) im = ImageOps.exif_transpose(im)
thumb = ImageOps.fit(im, size, Image.Resampling.LANCZOS) thumb = ImageOps.fit(im, size, Image.Resampling.LANCZOS)
thumb.save(thumb_file_path) thumb.save(thumb_file_path)
logger.info("Save thumb 'thumb_%s'.", _file) logger.info("Save thumb 'thumb_%s'.", _file)
img_dict = { img_dict = {
"title": _file, "title": _file,
"image_url": Path(gallery_path_url) / folder_name / _file, "image_url": Path(gallery_path_url) / folder_name / _file,
"thumb_url": Path(gallery_thumb_path_url) / folder_name / f"thumb_{_file}", "thumb_url": Path(gallery_thumb_path_url) / folder_name / f"thumb_{_file}",
} }
img_list.append(img_dict) img_list.append(img_dict)
return img_list # Sort images alphabetically by filename (case-insensitive) to ensure consistent ordering.
# Directory listings may return files in different orders.
return sorted(img_list, key=lambda x: x["title"].lower())
def get_folder_list():
if Path(gallery_path).exists():
return next(os.walk(gallery_path))[1] def get_folder_list():
if Path(gallery_path).exists():
return None return next(os.walk(gallery_path))[1]
return None

View File

@@ -1,275 +1,275 @@
from ckeditor_uploader.widgets import CKEditorUploadingWidget from ckeditor_uploader.widgets import CKEditorUploadingWidget
from django import forms from django import forms
from django.forms.widgets import CheckboxInput from django.forms.widgets import CheckboxInput
from django.utils.dates import MONTHS from django.utils.dates import MONTHS
from taggit.models import Tag from taggit.models import Tag
from .models import Event, FetMeeting, News, Post from .models import Event, FetMeeting, News, Post
class PostForm(forms.ModelForm): class PostForm(forms.ModelForm):
class Meta: class Meta:
model = Post model = Post
fields = [ fields = [
"title", "title",
"subtitle", "subtitle",
"tags", "tags",
"image", "image",
"body", "body",
"slug", "slug",
"author", "author",
"public_date", "public_date",
] ]
widgets = {"body": CKEditorUploadingWidget(config_name="default")} widgets = {"body": CKEditorUploadingWidget(config_name="default")}
class Media: class Media:
js = ( js = (
"js/auto_slug.js", # automatic slag completion via ajax "js/auto_slug.js", # automatic slag completion via ajax
"js/tag_completion.js", # to get a list for tag autocompletion via ajax "js/tag_completion.js", # to get a list for tag autocompletion via ajax
) )
class NewsForm(PostForm): class NewsForm(PostForm):
class Meta: class Meta:
model = News model = News
fields = "__all__" fields = "__all__"
help_texts = { help_texts = {
"tags": ( "tags": (
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen." "Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
), ),
"image": "Verwendbare Formate: ...", "image": "Verwendbare Formate: ...",
"is_pinned": ( "is_pinned": (
"Der Post soll als erster auf der Startseite angeheftet werden und sich " "Der Post soll als erster auf der Startseite angeheftet werden und sich "
"automatisch einen Monat nach der Veröffentlichung wieder lösen." "automatisch einen Monat nach der Veröffentlichung wieder lösen."
), ),
} }
labels = { labels = {
"title": "Titel", "title": "Titel",
"subtitle": "Untertitel", "subtitle": "Untertitel",
"image": "Hintergrundbild", "image": "Hintergrundbild",
"body": "Text", "body": "Text",
"slug": "Permalink", "slug": "Permalink",
"author": "Autor", "author": "Autor",
"public_date": "Veröffentlichung", "public_date": "Veröffentlichung",
"is_pinned": "Post anheften", "is_pinned": "Post anheften",
} }
widgets = {"body": CKEditorUploadingWidget(config_name="default")} widgets = {"body": CKEditorUploadingWidget(config_name="default")}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
author_qs = self.fields["author"].queryset.order_by("username") author_qs = self.fields["author"].queryset.order_by("username")
self.fields["author"].queryset = author_qs self.fields["author"].queryset = author_qs
class EventForm(PostForm): class EventForm(PostForm):
class Meta: class Meta:
model = Event model = Event
fields = "__all__" fields = "__all__"
help_texts = { help_texts = {
"tags": ( "tags": (
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen." "Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
), ),
"image": "Verwendbare Formate: Bildformate", "image": "Verwendbare Formate: Bildformate",
"is_pinned": ( "is_pinned": (
"Dieses Event soll als erstes auf der Startseite angeheftet werden und sich " "Dieses Event soll als erstes auf der Startseite angeheftet werden und sich "
"automatisch einen Monat nach der Veröffentlichung wieder lösen." "automatisch ein Tag nach dem Eventende wieder lösen."
), ),
} }
labels = { labels = {
"title": "Titel", "title": "Titel",
"subtitle": "Untertitel", "subtitle": "Untertitel",
"image": "Hintergrundbild", "image": "Hintergrundbild",
"body": "Text", "body": "Text",
"event_start": "Start des Events", "event_start": "Start des Events",
"event_end": "Ende des Events", "event_end": "Ende des Events",
"event_place": "Ort des Events", "event_place": "Ort des Events",
"slug": "Permalink", "slug": "Permalink",
"author": "Autor", "author": "Autor",
"public_date": "Veröffentlichung", "public_date": "Veröffentlichung",
"is_pinned": "Event anheften", "is_pinned": "Event anheften",
} }
widgets = {"body": CKEditorUploadingWidget(config_name="default")} widgets = {"body": CKEditorUploadingWidget(config_name="default")}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
author_qs = self.fields["author"].queryset.order_by("username") author_qs = self.fields["author"].queryset.order_by("username")
self.fields["author"].queryset = author_qs self.fields["author"].queryset = author_qs
self.fields["event_start"].required = True self.fields["event_start"].required = True
self.fields["event_end"].required = False self.fields["event_end"].required = False
if "event_place" in self.fields: if "event_place" in self.fields:
self.fields["event_place"].required = True self.fields["event_place"].required = True
class FetMeetingForm(PostForm): class FetMeetingForm(PostForm):
class Meta: class Meta:
model = FetMeeting model = FetMeeting
fields = ["event_start", "event_end", "event_place", "tags"] fields = ["event_start", "event_end", "event_place", "tags"]
labels = { labels = {
"event_start": "Start der Sitzung", "event_start": "Start der Sitzung",
"event_end": "Ende der Sitzung", "event_end": "Ende der Sitzung",
"event_place": "Ort der Sitzung", "event_place": "Ort der Sitzung",
} }
help_texts = { help_texts = {
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.", "event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
"tags": ( "tags": (
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen." "Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
), ),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["event_start"].required = True self.fields["event_start"].required = True
self.fields["event_end"].required = False self.fields["event_end"].required = False
self.fields["event_place"].initial = "FET" self.fields["event_place"].initial = "FET"
tags = [] tags = []
tags.append(Tag()) tags.append(Tag())
tags[0].name = "fachschaft" tags[0].name = "fachschaft"
self.fields["tags"].initial = tags self.fields["tags"].initial = tags
class PostSearchForm(forms.Form): class PostSearchForm(forms.Form):
year_choices = [("", "Alle")] year_choices = [("", "Alle")]
month_choices = [("", "Alle")] + list(MONTHS.items()) month_choices = [("", "Alle")] + list(MONTHS.items())
year = forms.ChoiceField(label="Jahr", choices=year_choices, required=False) year = forms.ChoiceField(label="Jahr", choices=year_choices, required=False)
month = forms.ChoiceField(label="Monat", choices=month_choices, required=False) month = forms.ChoiceField(label="Monat", choices=month_choices, required=False)
compact_view = forms.BooleanField( compact_view = forms.BooleanField(
label="Kompakte Ansicht", label="Kompakte Ansicht",
required=False, required=False,
widget=CheckboxInput, widget=CheckboxInput,
) )
fet_meeting_only = forms.BooleanField( fet_meeting_only = forms.BooleanField(
label="nur FET Sitzungen", label="nur FET Sitzungen",
required=False, required=False,
widget=CheckboxInput, widget=CheckboxInput,
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
try: try:
first_post = Post.objects.get_queryset().last() first_post = Post.objects.get_queryset().last()
last_post = Post.objects.get_queryset().first() last_post = Post.objects.get_queryset().first()
if first_post and last_post: if first_post and last_post:
years = range(last_post.date.year, first_post.date.year - 1, -1) years = range(last_post.date.year, first_post.date.year - 1, -1)
year_choices = [("", "Alle")] + [(i, i) for i in years] year_choices = [("", "Alle")] + [(i, i) for i in years]
self.fields["year"].choices = year_choices self.fields["year"].choices = year_choices
except Exception: except Exception:
pass pass
class NewsUpdateForm(forms.ModelForm): class NewsUpdateForm(forms.ModelForm):
class Meta: class Meta:
model = News model = News
fields = [ fields = [
"title", "title",
"status", "status",
"body", "body",
] ]
labels = { labels = {
"title": "Titel", "title": "Titel",
"image": "Hintergrundbild", "image": "Hintergrundbild",
"body": "Text", "body": "Text",
} }
widgets = {"body": CKEditorUploadingWidget(config_name="default")} widgets = {"body": CKEditorUploadingWidget(config_name="default")}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["title"].autofocus = True self.fields["title"].autofocus = True
class EventUpdateForm(forms.ModelForm): class EventUpdateForm(forms.ModelForm):
class Meta: class Meta:
model = Event model = Event
fields = [ fields = [
"title", "title",
"status", "status",
"event_start", "event_start",
"event_end", "event_end",
"event_place", "event_place",
] ]
labels = { labels = {
"title": "Titel", "title": "Titel",
"event_start": "Start des Events", "event_start": "Start des Events",
"event_end": "Ende des Events", "event_end": "Ende des Events",
"event_place": "Ort des Events", "event_place": "Ort des Events",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["event_start"].required = True self.fields["event_start"].required = True
self.fields["event_start"].autofocus = True self.fields["event_start"].autofocus = True
self.fields["event_end"].required = False self.fields["event_end"].required = False
if "event_place" in self.fields: if "event_place" in self.fields:
self.fields["event_place"].required = True self.fields["event_place"].required = True
class FetMeetingCreateForm(forms.ModelForm): class FetMeetingCreateForm(forms.ModelForm):
class Meta: class Meta:
model = FetMeeting model = FetMeeting
fields = ["event_start", "event_end", "event_place"] fields = ["event_start", "event_end", "event_place"]
help_texts = { help_texts = {
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.", "event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
} }
labels = { labels = {
"event_start": "Start der Sitzung", "event_start": "Start der Sitzung",
"event_end": "Ende der Sitzung", "event_end": "Ende der Sitzung",
"event_place": "Ort der Sitzung", "event_place": "Ort der Sitzung",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["event_start"].required = True self.fields["event_start"].required = True
self.fields["event_start"].autofocus = True self.fields["event_start"].autofocus = True
self.fields["event_end"].required = False self.fields["event_end"].required = False
self.fields["event_place"].initial = "FET" self.fields["event_place"].initial = "FET"
class FetMeetingUpdateForm(forms.ModelForm): class FetMeetingUpdateForm(forms.ModelForm):
class Meta: class Meta:
model = FetMeeting model = FetMeeting
fields = ["event_start", "event_end", "event_place"] fields = ["event_start", "event_end", "event_place"]
labels = { labels = {
"event_start": "Start der Sitzung", "event_start": "Start der Sitzung",
"event_end": "Ende der Sitzung", "event_end": "Ende der Sitzung",
"event_place": "Ort der Sitzung", "event_place": "Ort der Sitzung",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["event_start"].required = True self.fields["event_start"].required = True
self.fields["event_start"].autofocus = True self.fields["event_start"].autofocus = True
self.fields["event_end"].required = False self.fields["event_end"].required = False
self.fields["event_place"].initial = "FET" self.fields["event_place"].initial = "FET"

View File

@@ -1,201 +1,208 @@
import datetime import calendar
import datetime
from django.db import models
from django.db.models import Case, Q, When from django.db import models
from django.db.models import Case, Q, When
from .choices import PostType, Status from django.utils import timezone
from .choices import PostType, Status
class PublishedManager(models.Manager):
def published(self, public=True):
""" class PublishedManager(models.Manager):
publish all posts with status 'PUBLIC' def published(self, public=True):
""" """
return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset() publish all posts with status 'PUBLIC'
"""
def published_all(self, public=True): return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset()
"""
publish all posts with status 'PUBLIC' and 'ONLY_INTERN' def published_all(self, public=True):
""" """
return ( publish all posts with status 'PUBLIC' and 'ONLY_INTERN'
self.get_queryset().filter(~Q(status=Status.DRAFT)) if public else self.get_queryset() """
) return (
self.get_queryset().filter(~Q(status=Status.DRAFT)) if public else self.get_queryset()
)
class PostManager(PublishedManager, models.Manager):
def get_queryset(self):
qs = ( class PostManager(PublishedManager, models.Manager):
super() def get_queryset(self):
.get_queryset() qs = (
.annotate( super()
date=Case( .get_queryset()
When(post_type=PostType.NEWS, then="public_date"), .annotate(
When(post_type=PostType.EVENT, then="event_start__date"), date=Case(
When(post_type=PostType.FETMEETING, then="event_start__date"), When(post_type=PostType.NEWS, then="public_date"),
), When(post_type=PostType.EVENT, then="event_start__date"),
) When(post_type=PostType.FETMEETING, then="event_start__date"),
) ),
return qs.order_by("-date", "-id") )
)
def date_sorted(self, public=True): return qs.order_by("-date", "-id")
return self.published(public)
def date_sorted(self, public=True):
def date_filter( return self.published(public)
self,
public=True, def date_filter(
year=None, self,
month=None, public=True,
fet_meeting_only=None, year=None,
): month=None,
qs_filter = Q() fet_meeting_only=None,
):
if fet_meeting_only: qs_filter = Q()
qs_filter &= Q(post_type=PostType.FETMEETING)
if fet_meeting_only:
if year: qs_filter &= Q(post_type=PostType.FETMEETING)
qs_filter &= Q(date__year=year)
if month: if year:
qs_filter &= Q(date__month=month) qs_filter &= Q(date__year=year)
if month:
return self.published(public).filter(qs_filter) qs_filter &= Q(date__month=month)
return self.published(public).filter(qs_filter)
class ArticleManager(PublishedManager, models.Manager):
"""
Provide a query set only for "Article" class ArticleManager(PublishedManager, models.Manager):
regular fet meetings should not be contained in the news stream """
""" Provide a query set only for "Article"
regular fet meetings should not be contained in the news stream
def get_queryset(self): """
qs = super().get_queryset().filter(Q(post_type=PostType.NEWS) | Q(post_type=PostType.EVENT))
qs = qs.annotate( def get_queryset(self):
date=Case( qs = super().get_queryset().filter(Q(post_type=PostType.NEWS) | Q(post_type=PostType.EVENT))
When(post_type=PostType.NEWS, then="public_date"), qs = qs.annotate(
When(post_type=PostType.EVENT, then="event_start__date"), date=Case(
), When(post_type=PostType.NEWS, then="public_date"),
) When(post_type=PostType.EVENT, then="event_start__date"),
return qs.order_by("-date", "-id") ),
)
def date_sorted(self, public=True): return qs.order_by("-date", "-id")
return self.published(public)
def date_sorted(self, public=True):
def pinned(self, public=True): return self.published(public)
# Get date for pinned news that is max 1 month old.
post_date = datetime.datetime.now(tz=datetime.UTC).date() def pinned(self, public=True):
__month = post_date.month # Get date for pinned news that is max 1 month old.
__year = post_date.year post_date = timezone.now().date()
_day = post_date.day
if __month != 1: _month = post_date.month
__month -= 1 _year = post_date.year
else:
# If the current month is January, you get the date from December of previous year. if _month != 1:
__month = 12 _month -= 1
__year -= 1 else:
# If the current month is January, you get the date from December of previous year.
post_date = post_date.replace(year=__year, month=__month) _month = 12
_year -= 1
# Get date for event posts that is max 1 day old.
event_date = datetime.datetime.now(tz=datetime.UTC).date() - datetime.timedelta(1) # Clamp day to last day of target month (handles 30/31 and Feb)
last_day = calendar.monthrange(_year, _month)[1]
return ( safe_day = min(_day, last_day)
self.published(public)
.filter( post_date = post_date.replace(year=_year, month=_month, day=safe_day)
Q(is_pinned=True)
& ( # Get date for event posts that is max 1 day old.
(Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date)) event_date = timezone.now().date() - datetime.timedelta(1)
| (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date))
) return (
) self.published(public)
.first() .filter(
) Q(is_pinned=True)
& (
(Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date))
class NewsManager(PublishedManager, models.Manager): | (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date))
""" )
Provide a query set only for "News" )
""" .first()
)
def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.NEWS)
qs = qs.annotate( class NewsManager(PublishedManager, models.Manager):
date=Case( """
When(post_type=PostType.NEWS, then="public_date"), Provide a query set only for "News"
), """
)
return qs.order_by("-date") def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.NEWS)
qs = qs.annotate(
class AllEventManager(PublishedManager, models.Manager): date=Case(
""" When(post_type=PostType.NEWS, then="public_date"),
Provide a query set for all events ("Event" and "Fet Meeting") ),
""" )
return qs.order_by("-date")
def get_queryset(self):
qs = (
super() class AllEventManager(PublishedManager, models.Manager):
.get_queryset() """
.filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING)) Provide a query set for all events ("Event" and "Fet Meeting")
) """
qs = qs.annotate(
date=Case( def get_queryset(self):
When(post_type=PostType.EVENT, then="event_start__date"), qs = (
When(post_type=PostType.FETMEETING, then="event_start__date"), super()
), .get_queryset()
) .filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING))
return qs.order_by("-date") )
qs = qs.annotate(
def future_events(self, public=True): date=Case(
date_today = datetime.datetime.now(tz=datetime.UTC).date() When(post_type=PostType.EVENT, then="event_start__date"),
qs = self.published(public).filter(event_start__gt=date_today) When(post_type=PostType.FETMEETING, then="event_start__date"),
return qs.reverse() ),
)
return qs.order_by("-date")
class EventManager(PublishedManager, models.Manager):
""" def future_events(self, public=True):
Provide a query set only for "Events" date_today = timezone.now().date()
regular fet meetings should not be contained in the news stream qs = self.published(public).filter(event_start__gt=date_today)
""" return qs.reverse()
def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.EVENT) class EventManager(PublishedManager, models.Manager):
qs = qs.annotate( """
date=Case( Provide a query set only for "Events"
When(post_type=PostType.EVENT, then="event_start__date"), regular fet meetings should not be contained in the news stream
), """
)
return qs.order_by("-date") def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.EVENT)
def future_events(self, public=True): qs = qs.annotate(
date_today = datetime.datetime.now(tz=datetime.UTC).date() date=Case(
qs = self.published(public).filter(event_start__gt=date_today) When(post_type=PostType.EVENT, then="event_start__date"),
return qs.reverse() ),
)
def past_events(self, public=True): return qs.order_by("-date")
date_today = datetime.datetime.now(tz=datetime.UTC).date()
qs = self.published(public).filter(event_start__lt=date_today) def future_events(self, public=True):
return qs date_today = timezone.now().date()
qs = self.published(public).filter(event_start__gt=date_today)
return qs.reverse()
class FetMeetingManager(PublishedManager, models.Manager):
""" def past_events(self, public=True):
Provide a query set only for "Fet Meeting" date_today = timezone.now().date()
""" qs = self.published(public).filter(event_start__lt=date_today)
return qs
def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.FETMEETING)
qs = qs.annotate( class FetMeetingManager(PublishedManager, models.Manager):
date=Case( """
When(post_type=PostType.FETMEETING, then="event_start__date"), Provide a query set only for "Fet Meeting"
), """
)
return qs.order_by("-date") def get_queryset(self):
qs = super().get_queryset().filter(post_type=PostType.FETMEETING)
def future_events(self): qs = qs.annotate(
date_today = datetime.datetime.now(tz=datetime.UTC).date() date=Case(
qs = self.published().filter(event_start__gt=date_today) When(post_type=PostType.FETMEETING, then="event_start__date"),
return qs.reverse() ),
)
def past_events(self): return qs.order_by("-date")
date_today = datetime.datetime.now(tz=datetime.UTC).date()
qs = self.published().filter(event_start__lt=date_today) def future_events(self):
return qs date_today = timezone.now().date()
qs = self.published().filter(event_start__gt=date_today)
return qs.reverse()
def past_events(self):
date_today = timezone.now().date()
qs = self.published().filter(event_start__lt=date_today)
return qs

View File

@@ -1,124 +1,121 @@
from django.contrib import admin, messages from django.contrib import admin, messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from .forms import RentalAdminForm, RentalItemAdminForm from .forms import RentalAdminForm, RentalItemAdminForm
from .models import Rental, RentalItem from .models import Rental, RentalItem
from .utils import generate_rental_pdf from .utils import generate_rental_pdf
@admin.register(Rental) @admin.register(Rental)
class RentalAdmin(admin.ModelAdmin): class RentalAdmin(admin.ModelAdmin):
form = RentalAdminForm form = RentalAdminForm
model = Rental model = Rental
list_display = [ list_display = [
"id", "id",
"firstname", "firstname",
"surname", "surname",
"status", "status",
"total_disposit", "total_disposit",
"date_start", "date_start",
"date_end", "date_end",
] ]
ordering = ["-id"] ordering = ["-id"]
readonly_fields = ["total_disposit"] readonly_fields = ["total_disposit"]
fieldsets = ( fieldsets = (
( (
"Persönliche Daten", "Persönliche Daten",
{ {
"fields": ( "fields": (
("firstname", "surname"), ("firstname", "surname"),
("organization", "matriculation_number"), ("organization", "matriculation_number"),
("email", "phone"), ("email", "phone"),
), ),
}, },
), ),
( (
"Verleih", "Verleih",
{ {
"fields": ( "fields": (
("date_start", "date_end"), ("date_start", "date_end"),
"reason", "reason",
"rentalitems", "rentalitems",
"total_disposit", ("total_disposit", "intern"),
), ),
}, },
), ),
( (
"Sonstiges", "Sonstiges",
{ {
"fields": ( "fields": (
"comment", "comment",
"file_field", "file_field",
"status", "status",
), ),
}, },
), ),
) )
def add_view(self, request, form_url="", extra_context=None): def add_view(self, request, form_url="", extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
return super().add_view(request, form_url, extra_context=extra_context) return super().add_view(request, form_url, extra_context=extra_context)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
extra_context["generate_rental_pdf"] = True extra_context["generate_rental_pdf"] = True
return super().change_view(request, object_id, form_url, extra_context=extra_context) return super().change_view(request, object_id, form_url, extra_context=extra_context)
def response_change(self, request, obj): def response_change(self, request, obj):
if "_generate_rental_pdf" in request.POST: if "_generate_rental_pdf" in request.POST:
if generate_rental_pdf(obj): if generate_rental_pdf(obj):
self.message_user( self.message_user(
request, request,
"Neues Verleihformular wurde generiert.", "Neues Verleihformular wurde generiert.",
messages.SUCCESS, messages.SUCCESS,
) )
else: else:
self.message_user( self.message_user(
request, request,
( (
"Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf " "Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf "
"'Verleih genehmigt' gesetzt ist." "'Verleih genehmigt' gesetzt ist."
), ),
messages.WARNING, messages.WARNING,
) )
return HttpResponseRedirect(".") return HttpResponseRedirect(".")
return super().response_change(request, obj) return super().response_change(request, obj)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
obj.author = request.user obj.author = request.user
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
@admin.display(description="Kaution (EUR)") @admin.display(description="Kaution insgesamt")
def total_disposit(self, obj): def total_disposit(self, obj):
total_disposit = 0 total_disposit = obj.calc_total_deposit()
for elem in obj.rentalitems.all(): return f"{total_disposit}"
total_disposit += elem.deposit
return f"{total_disposit}" @admin.register(RentalItem)
class RentalItemAdmin(admin.ModelAdmin):
form = RentalItemAdminForm
@admin.register(RentalItem) model = RentalItem
class RentalItemAdmin(admin.ModelAdmin):
form = RentalItemAdminForm ordering = ["name"]
model = RentalItem
def add_view(self, request, form_url="", extra_context=None):
ordering = ["name"] extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
def add_view(self, request, form_url="", extra_context=None): return super().add_view(request, form_url, extra_context=extra_context)
extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." def change_view(self, request, object_id, form_url="", extra_context=None):
return super().add_view(request, form_url, extra_context=extra_context) extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
def change_view(self, request, object_id, form_url="", extra_context=None): return super().change_view(request, object_id, form_url, extra_context=extra_context)
extra_context = extra_context or {}
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." def save_model(self, request, obj, form, change):
return super().change_view(request, object_id, form_url, extra_context=extra_context) obj.author = request.user
super().save_model(request, obj, form, change)
def save_model(self, request, obj, form, change):
obj.author = request.user
super().save_model(request, obj, form, change)

View File

@@ -1,58 +1,65 @@
from django import forms from django import forms
from django.forms import DateInput from django.forms import DateInput
from .models import Rental, RentalItem from .models import Rental, RentalItem
class DateInput(DateInput): class DateInput(DateInput):
input_type = "date" input_type = "date"
class RentalCreateForm(forms.ModelForm): class RentalCreateForm(forms.ModelForm):
# Conformation # Conformation
conformation = forms.BooleanField( conformation = forms.BooleanField(
required=True, required=True,
label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."), label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."),
initial=False, initial=False,
) )
class Meta: class Meta:
model = Rental model = Rental
fields = [ fields = [
"firstname", "firstname",
"surname", "surname",
"matriculation_number", "matriculation_number",
"email", "email",
"phone", "phone",
"organization", "organization",
"date_start", "date_start",
"date_end", "date_end",
"reason", "reason",
"comment", "comment",
"rentalitems", "rentalitems",
] ]
widgets = { widgets = {
"date_start": DateInput(format=("%Y-%m-%d")), "date_start": DateInput(format=("%Y-%m-%d")),
"date_end": DateInput(format=("%Y-%m-%d")), "date_end": DateInput(format=("%Y-%m-%d")),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
self.fields["firstname"].autofocus = True self.fields["firstname"].autofocus = True
class RentalAdminForm(forms.ModelForm): class RentalAdminForm(forms.ModelForm):
class Meta: class Meta:
model = Rental model = Rental
fields = "__all__" fields = "__all__"
widgets = {"rentalitems": forms.CheckboxSelectMultiple()} widgets = {"rentalitems": forms.CheckboxSelectMultiple()}
help_texts = {
class RentalItemAdminForm(forms.ModelForm): "status": (
class Meta: "Wird der Status auf 'Verleih genehmigt' oder 'Verleih abgelehnt' gesetzt, wird "
model = RentalItem "eine E-Mail gesendet."
fields = "__all__" ),
}
class RentalItemAdminForm(forms.ModelForm):
class Meta:
model = RentalItem
fields = "__all__"

View File

@@ -1,53 +1,53 @@
import logging import logging
from django.core.mail import EmailMessage from django.conf import settings
from django.core.mail import EmailMessage
RENTAL_EMAIL = "verleih@fet.at"
logger = logging.getLogger(__name__) RENTAL_EMAIL = settings.EMAIL_HOST_USER
logger = logging.getLogger(__name__)
def send_mail_approved(obj):
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" def send_mail_approved(obj):
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
total_deposit = 0 total_deposit = obj.calc_total_deposit()
for rentalitem in obj.rentalitems.all():
total_deposit += rentalitem.deposit message = (
f"Hallo {obj.firstname},\n\n"
message = ( f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die "
f"Hallo {obj.firstname},\n" f"Gegenstände können am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit "
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die " "(Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden.\n"
f"Gegenstände können am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit " )
"(Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring "
f"den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n" if total_deposit > 0:
"Liebe Grüße,\n" message += f"Bitte bring den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n"
"das Verleih-Team"
) message += "\nLiebe Grüße,\ndas Verleih-Team"
email = EmailMessage( email = EmailMessage(
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
) )
try: try:
email.send() email.send()
except Exception as exc: except Exception as exc:
logger.error("Failed to send approval email for rental #%s. Error: %s", obj.id, exc) logger.info("Failed to send approval email for rental #%s. Error: %s", obj.id, exc)
def send_mail_rejected(obj): def send_mail_rejected(obj):
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
message = ( message = (
f"Hallo {obj.firstname},\n" f"Hallo {obj.firstname},\n\n"
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n" f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n\n"
"Liebe Grüße,\n" "Liebe Grüße,\n"
"das Verleih-Team" "das Verleih-Team"
) )
email = EmailMessage( email = EmailMessage(
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
) )
try: try:
email.send() email.send()
except Exception as exc: except Exception as exc:
logger.error("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc) logger.info("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc)

View File

@@ -0,0 +1,6 @@
from django.db import models
class RentalItemsManager(models.Manager):
def get_queryset(self):
return super().get_queryset().order_by("name")

View File

@@ -0,0 +1,55 @@
# Generated by Django 5.2.7 on 2025-10-30 12:05
import django.core.validators
import rental.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='RentalItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('description', models.CharField(blank=True, default='', max_length=128, verbose_name='Beschreibung')),
('deposit', models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Kaution (EUR)')),
('image', models.ImageField(blank=True, null=True, upload_to='rentalitems', verbose_name='Bild')),
('induction', models.BooleanField(default=False, verbose_name='Einschulung notwendig')),
('location', models.CharField(blank=True, default='', max_length=128, verbose_name='Standort')),
],
options={
'verbose_name': 'Verleihgegenstand',
'verbose_name_plural': 'Verleihgegenstände',
},
),
migrations.CreateModel(
name='Rental',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstname', models.CharField(max_length=128, verbose_name='Vorname')),
('surname', models.CharField(max_length=128, verbose_name='Nachname')),
('matriculation_number', models.CharField(max_length=8, verbose_name='Matrikelnummer')),
('email', models.EmailField(max_length=254, verbose_name='E-Mail')),
('phone', models.CharField(max_length=32, validators=[rental.validators.PhoneNumberValidator()], verbose_name='Telefonnummer')),
('organization', models.CharField(max_length=128, verbose_name='Organisation')),
('date_start', models.DateField(verbose_name='Abholdatum')),
('date_end', models.DateField(verbose_name='Rückgabedatum')),
('reason', models.TextField(max_length=500, verbose_name='Grund der Ausleihe')),
('comment', models.TextField(blank=True, default='', max_length=500, verbose_name='Kommentar')),
('file_field', models.FileField(blank=True, null=True, upload_to='uploads/rental/rental/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Verleihformular')),
('status', models.CharField(choices=[('S', 'Eingereicht'), ('A', 'Verleih genehmigt'), ('J', 'Verleih abgelehnt'), ('I', 'Verleihgegenstände ausgegeben'), ('R', 'Verleihgegenstände zurückgegeben')], default='S', max_length=1, verbose_name='Status')),
('rentalitems', models.ManyToManyField(to='rental.rentalitem', verbose_name='Verleihgegenstände')),
],
options={
'verbose_name': 'Verleih',
'verbose_name_plural': 'Verleih',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-30 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rental', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='rental',
name='intern',
field=models.BooleanField(default=False, verbose_name='Interner Verleih'),
),
]

View File

View File

@@ -3,6 +3,7 @@ from django.db import models
from django.forms import ValidationError from django.forms import ValidationError
from .mails import send_mail_approved, send_mail_rejected from .mails import send_mail_approved, send_mail_rejected
from .managers import RentalItemsManager
from .validators import PhoneNumberValidator from .validators import PhoneNumberValidator
@@ -22,6 +23,8 @@ class RentalItem(models.Model):
location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="") location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="")
objects = RentalItemsManager()
class Meta: class Meta:
verbose_name = "Verleihgegenstand" verbose_name = "Verleihgegenstand"
verbose_name_plural = "Verleihgegenstände" verbose_name_plural = "Verleihgegenstände"
@@ -41,6 +44,7 @@ class Rental(models.Model):
) )
organization = models.CharField(verbose_name="Organisation", max_length=128) organization = models.CharField(verbose_name="Organisation", max_length=128)
intern = models.BooleanField(verbose_name="Interner Verleih", default=False)
date_start = models.DateField(verbose_name="Abholdatum") date_start = models.DateField(verbose_name="Abholdatum")
date_end = models.DateField(verbose_name="Rückgabedatum") date_end = models.DateField(verbose_name="Rückgabedatum")
@@ -107,3 +111,12 @@ class Rental(models.Model):
if self.date_start > self.date_end: if self.date_start > self.date_end:
raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.") raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.")
def calc_total_deposit(self) -> int:
total_deposit = 0
if not self.intern:
for item in self.rentalitems.all():
total_deposit += item.deposit
return total_deposit

View File

@@ -10,7 +10,7 @@ urlpatterns = [
path("overview/", RentalListView.as_view(), name="index"), path("overview/", RentalListView.as_view(), name="index"),
path("request-rental/", RentalCreateView.as_view(), name="rental_create"), path("request-rental/", RentalCreateView.as_view(), name="rental_create"),
path( path(
"request-rental/<int:pk>/done/", "request-rental/done/",
RentalCreateDoneView.as_view(), RentalCreateDoneView.as_view(),
name="rental_create_done", name="rental_create_done",
), ),

View File

@@ -1,66 +1,61 @@
import io import io
from pathlib import Path
from django.contrib.staticfiles import finders
from django.core.files import File from django.core.files import File
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
from .models import Rental from .models import Rental
def generate_rental_pdf(rental: Rental) -> bool: def generate_rental_pdf(rental: Rental) -> bool:
if not rental or rental.status != Rental.Status.APPROVED: if not rental or rental.status != Rental.Status.APPROVED:
return False return False
# Get data for pdf # Get data for pdf
data = {} data = {}
data.update( data.update(
{ {
"Vorname": rental.firstname, "Vorname": rental.firstname,
"Nachname": rental.surname, "Nachname": rental.surname,
"Orga": rental.organization, "Orga": rental.organization,
"Matrikelnummer": rental.matriculation_number, "Matrikelnummer": rental.matriculation_number,
"E-Mail": rental.email, "E-Mail": rental.email,
"Telefonnummer": rental.phone, "Telefonnummer": rental.phone,
# Change to the correct date format # Change to the correct date format
"Abholdatum": str(rental.date_start.strftime("%d.%m.%Y")), "Abholdatum": str(rental.date_start.strftime("%d.%m.%Y")),
"Rückgabedatum": str(rental.date_end.strftime("%d.%m.%Y")), "Rückgabedatum": str(rental.date_end.strftime("%d.%m.%Y")),
}, },
) )
total_deposit = 0 for i, item in enumerate(rental.rentalitems.all(), start=1):
data.update(
for i, item in enumerate(rental.rentalitems.all(), start=1): {
total_deposit += item.deposit f"Produkt Row{i}": item.name,
data.update( f"Menge Row{i}": "1",
{ f"Kaution Row{i}": (str(item.deposit) if not rental.intern else "0"),
f"Produkt Row{i}": item.name, },
f"Menge Row{i}": "1", )
f"Kaution Row{i}": item.deposit,
}, total_deposit = rental.calc_total_deposit()
) data.update({"Gesamtkaution": str(total_deposit)})
data.update( # Write data in pdf
{ pdf_path_str = finders.find("rental/Verleihformular.pdf")
"Gesamtkaution": total_deposit, reader = PdfReader(pdf_path_str)
}, writer = PdfWriter()
) writer.append(reader)
# Write data in pdf writer.update_page_form_field_values(
pdf_path = Path(Path(__file__).parent) / "static/rental/Verleihformular.pdf" writer.pages[0],
reader = PdfReader(pdf_path) data,
writer = PdfWriter() )
writer.append(reader)
with io.BytesIO() as bytes_stream:
writer.update_page_form_field_values( writer.write(bytes_stream)
writer.pages[0], bytes_stream.seek(0)
data,
) # Save pdf in rental
rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf"
with io.BytesIO() as bytes_stream: rental.file_field.save(rental_name, File(bytes_stream, rental_name))
writer.write(bytes_stream)
return True
# Save pdf in rental
rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf"
rental.file_field.save(rental_name, File(bytes_stream, rental_name))
return True

View File

@@ -1,9 +1,12 @@
import calendar import calendar
import datetime import datetime
from datetime import date
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
@@ -11,91 +14,164 @@ from django.views.generic.edit import CreateView
from .forms import RentalCreateForm from .forms import RentalCreateForm
from .models import Rental, RentalItem from .models import Rental, RentalItem
# Maximum number of rental items per rental entry because of table size limitations in PDF file
RENTAL_ITEMS_MAX = 5
def _calc_days_from_current_month(month: date) -> list:
last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1])
return [month + datetime.timedelta(days=i) for i in range((last_day_of_month - month).days + 1)]
def _calc_days_from_prev_month(month: date) -> list:
days_of_prev_period = []
if month.weekday() != calendar.MONDAY:
for i in range(1, 7):
day = month + datetime.timedelta(days=-i)
days_of_prev_period.append(day)
if day.weekday() == calendar.MONDAY:
break
return sorted(days_of_prev_period)
def _calc_days_from_next_month(month: date) -> list:
last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1])
days_of_next_period = []
if last_day_of_month.weekday() != calendar.SUNDAY:
for i in range(1, 7):
day = last_day_of_month + datetime.timedelta(days=i)
days_of_next_period.append(day)
if day.weekday() == calendar.SUNDAY:
break
return days_of_next_period
def _get_display_period(view_type: str, period: str) -> date:
display_date: date = None
# Handle week view
if view_type == "week":
try:
# Parse the requested calendar week
display_date = (
datetime.datetime.strptime(f"{period}-1", "%G-KW%V-%u")
.replace(tzinfo=datetime.UTC)
.date()
)
except Exception:
# Get first day of the current week
today = timezone.now().date()
display_date = today - datetime.timedelta(days=today.weekday())
# Handle month view
else:
try:
# Parse the requested month
display_date = (
datetime.datetime.strptime(period, "%Y-%m").replace(tzinfo=datetime.UTC).date()
)
except Exception:
# Get the first day of the current month
display_date = timezone.now().date().replace(day=1)
return display_date
class RentalListView(ListView): class RentalListView(ListView):
model = Rental model = Rental
template_name = "rental/calendar.html" template_name = "rental/calendar.html"
# Month is the month displayed in the calendar (and should be the first day of the month) def __init__(self):
month = None super().__init__()
# Rental items to filter # Current display period and view settings
rentalitem_filters = [] self.display_period = None
self.view_type = "month" # Default view
self.rentalitem_filters = []
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Get the rental items from the filter (max. 4) # Get view parameters from request
self.rentalitem_filters = request.GET.getlist("rentalitems", [])[:4] _view_type = request.GET.get("view_type", "week")
if not self.rentalitem_filters: _period = request.GET.get("period_value", "")
for rentalitem in RentalItem.objects.all()[:4]: _prev_period = request.GET.get("prev_period", "")
self.rentalitem_filters.append(rentalitem.name) _next_period = request.GET.get("next_period", "")
# Get the displayed month from the request if _prev_period:
_date_str = request.GET.get("month", "") _period = _prev_period
if _date_str: elif _next_period:
self.month = ( _period = _next_period
datetime.datetime.strptime(_date_str, "%Y-%m").replace(tzinfo=datetime.UTC).date()
) self.view_type = _view_type
else: self.display_period = _get_display_period(_view_type, _period)
self.month = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1)
self.rentalitem_filters = request.GET.getlist("rentalitems", [])
if not self.rentalitem_filters:
items = RentalItem.objects.all()
self.rentalitem_filters = [item.name for item in items]
# Update request.GET
_request_get_list = request.GET.copy()
_request_get_list.pop("prev_period", None)
_request_get_list.pop("next_period", None)
_request_get_list["period_value"] = _period
request.GET = _request_get_list
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Calculate the day of the previous month from Monday if self.view_type != "week":
days_of_prev_month = [] # Add the displayed, previous and next month
context["view_period"] = self.display_period
context["prev_period"] = self.display_period + datetime.timedelta(days=-1)
context["next_period"] = self.display_period + datetime.timedelta(
days=calendar.monthrange(self.display_period.year, self.display_period.month)[1] + 1
)
if self.month.weekday() != calendar.MONDAY: # Add the days of the displayed, previous and next month
for i in range(1, 7): days_of_view_period = _calc_days_from_current_month(self.display_period)
day = self.month + datetime.timedelta(days=-i) context["days_of_view_period"] = days_of_view_period
days_of_prev_month.append(day) context["days_of_prev_period"] = _calc_days_from_prev_month(self.display_period)
if day.weekday() == calendar.MONDAY: context["days_of_next_period"] = _calc_days_from_next_month(self.display_period)
break
# Calculate the days of the next month until Sunday context["view_type"] = "month"
last_day_of_month = self.month.replace(
day=calendar.monthrange(self.month.year, self.month.month)[1]
)
days_of_next_month = []
if last_day_of_month.weekday() != calendar.SUNDAY: context["week_num"] = None
for i in range(1, 7):
day = last_day_of_month + datetime.timedelta(days=i)
days_of_next_month.append(day)
if day.weekday() == calendar.SUNDAY:
break
# Calculate the days of the displayed month else:
days_of_month = [ # Current week
self.month + datetime.timedelta(days=i) year, week_num, _ = self.display_period.isocalendar()
for i in range((last_day_of_month - self.month).days + 1) context["view_period"] = f"{year}-KW{week_num:02d}" # formats as "2025-KW02"
]
# Create a dictionary with the rental items for each day # Calculate previous week
rental_dict = {} prev_week = self.display_period - datetime.timedelta(days=7)
for rental in self.get_queryset(): prev_year, prev_week_num, _ = prev_week.isocalendar()
for day in days_of_month: context["prev_period"] = f"{prev_year}-KW{prev_week_num:02d}"
if rental["date_start"] <= day and rental["date_end"] >= day:
if day not in rental_dict:
rental_dict[day] = []
if rental["rentalitems__name"] not in rental_dict[day]: # Calculate next week
rental_dict[day].append(rental["rentalitems__name"]) next_week = self.display_period + datetime.timedelta(days=7)
next_year, next_week_num, _ = next_week.isocalendar()
context["next_period"] = f"{next_year}-KW{next_week_num:02d}"
# Add the displayed, previous and next month # Add days of week (7 days starting from first_day_of_week)
context["month"] = self.month days_of_view_period = [
context["prev_month"] = self.month + datetime.timedelta(days=-1) self.display_period + datetime.timedelta(days=i) for i in range(7)
context["next_month"] = self.month + datetime.timedelta( ]
days=calendar.monthrange(self.month.year, self.month.month)[1] + 1 context["days_of_view_period"] = days_of_view_period
) context["days_of_prev_period"] = []
context["days_of_next_period"] = []
# Add the days of the displayed, previous and next month context["view_type"] = "week"
context["days_of_month"] = days_of_month
context["days_of_prev_month"] = sorted(days_of_prev_month) context["week_num"] = week_num
context["days_of_next_month"] = days_of_next_month
# Get the current date for the calendar # Get the current date for the calendar
context["today"] = datetime.datetime.now(tz=datetime.UTC).date() context["today"] = timezone.now().date()
# Add rental items to the context for the filter # Add rental items to the context for the filter
context["rentalitems"] = RentalItem.objects.all() context["rentalitems"] = RentalItem.objects.all()
@@ -103,7 +179,18 @@ class RentalListView(ListView):
# Add the selected rental items to the context for the filter # Add the selected rental items to the context for the filter
context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters} context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters}
context["rental_dict"] = rental_dict # Create a dictionary with the rental items for each day
rental_dict = {}
for rental in self.get_queryset():
for day in days_of_view_period:
if rental["date_start"] <= day and rental["date_end"] >= day:
if day not in rental_dict:
rental_dict[day] = []
if rental["rentalitems__name"] not in rental_dict[day]:
rental_dict[day].append(rental["rentalitems__name"])
context["rental_dict"] = {k: sorted(v, key=str.casefold) for k, v in rental_dict.items()}
return context return context
@@ -118,20 +205,22 @@ class RentalListView(ListView):
) )
) )
last_day_of_month = self.month.replace( if self.view_type == "week":
day=calendar.monthrange(self.month.year, self.month.month)[1] qs_date_end = self.display_period + datetime.timedelta(days=6)
) else:
qs_date_end = self.display_period.replace(
day=calendar.monthrange(self.display_period.year, self.display_period.month)[1]
)
# Filter by date # Filter by date
qs_new = qs.filter(date_start__gte=self.month, date_start__lte=last_day_of_month) if self.display_period and qs_date_end:
qs_new |= qs.filter(date_end__gte=self.month, date_end__lte=last_day_of_month) qs_new = qs.filter(date_start__gte=self.display_period, date_start__lte=qs_date_end)
qs_new |= qs.filter(date_end__gte=self.display_period, date_end__lte=qs_date_end)
# Filter by rental items # Filter by rental items
qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct() qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct()
qs = qs.values("id", "date_start", "date_end", "rentalitems__name").distinct() return qs.values("id", "date_start", "date_end", "rentalitems__name").distinct()
return qs
class RentalCreateView(CreateView): class RentalCreateView(CreateView):
@@ -139,6 +228,35 @@ class RentalCreateView(CreateView):
model = Rental model = Rental
template_name = "rental/create.html" template_name = "rental/create.html"
def form_valid(self, form):
# Get unsaved base Rental with all form data
base_rental = form.save(commit=False)
items = list(form.cleaned_data["rentalitems"])
selected_items = sorted(items, key=lambda x: x.name.lower())
for i in range(0, len(selected_items), RENTAL_ITEMS_MAX):
batch = selected_items[i : i + RENTAL_ITEMS_MAX]
# Clone base_rental — copying all its field values
new_rental = Rental.objects.create(
firstname=base_rental.firstname,
surname=base_rental.surname,
matriculation_number=base_rental.matriculation_number,
email=base_rental.email,
phone=base_rental.phone,
organization=base_rental.organization,
date_start=base_rental.date_start,
date_end=base_rental.date_end,
reason=base_rental.reason,
comment=base_rental.comment,
status=base_rental.status,
)
# Important: Add M2M relations after the object has been saved
new_rental.rentalitems.add(*batch)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -147,26 +265,12 @@ class RentalCreateView(CreateView):
return context return context
def get_success_url(self): def get_success_url(self):
return reverse("rental:rental_create_done", kwargs={"pk": self.object.pk}) return reverse("rental:rental_create_done")
class RentalCreateDoneView(TemplateView): class RentalCreateDoneView(TemplateView):
template_name = "rental/create_done.html" template_name = "rental/create_done.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
obj = Rental.objects.get(pk=self.kwargs["pk"])
total_deposit = 0
for elem in obj.rentalitems.all():
total_deposit += elem.deposit
context["object"] = obj
context["total_deposit"] = total_deposit
return context
class RentalItemDetailView(DetailView): class RentalItemDetailView(DetailView):
model = RentalItem model = RentalItem
@@ -175,7 +279,7 @@ class RentalItemDetailView(DetailView):
def rental_calendar(request): def rental_calendar(request):
""" """
ICS-calendar for outlook, google calender, ... ICS-calendar for Outlook, Google Calendar, etc.
""" """
rentals = Rental.objects.all() rentals = Rental.objects.all()

View File

@@ -1,78 +1,78 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Meine Rechnungen / Honorarnoten{% endblock %} {% block title %}Meine Rechnungen{% endblock %}
{% block content %} {% block content %}
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1"> <main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Meine Rechnungen / Honorarnoten</h1> <h1 class="page-title">Meine Rechnungen</h1>
<a href="{% url 'finance:bill_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto"> <a href="{% url 'finance:bill_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto">
<i class="fa-solid fa-plus mr-1"></i> Rechnung einreichen <i class="fa-solid fa-plus mr-1"></i> Rechnung einreichen
</a> </a>
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-5xl">
<div class="overflow-x-scroll shadow rounded"> <div class="overflow-x-scroll shadow rounded">
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr> <tr>
<th class="text-right">Datum</th> <th class="text-right">Datum</th>
<th class="text-left">Verwendungszweck / Tätigkeit</th> <th class="text-left">Verwendungszweck / Tätigkeit</th>
<th class="text-right">Summe</th> <th class="text-right">Summe</th>
<th>Status</th> <th>Status</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for result in object_list %} {% for result in object_list %}
<tr> <tr>
<td class="text-right">{{ result.date|date:"d.m.Y" }}</td> <td class="text-right">{{ result.date|date:"d.m.Y" }}</td>
<td>{{ result.purpose }}</td> <td>{{ result.purpose }}</td>
<td class="text-right">{{ result.amount }}€</td> <td class="text-right">{{ result.amount }}€</td>
<td class="text-center"> <td class="text-center">
{% if result.model == "BILL" %} {% if result.model == "BILL" %}
{% if result.status == bill_status.SUBMITTED %} {% if result.status == bill_status.SUBMITTED %}
<span class="badge badge-info">{{ bill_status.SUBMITTED.label }}</span> <span class="badge badge-info">{{ bill_status.SUBMITTED.label }}</span>
{% elif result.status == bill_status.INCOMPLETED %} {% elif result.status == bill_status.INCOMPLETED %}
<span class="badge badge-danger">{{ bill_status.INCOMPLETED.label }}</span> <span class="badge badge-danger">{{ bill_status.INCOMPLETED.label }}</span>
{% elif result.status == bill_status.CLEARED %} {% elif result.status == bill_status.CLEARED %}
<span class="badge badge-warning">{{ bill_status.CLEARED.label }}</span> <span class="badge badge-warning">{{ bill_status.CLEARED.label }}</span>
{% elif result.status == bill_status.FINISHED %} {% elif result.status == bill_status.FINISHED %}
<span class="badge badge-success">{{ bill_status.FINISHED.label }}</span> <span class="badge badge-success">{{ bill_status.FINISHED.label }}</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if result.model == "BILL" %} {% if result.model == "BILL" %}
<a href="{% url 'finance:bill_update' result.id %}" class="btn btn-small btn-tertiary"><i class="fa-solid fa-pen-to-square" aria-label="Bearbeiten" title="Bearbeiten"></i></a> <a href="{% url 'finance:bill_update' result.id %}" class="btn btn-small btn-tertiary"><i class="fa-solid fa-pen-to-square" aria-label="Bearbeiten" title="Bearbeiten"></i></a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-4 w-full flex flex-col-reverse sm:flex-row gap-y-4 justify-between items-center"> <div class="mt-4 w-full flex flex-col-reverse sm:flex-row gap-y-4 justify-between items-center">
<div class="pagination-container"> <div class="pagination-container">
<div class="pagination"> <div class="pagination">
<span class="step-links"> <span class="step-links">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}"><i class="fa-solid fa-arrow-left" aria-label="Eine Seite zurück"></i> Zurück</a> <a href="?page={{ page_obj.previous_page_number }}"><i class="fa-solid fa-arrow-left" aria-label="Eine Seite zurück"></i> Zurück</a>
<a href="?page=1">1</a> <a href="?page=1">1</a>
{% endif %} {% endif %}
<span class="current active"> <span class="current active">
<a href="#" class="active">{{ page_obj.number }}</a> <a href="#" class="active">{{ page_obj.number }}</a>
</span> </span>
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a> <a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
<a href="?page={{ page_obj.next_page_number }}">Vor <i class="fa-solid fa-arrow-right" aria-label="Eine Seite vor"></i></a> <a href="?page={{ page_obj.next_page_number }}">Vor <i class="fa-solid fa-arrow-right" aria-label="Eine Seite vor"></i></a>
{% endif %} {% endif %}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
{% endblock content %} {% endblock content %}

View File

@@ -58,7 +58,7 @@
<li> <li>
<a href="{% url 'finance:bill_list' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white"> <a href="{% url 'finance:bill_list' %}" class="flex items-center py-2 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white">
<i class="fa-solid fa-list mr-2"></i> <i class="fa-solid fa-list mr-2"></i>
<span class="text-sm font-medium">Meine Rechnungen / Honorarnoten</span> <span class="text-sm font-medium">Meine Rechnungen</span>
</a> </a>
</li> </li>
<li> <li>

View File

@@ -1,192 +1,281 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}Verleih{% endblock %} {% block title %}Verleih{% endblock %}
{% block content %} {% block content %}
<main class="container mx-auto w-full px-4 my-8 flex-1"> <main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Verleih</h1> <h1 class="page-title">Verleih</h1>
<section class="text-center my-6"> <section class="text-center my-6">
<p class="my-6 text-gray-900 dark:text-gray-100"> <p class="my-6 text-gray-900 dark:text-gray-100">
Willkommen bei unserem Verleih! Willkommen bei unserem Verleih!
</p> </p>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'rental:calendar' %}" class="block btn btn-secondary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto my-6"><i class="fa-solid fa-calendar-days mr-2"></i>Verleih-Kalender abonnieren</a> <a
{% endif %} href="{% url 'rental:calendar' %}"
class="block btn btn-secondary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto my-6"
<a href="{% url 'rental:rental_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto"> title="Kalender im iCal-Format abonnieren"
<i class="fa-solid fa-plus mr-1"></i> Verleih anfragen >
</a> <i class="fa-solid fa-calendar-days mr-2"></i>Verleih-Kalender abonnieren
</section> </a>
{% endif %}
<form action="" method="GET">
<section> <a
<div class="grid grid-cols-1 gap-x-6 gap-y-6 lg:grid-cols-6 sm:grid-cols-3 pb-6"> href="{% url 'rental:rental_create' %}"
<button class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto"
id="filterDropdownButton" title="Neue Verleihanfrage erstellen"
data-dropdown-toggle="filterDropdown" >
class="w-full md:w-auto flex items-center justify-center py-2 px-4 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" <i class="fa-solid fa-plus mr-1"></i> Verleih anfragen
type="button" </a>
> </section>
<i class="h-4 w-4 mr-2 fa-solid fa-filter"></i>
Filter <form action="" method="GET">
<i class="-mr-1 ml-1.5 mt-1 w-5 h-5 fa-solid fa-chevron-down"></i> <section>
</button> <div class="grid grid-cols-1 gap-x-6 gap-y-6 xl:grid-cols-6 sm:grid-cols-3 pb-6">
<div id="filterDropdown" class="z-10 hidden w-56 p-3 bg-white rounded-lg shadow dark:bg-gray-700"> <button
<h6 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Verleihgegenstände</h6> id="filterDropdownButton"
<ul class="space-y-2 text-sm" aria-labelledby="filterDropdownButton"> data-dropdown-toggle="filterDropdown"
{% for item in rentalitems %} class="w-full md:w-auto flex items-center justify-center py-2 px-4 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
<li class="flex items-center"> type="button"
<input >
id="item_{{ item.id }}" <i class="h-4 w-4 mr-2 fa-solid fa-filter"></i>
type="checkbox" Filter
name="rentalitems" <i class="-mr-1 ml-1.5 mt-1 w-5 h-5 fa-solid fa-chevron-down"></i>
value="{{ item.name }}" </button>
{% for key, value in rentalitem_filters.items %} <div id="filterDropdown" class="z-10 hidden w-56 p-3 bg-white rounded-lg shadow dark:bg-gray-700">
{% if key == "rentalitems" and item.name in value %} <h6 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Verleihgegenstände</h6>
checked <ul class="space-y-2 text-sm" aria-labelledby="filterDropdownButton">
{% endif %} {% for item in rentalitems %}
{% endfor %} <li class="flex items-center">
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500" <input
> id="item_{{ item.id }}"
<label for="item_{{ item.id }}" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</label> type="checkbox"
</li> name="rentalitems"
{% endfor %} value="{{ item.name }}"
</ul> {% for key, value in rentalitem_filters.items %}
</div> {% if key == "rentalitems" and item.name in value %}
checked
<button type="submit" class="block btn btn-primary" name="" value="Submit">Filter anwenden</button> {% endif %}
</div> {% endfor %}
</section> class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
>
<section class="text-center"> <label for="item_{{ item.id }}" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</label>
<div class="wrapper mx-auto bg-white rounded shadow max-w-full"> </li>
<div class="header flex justify-between border-b p-2"> {% endfor %}
<span class="text-lg font-bold">{{ month|date:'F Y' }}</span> </ul>
</div>
<div class="buttons">
<button type="submit" class="p-1" name="month" value="{{ prev_month|date:'Y-m' }}"> <div class="flex w-full md:w-auto items-center justify-center py-2 px-4 border border-gray-200 rounded-sm dark:border-gray-700">
<i class="fa-regular fa-circle-left"></i> <input
</button> {% if view_type == 'month' %}checked{% endif %}
<button type="submit" class="p-1" name="month" value="{{ next_month|date:'Y-m' }}"> id="bordered-radio-1"
<i class="fa-regular fa-circle-right"></i> type="radio"
</button> value="month"
</div> name="view_type"
</div> title="Zur Monatsansicht wechseln"
class="ml-2 mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
<table class="table-fixed w-full mx-auto"> >
<thead> <label for="bordered-radio-1" class="w-full ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Monatsansicht</label>
<tr> </div>
<th class="p-2 border-r h-10 xl:text-sm text-xs"> <div class="flex w-full md:w-auto items-center justify-center py-2 px-4 border border-gray-200 rounded-sm dark:border-gray-700">
<span class="xl:block lg:block md:block sm:block hidden">Monday</span> <input
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span> {% if view_type == 'week' %}checked{% endif %}
</th> id="bordered-radio-2"
<th class="p-2 border-r h-10 xl:text-sm text-xs"> type="radio"
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span> value="week"
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span> name="view_type"
</th> title="Zur Wochenansicht wechseln"
<th class="p-2 border-r h-10 xl:text-sm text-xs"> class="ml-2 mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span> >
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span> <label for="bordered-radio-2" class="w-full ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Wochenansicht</label>
</th> </div>
<th class="p-2 border-r h-10 xl:text-sm text-xs">
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span> <input
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span> type="hidden" name="period_value"
</th> value="{% if view_type == 'month' %}{{ view_period|date:'Y-m' }}{% else %}{{ view_period }}{% endif %}"
<th class="p-2 border-r h-10 xl:text-sm text-xs"> >
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span> <button
</th> type="submit"
<th class="p-2 border-r h-10 xl:text-sm text-xs"> class="block btn btn-primary"
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span> title="Filter anwenden und Kalender aktualisieren"
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span> >
</th> Filter anwenden
<th class="p-2 border-r h-10 xl:text-sm text-xs"> </button>
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span> </div>
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span> </section>
</th>
</tr> <section class="text-center">
</thead> <div class="wrapper mx-auto bg-white rounded shadow max-w-full">
<div class="header flex justify-between border-b p-2">
<tbody> <span class="text-lg font-bold">
{% for day in days_of_prev_month %} {% if view_type == 'month' %}
{% if day.weekday == 0 %} {{ view_period|date:'F Y' }}
<tr class="text-center h-20"> {% else %}
{% endif %} {% if days_of_view_period.0.month != days_of_view_period.6.month %}
KW{{ week_num|stringformat:"02d" }} - {{ days_of_view_period.0|date:'F' }} / {{ days_of_view_period.6|date:'F Y' }}
<td class="border p-1 h-40"> {% else %}
<div class="flex flex-col h-40"> KW{{ week_num|stringformat:"02d" }} - {{ days_of_view_period.0|date:'F Y' }}
<div class="top h-5 w-full"> {% endif %}
<span class="text-gray-500 text-sm">{{ day.day }}</span> {% endif %}
</div> </span>
<div class="bottom flex-grow h-30 py-1 w-full"></div>
</div> <div class="buttons">
</td> <button
{% endfor %} type="submit" class="p-1" name="prev_period"
title="{% if view_type == 'month' %}Zum vorherigen Monat{% else %}Zur vorherigen Woche{% endif %}"
{% for day in days_of_month %} value="{% if view_type == 'month' %}{{ prev_period|date:'Y-m' }}{% else %}{{ prev_period }}{% endif %}"
{% if day.weekday == 0 %} >
<tr class="text-center h-20"> <i class="fa-regular fa-circle-left"></i>
{% endif %} </button>
<button
<td class="border p-1 h-40"> type="submit" class="p-1" name="next_period"
<div class="flex flex-col h-40"> title="{% if view_type == 'month' %}Zum nächsten Monat{% else %}Zur nächsten Woche{% endif %}"
<div class="top h-5 w-full"> value="{% if view_type == 'month' %}{{ next_period|date:'Y-m' }}{% else %}{{ next_period }}{% endif %}"
{% if day == today %} >
<span class="text-gray-100 dark:text-gray-900 border-2 border-blue-900 dark:border-blue-100 rounded-full bg-blue-900 dark:bg-blue-100">{{ day.day }}</span> <i class="fa-regular fa-circle-right"></i>
{% else %} </button>
<span class="text-gray-900 dark:text-gray-100">{{ day.day }}</span> </div>
{% endif %} </div>
</div>
<table class="table-fixed w-full mx-auto">
{% for key, names in rental_dict.items %} <thead>
{% if key == day %} <tr>
{% for name in names %} <th class="p-2 border-r h-10 xl:text-sm text-xs">
<div class="bottom h-30 py-1 w-full"> <span class="xl:block lg:block md:block sm:block hidden">Montag</span>
<div class="event bg-purple-400 text-white rounded p-1 text-sm"> <span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mo</span>
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span> </th>
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span> <th class="p-2 border-r h-10 xl:text-sm text-xs">
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span> <span class="xl:block lg:block md:block sm:block hidden">Dienstag</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span> <span class="xl:hidden lg:hidden md:hidden sm:hidden block">Di</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span> </th>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span> <th class="p-2 border-r h-10 xl:text-sm text-xs">
</div> <span class="xl:block lg:block md:block sm:block hidden">Mittwoch</span>
</div> <span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mi</span>
{% empty %} </th>
<div class="bottom flex-grow h-30 py-1 w-full"></div> <th class="p-2 border-r h-10 xl:text-sm text-xs">
{% endfor %} <span class="xl:block lg:block md:block sm:block hidden">Donnerstag</span>
{% endif %} <span class="xl:hidden lg:hidden md:hidden sm:hidden block">Do</span>
{% endfor %} </th>
</div> <th class="p-2 border-r h-10 xl:text-sm text-xs">
</td> <span class="xl:block lg:block md:block sm:block hidden">Freitag</span>
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fr</span>
{% if day.weekday == 6 %} </th>
</tr> <th class="p-2 border-r h-10 xl:text-sm text-xs">
{% endif %} <span class="xl:block lg:block md:block sm:block hidden">Samstag</span>
{% endfor %} <span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sa</span>
</th>
{% for day in days_of_next_month %} <th class="p-2 border-r h-10 xl:text-sm text-xs">
<td class="border p-1 h-40"> <span class="xl:block lg:block md:block sm:block hidden">Sonntag</span>
<div class="flex flex-col h-40"> <span class="xl:hidden lg:hidden md:hidden sm:hidden block">So</span>
<div class="top h-5 w-full"> </th>
<span class="text-gray-500 text-sm">{{ day.day }}</span> </tr>
</div> </thead>
<div class="bottom flex-grow h-30 py-1 w-full"></div>
</div> <tbody>
</td> {% for day in days_of_prev_period %}
{% if day.weekday == 0 %}
{% if day.weekday == 6 %} <tr class="text-center h-20">
</tr> {% endif %}
{% endif %}
{% endfor %} <td class="border p-1 h-40">
<div class="flex flex-col h-40">
</tbody> <div class="top h-5 w-full">
</table> <span class="text-gray-500 text-sm">{{ day.day }}</span>
</div> </div>
</section> <div class="bottom flex-grow h-30 py-1 w-full"></div>
</form> </div>
</main> </td>
{% endblock content %} {% endfor %}
{% for day in days_of_view_period %}
{% if day.weekday == 0 %}
<tr class="text-center h-40">
{% endif %}
<td class="border p-1 h-full">
<div
class="flex flex-col{% if view_type == 'month' %} h-40{% else %} h-full{% endif %}"
>
<div class="top h-5 w-full">
{% if day == today %}
<span class="text-gray-100 dark:text-gray-900 border-2 border-blue-900 dark:border-blue-100 rounded-full bg-blue-900 dark:bg-blue-100">{{ day.day }}</span>
{% else %}
<span class="text-gray-900 dark:text-gray-100">{{ day.day }}</span>
{% endif %}
</div>
{% for key, names in rental_dict.items %}
{% if key == day %}
{% if view_type == 'month' %}
{% for name in names|slice:":3" %}
<div class="bottom h-30 py-1 w-full">
<div class="event bg-purple-400 text-white rounded p-1 text-sm" title="{{ name }}">
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span>
</div>
</div>
{% endfor %}
{% if names|length > 3 %}
<div class="bottom h-30 py-1 w-full">
<a href="?view_type=week&period_value={{ day|date:'o-\K\WW' }}"
class="event bg-gray-300 hover:bg-gray-400 text-gray-700 rounded p-1 text-sm hover:underline block cursor-pointer"
title="Alle Verleihgegenstände in der Wochenansicht anzeigen">
<span class="event-name whitespace-nowrap sm:block hidden">+{{ names|length|add:"-3" }} weitere</span>
<span class="event-name whitespace-nowrap sm:hidden block">+{{ names|length|add:"-3" }}</span>
</a>
</div>
{% endif %}
{% else %}
{% for name in names %}
<div class="bottom h-30 py-1 w-full">
<div class="event bg-purple-400 text-white rounded p-1 text-sm" title="{{ name }}">
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span>
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span>
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
</div>
</td>
{% if day.weekday == 6 %}
</tr>
{% endif %}
{% endfor %}
{% for day in days_of_next_period %}
<td class="border p-1 h-40">
<div class="flex flex-col h-40">
<div class="top h-5 w-full">
<span class="text-gray-500 text-sm">{{ day.day }}</span>
</div>
<div class="bottom flex-grow h-30 py-1 w-full"></div>
</div>
</td>
{% if day.weekday == 6 %}
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</section>
</form>
</main>
{% endblock content %}

View File

@@ -1,121 +1,123 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block title %}Verleih Anfrage{% endblock %} {% block title %}Verleih Anfrage{% endblock %}
{% block content %} {% block content %}
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1"> <main class="container mx-auto w-full px-4 my-8 flex-1">
<h1 class="page-title">Verleih Anfrage</h1> <h1 class="page-title">Verleih Anfrage</h1>
<form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl"> <form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl">
{% csrf_token %} {% csrf_token %}
{% include "baseform/non_field_errors.html" %} {% include "baseform/non_field_errors.html" %}
<section> <section>
<h2>Persönliche Daten</h2> <h2>Persönliche Daten</h2>
<small>Bitte gib deine persönlichen Daten ein.</small> <small>Bitte gib deine persönlichen Daten ein.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6"> <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.firstname %} {% include "baseform/text.html" with field=form.firstname %}
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.surname %} {% include "baseform/text.html" with field=form.surname %}
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.organization %} {% include "baseform/text.html" with field=form.organization %}
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.matriculation_number %} {% include "baseform/text.html" with field=form.matriculation_number %}
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/email.html" with field=form.email %} {% include "baseform/email.html" with field=form.email %}
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
{% include "baseform/text.html" with field=form.phone %} {% include "baseform/text.html" with field=form.phone %}
</div> </div>
</div> </div>
</section> </section>
<section> <section>
<h2>Verleihgegenstände</h2> <h2>Verleihgegenstände</h2>
<small>Wähl deine gewünschten Verleihgegenstände aus.</small> <small>Wähl deine gewünschten Verleihgegenstände aus.</small>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6 pb-6 col-span-full"> <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6 pb-6 col-span-full">
{% if form.rentalitems.errors %} {% if form.rentalitems.errors %}
<div class="col-span-full alert alert-danger"> <div class="col-span-full alert alert-danger">
<div class="alert-body">{{ form.rentalitems.errors }}</div> <div class="alert-body">{{ form.rentalitems.errors }}</div>
</div> </div>
{% endif %} {% endif %}
<div id="{{ form.rentalitems.auto_id }}" class="col-span-full"> <div id="{{ form.rentalitems.auto_id }}" class="col-span-full">
{% for elem in form.rentalitems %} {% for elem in form.rentalitems %}
<div class="col-span-2 mb-2"> <div class="col-span-2 mb-2">
<label for="{{ elem.data.value }}">
<label for="{{ elem.id_for_label }}"> <input
<input type="checkbox"
type="checkbox" id="{{ elem.data.value }}"
id="{{ elem.id_for_label }}" name="{{ form.rentalitems.html_name }}"
name="{{ form.rentalitems.html_name }}" value="{{ elem.data.value }}"
value="{{ elem.data.value }}" {% if elem.data.selected %}checked{% endif %}
class="rounded border-gray-300 dark:border-none text-proprietary shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50" class="rounded border-gray-300 dark:border-none text-proprietary shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50"
> >
<a <a
href="{% url 'rental:rentalitem_detail' elem.data.value %}" href="{% url 'rental:rentalitem_detail' elem.data.value %}"
class="text-gray-700 dark:text-gray-200 underline hover:text-blue-700 dark:hover:text-blue-300" class="text-gray-700 dark:text-gray-200 underline hover:text-blue-700 dark:hover:text-blue-300"
>{{ elem.choice_label }}</a> title="Details zu {{ elem.choice_label }}"
</label> aria-label="Details zu {{ elem.choice_label }}"
>{{ elem.choice_label }}</a>
{% for item in rentalitems_addinfo %} </label>
{% if item.name == elem.choice_label and item.induction %}
<p class="text-xs text-gray-700 dark:text-gray-200">Einschulung erforderlich!</p> {% for item in rentalitems_addinfo %}
{% endif %} {% if item.name == elem.choice_label and item.induction %}
{% endfor %} <p class="text-xs text-gray-700 dark:text-gray-200">Einschulung erforderlich!</p>
</div> {% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div> {% endfor %}
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6"> </div>
<div class="sm:col-span-3"> </div>
{% include "baseform/date.html" with field=form.date_start %} <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
</div> <div class="sm:col-span-3">
<div class="sm:col-span-3"> {% include "baseform/date.html" with field=form.date_start %}
{% include "baseform/date.html" with field=form.date_end %} </div>
</div> <div class="sm:col-span-3">
<div class="col-span-full"> {% include "baseform/date.html" with field=form.date_end %}
{% include "baseform/textarea.html" with field=form.reason %} </div>
</div> <div class="col-span-full">
</div> {% include "baseform/textarea.html" with field=form.reason %}
</section> </div>
</div>
<section> </section>
<h2>Zusätzliche Informationen</h2>
<small>Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben.</small> <section>
<h2>Zusätzliche Informationen</h2>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6"> <small>Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben.</small>
<div class="col-span-full">
{% include "baseform/textarea.html" with field=form.comment %} <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
</div> <div class="col-span-full">
</div> {% include "baseform/textarea.html" with field=form.comment %}
</section> </div>
</div>
<section> </section>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
<div class="col-span-full text-gray-700 dark:text-gray-200"> <section>
{% include "baseform/checkbox.html" with field=form.conformation %} <div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
</div> <div class="col-span-full text-gray-700 dark:text-gray-200">
<a href="{% static 'rental/verleihregeln.pdf' %}" target='_blank' class="inline-flex items-center px-2 py-1"> {% include "baseform/checkbox.html" with field=form.conformation %}
<i class="fa-solid fa-file-pdf fa-fw text-red-800 dark:text-red-500 md:text-inherit group-hover:text-red-800 dark:group-hover:text-red-500"></i> </div>
<span class="ml-2 sm:ml-1 text-gray-700 dark:text-gray-200">Verleihregeln</span> <a href="{% static 'rental/verleihregeln.pdf' %}" target='_blank' class="inline-flex items-center px-2 py-1">
</a> <i class="fa-solid fa-file-pdf fa-fw text-red-800 dark:text-red-500 md:text-inherit group-hover:text-red-800 dark:group-hover:text-red-500"></i>
</div> <span class="ml-2 sm:ml-1 text-gray-700 dark:text-gray-200">Verleihregeln</span>
</section> </a>
</div>
<section class="flex justify-end"> </section>
<button type="submit" class="btn btn-primary w-full sm:w-auto" value="Einreichen">Anfrage abschicken</button>
</section> <section class="flex justify-end">
</form> <button type="submit" class="btn btn-primary w-full sm:w-auto" value="Einreichen">Anfrage abschicken</button>
</main> </section>
{% endblock content %} </form>
</main>
{% endblock content %}

View File

@@ -1,17 +1,22 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %} {% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %}
{% block content %} {% block content %}
<!-- Main Content --> <!-- Main Content -->
<main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl"> <main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl">
<section class="block w-full"> <section class="block w-full">
<p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de"> <p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
Deine Verleihanfrage mit der Nummer #{{ pk }} wurde erfolgreich eingereicht. Die Gegenstände können am {{ object.date_start }} während der Beratungszeit (Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring den Gesamtpfand in Höhe von {{ total_deposit }} € in bar mit. Deine Verleihanfrage ist eingegangen - danke dir! 🎉
</p> Wir kümmern uns jetzt darum und melden uns per E-Mail mit den nächsten Schritten.
<div class="mt-10 flex items-center justify-center"> </p>
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a> <p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
</div> Kleiner Hinweis: Bevor du die Sachen abholen kannst, wird deine Anfrage kurz geprüft und freigegeben.
</section> Sobald alles genehmigt ist, bekommst du von uns eine Mail.
</main> </p>
{% endblock content %} <div class="mt-10 flex items-center justify-center">
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a>
</div>
</section>
</main>
{% endblock content %}

View File

@@ -26,6 +26,12 @@ server {
location /files { location /files {
alias /usr/src/app/files/; alias /usr/src/app/files/;
}
location /assets/ {
alias /usr/src/app/assets/;
try_files $uri $uri/ =404;
add_header X-Content-Type-Options nosniff;
} }
# location /files/uploads/finance { # location /files/uploads/finance {