from django import forms from django.contrib import admin, messages from .models import Reservation, ReservationToken from .services import PerformanceNotAvailable, create_pending_reservation class ReservationAdminForm(forms.ModelForm): class Meta: model = Reservation fields = ("performance", "name", "email", "phone", "party_size", "notes") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["performance"].help_text = ( "Choose the exact performance. A pending reservation and confirmation email will be created." ) self.fields["party_size"].help_text = "Seats requested for this guest or group." self.fields["notes"].help_text = "Optional internal note for staff." def clean(self): cleaned_data = super().clean() performance = cleaned_data.get("performance") party_size = cleaned_data.get("party_size") if performance and not performance.is_booking_enabled: self.add_error("performance", "Booking is currently disabled for this performance.") if performance and party_size: available_seats = performance.available_seats() if party_size > available_seats: self.add_error( "party_size", f"Only {available_seats} seats are currently available for this performance.", ) return cleaned_data class ReservationTokenInline(admin.TabularInline): model = ReservationToken extra = 0 readonly_fields = ("token_hash", "used_at", "created_at") fields = ("purpose", "token_hash", "expires_at", "used_at", "created_at") can_delete = False @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): form = ReservationAdminForm list_display = ( "show_title", "performance_starts_at", "venue_name", "name", "email", "party_size", "status", "confirmation_state_display", "check_in_state_display", ) list_filter = ( "status", "performance__show", "performance__venue", "performance__starts_at", "confirmed_at", "qr_code_generated_at", ) search_fields = ( "name", "email", "phone", "performance__show__title", "performance__venue__name", ) inlines = (ReservationTokenInline,) list_select_related = ("performance", "performance__show", "performance__venue") readonly_fields = ( "status", "show_title", "performance_starts_at", "venue_name", "confirmation_state_display", "check_in_state_display", "created_at", "updated_at", "confirmed_at", "qr_code_generated_at", ) autocomplete_fields = ("performance",) def get_queryset(self, request): return super().get_queryset(request).select_related( "performance", "performance__show", "performance__venue", "check_in", ) def get_inlines(self, request, obj): if obj is None: return () return super().get_inlines(request, obj) def get_changeform_initial_data(self, request): initial = super().get_changeform_initial_data(request) performance_id = request.GET.get("performance") if performance_id: initial["performance"] = performance_id return initial def get_fieldsets(self, request, obj=None): if obj is None: return ( ( "Create manual reservation", { "description": ( "Use this form when staff needs to enter a reservation manually. " "The reservation stays pending and the standard confirmation email is sent automatically." ), "fields": ("performance", "name", "email", "phone", "party_size", "notes"), }, ), ) return ( ( "Reservation", { "fields": ( "performance", "show_title", "performance_starts_at", "venue_name", "name", "email", "phone", "party_size", "notes", ), }, ), ( "Operational status", { "fields": ( "status", "confirmation_state_display", "check_in_state_display", "confirmed_at", "qr_code_generated_at", ), }, ), ( "Timestamps", { "classes": ("collapse",), "fields": ("created_at", "updated_at"), }, ), ) def save_model(self, request, obj, form, change): if change: super().save_model(request, obj, form, change) return try: result = create_pending_reservation( performance_id=form.cleaned_data["performance"].id, name=form.cleaned_data["name"], email=form.cleaned_data["email"], phone=form.cleaned_data.get("phone", ""), party_size=form.cleaned_data["party_size"], notes=form.cleaned_data.get("notes", ""), ) except PerformanceNotAvailable as exc: form.add_error("performance", str(exc)) raise forms.ValidationError(str(exc)) from exc created = result.reservation obj.pk = created.pk obj._state.adding = False obj.performance = created.performance obj.status = created.status obj.name = created.name obj.email = created.email obj.phone = created.phone obj.party_size = created.party_size obj.notes = created.notes obj.created_at = created.created_at obj.updated_at = created.updated_at obj.confirmed_at = created.confirmed_at obj.qr_code_generated_at = created.qr_code_generated_at self.message_user( request, "Pending reservation created. The guest must confirm it from the email link before check-in.", level=messages.SUCCESS, ) @admin.display(description="Show", ordering="performance__show__title") def show_title(self, obj): return obj.performance.show.title @admin.display(description="Performance", ordering="performance__starts_at") def performance_starts_at(self, obj): return obj.performance.starts_at @admin.display(description="Venue", ordering="performance__venue__name") def venue_name(self, obj): return obj.performance.venue.name @admin.display(description="Confirmation") def confirmation_state_display(self, obj): if obj.status == Reservation.Status.CONFIRMED: return "Confirmed" if obj.status == Reservation.Status.EXPIRED: return "Confirmation expired" if obj.status == Reservation.Status.CANCELLED: return "Cancelled" return "Waiting for email confirmation" @admin.display(description="Check-in") def check_in_state_display(self, obj): return "Checked in" if hasattr(obj, "check_in") else "Not checked in" @admin.register(ReservationToken) class ReservationTokenAdmin(admin.ModelAdmin): list_display = ("reservation", "purpose", "expires_at", "used_at", "created_at", "token_preview") list_filter = ("purpose", "expires_at", "used_at", "created_at") search_fields = ("reservation__name", "reservation__email", "token_hash") readonly_fields = ("token_hash", "created_at", "used_at") list_select_related = ("reservation", "reservation__performance") autocomplete_fields = ("reservation",) @admin.display(description="Token hash") def token_preview(self, obj): return obj.token_hash[:12]