From 719774dcf48e94c6b88aafeb0cd786b7855c6b9f Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Sun, 26 Oct 2025 23:57:37 +0100 Subject: [PATCH 01/23] delete the word 'Honorarnoten' --- fet2020/templates/finance/index.html | 156 +++++++++++++-------------- fet2020/templates/home.html | 2 +- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/fet2020/templates/finance/index.html b/fet2020/templates/finance/index.html index 99003e69..9208b5e3 100644 --- a/fet2020/templates/finance/index.html +++ b/fet2020/templates/finance/index.html @@ -1,78 +1,78 @@ -{% extends "base.html" %} - -{% block title %}Meine Rechnungen / Honorarnoten{% endblock %} - -{% block content %} - -
-

Meine Rechnungen / Honorarnoten

- - Rechnung einreichen - - -
-
- - - - - - - - - - - - {% for result in object_list %} - - - - - - - - {% endfor %} - -
DatumVerwendungszweck / TätigkeitSummeStatus
{{ result.date|date:"d.m.Y" }}{{ result.purpose }}{{ result.amount }}€ - {% if result.model == "BILL" %} - {% if result.status == bill_status.SUBMITTED %} - {{ bill_status.SUBMITTED.label }} - {% elif result.status == bill_status.INCOMPLETED %} - {{ bill_status.INCOMPLETED.label }} - {% elif result.status == bill_status.CLEARED %} - {{ bill_status.CLEARED.label }} - {% elif result.status == bill_status.FINISHED %} - {{ bill_status.FINISHED.label }} - {% endif %} - {% endif %} - - {% if result.model == "BILL" %} - - {% endif %} -
-
- -
-
- -
-
-
-
-{% endblock content %} +{% extends "base.html" %} + +{% block title %}Meine Rechnungen{% endblock %} + +{% block content %} + +
+

Meine Rechnungen

