From 23b24ed0b93fb433d684dd1170f4944646a447cf Mon Sep 17 00:00:00 2001 From: Flo Ha Date: Sun, 31 May 2026 17:37:30 +0200 Subject: [PATCH] feat(suff): UX improvements and bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Style "Spenden" link as a compact right-aligned button - Replace radio buttons with checkboxes for Gratis/Direkt-bezahlt (toggleable) - Remove "Sonstiges" from payment method dropdown - Disable submit buttons on form submit to prevent double-clicks and give loading feedback (fixes drink_id=None bug caused by disabled button value not being submitted) - Block weak PINs (sequential and repeated digits) - Limit usernames to 2–20 characters - Style PIN errors consistently with other error messages - Add self-service "PIN ändern" page, shown above Logout in 2-column layout - Highlight own username in orange badge (matching staff-target cyan style) - Update booking window: 2026-05-30 10:00 – 2026-06-14 22:00 Co-Authored-By: Claude Sonnet 4.6 --- gaehsnitz/static/suff/style.css | 61 ++++++++++++++++++++- gaehsnitz/suff.py | 67 ++++++++++++++++++------ gaehsnitz/suff_urls.py | 2 + gaehsnitz/templates/suff/base.html | 25 +++++++++ gaehsnitz/templates/suff/change_pin.html | 19 +++++++ gaehsnitz/templates/suff/me.html | 28 +++++++--- gaehsnitz/templates/suff/pin.html | 2 +- 7 files changed, 180 insertions(+), 24 deletions(-) create mode 100644 gaehsnitz/templates/suff/change_pin.html diff --git a/gaehsnitz/static/suff/style.css b/gaehsnitz/static/suff/style.css index a68da67..e6d39eb 100644 --- a/gaehsnitz/static/suff/style.css +++ b/gaehsnitz/static/suff/style.css @@ -140,6 +140,25 @@ button { transition: transform 80ms ease, background-color 100ms ease; } +a.btn-primary { + display: block; + width: fit-content; + margin-left: auto; + background-color: #EE9933; + color: #161616; + font-size: 1rem; + font-weight: bold; + padding: 10px 18px; + border-radius: 6px; + cursor: pointer; + text-decoration: none; + transition: background-color 100ms ease; +} + +a.btn-primary:hover, a.btn-primary:focus { + background-color: #FFCC77; +} + button:active { transform: scale(0.96); } @@ -480,6 +499,14 @@ section { border: 1px solid rgba(102, 221, 238, 0.4); } +.self-username { + color: #EE9933; + background-color: rgba(238, 153, 51, 0.12); + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba(238, 153, 51, 0.4); +} + .confirm-actions > button, .confirm-actions > a { flex: 1; @@ -520,10 +547,42 @@ section { color: #FFCC77; } -.logout-form { +.bottom-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; margin-top: 24px; } +.bottom-actions form { + margin: 0; +} + +.bottom-actions button { + width: 100%; +} + +.btn-secondary-link { + display: flex; + align-items: center; + justify-content: center; + padding: 14px 20px; + font-size: 1.1rem; + font-weight: bold; + border-radius: 6px; + border: 2px solid #885522; + color: #885522; + text-decoration: none; + text-align: center; + min-height: 52px; + box-sizing: border-box; +} + +.btn-secondary-link:hover, .btn-secondary-link:focus { + background-color: rgba(80, 40, 10, 0.4); + color: #FFCC77; +} + .toast { background-color: rgba(238, 153, 51, 0.15); border: 2px solid #EE9933; diff --git a/gaehsnitz/suff.py b/gaehsnitz/suff.py index 1d01aed..aa8df24 100644 --- a/gaehsnitz/suff.py +++ b/gaehsnitz/suff.py @@ -22,11 +22,9 @@ User = get_user_model() ANONYMOUS_USERNAME = "anonym" BERLIN = ZoneInfo("Europe/Berlin") -# TEST WINDOW (2026-05-15 – 2026-05-31): enabled early for pre-festival testing. -# Original festival phase: BOOKING_START = 2026-06-11, BOOKING_END = 2026-06-14. -# Switch back to original dates before the festival. -BOOKING_START = datetime(2026, 5, 15, 0, 0, 0, tzinfo=BERLIN) -BOOKING_END = datetime(2026, 5, 31, 23, 59, 59, tzinfo=BERLIN) +# Festival window: 2026-05-30 10:00 – 2026-06-14 22:00 Berlin time. +BOOKING_START = datetime(2026, 5, 30, 10, 0, 0, tzinfo=BERLIN) +BOOKING_END = datetime(2026, 6, 14, 22, 0, 0, tzinfo=BERLIN) DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4} DAY_CUTOFF_HOUR = 6 @@ -67,7 +65,13 @@ def _current_festival_day(): def _normalize_name(raw): - return slugify(raw or "")[:150] + return slugify(raw or "")[:20] + + +def _username_error(username): + if len(username) < 2: + return "Name muss mindestens 2 Zeichen lang sein." + return None def _user_has_activity(user): @@ -78,6 +82,17 @@ def _is_anonymous(user): return user.username == ANONYMOUS_USERNAME +_WEAK_PINS = {"123", "234", "345", "456", "567", "678", "789", "987", "876", "765", "654", "543", "432", "321"} | {d * 3 for d in "0123456789"} + + +def _pin_error(pin): + if not (pin.isdigit() and len(pin) == 3): + return "PIN muss aus genau 3 Ziffern bestehen." + if pin in _WEAK_PINS: + return "Diese PIN ist zu einfach. Bitte eine andere wählen." + return None + + def _staff_required(view): def wrapped(request, *args, **kwargs): if not request.user.is_authenticated: @@ -126,7 +141,7 @@ def _tab_context(user): "paid": paid, "open_balance": total - paid, "user_payments": payments, - "payment_methods": UserPayment.Method.choices, + "payment_methods": [(m, l) for m, l in UserPayment.Method.choices if m != "other"], } @@ -142,8 +157,8 @@ def name_view(request): if request.method == "POST": raw_name = request.POST.get("name", "") username = _normalize_name(raw_name) - if not username: - error = "Bitte einen Namen eingeben." + if not username or (error := _username_error(username)): + error = error or "Bitte einen Namen eingeben." else: try: existing = User.objects.get(username=username) @@ -186,8 +201,8 @@ def pin_view(request): 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." + if error := _pin_error(pin): + pass elif mode == "create": if User.objects.filter(username=username).exists(): error = "Name bereits vergeben." @@ -242,6 +257,26 @@ def pin_view(request): ) +@login_required +@require_http_methods(["GET", "POST"]) +def change_pin_view(request): + redirect, phase = _require_open(request) + if redirect: + return redirect + + error = None + if request.method == "POST": + pin = request.POST.get("pin", "") + if error := _pin_error(pin): + pass + else: + request.user.set_pin(pin) + request.user.save() + return HttpResponseRedirect(reverse("suff:me")) + + return render(request, "suff/change_pin.html", {"phase": phase, "error": error}) + + @login_required @require_http_methods(["GET"]) def me_view(request): @@ -447,10 +482,12 @@ def staff_register_view(request): if not username: error = "Bitte einen Namen eingeben." + elif error := _username_error(username): + pass elif User.objects.filter(username=username).exists(): error = "Name bereits vergeben." - elif pin and not (pin.isdigit() and len(pin) == 3): - error = "PIN muss aus genau 3 Ziffern bestehen (oder leer lassen)." + elif pin and (error := _pin_error(pin)): + pass else: user = User(username=username) if pin: @@ -484,8 +521,8 @@ def staff_pin_reset_view(request, username): error = None if request.method == "POST": pin = (request.POST.get("pin") or "").strip() - if not (pin.isdigit() and len(pin) == 3): - error = "PIN muss aus genau 3 Ziffern bestehen." + if error := _pin_error(pin): + pass else: target.set_pin(pin) target.save() diff --git a/gaehsnitz/suff_urls.py b/gaehsnitz/suff_urls.py index db36598..90c98d4 100644 --- a/gaehsnitz/suff_urls.py +++ b/gaehsnitz/suff_urls.py @@ -2,6 +2,7 @@ from django.urls import path from gaehsnitz.suff import ( book_view, + change_pin_view, closed_view, dashboard_view, delete_consumption_view, @@ -28,6 +29,7 @@ urlpatterns = [ path("closed/", closed_view, name="closed"), path("pin/", pin_view, name="pin"), path("me/", me_view, name="me"), + path("me/change-pin/", change_pin_view, name="change_pin"), path("book/", book_view, name="book"), path("book//delete/", delete_consumption_view, name="delete_consumption"), path("pay/", pay_view, name="pay"), diff --git a/gaehsnitz/templates/suff/base.html b/gaehsnitz/templates/suff/base.html index 8410adc..092ece1 100644 --- a/gaehsnitz/templates/suff/base.html +++ b/gaehsnitz/templates/suff/base.html @@ -14,5 +14,30 @@

Suff

{% block content %}{% endblock %} + diff --git a/gaehsnitz/templates/suff/change_pin.html b/gaehsnitz/templates/suff/change_pin.html new file mode 100644 index 0000000..c6eb784 --- /dev/null +++ b/gaehsnitz/templates/suff/change_pin.html @@ -0,0 +1,19 @@ +{% extends "suff/base.html" %} + +{% block content %} +

PIN ändern

+ +{% if error %}

{{ error }}

{% endif %} + +
+ {% csrf_token %} + + +
+ +

Abbrechen

+{% endblock %} diff --git a/gaehsnitz/templates/suff/me.html b/gaehsnitz/templates/suff/me.html index 407fbc2..135ccb0 100644 --- a/gaehsnitz/templates/suff/me.html +++ b/gaehsnitz/templates/suff/me.html @@ -1,7 +1,7 @@ {% extends "suff/base.html" %} {% block content %} -

Hallo {{ tab_user.username }}

+

Hallo {{ tab_user.username }}

Tipp dich rein, sobald du was trinkst. Am Ende deines Besuchs kannst du @@ -55,13 +55,24 @@

{% csrf_token %} +
{% for drink in drinks %}
{% endif %} - - {% csrf_token %} - -
+
+ PIN ändern +
+ {% csrf_token %} + +
+
{% endblock %} diff --git a/gaehsnitz/templates/suff/pin.html b/gaehsnitz/templates/suff/pin.html index f69768d..954e526 100644 --- a/gaehsnitz/templates/suff/pin.html +++ b/gaehsnitz/templates/suff/pin.html @@ -13,7 +13,7 @@

Gib deine 3-stellige PIN ein.

{% endif %} -{% if error %}

{{ error }}

{% endif %} +{% if error %}

{{ error }}

{% endif %}
{% csrf_token %}