from __future__ import annotations import secrets import string 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.models import EXAM_VOUCHER_PATTERN, Exam, ExamVoucher from .base import PermissionRequiredAdminMixin def _generate_exam_voucher_code() -> str: alphabet = string.ascii_uppercase + string.digits random_chars = "".join(secrets.choice(alphabet) for _ in range(25)) formatted_string = "-".join( random_chars[i : i + 5] for i in range(0, 25, 5) ) return f"EXAM-{formatted_string}" class ExamVoucherForm(forms.ModelForm): """Form for creating/updating exam vouchers.""" class Meta: model = ExamVoucher fields = [ "unique_code", "exam", "valid_before", "marketing_code", ] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.fields["unique_code"].required = False self.fields["exam"].queryset = Exam.objects.order_by("code") def clean_unique_code(self) -> str: raw = (self.cleaned_data.get("unique_code") or "").strip() if not raw: if self.instance and self.instance.pk: raise forms.ValidationError("Unique code is required.") max_attempts = 5 for _ in range(max_attempts): code = _generate_exam_voucher_code() if not ExamVoucher.objects.filter(unique_code=code).exists(): return code raise forms.ValidationError( "Failed to generate unique voucher code, please retry." ) code = raw.upper() if not EXAM_VOUCHER_PATTERN.match(code): raise forms.ValidationError("Invalid exam voucher code format") return code class ExamVoucherListView(PermissionRequiredAdminMixin, MultipleObjectMixin, View): """List view for exam vouchers with search and filters.""" model = ExamVoucher permission_required = "labs.view_examvoucher" paginate_by = 10 page_kwarg = "page" ordering = "-created_at" search_fields = ["unique_code", "marketing_code"] component = "exam-vouchers/Index" def get_queryset(self): queryset = self.model._default_manager.select_related("exam").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) used = self.request.GET.get("used") if used in ("Y", "N"): queryset = queryset.filter(used=used) exam_id = self.request.GET.get("exam_id") if exam_id and exam_id.isdigit(): queryset = queryset.filter(exam_id=int(exam_id)) return queryset def serialize_voucher(self, voucher: ExamVoucher) -> Dict[str, Any]: return { "id": str(voucher.id), "unique_code": voucher.unique_code, "exam": { "id": voucher.exam_id, "code": voucher.exam.code, }, "valid_before": voucher.valid_before.isoformat() if voucher.valid_before else None, "used": voucher.used, "marketing_code": voucher.marketing_code, "created_at": voucher.created_at.isoformat(), "updated_at": voucher.updated_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, } exams = list(Exam.objects.values("id", "code").order_by("code")) return render( request, self.component, { "vouchers": vouchers, "exams": exams, "filters": { "q": request.GET.get("q", "").strip(), "used": request.GET.get("used"), "exam_id": request.GET.get("exam_id"), }, "pagination": pagination, }, ) class ExamVoucherCreateView(PermissionRequiredAdminMixin, FormMixin, View): """Create view for exam vouchers.""" permission_required = "labs.add_examvoucher" form_class = ExamVoucherForm component = "exam-vouchers/Create" def get(self, request: HttpRequest) -> HttpResponse: exams = list(Exam.objects.values("id", "code").order_by("code")) return render( request, self.component, { "exams": exams, "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"] = "Exam voucher created" return redirect(f"/admin/exam-vouchers/{voucher.id}/") def form_invalid(self, form: forms.ModelForm) -> HttpResponse: exams = list(Exam.objects.values("id", "code").order_by("code")) return render( self.request, self.component, { "exams": exams, "form": dict(form.data), "errors": form.errors, }, ) class ExamVoucherDetailView( PermissionRequiredAdminMixin, FormMixin, SingleObjectMixin, View ): """Detail/Edit view for a single exam voucher.""" model = ExamVoucher permission_required = "labs.view_examvoucher" form_class = ExamVoucherForm component = "exam-vouchers/Detail" def get_queryset(self): return super().get_queryset().select_related("exam") def serialize_voucher(self, voucher: ExamVoucher) -> Dict[str, Any]: return { "id": str(voucher.id), "unique_code": voucher.unique_code, "exam": {"id": voucher.exam_id, "code": voucher.exam.code}, "valid_before": voucher.valid_before.isoformat() if voucher.valid_before else None, "used": voucher.used, "marketing_code": voucher.marketing_code, "created_at": voucher.created_at.isoformat(), "updated_at": voucher.updated_at.isoformat(), "redeem_at": voucher.redeem_at.isoformat() if voucher.redeem_at else None, "redeem_by": voucher.redeem_by_id, } 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() exams = list(Exam.objects.values("id", "code").order_by("code")) return render( request, self.component, { "voucher": self.serialize_voucher(self.object), "exams": exams, }, ) def post(self, request: HttpRequest, pk: int) -> HttpResponse: if not request.user.has_perm("labs.change_examvoucher"): return redirect(f"/admin/exam-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"] = "Exam voucher updated" return redirect(f"/admin/exam-vouchers/{self.object.id}/") def form_invalid(self, form: forms.ModelForm) -> HttpResponse: exams = list(Exam.objects.values("id", "code").order_by("code")) return render( self.request, self.component, { "voucher": self.serialize_voucher(self.object), "exams": exams, "errors": form.errors, "form": dict(form.data), }, )