Source code for django_program.registration.admin

"""Django admin configuration for the registration app."""

from decimal import Decimal

from django.contrib import admin
from django.db.models import QuerySet, Sum, Value
from django.db.models.functions import Coalesce
from django.http import HttpRequest  # noqa: TC002

from django_program.registration.badge import Badge, BadgeTemplate
from django_program.registration.checkin import CheckIn, DoorCheck, ProductRedemption
from django_program.registration.conditions import (
    DiscountForCategory,
    DiscountForProduct,
    GroupMemberCondition,
    IncludedProductCondition,
    SpeakerCondition,
    TimeOrStockLimitCondition,
)
from django_program.registration.models import (
    AddOn,
    Attendee,
    Cart,
    CartItem,
    Credit,
    EventProcessingException,
    Order,
    OrderLineItem,
    Payment,
    StripeCustomer,
    StripeEvent,
    TicketType,
    Voucher,
)
from django_program.registration.purchase_order import (
    PurchaseOrder,
    PurchaseOrderCreditNote,
    PurchaseOrderLineItem,
    PurchaseOrderPayment,
)
from django_program.registration.terminal import TerminalPayment


[docs] @admin.register(Attendee) class AttendeeAdmin(admin.ModelAdmin): """Admin interface for viewing and managing conference attendees.""" list_display = ("user", "conference", "access_code", "completed_registration", "checked_in_at") list_filter = ("conference", "completed_registration") search_fields = ("user__username", "user__email", "access_code") readonly_fields = ("access_code",)
[docs] @admin.register(TicketType) class TicketTypeAdmin(admin.ModelAdmin): """Admin interface for managing ticket types. Provides filtering by conference and active status, search by name and slug, and auto-population of the slug from the ticket name. """ list_display = ("name", "conference", "price", "is_active", "bulk_enabled", "order") list_filter = ("conference", "is_active", "bulk_enabled") search_fields = ("name", "slug") prepopulated_fields = {"slug": ("name",)}
[docs] @admin.register(AddOn) class AddOnAdmin(admin.ModelAdmin): """Admin interface for managing add-ons. Uses ``filter_horizontal`` for the ``requires_ticket_types`` many-to-many field to provide a friendlier selection widget. """ list_display = ("name", "conference", "price", "is_active", "bulk_enabled") list_filter = ("conference", "is_active", "bulk_enabled") search_fields = ("name", "slug") prepopulated_fields = {"slug": ("name",)} filter_horizontal = ("requires_ticket_types",)
[docs] @admin.register(Voucher) class VoucherAdmin(admin.ModelAdmin): """Admin interface for managing vouchers. Displays usage counts alongside the voucher configuration and allows filtering by conference, type, and active status. """ list_display = ( "code", "conference", "voucher_type", "discount_value", "times_used", "max_uses", "is_active", ) list_filter = ("conference", "voucher_type", "is_active") search_fields = ("code",) filter_horizontal = ("applicable_ticket_types", "applicable_addons")
[docs] class CartItemInline(admin.TabularInline): """Inline display of cart items within the cart admin. Items are shown as read-only since they are managed through the storefront, not directly in the admin. """ model = CartItem extra = 0 readonly_fields = ("ticket_type", "addon", "quantity")
[docs] @admin.register(Cart) class CartAdmin(admin.ModelAdmin): """Admin interface for viewing shopping carts. Carts are primarily managed by the storefront; the admin provides a read-oriented view with inline cart items. """ list_display = ("user", "conference", "status", "voucher", "expires_at") list_filter = ("conference", "status") inlines = (CartItemInline,)
[docs] class OrderLineItemInline(admin.TabularInline): """Inline display of order line items within the order admin. Line items are immutable snapshots from checkout and are shown read-only. """ model = OrderLineItem extra = 0 readonly_fields = ( "description", "quantity", "unit_price", "discount_amount", "line_total", "ticket_type", "addon", )
[docs] class PaymentInline(admin.TabularInline): """Inline display of payments within the order admin.""" model = Payment extra = 0
[docs] @admin.register(Order) class OrderAdmin(admin.ModelAdmin): """Admin interface for managing orders. Displays order reference, user, status, and financial totals. Money fields are read-only to prevent accidental edits; changes should flow through the payment and refund workflows instead. """ list_display = ("reference", "user", "conference", "status", "total", "created_at") list_filter = ("conference", "status") search_fields = ("reference", "user__email", "billing_email") readonly_fields = ("subtotal", "discount_amount", "total") inlines = (OrderLineItemInline, PaymentInline)
[docs] @admin.register(Credit) class CreditAdmin(admin.ModelAdmin): """Admin interface for managing store credits. Provides filtering by conference and credit status, and displays the amount and creation date at a glance. """ list_display = ("user", "conference", "amount", "status", "created_at") list_filter = ("conference", "status")
[docs] @admin.register(StripeCustomer) class StripeCustomerAdmin(admin.ModelAdmin): """Read-only admin for Stripe customer mappings.""" list_display = ("user", "conference", "stripe_customer_id", "created_at") list_filter = ("conference",) search_fields = ("user__email", "stripe_customer_id") readonly_fields = ("user", "conference", "stripe_customer_id", "created_at")
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: StripeCustomer | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: StripeCustomer | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] @admin.register(StripeEvent) class StripeEventAdmin(admin.ModelAdmin): """Read-only admin for Stripe webhook events.""" list_display = ("stripe_id", "kind", "processed", "livemode", "created_at") list_filter = ("kind", "processed", "livemode") search_fields = ("stripe_id", "customer_id") readonly_fields = ( "stripe_id", "kind", "livemode", "payload", "customer_id", "processed", "api_version", "created_at", )
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: StripeEvent | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: StripeEvent | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] @admin.register(EventProcessingException) class EventProcessingExceptionAdmin(admin.ModelAdmin): """Read-only admin for webhook processing errors.""" list_display = ("message", "event", "created_at") list_filter = ("created_at",) search_fields = ("message",) readonly_fields = ("event", "data", "message", "traceback", "created_at")
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: EventProcessingException | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: EventProcessingException | None = None) -> bool: # noqa: ARG002, D102 return False
# -- Condition / Discount admin -----------------------------------------------
[docs] @admin.register(TimeOrStockLimitCondition) class TimeOrStockLimitConditionAdmin(admin.ModelAdmin): """Admin for time-window and stock-limited conditions.""" list_display = ( "name", "conference", "is_active", "priority", "discount_type", "discount_value", "start_time", "end_time", "times_used", "limit", ) list_filter = ("conference", "is_active", "discount_type") search_fields = ("name",) filter_horizontal = ("applicable_ticket_types", "applicable_addons")
[docs] @admin.register(SpeakerCondition) class SpeakerConditionAdmin(admin.ModelAdmin): """Admin for speaker-based discount conditions.""" list_display = ( "name", "conference", "is_active", "priority", "discount_type", "discount_value", "is_presenter", "is_copresenter", ) list_filter = ("conference", "is_active", "is_presenter", "is_copresenter") search_fields = ("name",) filter_horizontal = ("applicable_ticket_types", "applicable_addons")
[docs] @admin.register(GroupMemberCondition) class GroupMemberConditionAdmin(admin.ModelAdmin): """Admin for group-membership-based discount conditions.""" list_display = ("name", "conference", "is_active", "priority", "discount_type", "discount_value") list_filter = ("conference", "is_active") search_fields = ("name",) filter_horizontal = ("applicable_ticket_types", "applicable_addons", "groups")
[docs] @admin.register(IncludedProductCondition) class IncludedProductConditionAdmin(admin.ModelAdmin): """Admin for included-product discount conditions.""" list_display = ("name", "conference", "is_active", "priority", "discount_type", "discount_value") list_filter = ("conference", "is_active") search_fields = ("name",) filter_horizontal = ("applicable_ticket_types", "applicable_addons", "enabling_ticket_types")
[docs] @admin.register(DiscountForProduct) class DiscountForProductAdmin(admin.ModelAdmin): """Admin for direct product discounts.""" list_display = ( "name", "conference", "is_active", "priority", "discount_type", "discount_value", "start_time", "end_time", "times_used", "limit", ) list_filter = ("conference", "is_active", "discount_type") search_fields = ("name",) filter_horizontal = ("applicable_ticket_types", "applicable_addons")
[docs] @admin.register(DiscountForCategory) class DiscountForCategoryAdmin(admin.ModelAdmin): """Admin for category-wide percentage discounts.""" list_display = ( "name", "conference", "is_active", "priority", "percentage", "apply_to_tickets", "apply_to_addons", "times_used", "limit", ) list_filter = ("conference", "is_active", "apply_to_tickets", "apply_to_addons") search_fields = ("name",)
# -- Check-in admin -----------------------------------------------------------
[docs] @admin.register(CheckIn) class CheckInAdmin(admin.ModelAdmin): """Admin interface for viewing conference check-in records.""" list_display = ("attendee", "conference", "station", "checked_in_by", "checked_in_at") list_filter = ("conference", "station") search_fields = ("attendee__user__username", "attendee__user__email", "attendee__access_code") readonly_fields = ("attendee", "conference", "checked_in_at", "checked_in_by", "station", "note")
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: CheckIn | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: CheckIn | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] @admin.register(DoorCheck) class DoorCheckAdmin(admin.ModelAdmin): """Admin interface for viewing per-product door check records.""" list_display = ("attendee", "conference", "ticket_type", "addon", "station", "checked_by", "checked_at") list_filter = ("conference", "station") search_fields = ("attendee__user__username", "attendee__user__email", "attendee__access_code") readonly_fields = ( "attendee", "ticket_type", "addon", "conference", "checked_at", "checked_by", "station", )
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: DoorCheck | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: DoorCheck | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] @admin.register(ProductRedemption) class ProductRedemptionAdmin(admin.ModelAdmin): """Read-only admin for viewing product redemption audit records.""" list_display = ("attendee", "order_line_item", "conference", "redeemed_by", "redeemed_at") list_filter = ("conference",) search_fields = ("attendee__user__username", "attendee__user__email", "attendee__access_code") readonly_fields = ("attendee", "order_line_item", "conference", "redeemed_at", "redeemed_by", "note")
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: ProductRedemption | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: ProductRedemption | None = None) -> bool: # noqa: ARG002, D102 return False
# -- Badge admin --------------------------------------------------------------
[docs] @admin.register(BadgeTemplate) class BadgeTemplateAdmin(admin.ModelAdmin): """Admin interface for managing badge layout templates.""" list_display = ("name", "conference", "is_default", "width_mm", "height_mm") list_filter = ("conference", "is_default") search_fields = ("name",) prepopulated_fields = {"slug": ("name",)}
[docs] @admin.register(Badge) class BadgeAdmin(admin.ModelAdmin): """Read-only admin for viewing generated badges.""" list_display = ("attendee", "format", "generated_at", "created_at") list_filter = ("format", "generated_at") search_fields = ("attendee__user__username", "attendee__user__email", "attendee__access_code") readonly_fields = ("attendee", "template", "format", "file", "generated_at")
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: Badge | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: Badge | None = None) -> bool: # noqa: ARG002, D102 return False
# -- Terminal payment admin ---------------------------------------------------
[docs] @admin.register(TerminalPayment) class TerminalPaymentAdmin(admin.ModelAdmin): """Read-only admin for Stripe Terminal payment records.""" list_display = ( "payment_intent_id", "conference", "capture_status", "card_brand", "card_last4", "reader_id", "created_at", ) list_filter = ("conference", "capture_status", "card_brand") search_fields = ("payment_intent_id", "reader_id", "terminal_id", "card_last4") readonly_fields = ( "payment", "conference", "terminal_id", "reader_id", "payment_intent_id", "capture_status", "captured_at", "cancelled_at", "card_brand", "card_last4", "receipt_url", "created_at", "updated_at", )
[docs] def has_add_permission(self, request: HttpRequest) -> bool: # noqa: ARG002, D102 return False
[docs] def has_change_permission(self, request: HttpRequest, obj: TerminalPayment | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: TerminalPayment | None = None) -> bool: # noqa: ARG002, D102 return False
# -- Purchase Order admin -----------------------------------------------------
[docs] class PurchaseOrderLineItemInline(admin.TabularInline): """Inline display of purchase order line items. Line items are pricing snapshots and ``line_total`` is read-only to prevent manual edits that would desynchronize the PO totals. """ model = PurchaseOrderLineItem extra = 0 readonly_fields = ("line_total",)
[docs] class PurchaseOrderPaymentInline(admin.TabularInline): """Read-only inline display of payments recorded against a purchase order. Payments should be recorded through the management dashboard to ensure proper status transitions. The inline is read-only to prevent bypassing the service layer invariants. """ model = PurchaseOrderPayment extra = 0 readonly_fields = ("amount", "method", "reference", "payment_date", "entered_by", "note", "created_at")
[docs] def has_add_permission(self, request: HttpRequest, obj: PurchaseOrder | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: PurchaseOrder | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] class PurchaseOrderCreditNoteInline(admin.TabularInline): """Read-only inline display of credit notes issued against a purchase order. Credit notes should be issued through the management dashboard to ensure proper status recalculation. The inline is read-only for audit integrity. """ model = PurchaseOrderCreditNote extra = 0 readonly_fields = ("amount", "reason", "issued_by", "created_at")
[docs] def has_add_permission(self, request: HttpRequest, obj: PurchaseOrder | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] def has_delete_permission(self, request: HttpRequest, obj: PurchaseOrder | None = None) -> bool: # noqa: ARG002, D102 return False
[docs] @admin.register(PurchaseOrder) class PurchaseOrderAdmin(admin.ModelAdmin): """Admin interface for managing corporate purchase orders. Displays the PO reference, organization, status, and financial summary. Money fields are read-only to prevent manual edits; changes should flow through the payment recording and credit note workflows. """ list_display = ( "reference", "organization_name", "conference", "status", "total", "balance_due_display", "created_at", ) list_filter = ("conference", "status") search_fields = ("reference", "organization_name", "contact_email") readonly_fields = ("reference", "subtotal", "total", "balance_due_display", "total_paid_display") inlines = (PurchaseOrderLineItemInline, PurchaseOrderPaymentInline, PurchaseOrderCreditNoteInline)
[docs] def get_queryset(self, request: HttpRequest) -> QuerySet[PurchaseOrder]: """Annotate payment and credit totals to avoid N+1 aggregate queries.""" return ( super() .get_queryset(request) .annotate( _annotated_total_paid=Coalesce(Sum("payments__amount"), Value(Decimal("0.00"))), _annotated_total_credited=Coalesce(Sum("credit_notes__amount"), Value(Decimal("0.00"))), ) )
[docs] @admin.display(description="Balance Due") def balance_due_display(self, obj: PurchaseOrder) -> str: """Render the computed balance due for the list and detail views.""" return str(obj.balance_due)
[docs] @admin.display(description="Total Paid") def total_paid_display(self, obj: PurchaseOrder) -> str: """Render the computed total paid for the detail view.""" return str(obj.total_paid)