2d5ec8fc6d
Dark theme matching GOA palette, standalone microsite (no nav). - Landing/login: GOA subhead + big "Suff" wordmark, large tap targets - me page: 2-col 4:3 drink grid, bordered total box, day-grouped history with zebra rows, emoji empty-state - Booking confirmation toast (amber, 5s, then 800ms CSS collapse) - Touch feedback via :active scale, SVG beer favicon - no_pin.html link-buttons styled
216 lines
6.5 KiB
Python
216 lines
6.5 KiB
Python
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth import authenticate, login, logout
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import F, Sum
|
|
from django.http import Http404, HttpResponseRedirect
|
|
from django.shortcuts import render
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.text import slugify
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from gaehsnitz.models import Consumption, Drink
|
|
|
|
User = get_user_model()
|
|
|
|
BERLIN = ZoneInfo("Europe/Berlin")
|
|
BOOKING_START = datetime(2026, 6, 11, 0, 0, 0, tzinfo=BERLIN)
|
|
BOOKING_END = datetime(2026, 6, 14, 23, 59, 59, tzinfo=BERLIN)
|
|
READONLY_END = datetime(2026, 6, 21, 23, 59, 59, tzinfo=BERLIN)
|
|
|
|
DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4}
|
|
|
|
|
|
def _now():
|
|
return timezone.now().astimezone(BERLIN)
|
|
|
|
|
|
def _phase():
|
|
if not settings.PRODUCTION:
|
|
return "booking"
|
|
now = _now()
|
|
if now < BOOKING_START:
|
|
return "before"
|
|
if now <= BOOKING_END:
|
|
return "booking"
|
|
if now <= READONLY_END:
|
|
return "readonly"
|
|
return "closed"
|
|
|
|
|
|
def _require_open(request):
|
|
"""Raise 404 once tool is fully closed. Return current phase otherwise."""
|
|
phase = _phase()
|
|
if phase == "closed":
|
|
raise Http404
|
|
return phase
|
|
|
|
|
|
def _current_festival_day():
|
|
weekday = _now().weekday()
|
|
return DAY_BY_WEEKDAY.get(weekday, 4)
|
|
|
|
|
|
def _normalize_name(raw):
|
|
return slugify(raw or "")[:150]
|
|
|
|
|
|
def _tab_context(user):
|
|
consumption = user.consumption_list.select_related("drink").order_by("-created_at", "-id")
|
|
paid = (
|
|
user.paid_drinks.annotate(cost=F("amount") * F("drink__sale_price_per_bottle")).aggregate(total=Sum("cost"))[
|
|
"total"
|
|
]
|
|
or 0
|
|
)
|
|
return {
|
|
"tab_user": user,
|
|
"consumption_list": consumption,
|
|
"total": paid,
|
|
}
|
|
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def name_view(request):
|
|
phase = _require_open(request)
|
|
if request.user.is_authenticated:
|
|
return HttpResponseRedirect(reverse("suff:me"))
|
|
|
|
error = None
|
|
if request.method == "POST":
|
|
raw_name = request.POST.get("name", "")
|
|
username = _normalize_name(raw_name)
|
|
if not username:
|
|
error = "Bitte einen Namen eingeben."
|
|
else:
|
|
try:
|
|
existing = User.objects.get(username=username)
|
|
except User.DoesNotExist:
|
|
existing = None
|
|
|
|
if existing is None:
|
|
request.session["pending_username"] = username
|
|
request.session["pending_mode"] = "create"
|
|
return HttpResponseRedirect(reverse("suff:pin"))
|
|
|
|
if not existing.pin:
|
|
return render(
|
|
request,
|
|
"suff/no_pin.html",
|
|
{"phase": phase, "username": username},
|
|
)
|
|
|
|
request.session["pending_username"] = username
|
|
request.session["pending_mode"] = "login"
|
|
return HttpResponseRedirect(reverse("suff:pin"))
|
|
|
|
return render(request, "suff/name.html", {"phase": phase, "error": error})
|
|
|
|
|
|
@require_http_methods(["GET", "POST"])
|
|
def pin_view(request):
|
|
phase = _require_open(request)
|
|
username = request.session.get("pending_username")
|
|
mode = request.session.get("pending_mode")
|
|
if not username or mode not in ("create", "login"):
|
|
return HttpResponseRedirect(reverse("suff:name"))
|
|
|
|
error = None
|
|
if request.method == "POST":
|
|
pin = request.POST.get("pin", "")
|
|
if not (pin.isdigit() and len(pin) == 3):
|
|
error = "PIN muss aus genau 3 Ziffern bestehen."
|
|
elif mode == "create":
|
|
if User.objects.filter(username=username).exists():
|
|
error = "Name bereits vergeben."
|
|
return HttpResponseRedirect(reverse("suff:name"))
|
|
user = User(username=username)
|
|
user.set_pin(pin)
|
|
user.save()
|
|
authed = authenticate(request, username=username, pin=pin)
|
|
if authed is None:
|
|
error = "Login nach Anlegen fehlgeschlagen."
|
|
else:
|
|
login(request, authed, backend="gaehsnitz.auth_backends.PinBackend")
|
|
request.session.pop("pending_username", None)
|
|
request.session.pop("pending_mode", None)
|
|
return HttpResponseRedirect(reverse("suff:me"))
|
|
else:
|
|
authed = authenticate(request, username=username, pin=pin)
|
|
if authed is None:
|
|
error = "Falsche PIN."
|
|
else:
|
|
login(request, authed, backend="gaehsnitz.auth_backends.PinBackend")
|
|
request.session.pop("pending_username", None)
|
|
request.session.pop("pending_mode", None)
|
|
return HttpResponseRedirect(reverse("suff:me"))
|
|
|
|
return render(
|
|
request,
|
|
"suff/pin.html",
|
|
{
|
|
"phase": phase,
|
|
"error": error,
|
|
"mode": mode,
|
|
"username": username,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET"])
|
|
def me_view(request):
|
|
phase = _require_open(request)
|
|
drinks = Drink.objects.order_by("name") if phase == "booking" else Drink.objects.none()
|
|
booked_drink = None
|
|
booked_id = request.GET.get("booked")
|
|
if booked_id:
|
|
try:
|
|
booked_drink = Drink.objects.get(pk=int(booked_id))
|
|
except (Drink.DoesNotExist, TypeError, ValueError):
|
|
booked_drink = None
|
|
context = _tab_context(request.user)
|
|
context.update(
|
|
{
|
|
"phase": phase,
|
|
"drinks": drinks,
|
|
"current_day": _current_festival_day(),
|
|
"booked_drink": booked_drink,
|
|
}
|
|
)
|
|
return render(request, "suff/me.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def book_view(request):
|
|
phase = _require_open(request)
|
|
if phase != "booking":
|
|
raise Http404
|
|
|
|
drink_id = request.POST.get("drink_id")
|
|
try:
|
|
drink = Drink.objects.get(pk=int(drink_id))
|
|
except (Drink.DoesNotExist, TypeError, ValueError):
|
|
return HttpResponseRedirect(reverse("suff:me"))
|
|
|
|
Consumption.objects.create(
|
|
user=request.user,
|
|
drink=drink,
|
|
amount=1,
|
|
day=_current_festival_day(),
|
|
for_free=False,
|
|
)
|
|
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
def logout_view(request):
|
|
_require_open(request)
|
|
logout(request)
|
|
return HttpResponseRedirect(reverse("suff:name"))
|