import json import requests from io import BytesIO from django.http import HttpResponse from django.shortcuts import get_object_or_404 from rest_framework import viewsets, permissions, status, views from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Q from reportlab.lib import colors from reportlab.lib.pagesizes import letter, landscape from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from .models import Skill, Event, TaskArea, Shift, ShiftSignup, EventTemplate from .serializers import ( SkillSerializer, EventSerializer, TaskAreaSerializer, ShiftSerializer, EventTemplateSerializer ) class SkillViewSet(viewsets.ModelViewSet): queryset = Skill.objects.all().order_by('name') serializer_class = SkillSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] class IsAdminOrEventCreator(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True if not request.user or not request.user.is_authenticated: return False if request.user.is_admin_user or request.user.is_staff or request.user.is_superuser: return True return obj.created_by == request.user class IsTaskAreaOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True if not request.user or not request.user.is_authenticated: return False if request.user.is_admin_user or request.user.is_staff or request.user.is_superuser: return True return obj.event.created_by == request.user class IsShiftOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True if not request.user or not request.user.is_authenticated: return False if request.user.is_admin_user or request.user.is_staff or request.user.is_superuser: return True return obj.task_area.event.created_by == request.user class EventViewSet(viewsets.ModelViewSet): serializer_class = EventSerializer permission_classes = [IsAdminOrEventCreator] def get_queryset(self): user = self.request.user if user.is_authenticated and (user.is_admin_user or user.is_staff or user.is_superuser): return Event.objects.all().order_by('-start_date', '-id') if user.is_authenticated: return Event.objects.filter(Q(is_active=True) | Q(created_by=user)).order_by('-start_date', '-id') return Event.objects.filter(is_active=True).order_by('-start_date', '-id') def perform_create(self, serializer): serializer.save(created_by=self.request.user if self.request.user.is_authenticated else None) @action(detail=True, methods=['get']) def matrix(self, request, pk=None): event = self.get_object() serializer = EventSerializer(event, context={'request': request}) return Response(serializer.data) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def save_as_template(self, request, pk=None): event = self.get_object() name = request.data.get('name', f"Vorlage von {event.title}") description = request.data.get('description', '') # Build template structure template_task_areas = [] for ta in event.task_areas.all(): shifts_data = [] for shift in ta.shifts.all(): shifts_data.append({ 'title': shift.title, 'start_time': shift.start_time, 'end_time': shift.end_time, 'max_participants': shift.max_participants, 'required_skill_names': list(shift.required_skills.values_list('name', flat=True)) }) template_task_areas.append({ 'name': ta.name, 'description': ta.description, 'order': ta.order, 'shifts': shifts_data }) template = EventTemplate.objects.create( name=name, description=description, template_data={'task_areas': template_task_areas}, created_by=request.user ) return Response(EventTemplateSerializer(template).data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['get']) def export_pdf(self, request, pk=None): event = self.get_object() buffer = BytesIO() doc = SimpleDocTemplate( buffer, pagesize=landscape(letter), rightMargin=30, leftMargin=30, topMargin=30, bottomMargin=30 ) elements = [] styles = getSampleStyleSheet() title_style = ParagraphStyle( 'DocTitle', parent=styles['Heading1'], fontSize=20, leading=24, textColor=colors.HexColor('#1e293b'), spaceAfter=6 ) sub_style = ParagraphStyle( 'DocSub', parent=styles['Normal'], fontSize=10, leading=12, textColor=colors.HexColor('#64748b'), spaceAfter=15 ) elements.append(Paragraph(f"Schichtplan: {event.title}", title_style)) loc_str = f" • Ort: {event.location}" if event.location else "" elements.append(Paragraph(f"Zeitraum: {event.start_date} bis {event.end_date}{loc_str}", sub_style)) # Build Table Data table_data = [] headers = ["Bereich", "Schicht & Zeit", "Benötigte Qualifikationen", "Eingetragene Personen"] table_data.append([Paragraph(f"{h}", styles['Normal']) for h in headers]) for ta in event.task_areas.all(): for shift in ta.shifts.all(): skills_list = ", ".join([s.name for s in shift.required_skills.all()]) or "Keine" signups = shift.signups.all() signups_list = [] for su in signups: if request.user.is_authenticated: display = su.user.get_display_name() if su.user else (su.guest_name or "Gast") else: display = "Belegt (Anonym)" signups_list.append(display) while len(signups_list) < shift.max_participants: signups_list.append("[ Offen ]") signups_str = "
".join(signups_list) row = [ Paragraph(f"{ta.name}", styles['Normal']), Paragraph(f"{shift.title}
{shift.start_time} - {shift.end_time}", styles['Normal']), Paragraph(skills_list, styles['Normal']), Paragraph(signups_str, styles['Normal']) ] table_data.append(row) if len(table_data) == 1: table_data.append([Paragraph("Keine Schichten angelegt", styles['Normal']), "", "", ""]) pdf_table = Table(table_data, colWidths=[150, 160, 160, 250]) pdf_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f1f5f9')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#0f172a')), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#cbd5e1')), ('PADDING', (0, 0), (-1, -1), 8), ])) elements.append(pdf_table) doc.build(elements) pdf = buffer.getvalue() buffer.close() response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="schichtplan_{event.id}.pdf"' return response class TaskAreaViewSet(viewsets.ModelViewSet): queryset = TaskArea.objects.all() serializer_class = TaskAreaSerializer permission_classes = [IsTaskAreaOwnerOrAdmin] def perform_create(self, serializer): event = serializer.validated_data.get('event') user = self.request.user if not user.is_authenticated: raise permissions.PermissionDenied("Nicht angemeldet.") if not (user.is_admin_user or user.is_staff or user.is_superuser or (event and event.created_by == user)): raise permissions.PermissionDenied("Keine Berechtigung, Bereiche für dieses Event hinzuzufügen.") serializer.save() class ShiftViewSet(viewsets.ModelViewSet): queryset = Shift.objects.all() serializer_class = ShiftSerializer permission_classes = [IsShiftOwnerOrAdmin] def perform_create(self, serializer): task_area = serializer.validated_data.get('task_area') user = self.request.user if not user.is_authenticated: raise permissions.PermissionDenied("Nicht angemeldet.") if not (user.is_admin_user or user.is_staff or user.is_superuser or (task_area and task_area.event.created_by == user)): raise permissions.PermissionDenied("Keine Berechtigung, Schichten für dieses Event hinzuzufügen.") serializer.save() class ShiftSignupView(views.APIView): permission_classes = [permissions.AllowAny] def post(self, request, shift_id): shift = get_object_or_404(Shift, pk=shift_id) # Check capacity if shift.signups.count() >= shift.max_participants: return Response({'error': 'Diese Schicht ist bereits vollständig belegt.'}, status=status.HTTP_400_BAD_REQUEST) # Authenticated User flow if request.user.is_authenticated: if ShiftSignup.objects.filter(shift=shift, user=request.user).exists(): return Response({'error': 'Du bist für diese Schicht bereits eingetragen.'}, status=status.HTTP_400_BAD_REQUEST) required_skills = shift.required_skills.all() if required_skills.exists(): user_skills = request.user.skills.all() missing_skills = [s.name for s in required_skills if s not in user_skills] if missing_skills: missing_str = ", ".join(missing_skills) return Response({ 'error': f'Dir fehlen folgende erforderliche Fähigkeiten für diese Schicht: {missing_str}' }, status=status.HTTP_400_BAD_REQUEST) signup = ShiftSignup.objects.create(shift=shift, user=request.user) return Response({'message': 'Erfolgreich für Schicht eingetragen.', 'signup_id': signup.id}, status=status.HTTP_201_CREATED) # Guest flow else: guest_name = request.data.get('guest_name', '').strip() captcha_token = request.data.get('captcha_token', '').strip() if not guest_name: return Response({'error': 'Bitte gib einen Namen an.'}, status=status.HTTP_400_BAD_REQUEST) if not captcha_token: return Response({'error': 'Bitte löse das Captcha um dich einzutragen.'}, status=status.HTTP_400_BAD_REQUEST) if shift.required_skills.exists(): required_names = ", ".join([s.name for s in shift.required_skills.all()]) return Response({ 'error': f'Gäste können keine Fähigkeiten haben. Diese Schicht erfordert: {required_names}. Bitte registriere ein Konto mit den passenden Fähigkeiten.' }, status=status.HTTP_400_BAD_REQUEST) if not request.session.session_key: request.session.create() session_key = request.session.session_key request.session['guest_name'] = guest_name if ShiftSignup.objects.filter(shift=shift, guest_session_key=session_key).exists(): return Response({'error': 'Du bist mit deiner Gast-Sitzung bereits für diese Schicht eingetragen.'}, status=status.HTTP_400_BAD_REQUEST) signup = ShiftSignup.objects.create( shift=shift, guest_name=guest_name, guest_session_key=session_key ) return Response({ 'message': 'Als Gast erfolgreich eingetragen! Dein Name wurde in deiner Sitzung gespeichert.', 'signup_id': signup.id }, status=status.HTTP_201_CREATED) def delete(self, request, shift_id, signup_id=None): shift = get_object_or_404(Shift, pk=shift_id) if not signup_id: signup_id = request.data.get('signup_id') or request.query_params.get('signup_id') if not signup_id and request.body: try: body_data = json.loads(request.body.decode('utf-8')) signup_id = body_data.get('signup_id') except Exception: pass if request.user.is_authenticated: user = request.user is_manager = user.is_admin_user or user.is_staff or user.is_superuser or (shift.task_area.event.created_by == user) if signup_id and is_manager: signup = ShiftSignup.objects.filter(shift=shift, pk=signup_id).first() if signup: name = signup.user.get_display_name() if signup.user else (signup.guest_name or "Gast") signup.delete() return Response({'message': f'Eintragung von {name} wurde von der Schicht entfernt.'}) return Response({'error': 'Eintragung nicht gefunden.'}, status=status.HTTP_404_NOT_FOUND) signup = ShiftSignup.objects.filter(shift=shift, user=user).first() if signup: signup.delete() return Response({'message': 'Eintragung storniert.'}) return Response({'error': 'Keine passende eigene Eintragung gefunden.'}, status=status.HTTP_404_NOT_FOUND) else: session_key = request.session.session_key if not session_key: return Response({'error': 'Keine Gast-Sitzung gefunden.'}, status=status.HTTP_400_BAD_REQUEST) signup = ShiftSignup.objects.filter(shift=shift, guest_session_key=session_key).first() if signup: signup.delete() return Response({'message': 'Gast-Eintragung storniert.'}) return Response({'error': 'Keine passende Gast-Eintragung gefunden.'}, status=status.HTTP_404_NOT_FOUND) class EventTemplateViewSet(viewsets.ModelViewSet): queryset = EventTemplate.objects.all().order_by('-created_at') serializer_class = EventTemplateSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): serializer.save(created_by=self.request.user if self.request.user.is_authenticated else None) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def instantiate(self, request, pk=None): template = self.get_object() title = request.data.get('title', f"Veranstaltung: {template.name}") start_date = request.data.get('start_date') end_date = request.data.get('end_date') location = request.data.get('location', '') if not start_date or not end_date: return Response({'error': 'start_date und end_date sind erforderlich.'}, status=status.HTTP_400_BAD_REQUEST) event = Event.objects.create( title=title, description=template.description, location=location, start_date=start_date, end_date=end_date, created_by=request.user ) template_task_areas = template.template_data.get('task_areas', []) for ta_data in template_task_areas: ta = TaskArea.objects.create( event=event, name=ta_data.get('name', 'Bereich'), description=ta_data.get('description', ''), order=ta_data.get('order', 0) ) for shift_data in ta_data.get('shifts', []): shift = Shift.objects.create( task_area=ta, title=shift_data.get('title', 'Schicht'), start_time=shift_data.get('start_time', '18:00'), end_time=shift_data.get('end_time', '22:00'), max_participants=shift_data.get('max_participants', 1) ) skill_ids = shift_data.get('required_skill_ids', []) skill_names = shift_data.get('required_skill_names', []) if skill_ids: shift.required_skills.set(Skill.objects.filter(id__in=skill_ids)) elif skill_names: shift.required_skills.set(Skill.objects.filter(name__in=skill_names)) return Response({ 'message': f'Veranstaltung "{event.title}" wurde erfolgreich aus der Vorlage erstellt.', 'event_id': event.id }, status=status.HTTP_201_CREATED)