+ + Rechnung einreichen + + +
+
+ + + + + + + + + + + + {% for result in object_list %} + + + + + + + + {% endfor %} + +
DatumVerwendungszweck / TätigkeitSummeStatus
{{ result.date|date:"d.m.Y" }}{{ result.purpose }}{{ result.amount }}€ + {% if result.model == "BILL" %} + {% if result.status == bill_status.SUBMITTED %} + {{ bill_status.SUBMITTED.label }} + {% elif result.status == bill_status.INCOMPLETED %} + {{ bill_status.INCOMPLETED.label }} + {% elif result.status == bill_status.CLEARED %} + {{ bill_status.CLEARED.label }} + {% elif result.status == bill_status.FINISHED %} + {{ bill_status.FINISHED.label }} + {% endif %} + {% endif %} + + {% if result.model == "BILL" %} + + {% endif %} +
+
+ +
+
+ +
+
+
+
+{% endblock content %} diff --git a/fet2020/templates/home.html b/fet2020/templates/home.html index ee9563f8..882c5075 100644 --- a/fet2020/templates/home.html +++ b/fet2020/templates/home.html @@ -58,7 +58,7 @@
  • - Meine Rechnungen / Honorarnoten + Meine Rechnungen
  • From 153e937bfe525f91f6cb47d9352f87a541006eec Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Mon, 27 Oct 2025 20:00:58 +0100 Subject: [PATCH 02/23] add conf to have access to folder assets --- docker-compose.yml | 6 ++++-- nginx/nginx.conf | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 015a4c45..d8cd329d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,14 +10,15 @@ services: - django-homepage volumes: - files-volume:/usr/src/app/files + - ./assets:/usr/src/app/assets:ro networks: - fet-network django-homepage: container_name: django-container image: django-image:latest environment: - HOST_NAME: "fet.at" - DEBUG: "False" + HOST_NAME: "fet.at" + DEBUG: "False" LDAP: "True" SECRET_KEY: "sae34sADfrFr89E!Gl#f!34hdjGR#!jopi4qFEr#4R56rT56zT2#wE1!feGp" MYSQL_USER: "user" @@ -32,6 +33,7 @@ services: - ./fet2020:/usr/src/app - ./gallery:/usr/src/app/files/uploads/gallery:shared - files-volume:/usr/src/app/files + - ./assets:/usr/src/app/assets:ro networks: - fet-network - django-db-network diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 605647bc..f19584b4 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -26,6 +26,12 @@ server { location /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 { From 6e57c28d4b1fbe3317c5180a76f3762b5e65c4d2 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Mon, 27 Oct 2025 20:02:33 +0100 Subject: [PATCH 03/23] add conf to have access to folder gallery --- docker-compose.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d8cd329d..a43ca451 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,8 @@ services: depends_on: - django-homepage volumes: - - files-volume:/usr/src/app/files + - files-volume:/usr/src/app/files + - ./gallery:/usr/src/app/files/uploads/gallery - ./assets:/usr/src/app/assets:ro networks: - fet-network @@ -17,8 +18,8 @@ services: container_name: django-container image: django-image:latest environment: - HOST_NAME: "fet.at" - DEBUG: "False" + HOST_NAME: "fet.at" + DEBUG: "False" LDAP: "True" SECRET_KEY: "sae34sADfrFr89E!Gl#f!34hdjGR#!jopi4qFEr#4R56rT56zT2#wE1!feGp" MYSQL_USER: "user" From 0bb313bbed30579eb6097ff5b5747b0167e5ef00 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Mon, 27 Oct 2025 20:16:05 +0100 Subject: [PATCH 04/23] fix gitignore --- .gitignore | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index a900da2a..118758a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,21 @@ -.env/* -*.pyc -*_design1 -fet2020/.env/* -*.sqlite3 -.theia/* -.flake8 -migrate -run -*.pid -*~ -APIKEY.txt -tmp -.ruff_cache -.venv -etherpad -files -flowbite -gallery -tailwind -whoosh_index +.env/* +*.pyc +*_design1 +fet2020/.env/* +*.sqlite3 +.theia/* +.flake8 +migrate +run +*.pid +*~ +APIKEY.txt +tmp +.ruff_cache +.venv +etherpad +files +flowbite +gallery/* +tailwind +whoosh_index From 9eaf3ecdd8128aad21e5d9e6bfe1569abb1e76b6 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Mon, 27 Oct 2025 20:18:01 +0100 Subject: [PATCH 05/23] add migrations --- ...ption_alter_album_photographer_and_more.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 fet2020/gallery/migrations/0003_alter_album_description_alter_album_photographer_and_more.py diff --git a/fet2020/gallery/migrations/0003_alter_album_description_alter_album_photographer_and_more.py b/fet2020/gallery/migrations/0003_alter_album_description_alter_album_photographer_and_more.py new file mode 100644 index 00000000..d53b6ecf --- /dev/null +++ b/fet2020/gallery/migrations/0003_alter_album_description_alter_album_photographer_and_more.py @@ -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'), + ), + ] From 9cc1068e638ca903b8ce988def04000f72d4897c Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Mon, 27 Oct 2025 20:18:21 +0100 Subject: [PATCH 06/23] Sort image list alphabetically --- fet2020/gallery/utils.py | 118 ++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/fet2020/gallery/utils.py b/fet2020/gallery/utils.py index 44073387..9068bc05 100644 --- a/fet2020/gallery/utils.py +++ b/fet2020/gallery/utils.py @@ -1,58 +1,60 @@ -import logging -import os -from pathlib import Path - -from django.conf import settings -from django.core.validators import get_available_image_extensions -from PIL import Image, ImageOps - -gallery_path = Path(settings.MEDIA_ROOT) / 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_url = Path(settings.MEDIA_URL) / settings.GALLERY["thumb_path"] -logger = logging.getLogger(__name__) -size = (320, 320) - -Image.logger.setLevel(level=logging.INFO) - - -def get_image_list(folder_name: str) -> list: - image_path = Path(gallery_path) / folder_name - thumb_path = Path(gallery_thumb_path) / folder_name - img_list = [] - - if not Path(image_path).exists(): - logger.info("Image path '%s' not found.", image_path) - return img_list - - Path(thumb_path).mkdir(exist_ok=True) - - for _file in os.listdir(image_path): - if Path(_file).suffix.lower()[1:] not in get_available_image_extensions(): - continue - - thumb_file_path = Path(thumb_path) / f"thumb_{_file}" - if not Path(thumb_file_path).exists(): - with Image.open(Path(image_path) / _file, "r") as im: - if im._getexif() is not None: - im = ImageOps.exif_transpose(im) - - thumb = ImageOps.fit(im, size, Image.Resampling.LANCZOS) - thumb.save(thumb_file_path) - logger.info("Save thumb 'thumb_%s'.", _file) - - img_dict = { - "title": _file, - "image_url": Path(gallery_path_url) / folder_name / _file, - "thumb_url": Path(gallery_thumb_path_url) / folder_name / f"thumb_{_file}", - } - img_list.append(img_dict) - - return img_list - - -def get_folder_list(): - if Path(gallery_path).exists(): - return next(os.walk(gallery_path))[1] - - return None +import logging +import os +from pathlib import Path + +from django.conf import settings +from django.core.validators import get_available_image_extensions +from PIL import Image, ImageOps + +gallery_path = Path(settings.MEDIA_ROOT) / 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_url = Path(settings.MEDIA_URL) / settings.GALLERY["thumb_path"] +logger = logging.getLogger(__name__) +size = (320, 320) + +Image.logger.setLevel(level=logging.INFO) + + +def get_image_list(folder_name: str) -> list: + image_path = Path(gallery_path) / folder_name + thumb_path = Path(gallery_thumb_path) / folder_name + img_list = [] + + if not Path(image_path).exists(): + logger.info("Image path '%s' not found.", image_path) + return img_list + + Path(thumb_path).mkdir(exist_ok=True) + + for _file in os.listdir(image_path): + if Path(_file).suffix.lower()[1:] not in get_available_image_extensions(): + continue + + thumb_file_path = Path(thumb_path) / f"thumb_{_file}" + if not Path(thumb_file_path).exists(): + with Image.open(Path(image_path) / _file, "r") as im: + if im._getexif() is not None: + im = ImageOps.exif_transpose(im) + + thumb = ImageOps.fit(im, size, Image.Resampling.LANCZOS) + thumb.save(thumb_file_path) + logger.info("Save thumb 'thumb_%s'.", _file) + + img_dict = { + "title": _file, + "image_url": Path(gallery_path_url) / folder_name / _file, + "thumb_url": Path(gallery_thumb_path_url) / folder_name / f"thumb_{_file}", + } + img_list.append(img_dict) + + # 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] + + return None From e10fa77c3ac69008b1998b897c6453a2abbc2e1e Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Wed, 29 Oct 2025 22:29:10 +0100 Subject: [PATCH 07/23] add week view --- fet2020/rental/views.py | 241 +++++++++---- fet2020/templates/rental/calendar.html | 473 +++++++++++++++---------- 2 files changed, 453 insertions(+), 261 deletions(-) diff --git a/fet2020/rental/views.py b/fet2020/rental/views.py index f6116094..022cb7f1 100644 --- a/fet2020/rental/views.py +++ b/fet2020/rental/views.py @@ -1,5 +1,6 @@ import calendar import datetime +from datetime import date from django.db.models import Q from django.shortcuts import render @@ -12,87 +13,176 @@ from .forms import RentalCreateForm from .models import Rental, RentalItem +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) -> tuple[date, bool]: + display_date: date = None + changed: bool = False # Indicate if the view has changed (used to reset filters) + + # 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 = datetime.datetime.now(tz=datetime.UTC).date() + display_date = today - datetime.timedelta(days=today.weekday()) + + changed = True + + # 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 = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) + + changed = True + + return display_date, changed + + +def _get_rental_filters(view_type: str, filters: list) -> list: + if not filters: + items = RentalItem.objects.all() + # if view_type == "month": + # items = items[:4] + filters = [item.name for item in items] + # else: + # if view_type == "month": + # filters = filters[:4] + + return filters + + class RentalListView(ListView): model = Rental template_name = "rental/calendar.html" - # Month is the month displayed in the calendar (and should be the first day of the month) - month = None - # Rental items to filter - rentalitem_filters = [] + def __init__(self): + super().__init__() + # Current display period and view settings + self.display_period = None + self.view_type = "month" # Default view + self.rentalitem_filters = [] def get(self, request, *args, **kwargs): - # Get the rental items from the filter (max. 4) - self.rentalitem_filters = request.GET.getlist("rentalitems", [])[:4] - if not self.rentalitem_filters: - for rentalitem in RentalItem.objects.all()[:4]: - self.rentalitem_filters.append(rentalitem.name) + # Get view parameters from request + _view_type = request.GET.get("view_type", "week") + _period = request.GET.get("period_value", "") + _prev_period = request.GET.get("prev_period", "") + _next_period = request.GET.get("next_period", "") - # Get the displayed month from the request - _date_str = request.GET.get("month", "") - if _date_str: - self.month = ( - datetime.datetime.strptime(_date_str, "%Y-%m").replace(tzinfo=datetime.UTC).date() - ) - else: - self.month = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) + if _prev_period: + _period = _prev_period + elif _next_period: + _period = _next_period + + self.view_type = _view_type + self.display_period, changed = _get_display_period(_view_type, _period) + + _filters = request.GET.getlist("rentalitems", []) + # Reset filter if switched to week view + # if changed: + # _filters = [] + self.rentalitem_filters = _get_rental_filters(_view_type, _filters) + + # 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) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Calculate the day of the previous month from Monday - days_of_prev_month = [] + if self.view_type != "week": + # 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: - for i in range(1, 7): - day = self.month + datetime.timedelta(days=-i) - days_of_prev_month.append(day) - if day.weekday() == calendar.MONDAY: - break + # Add the days of the displayed, previous and next month + days_of_view_period = _calc_days_from_current_month(self.display_period) + context["days_of_view_period"] = days_of_view_period + context["days_of_prev_period"] = _calc_days_from_prev_month(self.display_period) + context["days_of_next_period"] = _calc_days_from_next_month(self.display_period) - # Calculate the days of the next month until Sunday - last_day_of_month = self.month.replace( - day=calendar.monthrange(self.month.year, self.month.month)[1] - ) - days_of_next_month = [] + context["view_type"] = "month" - 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_month.append(day) - if day.weekday() == calendar.SUNDAY: - break + context["week_num"] = None - # Calculate the days of the displayed month - days_of_month = [ - self.month + datetime.timedelta(days=i) - for i in range((last_day_of_month - self.month).days + 1) - ] + else: + # Current week + year, week_num, _ = self.display_period.isocalendar() + context["view_period"] = f"{year}-KW{week_num:02d}" # formats as "2025-KW02" - # Create a dictionary with the rental items for each day - rental_dict = {} - for rental in self.get_queryset(): - for day in days_of_month: - if rental["date_start"] <= day and rental["date_end"] >= day: - if day not in rental_dict: - rental_dict[day] = [] + # Calculate previous week + prev_week = self.display_period - datetime.timedelta(days=7) + prev_year, prev_week_num, _ = prev_week.isocalendar() + context["prev_period"] = f"{prev_year}-KW{prev_week_num:02d}" - if rental["rentalitems__name"] not in rental_dict[day]: - rental_dict[day].append(rental["rentalitems__name"]) + # Calculate next week + 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 - context["month"] = self.month - context["prev_month"] = self.month + datetime.timedelta(days=-1) - context["next_month"] = self.month + datetime.timedelta( - days=calendar.monthrange(self.month.year, self.month.month)[1] + 1 - ) + # Add days of week (7 days starting from first_day_of_week) + days_of_view_period = [ + self.display_period + datetime.timedelta(days=i) for i in range(7) + ] + 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["days_of_month"] = days_of_month - context["days_of_prev_month"] = sorted(days_of_prev_month) - context["days_of_next_month"] = days_of_next_month + context["view_type"] = "week" + + context["week_num"] = week_num # Get the current date for the calendar context["today"] = datetime.datetime.now(tz=datetime.UTC).date() @@ -103,6 +193,17 @@ class RentalListView(ListView): # Add the selected rental items to the context for the filter context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters} + # 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"] = rental_dict return context @@ -118,20 +219,22 @@ class RentalListView(ListView): ) ) - last_day_of_month = self.month.replace( - day=calendar.monthrange(self.month.year, self.month.month)[1] - ) + if self.view_type == "week": + 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 - qs_new = qs.filter(date_start__gte=self.month, date_start__lte=last_day_of_month) - qs_new |= qs.filter(date_end__gte=self.month, date_end__lte=last_day_of_month) + if self.display_period and qs_date_end: + 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 qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct() - qs = qs.values("id", "date_start", "date_end", "rentalitems__name").distinct() - - return qs + return qs.values("id", "date_start", "date_end", "rentalitems__name").distinct() class RentalCreateView(CreateView): @@ -175,7 +278,7 @@ class RentalItemDetailView(DetailView): def rental_calendar(request): """ - ICS-calendar for outlook, google calender, ... + ICS-calendar for Outlook, Google Calendar, etc. """ rentals = Rental.objects.all() diff --git a/fet2020/templates/rental/calendar.html b/fet2020/templates/rental/calendar.html index 8cca61f5..722da159 100644 --- a/fet2020/templates/rental/calendar.html +++ b/fet2020/templates/rental/calendar.html @@ -1,192 +1,281 @@ -{% extends 'base.html' %} - -{% load static %} - -{% block title %}Verleih{% endblock %} - -{% block content %} -
    -

    Verleih

    - -
    -

    - Willkommen bei unserem Verleih! -

    - - {% if user.is_authenticated %} - Verleih-Kalender abonnieren - {% endif %} - - - Verleih anfragen - -
    - -
    -
    -
    - - - - -
    -
    - -
    -
    -
    - {{ month|date:'F Y' }} - -
    - - -
    -
    - - - - - - - - - - - - - - - - {% for day in days_of_prev_month %} - {% if day.weekday == 0 %} - - {% endif %} - - - {% endfor %} - - {% for day in days_of_month %} - {% if day.weekday == 0 %} - - {% endif %} - - - - {% if day.weekday == 6 %} - - {% endif %} - {% endfor %} - - {% for day in days_of_next_month %} - - - {% if day.weekday == 6 %} - - {% endif %} - {% endfor %} - - -
    - - Mon - - - Tue - - - Wed - - - Thu - - - Fri - - - Sat - - - Sun -
    -
    -
    - {{ day.day }} -
    -
    -
    -
    -
    -
    - {% if day == today %} - {{ day.day }} - {% else %} - {{ day.day }} - {% endif %} -
    - - {% for key, names in rental_dict.items %} - {% if key == day %} - {% for name in names %} -
    -
    - - - - - - {{ name|truncatechars:3 }} -
    -
    - {% empty %} -
    - {% endfor %} - {% endif %} - {% endfor %} -
    -
    -
    -
    - {{ day.day }} -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock content %} +{% extends 'base.html' %} + +{% load static %} + +{% block title %}Verleih{% endblock %} + +{% block content %} +
    +

    Verleih

    + +
    +

    + Willkommen bei unserem Verleih! +

    + + {% if user.is_authenticated %} + + Verleih-Kalender abonnieren + + {% endif %} + + + Verleih anfragen + +
    + +
    +
    +
    + + + +
    + + +
    +
    + + +
    + + + + +
    +
    + +
    +
    +
    + + {% if view_type == 'month' %} + {{ view_period|date:'F Y' }} + {% else %} + {% 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' }} + {% else %} + KW{{ week_num|stringformat:"02d" }} - {{ days_of_view_period.0|date:'F Y' }} + {% endif %} + {% endif %} + + +
    + + +
    +
    + + + + + + + + + + + + + + + + {% for day in days_of_prev_period %} + {% if day.weekday == 0 %} + + {% endif %} + + + {% endfor %} + + {% for day in days_of_view_period %} + {% if day.weekday == 0 %} + + {% endif %} + + + + {% if day.weekday == 6 %} + + {% endif %} + {% endfor %} + + {% for day in days_of_next_period %} + + + {% if day.weekday == 6 %} + + {% endif %} + {% endfor %} + + +
    + + Mo + + + Di + + + Mi + + + Do + + + Fr + + + Sa + + + So +
    +
    +
    + {{ day.day }} +
    +
    +
    +
    +
    +
    + {% if day == today %} + {{ day.day }} + {% else %} + {{ day.day }} + {% endif %} +
    + + {% for key, names in rental_dict.items %} + {% if key == day %} + {% if view_type == 'month' %} + {% for name in names|slice:":3" %} +
    +
    + + + + + + {{ name|truncatechars:3 }} +
    +
    + {% endfor %} + {% if names|length > 3 %} + + {% endif %} + {% else %} + {% for name in names %} +
    +
    + + + + + + {{ name|truncatechars:3 }} +
    +
    + {% endfor %} + {% endif %} + {% endif %} + {% endfor %} +
    +
    +
    +
    + {{ day.day }} +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock content %} From 370577493a762fb2f0a217795bc6af28548fddc1 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Wed, 29 Oct 2025 22:36:11 +0100 Subject: [PATCH 08/23] optimize function --- fet2020/rental/views.py | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/fet2020/rental/views.py b/fet2020/rental/views.py index 022cb7f1..88867adc 100644 --- a/fet2020/rental/views.py +++ b/fet2020/rental/views.py @@ -46,9 +46,8 @@ def _calc_days_from_next_month(month: date) -> list: return days_of_next_period -def _get_display_period(view_type: str, period: str) -> tuple[date, bool]: +def _get_display_period(view_type: str, period: str) -> date: display_date: date = None - changed: bool = False # Indicate if the view has changed (used to reset filters) # Handle week view if view_type == "week": @@ -64,8 +63,6 @@ def _get_display_period(view_type: str, period: str) -> tuple[date, bool]: today = datetime.datetime.now(tz=datetime.UTC).date() display_date = today - datetime.timedelta(days=today.weekday()) - changed = True - # Handle month view else: try: @@ -77,22 +74,7 @@ def _get_display_period(view_type: str, period: str) -> tuple[date, bool]: # Get the first day of the current month display_date = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) - changed = True - - return display_date, changed - - -def _get_rental_filters(view_type: str, filters: list) -> list: - if not filters: - items = RentalItem.objects.all() - # if view_type == "month": - # items = items[:4] - filters = [item.name for item in items] - # else: - # if view_type == "month": - # filters = filters[:4] - - return filters + return display_date class RentalListView(ListView): @@ -119,13 +101,12 @@ class RentalListView(ListView): _period = _next_period self.view_type = _view_type - self.display_period, changed = _get_display_period(_view_type, _period) + self.display_period = _get_display_period(_view_type, _period) - _filters = request.GET.getlist("rentalitems", []) - # Reset filter if switched to week view - # if changed: - # _filters = [] - self.rentalitem_filters = _get_rental_filters(_view_type, _filters) + 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() From 4d35e498c555cc97ea76f4e798f5dc7fd8359989 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 10:32:46 +0100 Subject: [PATCH 09/23] fix: wrong binding for checked box --- fet2020/templates/rental/create.html | 244 ++++++++++++++------------- 1 file changed, 123 insertions(+), 121 deletions(-) diff --git a/fet2020/templates/rental/create.html b/fet2020/templates/rental/create.html index 1c95a97a..c4425da0 100644 --- a/fet2020/templates/rental/create.html +++ b/fet2020/templates/rental/create.html @@ -1,121 +1,123 @@ -{% extends 'base.html' %} - -{% load static %} - -{% block title %}Verleih Anfrage{% endblock %} - -{% block content %} - -
    -

    Verleih Anfrage

    - -
    - {% csrf_token %} - {% include "baseform/non_field_errors.html" %} - -
    -

    Persönliche Daten

    - Bitte gib deine persönlichen Daten ein. - -
    -
    - {% include "baseform/text.html" with field=form.firstname %} -
    -
    - {% include "baseform/text.html" with field=form.surname %} -
    -
    - {% include "baseform/text.html" with field=form.organization %} -
    -
    - {% include "baseform/text.html" with field=form.matriculation_number %} -
    -
    - {% include "baseform/email.html" with field=form.email %} -
    -
    - {% include "baseform/text.html" with field=form.phone %} -
    -
    -
    - -
    -

    Verleihgegenstände

    - Wähl deine gewünschten Verleihgegenstände aus. - -
    - {% if form.rentalitems.errors %} -
    -
    {{ form.rentalitems.errors }}
    -
    - {% endif %} - -
    - {% for elem in form.rentalitems %} -
    - - - - {% for item in rentalitems_addinfo %} - {% if item.name == elem.choice_label and item.induction %} -

    Einschulung erforderlich!

    - {% endif %} - {% endfor %} -
    - {% endfor %} -
    -
    -
    -
    - {% include "baseform/date.html" with field=form.date_start %} -
    -
    - {% include "baseform/date.html" with field=form.date_end %} -
    -
    - {% include "baseform/textarea.html" with field=form.reason %} -
    -
    -
    - -
    -

    Zusätzliche Informationen

    - Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben. - -
    -
    - {% include "baseform/textarea.html" with field=form.comment %} -
    -
    -
    - -
    -
    -
    - {% include "baseform/checkbox.html" with field=form.conformation %} -
    - - - Verleihregeln - -
    -
    - -
    - -
    -
    -
    -{% endblock content %} +{% extends 'base.html' %} + +{% load static %} + +{% block title %}Verleih Anfrage{% endblock %} + +{% block content %} + +
    +

    Verleih Anfrage

    + +
    + {% csrf_token %} + {% include "baseform/non_field_errors.html" %} + +
    +

    Persönliche Daten

    + Bitte gib deine persönlichen Daten ein. + +
    +
    + {% include "baseform/text.html" with field=form.firstname %} +
    +
    + {% include "baseform/text.html" with field=form.surname %} +
    +
    + {% include "baseform/text.html" with field=form.organization %} +
    +
    + {% include "baseform/text.html" with field=form.matriculation_number %} +
    +
    + {% include "baseform/email.html" with field=form.email %} +
    +
    + {% include "baseform/text.html" with field=form.phone %} +
    +
    +
    + +
    +

    Verleihgegenstände

    + Wähl deine gewünschten Verleihgegenstände aus. + +
    + {% if form.rentalitems.errors %} +
    +
    {{ form.rentalitems.errors }}
    +
    + {% endif %} + +
    + {% for elem in form.rentalitems %} +
    + + + {% for item in rentalitems_addinfo %} + {% if item.name == elem.choice_label and item.induction %} +

    Einschulung erforderlich!

    + {% endif %} + {% endfor %} +
    + {% endfor %} +
    +
    +
    +
    + {% include "baseform/date.html" with field=form.date_start %} +
    +
    + {% include "baseform/date.html" with field=form.date_end %} +
    +
    + {% include "baseform/textarea.html" with field=form.reason %} +
    +
    +
    + +
    +

    Zusätzliche Informationen

    + Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben. + +
    +
    + {% include "baseform/textarea.html" with field=form.comment %} +
    +
    +
    + +
    +
    +
    + {% include "baseform/checkbox.html" with field=form.conformation %} +
    + + + Verleihregeln + +
    +
    + +
    + +
    +
    +
    +{% endblock content %} From 0e1a61cefc59a6289986939fe1d504c2b04f4c4d Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 12:45:01 +0100 Subject: [PATCH 10/23] sorted all rentalitems alphabetically --- fet2020/rental/managers.py | 6 ++++++ fet2020/rental/models.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 fet2020/rental/managers.py diff --git a/fet2020/rental/managers.py b/fet2020/rental/managers.py new file mode 100644 index 00000000..9f2cc974 --- /dev/null +++ b/fet2020/rental/managers.py @@ -0,0 +1,6 @@ +from django.db import models + + +class RentalItemsManager(models.Manager): + def get_queryset(self): + return super().get_queryset().order_by("name") diff --git a/fet2020/rental/models.py b/fet2020/rental/models.py index 60a68c76..c57b8b10 100644 --- a/fet2020/rental/models.py +++ b/fet2020/rental/models.py @@ -2,7 +2,8 @@ from django.core.validators import FileExtensionValidator from django.db import models 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 @@ -22,6 +23,8 @@ class RentalItem(models.Model): location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="") + objects = RentalItemsManager() + class Meta: verbose_name = "Verleihgegenstand" verbose_name_plural = "Verleihgegenstände" From 5d9ad679defe2e0a1e173d11280f1cff360c3f90 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 12:59:11 +0100 Subject: [PATCH 11/23] Fix: auto-split rentals with >5 items; update create_done text --- fet2020/rental/urls.py | 2 +- fet2020/rental/views.py | 52 +++++++++++++++-------- fet2020/templates/rental/create_done.html | 39 +++++++++-------- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/fet2020/rental/urls.py b/fet2020/rental/urls.py index db656620..4712200f 100644 --- a/fet2020/rental/urls.py +++ b/fet2020/rental/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ path("overview/", RentalListView.as_view(), name="index"), path("request-rental/", RentalCreateView.as_view(), name="rental_create"), path( - "request-rental//done/", + "request-rental/done/", RentalCreateDoneView.as_view(), name="rental_create_done", ), diff --git a/fet2020/rental/views.py b/fet2020/rental/views.py index 88867adc..c4808b7c 100644 --- a/fet2020/rental/views.py +++ b/fet2020/rental/views.py @@ -3,6 +3,7 @@ import datetime from datetime import date from django.db.models import Q +from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse from django.views.generic import ListView, TemplateView @@ -12,6 +13,9 @@ from django.views.generic.edit import CreateView from .forms import RentalCreateForm 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]) @@ -185,7 +189,7 @@ class RentalListView(ListView): if rental["rentalitems__name"] not in rental_dict[day]: rental_dict[day].append(rental["rentalitems__name"]) - context["rental_dict"] = rental_dict + context["rental_dict"] = {k: sorted(v, key=str.casefold) for k, v in rental_dict.items()} return context @@ -223,6 +227,35 @@ class RentalCreateView(CreateView): model = Rental 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): context = super().get_context_data(**kwargs) @@ -231,26 +264,11 @@ class RentalCreateView(CreateView): return context 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): 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): model = RentalItem diff --git a/fet2020/templates/rental/create_done.html b/fet2020/templates/rental/create_done.html index bb909c04..48be356c 100644 --- a/fet2020/templates/rental/create_done.html +++ b/fet2020/templates/rental/create_done.html @@ -1,17 +1,22 @@ -{% extends 'base.html' %} - -{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %} - -{% block content %} - -
    -
    -

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

    - -
    -
    -{% endblock content %} +{% extends 'base.html' %} + +{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %} + +{% block content %} + +
    +
    +

    + Deine Verleihanfrage ist eingegangen - danke dir! 🎉 + Wir kümmern uns jetzt darum und melden uns per E-Mail mit den nächsten Schritten. +

    +

    + Kleiner Hinweis: Bevor du die Sachen abholen kannst, wird deine Anfrage kurz geprüft und freigegeben. + Sobald alles genehmigt ist, bekommst du von uns eine Mail. +

    + +
    +
    +{% endblock content %} From 07449db1288b0aad5736efa130f1ca8923c42dda Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 13:06:28 +0100 Subject: [PATCH 12/23] add migrations --- fet2020/rental/migrations/0001_initial.py | 55 +++++++++++++++++++++++ fet2020/rental/migrations/__init__.py | 0 2 files changed, 55 insertions(+) create mode 100644 fet2020/rental/migrations/0001_initial.py create mode 100644 fet2020/rental/migrations/__init__.py diff --git a/fet2020/rental/migrations/0001_initial.py b/fet2020/rental/migrations/0001_initial.py new file mode 100644 index 00000000..fc696be8 --- /dev/null +++ b/fet2020/rental/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/fet2020/rental/migrations/__init__.py b/fet2020/rental/migrations/__init__.py new file mode 100644 index 00000000..e69de29b From 2204c07deb5e48b1a43c87dcebed5888903f26bd Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 13:58:11 +0100 Subject: [PATCH 13/23] fix: path to pdf file --- fet2020/rental/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fet2020/rental/utils.py b/fet2020/rental/utils.py index 4c4ff0b2..afd9cf29 100644 --- a/fet2020/rental/utils.py +++ b/fet2020/rental/utils.py @@ -1,6 +1,6 @@ import io -from pathlib import Path +from django.contrib.staticfiles import finders from django.core.files import File from pypdf import PdfReader, PdfWriter @@ -46,8 +46,8 @@ def generate_rental_pdf(rental: Rental) -> bool: ) # Write data in pdf - pdf_path = Path(Path(__file__).parent) / "static/rental/Verleihformular.pdf" - reader = PdfReader(pdf_path) + pdf_path_str = finders.find("rental/Verleihformular.pdf") + reader = PdfReader(pdf_path_str) writer = PdfWriter() writer.append(reader) @@ -57,7 +57,8 @@ def generate_rental_pdf(rental: Rental) -> bool: ) with io.BytesIO() as bytes_stream: - writer.write(bytes_stream) + writer.write(bytes_stream) + bytes_stream.seek(0) # Save pdf in rental rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf" From 4a7076b1203df5b903e3c4faa8ee54d2d0787258 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 13:59:00 +0100 Subject: [PATCH 14/23] fix: third line in products in pdf file --- .../rental/static/rental/Verleihformular.pdf | Bin 72886 -> 72084 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/fet2020/rental/static/rental/Verleihformular.pdf b/fet2020/rental/static/rental/Verleihformular.pdf index db8dce118a0362e588238a29c6bab35ad16f8d9c..eba462bd14e84be6c1a43d26b27eaf86faf57907 100644 GIT binary patch delta 8540 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 delta 9289 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 ic*$K1mIeax>F34qMn@3v-XWY|pn?AJ@pmzY Date: Thu, 30 Oct 2025 14:05:53 +0100 Subject: [PATCH 15/23] add internal rentals with zero deposit --- fet2020/rental/admin.py | 250 ++++++++++++++++++++------------------- fet2020/rental/models.py | 3 +- fet2020/rental/utils.py | 130 ++++++++++---------- 3 files changed, 192 insertions(+), 191 deletions(-) diff --git a/fet2020/rental/admin.py b/fet2020/rental/admin.py index 42227a1d..b8f60b8a 100644 --- a/fet2020/rental/admin.py +++ b/fet2020/rental/admin.py @@ -1,124 +1,126 @@ -from django.contrib import admin, messages -from django.http import HttpResponseRedirect - -from .forms import RentalAdminForm, RentalItemAdminForm -from .models import Rental, RentalItem -from .utils import generate_rental_pdf - - -@admin.register(Rental) -class RentalAdmin(admin.ModelAdmin): - form = RentalAdminForm - model = Rental - - list_display = [ - "id", - "firstname", - "surname", - "status", - "total_disposit", - "date_start", - "date_end", - ] - ordering = ["-id"] - - readonly_fields = ["total_disposit"] - - fieldsets = ( - ( - "Persönliche Daten", - { - "fields": ( - ("firstname", "surname"), - ("organization", "matriculation_number"), - ("email", "phone"), - ), - }, - ), - ( - "Verleih", - { - "fields": ( - ("date_start", "date_end"), - "reason", - "rentalitems", - "total_disposit", - ), - }, - ), - ( - "Sonstiges", - { - "fields": ( - "comment", - "file_field", - "status", - ), - }, - ), - ) - - def add_view(self, request, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().add_view(request, form_url, extra_context=extra_context) - - def change_view(self, request, object_id, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - extra_context["generate_rental_pdf"] = True - return super().change_view(request, object_id, form_url, extra_context=extra_context) - - def response_change(self, request, obj): - if "_generate_rental_pdf" in request.POST: - if generate_rental_pdf(obj): - self.message_user( - request, - "Neues Verleihformular wurde generiert.", - messages.SUCCESS, - ) - else: - self.message_user( - request, - ( - "Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf " - "'Verleih genehmigt' gesetzt ist." - ), - messages.WARNING, - ) - return HttpResponseRedirect(".") - return super().response_change(request, obj) - - def save_model(self, request, obj, form, change): - obj.author = request.user - super().save_model(request, obj, form, change) - - @admin.display(description="Kaution (EUR)") - def total_disposit(self, obj): - total_disposit = 0 - for elem in obj.rentalitems.all(): - total_disposit += elem.deposit - - return f"{total_disposit}" - - -@admin.register(RentalItem) -class RentalItemAdmin(admin.ModelAdmin): - form = RentalItemAdminForm - model = RentalItem - - ordering = ["name"] - - def add_view(self, request, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().add_view(request, form_url, extra_context=extra_context) - - def change_view(self, request, object_id, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().change_view(request, object_id, form_url, extra_context=extra_context) - - def save_model(self, request, obj, form, change): - obj.author = request.user - super().save_model(request, obj, form, change) +from django.contrib import admin, messages +from django.http import HttpResponseRedirect + +from .forms import RentalAdminForm, RentalItemAdminForm +from .models import Rental, RentalItem +from .utils import generate_rental_pdf + + +@admin.register(Rental) +class RentalAdmin(admin.ModelAdmin): + form = RentalAdminForm + model = Rental + + list_display = [ + "id", + "firstname", + "surname", + "status", + "total_disposit", + "date_start", + "date_end", + ] + ordering = ["-id"] + + readonly_fields = ["total_disposit"] + + fieldsets = ( + ( + "Persönliche Daten", + { + "fields": ( + ("firstname", "surname"), + ("organization", "matriculation_number"), + ("email", "phone"), + ), + }, + ), + ( + "Verleih", + { + "fields": ( + ("date_start", "date_end"), + "reason", + "rentalitems", + ("total_disposit", "intern"), + ), + }, + ), + ( + "Sonstiges", + { + "fields": ( + "comment", + "file_field", + "status", + ), + }, + ), + ) + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view(request, form_url, extra_context=extra_context) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + extra_context["generate_rental_pdf"] = True + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def response_change(self, request, obj): + if "_generate_rental_pdf" in request.POST: + if generate_rental_pdf(obj): + self.message_user( + request, + "Neues Verleihformular wurde generiert.", + messages.SUCCESS, + ) + else: + self.message_user( + request, + ( + "Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf " + "'Verleih genehmigt' gesetzt ist." + ), + messages.WARNING, + ) + return HttpResponseRedirect(".") + return super().response_change(request, obj) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) + + @admin.display(description="Kaution (EUR)") + def total_disposit(self, obj): + total_disposit = 0 + + if not obj.intern: + for elem in obj.rentalitems.all(): + total_disposit += elem.deposit + + return f"{total_disposit}" + + +@admin.register(RentalItem) +class RentalItemAdmin(admin.ModelAdmin): + form = RentalItemAdminForm + model = RentalItem + + ordering = ["name"] + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view(request, form_url, extra_context=extra_context) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) diff --git a/fet2020/rental/models.py b/fet2020/rental/models.py index c57b8b10..6a31d3ab 100644 --- a/fet2020/rental/models.py +++ b/fet2020/rental/models.py @@ -2,7 +2,7 @@ from django.core.validators import FileExtensionValidator from django.db import models 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 @@ -44,6 +44,7 @@ class Rental(models.Model): ) 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_end = models.DateField(verbose_name="Rückgabedatum") diff --git a/fet2020/rental/utils.py b/fet2020/rental/utils.py index afd9cf29..72404a63 100644 --- a/fet2020/rental/utils.py +++ b/fet2020/rental/utils.py @@ -1,67 +1,65 @@ -import io - -from django.contrib.staticfiles import finders -from django.core.files import File -from pypdf import PdfReader, PdfWriter - -from .models import Rental - - -def generate_rental_pdf(rental: Rental) -> bool: - if not rental or rental.status != Rental.Status.APPROVED: - return False - - # Get data for pdf - data = {} - data.update( - { - "Vorname": rental.firstname, - "Nachname": rental.surname, - "Orga": rental.organization, - "Matrikelnummer": rental.matriculation_number, - "E-Mail": rental.email, - "Telefonnummer": rental.phone, - # Change to the correct date format - "Abholdatum": str(rental.date_start.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): - total_deposit += item.deposit - data.update( - { - f"Produkt Row{i}": item.name, - f"Menge Row{i}": "1", - f"Kaution Row{i}": item.deposit, - }, - ) - - data.update( - { - "Gesamtkaution": total_deposit, - }, - ) - - # Write data in pdf - pdf_path_str = finders.find("rental/Verleihformular.pdf") - reader = PdfReader(pdf_path_str) - writer = PdfWriter() - writer.append(reader) - - writer.update_page_form_field_values( - writer.pages[0], - data, - ) - - with io.BytesIO() as bytes_stream: +import io + +from django.contrib.staticfiles import finders +from django.core.files import File +from pypdf import PdfReader, PdfWriter + +from .models import Rental + + +def generate_rental_pdf(rental: Rental) -> bool: + if not rental or rental.status != Rental.Status.APPROVED: + return False + + # Get data for pdf + data = {} + data.update( + { + "Vorname": rental.firstname, + "Nachname": rental.surname, + "Orga": rental.organization, + "Matrikelnummer": rental.matriculation_number, + "E-Mail": rental.email, + "Telefonnummer": rental.phone, + # Change to the correct date format + "Abholdatum": str(rental.date_start.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): + if not rental.intern: + total_deposit += item.deposit + + data.update( + { + f"Produkt Row{i}": item.name, + f"Menge Row{i}": "1", + f"Kaution Row{i}": (str(item.deposit) if not rental.intern else "0"), + }, + ) + + data.update({"Gesamtkaution": str(total_deposit)}) + + # Write data in pdf + pdf_path_str = finders.find("rental/Verleihformular.pdf") + reader = PdfReader(pdf_path_str) + writer = PdfWriter() + writer.append(reader) + + writer.update_page_form_field_values( + writer.pages[0], + data, + ) + + with io.BytesIO() as bytes_stream: writer.write(bytes_stream) - bytes_stream.seek(0) - - # 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 + bytes_stream.seek(0) + + # 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 From 8ff3905657d6171b68e3e8051edae456a16f86ca Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 15:00:20 +0100 Subject: [PATCH 16/23] Add total deposit calculation to Rental model --- fet2020/rental/admin.py | 11 +++-------- fet2020/rental/models.py | 9 +++++++++ fet2020/rental/utils.py | 6 +----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/fet2020/rental/admin.py b/fet2020/rental/admin.py index b8f60b8a..5bb7487f 100644 --- a/fet2020/rental/admin.py +++ b/fet2020/rental/admin.py @@ -93,15 +93,10 @@ class RentalAdmin(admin.ModelAdmin): obj.author = request.user super().save_model(request, obj, form, change) - @admin.display(description="Kaution (EUR)") + @admin.display(description="Kaution insgesamt") def total_disposit(self, obj): - total_disposit = 0 - - if not obj.intern: - for elem in obj.rentalitems.all(): - total_disposit += elem.deposit - - return f"{total_disposit}" + total_disposit = obj.calc_total_deposit() + return f"{total_disposit} €" @admin.register(RentalItem) diff --git a/fet2020/rental/models.py b/fet2020/rental/models.py index 6a31d3ab..f874c00b 100644 --- a/fet2020/rental/models.py +++ b/fet2020/rental/models.py @@ -111,3 +111,12 @@ class Rental(models.Model): if self.date_start > self.date_end: 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 diff --git a/fet2020/rental/utils.py b/fet2020/rental/utils.py index 72404a63..0f52a2dd 100644 --- a/fet2020/rental/utils.py +++ b/fet2020/rental/utils.py @@ -27,12 +27,7 @@ def generate_rental_pdf(rental: Rental) -> bool: }, ) - total_deposit = 0 - for i, item in enumerate(rental.rentalitems.all(), start=1): - if not rental.intern: - total_deposit += item.deposit - data.update( { f"Produkt Row{i}": item.name, @@ -41,6 +36,7 @@ def generate_rental_pdf(rental: Rental) -> bool: }, ) + total_deposit = rental.calc_total_deposit() data.update({"Gesamtkaution": str(total_deposit)}) # Write data in pdf From 65ac5ae18e448fbcb6ccebbcb4c2bf7454395656 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 15:05:36 +0100 Subject: [PATCH 17/23] fix: sending mail; add email host user and pwd for authentication --- Readme.md | 2 + docker-compose.yml | 6 +- fet2020/fet2020/settings.py | 4 ++ fet2020/rental/mails.py | 106 ++++++++++++++++++------------------ 4 files changed, 63 insertions(+), 55 deletions(-) diff --git a/Readme.md b/Readme.md index 5d9f6bd2..028f75f6 100644 --- a/Readme.md +++ b/Readme.md @@ -96,6 +96,8 @@ docker build -t django-nginx-image -f nginx/Dockerfile ./nginx ### Start docker container +Add email password for 'Verleih' account to EMAIL_HOST_PASSWORD in the docker compose file! + Build the docker containers: ```bash diff --git a/docker-compose.yml b/docker-compose.yml index a43ca451..76797720 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: depends_on: - django-homepage volumes: - - files-volume:/usr/src/app/files + - files-volume:/usr/src/app/files - ./gallery:/usr/src/app/files/uploads/gallery - ./assets:/usr/src/app/assets:ro networks: @@ -24,7 +24,9 @@ services: SECRET_KEY: "sae34sADfrFr89E!Gl#f!34hdjGR#!jopi4qFEr#4R56rT56zT2#wE1!feGp" MYSQL_USER: "user" MYSQL_PASSWORD: "hgu" - ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3" + ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3" + EMAIL_HOST_USER: "verleih@fet.at" + EMAIL_HOST_PASSWORD: "" depends_on: mysql: condition: service_healthy diff --git a/fet2020/fet2020/settings.py b/fet2020/fet2020/settings.py index 68add289..7b564f73 100644 --- a/fet2020/fet2020/settings.py +++ b/fet2020/fet2020/settings.py @@ -18,6 +18,8 @@ env = environ.Env( ETHERPAD_GROUP=(str, ""), GALLERY_PATH=(str, "uploads/gallery"), MC_MASTERPASSWORD=(str, ""), + EMAIL_HOST_USER=(str, ""), + EMAIL_HOST_PASSWORD=(str, ""), ) # 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_HOST = "buran.htu.tuwien.ac.at" EMAIL_PORT = 587 +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_USE_TLS = True diff --git a/fet2020/rental/mails.py b/fet2020/rental/mails.py index 967ac989..47c0f3eb 100644 --- a/fet2020/rental/mails.py +++ b/fet2020/rental/mails.py @@ -1,53 +1,53 @@ -import logging - -from django.core.mail import EmailMessage - -RENTAL_EMAIL = "verleih@fet.at" -logger = logging.getLogger(__name__) - - -def send_mail_approved(obj): - subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" - - total_deposit = 0 - for rentalitem in obj.rentalitems.all(): - total_deposit += rentalitem.deposit - - message = ( - f"Hallo {obj.firstname},\n" - f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die " - 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" - "Liebe Grüße,\n" - "das Verleih-Team" - ) - - email = EmailMessage( - subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] - ) - - try: - email.send() - except Exception as exc: - logger.error("Failed to send approval email for rental #%s. Error: %s", obj.id, exc) - - -def send_mail_rejected(obj): - subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" - - message = ( - f"Hallo {obj.firstname},\n" - f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n" - "Liebe Grüße,\n" - "das Verleih-Team" - ) - - email = EmailMessage( - subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] - ) - - try: - email.send() - except Exception as exc: - logger.error("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc) +import logging + +from django.conf import settings +from django.core.mail import EmailMessage + +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()}" + total_deposit = obj.calc_total_deposit() + + message = ( + f"Hallo {obj.firstname},\n\n" + f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die " + 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.\n" + ) + + if total_deposit > 0: + message += f"Bitte bring den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n" + + message += "\nLiebe Grüße,\ndas Verleih-Team" + + email = EmailMessage( + subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] + ) + + try: + email.send() + except Exception as exc: + logger.info("Failed to send approval email for rental #%s. Error: %s", obj.id, exc) + + +def send_mail_rejected(obj): + subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}" + + message = ( + f"Hallo {obj.firstname},\n\n" + f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n\n" + "Liebe Grüße,\n" + "das Verleih-Team" + ) + + email = EmailMessage( + subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL] + ) + + try: + email.send() + except Exception as exc: + logger.info("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc) From b9943b7d4176403598a3fe0b8619e7ed2df45144 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 15:20:14 +0100 Subject: [PATCH 18/23] add help text for status --- fet2020/rental/forms.py | 123 +++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/fet2020/rental/forms.py b/fet2020/rental/forms.py index 366ce88f..fdf4eb0a 100644 --- a/fet2020/rental/forms.py +++ b/fet2020/rental/forms.py @@ -1,58 +1,65 @@ -from django import forms -from django.forms import DateInput - -from .models import Rental, RentalItem - - -class DateInput(DateInput): - input_type = "date" - - -class RentalCreateForm(forms.ModelForm): - # Conformation - conformation = forms.BooleanField( - required=True, - label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."), - initial=False, - ) - - class Meta: - model = Rental - - fields = [ - "firstname", - "surname", - "matriculation_number", - "email", - "phone", - "organization", - "date_start", - "date_end", - "reason", - "comment", - "rentalitems", - ] - - widgets = { - "date_start": DateInput(format=("%Y-%m-%d")), - "date_end": DateInput(format=("%Y-%m-%d")), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) # to get the self.fields set - - self.fields["firstname"].autofocus = True - - -class RentalAdminForm(forms.ModelForm): - class Meta: - model = Rental - fields = "__all__" - - widgets = {"rentalitems": forms.CheckboxSelectMultiple()} - - -class RentalItemAdminForm(forms.ModelForm): - class Meta: - model = RentalItem - fields = "__all__" +from django import forms +from django.forms import DateInput + +from .models import Rental, RentalItem + + +class DateInput(DateInput): + input_type = "date" + + +class RentalCreateForm(forms.ModelForm): + # Conformation + conformation = forms.BooleanField( + required=True, + label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."), + initial=False, + ) + + class Meta: + model = Rental + + fields = [ + "firstname", + "surname", + "matriculation_number", + "email", + "phone", + "organization", + "date_start", + "date_end", + "reason", + "comment", + "rentalitems", + ] + + widgets = { + "date_start": DateInput(format=("%Y-%m-%d")), + "date_end": DateInput(format=("%Y-%m-%d")), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # to get the self.fields set + + self.fields["firstname"].autofocus = True + + +class RentalAdminForm(forms.ModelForm): + class Meta: + model = Rental + fields = "__all__" + + widgets = {"rentalitems": forms.CheckboxSelectMultiple()} + + help_texts = { + "status": ( + "Wird der Status auf 'Verleih genehmigt' oder 'Verleih abgelehnt' gesetzt, wird " + "eine E-Mail gesendet." + ), + } + + +class RentalItemAdminForm(forms.ModelForm): + class Meta: + model = RentalItem + fields = "__all__" From 585bc60676bab55bf5643f6f257b273eedc61187 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 15:39:58 +0100 Subject: [PATCH 19/23] update assets --- assets/rental/Verleihformular.pdf | Bin 72886 -> 72084 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/rental/Verleihformular.pdf b/assets/rental/Verleihformular.pdf index db8dce118a0362e588238a29c6bab35ad16f8d9c..eba462bd14e84be6c1a43d26b27eaf86faf57907 100644 GIT binary patch delta 8540 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 delta 9289 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 ic*$K1mIeax>F34qMn@3v-XWY|pn?AJ@pmzY Date: Thu, 30 Oct 2025 15:40:49 +0100 Subject: [PATCH 20/23] add migrations --- .../rental/migrations/0002_rental_intern.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 fet2020/rental/migrations/0002_rental_intern.py diff --git a/fet2020/rental/migrations/0002_rental_intern.py b/fet2020/rental/migrations/0002_rental_intern.py new file mode 100644 index 00000000..60e4df62 --- /dev/null +++ b/fet2020/rental/migrations/0002_rental_intern.py @@ -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'), + ), + ] From be581675cdc941c69f4c416cc26f542adf23840b Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Thu, 30 Oct 2025 15:42:48 +0100 Subject: [PATCH 21/23] update homepage version to 2.2.1 --- Readme.md | 4 ++++ fet2020/fet2020/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 028f75f6..05a2a545 100644 --- a/Readme.md +++ b/Readme.md @@ -136,6 +136,10 @@ ckeditor -> django-prose-editor ## Version History +2.2.1 + +* Fix rental (view, pdf file, sending mail) + 2.2.0 * Add rental diff --git a/fet2020/fet2020/__init__.py b/fet2020/fet2020/__init__.py index 3b383497..05b94323 100644 --- a/fet2020/fet2020/__init__.py +++ b/fet2020/fet2020/__init__.py @@ -1,6 +1,6 @@ from django.utils.version import get_version -VERSION = (2, 2, 0, "final", 0) +VERSION = (2, 2, 1, "final", 0) BUILD = 0 __version__ = get_version(VERSION) From b50c010b3b558e7628e177fb66e434e4d3817662 Mon Sep 17 00:00:00 2001 From: sebivh Date: Thu, 30 Oct 2025 23:21:25 +0100 Subject: [PATCH 22/23] Add persistant Storage to Container Databases --- .gitignore | 1 + docker-compose.yml | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 118758a1..6d45635e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ flowbite gallery/* tailwind whoosh_index +databases/django diff --git a/docker-compose.yml b/docker-compose.yml index a43ca451..ee292186 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: depends_on: - django-homepage 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: @@ -82,7 +82,8 @@ services: MYSQL_CHARSET: utf8 MYSQL_ALLOW_EMPTY_PASSWORD: "yes" volumes: - - mysql-volume:/docker-entrypoint-initdb.d/ + - ./inits/django:/docker-entrypoint-initdb.d/ + - ./databases/django:/var/lib/mysql:Z networks: - django-db-network healthcheck: @@ -100,7 +101,8 @@ services: MYSQL_CHARSET: utf8 MYSQL_ALLOW_EMPTY_PASSWORD: "yes" volumes: - - etherpad-mysql-volume:/docker-entrypoint-initdb.d/ + - ./init/etherpad:/docker-entrypoint-initdb.d/ + - ./databases/etherpad:/var/lib/mysql:Z networks: - etherpad-db-network healthcheck: From a3b252c9bee11cde036f18a0db40f3c94e45a487 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Fri, 31 Oct 2025 13:30:43 +0100 Subject: [PATCH 23/23] Fix: Find the current last day; Use the correct get now time function --- fet2020/finance/forms.py | 3 +- fet2020/finance/utils.py | 4 +- fet2020/posts/managers.py | 409 +++++++++++++++++++------------------- fet2020/rental/views.py | 10 +- 4 files changed, 218 insertions(+), 208 deletions(-) diff --git a/fet2020/finance/forms.py b/fet2020/finance/forms.py index 4968750b..77003fec 100644 --- a/fet2020/finance/forms.py +++ b/fet2020/finance/forms.py @@ -6,6 +6,7 @@ from django import forms from django.core.validators import ValidationError from django.db.models import Count, Q from django.forms import DateInput +from django.utils import timezone from members.models import Member @@ -412,7 +413,7 @@ class BillAdminForm(forms.ModelForm): self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter( ( 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) ) diff --git a/fet2020/finance/utils.py b/fet2020/finance/utils.py index 270f5765..32121e62 100644 --- a/fet2020/finance/utils.py +++ b/fet2020/finance/utils.py @@ -1,8 +1,8 @@ -import datetime import io from pathlib import Path from django.core.files import File +from django.utils import timezone from pypdf import PdfReader, PdfWriter from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814 @@ -34,7 +34,7 @@ def generate_pdf(wiref): ) # Get budget year - today = datetime.datetime.now(tz=datetime.UTC).date() + today = timezone.now().date() if today.month < 7: budget_year = f"{today.year - 1}-{today.year}" else: diff --git a/fet2020/posts/managers.py b/fet2020/posts/managers.py index db874880..ff4d40b1 100644 --- a/fet2020/posts/managers.py +++ b/fet2020/posts/managers.py @@ -1,201 +1,208 @@ -import datetime - -from django.db import models -from django.db.models import Case, Q, When - -from .choices import PostType, Status - - -class PublishedManager(models.Manager): - def published(self, public=True): - """ - publish all posts with status 'PUBLIC' - """ - return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset() - - def published_all(self, public=True): - """ - publish all posts with status 'PUBLIC' and 'ONLY_INTERN' - """ - 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 = ( - super() - .get_queryset() - .annotate( - date=Case( - 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 self.published(public) - - def date_filter( - self, - public=True, - year=None, - month=None, - fet_meeting_only=None, - ): - qs_filter = Q() - - if fet_meeting_only: - qs_filter &= Q(post_type=PostType.FETMEETING) - - if year: - qs_filter &= Q(date__year=year) - if month: - 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" - 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( - 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 self.published(public) - - def pinned(self, public=True): - # Get date for pinned news that is max 1 month old. - post_date = datetime.datetime.now(tz=datetime.UTC).date() - __month = post_date.month - __year = post_date.year - - if __month != 1: - __month -= 1 - else: - # If the current month is January, you get the date from December of previous year. - __month = 12 - __year -= 1 - - post_date = post_date.replace(year=__year, month=__month) - - # Get date for event posts that is max 1 day old. - event_date = datetime.datetime.now(tz=datetime.UTC).date() - datetime.timedelta(1) - - return ( - self.published(public) - .filter( - Q(is_pinned=True) - & ( - (Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date)) - | (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date)) - ) - ) - .first() - ) - - -class NewsManager(PublishedManager, models.Manager): - """ - Provide a query set only for "News" - """ - - def get_queryset(self): - qs = super().get_queryset().filter(post_type=PostType.NEWS) - qs = qs.annotate( - date=Case( - When(post_type=PostType.NEWS, then="public_date"), - ), - ) - return qs.order_by("-date") - - -class AllEventManager(PublishedManager, models.Manager): - """ - Provide a query set for all events ("Event" and "Fet Meeting") - """ - - def get_queryset(self): - qs = ( - super() - .get_queryset() - .filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING)) - ) - qs = qs.annotate( - date=Case( - When(post_type=PostType.EVENT, then="event_start__date"), - When(post_type=PostType.FETMEETING, then="event_start__date"), - ), - ) - return qs.order_by("-date") - - def future_events(self, public=True): - date_today = datetime.datetime.now(tz=datetime.UTC).date() - qs = self.published(public).filter(event_start__gt=date_today) - return qs.reverse() - - -class EventManager(PublishedManager, models.Manager): - """ - Provide a query set only for "Events" - regular fet meetings should not be contained in the news stream - """ - - def get_queryset(self): - qs = super().get_queryset().filter(post_type=PostType.EVENT) - qs = qs.annotate( - date=Case( - When(post_type=PostType.EVENT, then="event_start__date"), - ), - ) - return qs.order_by("-date") - - def future_events(self, public=True): - date_today = datetime.datetime.now(tz=datetime.UTC).date() - qs = self.published(public).filter(event_start__gt=date_today) - return qs.reverse() - - def past_events(self, public=True): - date_today = datetime.datetime.now(tz=datetime.UTC).date() - qs = self.published(public).filter(event_start__lt=date_today) - return qs - - -class FetMeetingManager(PublishedManager, models.Manager): - """ - Provide a query set only for "Fet Meeting" - """ - - def get_queryset(self): - qs = super().get_queryset().filter(post_type=PostType.FETMEETING) - qs = qs.annotate( - date=Case( - When(post_type=PostType.FETMEETING, then="event_start__date"), - ), - ) - return qs.order_by("-date") - - def future_events(self): - date_today = datetime.datetime.now(tz=datetime.UTC).date() - qs = self.published().filter(event_start__gt=date_today) - return qs.reverse() - - def past_events(self): - date_today = datetime.datetime.now(tz=datetime.UTC).date() - qs = self.published().filter(event_start__lt=date_today) - return qs +import calendar +import datetime + +from django.db import models +from django.db.models import Case, Q, When +from django.utils import timezone + +from .choices import PostType, Status + + +class PublishedManager(models.Manager): + def published(self, public=True): + """ + publish all posts with status 'PUBLIC' + """ + return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset() + + def published_all(self, public=True): + """ + publish all posts with status 'PUBLIC' and 'ONLY_INTERN' + """ + 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 = ( + super() + .get_queryset() + .annotate( + date=Case( + 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 self.published(public) + + def date_filter( + self, + public=True, + year=None, + month=None, + fet_meeting_only=None, + ): + qs_filter = Q() + + if fet_meeting_only: + qs_filter &= Q(post_type=PostType.FETMEETING) + + if year: + qs_filter &= Q(date__year=year) + if month: + 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" + 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( + 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 self.published(public) + + def pinned(self, public=True): + # Get date for pinned news that is max 1 month old. + post_date = timezone.now().date() + _day = post_date.day + _month = post_date.month + _year = post_date.year + + if _month != 1: + _month -= 1 + else: + # If the current month is January, you get the date from December of previous year. + _month = 12 + _year -= 1 + + # Clamp day to last day of target month (handles 30/31 and Feb) + last_day = calendar.monthrange(_year, _month)[1] + safe_day = min(_day, last_day) + + post_date = post_date.replace(year=_year, month=_month, day=safe_day) + + # Get date for event posts that is max 1 day old. + event_date = timezone.now().date() - datetime.timedelta(1) + + return ( + self.published(public) + .filter( + Q(is_pinned=True) + & ( + (Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date)) + | (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date)) + ) + ) + .first() + ) + + +class NewsManager(PublishedManager, models.Manager): + """ + Provide a query set only for "News" + """ + + def get_queryset(self): + qs = super().get_queryset().filter(post_type=PostType.NEWS) + qs = qs.annotate( + date=Case( + When(post_type=PostType.NEWS, then="public_date"), + ), + ) + return qs.order_by("-date") + + +class AllEventManager(PublishedManager, models.Manager): + """ + Provide a query set for all events ("Event" and "Fet Meeting") + """ + + def get_queryset(self): + qs = ( + super() + .get_queryset() + .filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING)) + ) + qs = qs.annotate( + date=Case( + When(post_type=PostType.EVENT, then="event_start__date"), + When(post_type=PostType.FETMEETING, then="event_start__date"), + ), + ) + return qs.order_by("-date") + + def future_events(self, public=True): + date_today = timezone.now().date() + qs = self.published(public).filter(event_start__gt=date_today) + return qs.reverse() + + +class EventManager(PublishedManager, models.Manager): + """ + Provide a query set only for "Events" + regular fet meetings should not be contained in the news stream + """ + + def get_queryset(self): + qs = super().get_queryset().filter(post_type=PostType.EVENT) + qs = qs.annotate( + date=Case( + When(post_type=PostType.EVENT, then="event_start__date"), + ), + ) + return qs.order_by("-date") + + def future_events(self, public=True): + date_today = timezone.now().date() + qs = self.published(public).filter(event_start__gt=date_today) + return qs.reverse() + + def past_events(self, public=True): + date_today = timezone.now().date() + qs = self.published(public).filter(event_start__lt=date_today) + return qs + + +class FetMeetingManager(PublishedManager, models.Manager): + """ + Provide a query set only for "Fet Meeting" + """ + + def get_queryset(self): + qs = super().get_queryset().filter(post_type=PostType.FETMEETING) + qs = qs.annotate( + date=Case( + When(post_type=PostType.FETMEETING, then="event_start__date"), + ), + ) + return qs.order_by("-date") + + def future_events(self): + 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 diff --git a/fet2020/rental/views.py b/fet2020/rental/views.py index c4808b7c..4815d588 100644 --- a/fet2020/rental/views.py +++ b/fet2020/rental/views.py @@ -6,6 +6,7 @@ from django.db.models import Q from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse +from django.utils import timezone from django.views.generic import ListView, TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView @@ -64,7 +65,7 @@ def _get_display_period(view_type: str, period: str) -> date: ) except Exception: # Get first day of the current week - today = datetime.datetime.now(tz=datetime.UTC).date() + today = timezone.now().date() display_date = today - datetime.timedelta(days=today.weekday()) # Handle month view @@ -76,7 +77,7 @@ def _get_display_period(view_type: str, period: str) -> date: ) except Exception: # Get the first day of the current month - display_date = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) + display_date = timezone.now().date().replace(day=1) return display_date @@ -170,7 +171,7 @@ class RentalListView(ListView): context["week_num"] = week_num # 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 context["rentalitems"] = RentalItem.objects.all() @@ -234,7 +235,7 @@ class RentalCreateView(CreateView): 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] + batch = selected_items[i : i + RENTAL_ITEMS_MAX] # Clone base_rental — copying all its field values new_rental = Rental.objects.create( @@ -266,6 +267,7 @@ class RentalCreateView(CreateView): def get_success_url(self): return reverse("rental:rental_create_done") + class RentalCreateDoneView(TemplateView): template_name = "rental/create_done.html"