Files
gaehsnitz/gaehsnitz/admin.py
T
flo 47d46e8e6f Add suff drink booking tool with PIN auth
Self-service drink tab at /suff/ for festival attendees. Users log in
with username + 3-digit PIN stored in a separate User.pin field, so
staff/admin accounts can keep their strong password for /admin/ and
also use the drink tool with the same username. PINs for staff users
must be set from the admin panel via a dedicated "PIN setzen" view to
prevent account takeover by name collision.

Time-gated to the festival window (Thu–Sun in Berlin tz) with phases
before/booking/readonly/closed; in non-production mode the tool is
always in booking phase for local testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:05:25 +02:00

242 lines
6.8 KiB
Python

from django import forms
from django.contrib import admin, messages
from django.contrib.admin.utils import unquote
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from gaehsnitz.models import Donation, Payment, Drink, Consumption
from gaehsnitz.templatetags.money import euro
User = get_user_model()
class ConsumptionInline(admin.TabularInline):
model = Consumption
extra = 0
class SetPinForm(forms.Form):
pin = forms.CharField(
label="Neue PIN (3 Ziffern)",
min_length=3,
max_length=3,
)
def clean_pin(self):
pin = self.cleaned_data["pin"]
if not pin.isdigit():
raise forms.ValidationError("PIN muss aus genau 3 Ziffern bestehen.")
return pin
@admin.register(User)
class CustomUserAdmin(UserAdmin):
list_display = ("username", "consumed_drinks_price")
ordering = ("username",)
list_filter = []
fieldsets = (
(
None,
{
"fields": (
"username",
"password",
"pin_status",
)
},
),
("BALANCE", {"fields": ("consumed_drinks_price",)}),
)
readonly_fields = ("consumed_drinks_price", "pin_status")
inlines = (ConsumptionInline,)
def consumed_drinks_price(self, user: User):
return euro(user.consumed_drinks_price)
@admin.display(description="PIN")
def pin_status(self, user: User):
status = "gesetzt" if user.pin else "nicht gesetzt"
if user.pk is None:
return status
url = reverse("admin:gaehsnitz_user_set_pin", args=[user.pk])
return format_html('{} &nbsp; <a href="{}">PIN setzen</a>', status, url)
def get_urls(self):
urls = super().get_urls()
custom = [
path(
"<id>/pin/",
self.admin_site.admin_view(self.set_pin_view),
name="gaehsnitz_user_set_pin",
),
]
return custom + urls
def set_pin_view(self, request, id):
if not self.has_change_permission(request):
raise Http404
user = self.get_object(request, unquote(id))
if user is None:
raise Http404
if request.method == "POST":
form = SetPinForm(request.POST)
if form.is_valid():
user.set_pin(form.cleaned_data["pin"])
user.save(update_fields=["pin"])
messages.success(request, f"PIN für {user.username} gesetzt.")
return HttpResponseRedirect(reverse("admin:gaehsnitz_user_change", args=[user.pk]))
else:
form = SetPinForm()
context = {
**self.admin_site.each_context(request),
"title": f"PIN setzen für {user.username}",
"opts": self.model._meta,
"original": user,
"form": form,
}
return TemplateResponse(request, "admin/gaehsnitz/user/set_pin.html", context)
@admin.register(Donation)
class DonationAdmin(admin.ModelAdmin):
list_display = ("date", "amount", "note")
ordering = ("-date",)
@admin.display(ordering="amount")
def amount(self, donation: Donation):
return euro(donation.amount)
@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
list_display = ("date", "purpose", "amount")
ordering = ("-date",)
@admin.display(ordering="amount")
def amount(self, payment: Payment):
return euro(payment.amount)
@admin.register(Drink)
class DrinkAdmin(admin.ModelAdmin):
list_display = ("name", "purchase_price_per_crate", "crates_purchased", "purchase_price_total")
fieldsets = (
(None, {"fields": ("name",)}),
(
"crates",
{
"fields": (
"crates_ordered",
"crates_purchased",
"crates_returned",
)
},
),
(
"bottles",
{
"fields": (
"bottles_per_crate",
"bottles_total",
"bottles_returned",
)
},
),
(
"amount",
{
"fields": (
"bottle_size",
"amount_per_crate",
"amount_total",
)
},
),
(
"purchase",
{
"fields": (
"purchase_price_per_crate",
"purchase_price_per_bottle",
"purchase_price_total",
)
},
),
(
"deposit",
{
"fields": (
"deposit_per_crate",
"deposit_total",
"deposit_refund",
"deposit_kept",
)
},
),
(
"sales",
{
"fields": (
"sale_price_per_bottle",
"bottles_sold",
"sales_purchase_value",
"sale_price_total",
"bottles_given_away",
"giveaway_purchase_value",
"balance",
)
},
),
)
readonly_fields = (
"bottles_total",
"bottles_returned",
"amount_per_crate",
"amount_total",
"purchase_price_per_bottle",
"purchase_price_total",
"deposit_total",
"deposit_refund",
"deposit_kept",
"bottles_sold",
"sales_purchase_value",
"sale_price_total",
"bottles_given_away",
"giveaway_purchase_value",
"balance",
)
def purchase_price_per_bottle(self, drink: Drink):
return euro(drink.purchase_price_per_bottle)
def purchase_price_total(self, drink: Drink):
return euro(drink.purchase_price_total)
def deposit_total(self, drink: Drink):
return euro(drink.deposit_total)
def deposit_refund(self, drink: Drink):
return euro(drink.deposit_refund)
def deposit_kept(self, drink: Drink):
return euro(drink.deposit_kept)
def sales_purchase_value(self, drink: Drink):
return euro(drink.sales_purchase_value)
def sale_price_total(self, drink: Drink):
return euro(drink.sale_price_total)
def giveaway_purchase_value(self, drink: Drink):
return euro(drink.giveaway_purchase_value)
def balance(self, drink: Drink):
return mark_safe(f"<b>{euro(drink.balance)}</b>")