"""Views: dünn gehalten, die Fachlogik liegt im Modell. Aktionen (übernehmen/erledigt/absagen) liefern bei HTMX-Anfragen nur die aktualisierte Fahrt-Karte zurück (Inline-Update ohne Reload), sonst einen klassischen Redirect — funktioniert also auch ohne JavaScript. """ from __future__ import annotations from django.conf import settings from django.contrib import messages from django.contrib.auth import login, logout from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.db.models import Count from django.http import HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.views.decorators.http import require_POST from .forms import AvailabilityForm, RideForm from .models import STATUS_COLORS, Ride, RideStatus def _is_htmx(request) -> bool: return request.headers.get("HX-Request") == "true" def _status_counts() -> list[dict]: counts = { row["status"]: row["n"] for row in Ride.objects.values("status").annotate(n=Count("id")) } return [ {"label": s.label, "n": counts.get(s.value, 0), "color": STATUS_COLORS[s.value]} for s in RideStatus ] # --- Einstieg / Demo-Login ----------------------------------------------- def home(request): if request.user.is_authenticated: return redirect("dashboard") demo_users = ( User.objects.filter(profile__isnull=False) .select_related("profile") .order_by("profile__role") ) return render(request, "rides/home.html", {"demo_users": demo_users}) @require_POST def demo_login(request, user_id: int): """Schneller Rollenwechsel für die Demo — nur bei DEBUG aktiv.""" if not settings.DEMO_MODE: return HttpResponseForbidden("Demo-Login ist deaktiviert.") user = get_object_or_404(User, pk=user_id) login(request, user) return redirect("dashboard") def logout_view(request): logout(request) return redirect("home") # --- Übersicht ----------------------------------------------------------- @login_required def dashboard(request): profile = getattr(request.user, "profile", None) role = profile.role if profile else None ctx: dict = {"role": role} if profile and profile.is_coordinator: ctx["status_counts"] = _status_counts() ctx["rides"] = Ride.objects.upcoming().select_related("fahrgast", "assigned_pilot") elif profile and profile.is_pilot: ctx["open_rides"] = Ride.objects.open_for_pilots().select_related("fahrgast") ctx["my_rides"] = ( Ride.objects.filter(assigned_pilot=request.user) .exclude(status=RideStatus.CANCELLED) .select_related("fahrgast") ) ctx["availabilities"] = request.user.availabilities.filter(end__gte=timezone.now()) ctx["availability_form"] = AvailabilityForm() else: ctx["my_requests"] = Ride.objects.filter(requested_by=request.user).select_related( "fahrgast" ) return render(request, "rides/dashboard.html", ctx) # --- Fahrten ------------------------------------------------------------- @login_required def ride_detail(request, pk: int): ride = get_object_or_404( Ride.objects.select_related("fahrgast__einrichtung", "assigned_pilot"), pk=pk, ) if not ride.visible_to(request.user): return HttpResponseForbidden("Diese Fahrt ist für dich nicht sichtbar.") return render( request, "rides/ride_detail.html", {"ride": ride, "show_notes": ride.notes_visible_to(request.user)}, ) @login_required def ride_create(request): profile = getattr(request.user, "profile", None) if not (profile and (profile.is_coordinator or profile.is_requester)): return HttpResponseForbidden("Nur Koordination und Besteller:innen dürfen Fahrten anlegen.") if request.method == "POST": form = RideForm(request.POST) if form.is_valid(): ride = form.save(commit=False) ride.requested_by = request.user ride.status = RideStatus.PLANNED if profile.is_coordinator else RideStatus.REQUESTED from .services import route_geojson ride.route_geojson = route_geojson( (ride.pickup_lat, ride.pickup_lng), (ride.dest_lat, ride.dest_lng) ) ride.save() messages.success(request, "Fahrt angelegt.") return redirect("ride_detail", pk=ride.pk) else: form = RideForm() return render(request, "rides/ride_form.html", {"form": form}) def _card_response(request, ride, **flags): """Aktualisierte Fahrt-Karte (für HTMX-Inline-Update).""" return render(request, "rides/_ride_card.html", {"ride": ride, **flags}) @require_POST @login_required def ride_claim(request, pk: int): ride = get_object_or_404(Ride, pk=pk) profile = getattr(request.user, "profile", None) if not (profile and profile.is_pilot): return HttpResponseForbidden("Nur Pilot:innen können Fahrten übernehmen.") try: ride.claim(request.user) except ValueError as exc: messages.error(request, str(exc)) return redirect(request.POST.get("next") or "dashboard") if _is_htmx(request): return _card_response(request, ride, can_finish=True) messages.success(request, "Fahrt übernommen.") return redirect(request.POST.get("next") or "dashboard") @require_POST @login_required def ride_done(request, pk: int): ride = get_object_or_404(Ride, pk=pk) profile = getattr(request.user, "profile", None) allowed = profile and (profile.is_coordinator or ride.assigned_pilot_id == request.user.id) if not allowed: return HttpResponseForbidden() try: ride.mark_done() except ValueError as exc: messages.error(request, str(exc)) return redirect(request.POST.get("next") or "dashboard") if _is_htmx(request): return _card_response(request, ride) messages.success(request, "Fahrt als erledigt markiert.") return redirect(request.POST.get("next") or "dashboard") @require_POST @login_required def ride_cancel(request, pk: int): ride = get_object_or_404(Ride, pk=pk) profile = getattr(request.user, "profile", None) if not (profile and profile.is_coordinator): return HttpResponseForbidden() try: ride.cancel() except ValueError as exc: messages.error(request, str(exc)) return redirect(request.POST.get("next") or "dashboard") if _is_htmx(request): return _card_response(request, ride) messages.success(request, "Fahrt abgesagt.") return redirect(request.POST.get("next") or "dashboard") # --- Verfügbarkeit ------------------------------------------------------- @require_POST @login_required def availability_add(request): profile = getattr(request.user, "profile", None) if not (profile and profile.is_pilot): return HttpResponseForbidden() form = AvailabilityForm(request.POST) if form.is_valid(): av = form.save(commit=False) av.pilot = request.user av.save() messages.success(request, "Verfügbarkeit gespeichert.") else: messages.error(request, "Bitte Beginn und Ende prüfen.") return redirect("dashboard") # --- Karte / öffentlich -------------------------------------------------- @login_required def map_view(request): return render(request, "rides/map.html") @login_required def rides_geojson(request): features = [ { "type": "Feature", "geometry": {"type": "Point", "coordinates": [ride.pickup_lng, ride.pickup_lat]}, "properties": { "label": ride.pickup_label, "status": ride.get_status_display(), "color": ride.status_color, "when": timezone.localtime(ride.scheduled_at).strftime("%d.%m. %H:%M"), "url": reverse("ride_detail", args=[ride.pk]), }, } for ride in Ride.objects.visible_to(request.user) ] return JsonResponse({"type": "FeatureCollection", "features": features}) def coverage(request): """Öffentliche Einzugsgebiet-Karte — keine Personendaten.""" return render(request, "rides/coverage.html") @login_required def stats(request): profile = getattr(request.user, "profile", None) if not (profile and profile.is_coordinator): return HttpResponseForbidden() done = Ride.objects.filter(status=RideStatus.DONE) per_pilot = ( done.values("assigned_pilot__first_name", "assigned_pilot__username") .annotate(n=Count("id")) .order_by("-n") ) return render( request, "rides/stats.html", { "total_done": done.count(), "per_pilot": per_pilot, "status_counts": _status_counts(), }, )