From 9ad8a72acaa291e1464e5e41c27a6c62c2ceb14f Mon Sep 17 00:00:00 2001
From: crosspolar <18083323+crosspolar@users.noreply.github.com>
Date: Sun, 12 Jul 2026 08:55:26 +0200
Subject: [PATCH 01/23] init
---
.../coop/templates/coop/user_profile_pdf.html | 84 +++++++++++++++++++
tapir/coop/urls.py | 5 ++
tapir/coop/views/shareowner.py | 44 +++++++++-
3 files changed, 132 insertions(+), 1 deletion(-)
create mode 100644 tapir/coop/templates/coop/user_profile_pdf.html
diff --git a/tapir/coop/templates/coop/user_profile_pdf.html b/tapir/coop/templates/coop/user_profile_pdf.html
new file mode 100644
index 000000000..66ebed040
--- /dev/null
+++ b/tapir/coop/templates/coop/user_profile_pdf.html
@@ -0,0 +1,84 @@
+
+{% load static %}
+{% load django_bootstrap5 %}
+{% load accounts %}
+{% load utils %}
+
+
+
+ Mitgliedsprofil - {{ person.get_display_name }}
+
+
+
+
Stammdaten
+
+ - Name
+ -
+ {{ person.first_name }} {{ person.last_name }}
+
+ - E‑Mail
+ -
+ {{ person.email }}
+
+ - Telefon
+ -
+ {{ person.phone_number }}
+
+ - Geburtsdatum
+ -
+ {% if person.birthdate %}
+ {{ person.birthdate|date:"Y-m-d" }}
+ {% else %}
+ -
+ {% endif %}
+
+ - Adresse
+ -
+ {{ person.get_display_address }}
+
+ - Sprache
+ -
+ {{ person.get_preferred_language_display }}
+
+
+
+
+
Schichten
+ {% if attendances %}
+
+
+
+ | Datum |
+ Bezeichnung |
+ State |
+
+
+
+ {% for att in attendances %}
+
+ |
+ {% if att.slot and att.slot.shift and att.slot.shift.start_time %}
+ {{ att.slot.shift.start_time|date:"Y-m-d H-i" }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+ {% if att.slot and att.slot.shift %}
+ {{ att.slot.shift.title|default:att.slot.name|default:"(ohne Bezeichnung)" }}
+ {% else %}
+ (Slot entfernt)
+ {% endif %}
+ |
+ {{ att.get_state_display }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
Keine abgeschlossenen Schichten gefunden.
+ {% endif %}
+
+ Generiert am {{ generated_at|date:"Y-m-d H:i" }}
+
+
diff --git a/tapir/coop/urls.py b/tapir/coop/urls.py
index d554cc392..0444981fc 100644
--- a/tapir/coop/urls.py
+++ b/tapir/coop/urls.py
@@ -255,4 +255,9 @@
views.MemberSelfRegisterApiView.as_view(),
name="member_self_register",
),
+ path(
+ "member//profile_pdf",
+ views.UserProfilePDFView.as_view(),
+ name="user_profile_pdf",
+ ),
]
diff --git a/tapir/coop/views/shareowner.py b/tapir/coop/views/shareowner.py
index a9e330546..cbddd4d50 100644
--- a/tapir/coop/views/shareowner.py
+++ b/tapir/coop/views/shareowner.py
@@ -4,6 +4,7 @@
import django_filters
import django_tables2
+import weasyprint
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
@@ -18,7 +19,7 @@
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404, redirect
-from django.template import Context, Template
+from django.template import Context, Template, loader
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
@@ -1105,3 +1106,44 @@ def get_context_data(self, **kwargs):
def get_success_url(self):
return self.get_share_owner().get_absolute_url()
+
+
+class UserProfilePDFView(generic.DetailView):
+ model = ShareOwner
+ pk_url_kwarg = "user_id"
+ template_name = "coop/user_profile_pdf.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ shareowner = self.object
+ person = getattr(shareowner, "user", None) or shareowner
+
+ attendances = []
+ if shareowner.user_id and shareowner.user is not None:
+ attendances = shareowner.user.shift_attesndances.select_related(
+ "slot", "slot__shift"
+ ).order_by("slot__shift__start_time")
+
+ context.update(
+ {
+ "person": person,
+ "attendances": attendances,
+ "generated_at": timezone.now(),
+ }
+ )
+ return context
+
+ def render_to_response(self, context, **response_kwargs):
+ request = self.request
+ html_string = loader.render_to_string(
+ self.template_name, context, request=request
+ )
+ html = weasyprint.HTML(
+ string=html_string, base_url=request.build_absolute_uri("/")
+ )
+ pdf = html.write_pdf()
+
+ response = HttpResponse(pdf, content_type="application/pdf")
+ filename = f"user-{self.object.pk}-profile.pdf"
+ response["Content-Disposition"] = f'inline; filename="{filename}"'
+ return response
From 2ab505e80b81eb64e46e35addbe5fa3aec5afa61 Mon Sep 17 00:00:00 2001
From: crosspolar <18083323+crosspolar@users.noreply.github.com>
Date: Sun, 12 Jul 2026 09:17:38 +0200
Subject: [PATCH 02/23] include button
---
.../coop/tags/user_coop_share_ownership_list_tag.html | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tapir/coop/templates/coop/tags/user_coop_share_ownership_list_tag.html b/tapir/coop/templates/coop/tags/user_coop_share_ownership_list_tag.html
index 7340e7a33..4bf5a2b96 100644
--- a/tapir/coop/templates/coop/tags/user_coop_share_ownership_list_tag.html
+++ b/tapir/coop/templates/coop/tags/user_coop_share_ownership_list_tag.html
@@ -12,6 +12,10 @@