from __future__ import annotations from datetime import date from typing import Any, Dict, List from django import forms from django.db.models import Q from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect from django.views import View from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import FormMixin from django.views.generic.list import MultipleObjectMixin from inertia import render from labs.lab_manager_v2.lab_spec import get_all_labs from labs.models import EventVoucher from .base import PermissionRequiredAdminMixin class EventVoucherForm(forms.ModelForm): """Form for creating/updating event vouchers with lab codes text support.""" lab_codes_text = forms.CharField( widget=forms.Textarea(attrs={"rows": 6, "cols": 40}), label="Lab Codes (one per line)", required=False, help_text="Enter lab codes one per line.", ) class Meta: model = EventVoucher fields = [ "unique_code", "start_begin_date", "start_end_date", "marketing_code", "event_name", "credits_cost", ] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) if self.instance and isinstance(self.instance.lab_codes, list): self.fields["lab_codes_text"].initial = "\n".join(self.instance.lab_codes) def clean_lab_codes_text(self) -> list[str]: lab_codes_text = self.cleaned_data.get("lab_codes_text", "") if not lab_codes_text: return [] lab_codes = [line.strip() for line in lab_codes_text.split("\n") if line.strip()] available = set(get_all_labs()) for code in lab_codes: if code not in available: raise forms.ValidationError( f"Lab code `{code}` is not valid. Please enter a valid lab code." ) return lab_codes def save(self, commit: bool = True) -> EventVoucher: instance: EventVoucher = super().save(commit=False) instance.lab_codes = self.cleaned_data.get("lab_codes_text", []) # TODO: remove this after all vouchers are updated instance.lab_group_name = "-" if commit: instance.save() return instance class EventVoucherListView(PermissionRequiredAdminMixin, MultipleObjectMixin, View): """List view for event vouchers with search and filters.""" model = EventVoucher permission_required = "labs.view_eventvoucher" paginate_by = 10 page_kwarg = "page" ordering = "-created_at" search_fields = ["unique_code", "marketing_code"] component = "event-vouchers/Index" def get_queryset(self): queryset = self.model._default_manager.all() if self.ordering: queryset = queryset.order_by(self.ordering) query = self.request.GET.get("q", "").strip() if query: conditions = Q() for field in self.search_fields: conditions |= Q(**{f"{field}__icontains": query}) queryset = queryset.filter(conditions) start_begin_date = self._parse_date(self.request.GET.get("start_begin_date")) if start_begin_date: queryset = queryset.filter(start_begin_date__gte=start_begin_date) start_end_date = self._parse_date(self.request.GET.get("start_end_date")) if start_end_date: queryset = queryset.filter(start_end_date__lte=start_end_date) return queryset def serialize_voucher(self, voucher: EventVoucher) -> Dict[str, Any]: return { "id": str(voucher.id), "unique_code": voucher.unique_code, "lab_codes": voucher.lab_codes or [], "start_begin_date": voucher.start_begin_date.isoformat() if voucher.start_begin_date else None, "start_end_date": voucher.start_end_date.isoformat() if voucher.start_end_date else None, "marketing_code": voucher.marketing_code, "event_name": voucher.event_name, "credits_cost": voucher.credits_cost, "created_at": voucher.created_at.isoformat(), } def get(self, request: HttpRequest) -> HttpResponse: self.object_list = self.get_queryset() paginate_by = self.get_paginate_by(self.object_list) if paginate_by: paginator, page, object_list, is_paginated = self.paginate_queryset( self.object_list, paginate_by ) else: paginator = None page = None object_list = self.object_list is_paginated = False vouchers: List[Dict[str, Any]] = [ self.serialize_voucher(v) for v in object_list ] pagination: Dict[str, Any] | None = None if paginator and page: pagination = { "page": page.number, "per_page": paginator.per_page, "total": paginator.count, "num_pages": paginator.num_pages, "is_paginated": is_paginated, } return render( request, self.component, { "vouchers": vouchers, "filters": { "q": request.GET.get("q", "").strip(), "start_begin_date": request.GET.get("start_begin_date", ""), "start_end_date": request.GET.get("start_end_date", ""), }, "pagination": pagination, }, ) def _parse_date(self, value: str | None) -> date | None: if not value: return None try: return date.fromisoformat(value) except ValueError: return None class EventVoucherCreateView(PermissionRequiredAdminMixin, FormMixin, View): """Create view for event vouchers.""" permission_required = "labs.add_eventvoucher" form_class = EventVoucherForm component = "event-vouchers/Create" def get(self, request: HttpRequest) -> HttpResponse: return render( request, self.component, { "form": {}, "errors": {}, }, ) def post(self, request: HttpRequest) -> HttpResponse: form = self.get_form() if form.is_valid(): return self.form_valid(form) return self.form_invalid(form) def form_valid(self, form: forms.ModelForm) -> HttpResponse: voucher = form.save() self.request.session["flash_success"] = "Event voucher created" return redirect(f"/admin/event-vouchers/{voucher.id}/") def form_invalid(self, form: forms.ModelForm) -> HttpResponse: return render( self.request, self.component, { "form": dict(form.data), "errors": form.errors, }, ) class EventVoucherDetailView( PermissionRequiredAdminMixin, FormMixin, SingleObjectMixin, View ): """Detail/Edit view for a single event voucher.""" model = EventVoucher permission_required = "labs.view_eventvoucher" form_class = EventVoucherForm component = "event-vouchers/Detail" def serialize_voucher(self, voucher: EventVoucher) -> Dict[str, Any]: return { "id": str(voucher.id), "unique_code": voucher.unique_code, "lab_codes": voucher.lab_codes or [], "start_begin_date": voucher.start_begin_date.isoformat() if voucher.start_begin_date else None, "start_end_date": voucher.start_end_date.isoformat() if voucher.start_end_date else None, "marketing_code": voucher.marketing_code, "event_name": voucher.event_name, "credits_cost": voucher.credits_cost, "created_at": voucher.created_at.isoformat(), "updated_at": voucher.updated_at.isoformat(), } def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super().get_form_kwargs() kwargs["instance"] = self.object return kwargs def get(self, request: HttpRequest, pk: int) -> HttpResponse: self.object = self.get_object() return render( request, self.component, { "voucher": self.serialize_voucher(self.object), }, ) def post(self, request: HttpRequest, pk: int) -> HttpResponse: if not request.user.has_perm("labs.change_eventvoucher"): return redirect(f"/admin/event-vouchers/{pk}/") self.object = self.get_object() form = self.get_form() if form.is_valid(): return self.form_valid(form) return self.form_invalid(form) def form_valid(self, form: forms.ModelForm) -> HttpResponse: form.save() self.request.session["flash_success"] = "Event voucher updated" return redirect(f"/admin/event-vouchers/{self.object.id}/") def form_invalid(self, form: forms.ModelForm) -> HttpResponse: return render( self.request, self.component, { "voucher": self.serialize_voucher(self.object), "errors": form.errors, "form": dict(form.data), }, )