"""Event and gallery API schemas.""" from uuid import UUID from ninja import ModelSchema, Schema from pydantic import field_validator from typing import Literal, Optional, List from datetime import datetime from apps.blog.api.schemas import AuthorSchema from apps.events.models import Event, Registration from apps.gallery.models import Gallery from apps.payments.models import Payment from core.media import BLUR_VARIANT, PREVIEW_VARIANT, THUMBNAIL_VARIANT, derivative_url class EventGallerySchema(ModelSchema): """Schema representing gallery items associated with an event.""" uploaded_by: AuthorSchema file_size_mb: float markdown_url: str absolute_image_url: Optional[str] = None absolute_image_preview_url: Optional[str] = None absolute_image_blur_url: Optional[str] = None class Config: model = Gallery model_fields = ['id', 'title', 'description', 'image', 'alt_text', 'width', 'height', 'is_public', 'created_at'] @staticmethod def resolve_absolute_image_url(obj, context): request = context['request'] if obj.image and hasattr(obj.image, 'url'): return request.build_absolute_uri(obj.image.url) return None @staticmethod def resolve_absolute_image_preview_url(obj, context): request = context["request"] url = derivative_url(obj.image, PREVIEW_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_absolute_image_blur_url(obj, context): request = context["request"] url = derivative_url(obj.image, BLUR_VARIANT) return request.build_absolute_uri(url) if url else None class EventSchema(ModelSchema): """Schema providing full event details for API responses.""" gallery_images: List[EventGallerySchema] description_html: str registration_count: int absolute_featured_image_url: Optional[str] = None absolute_featured_image_thumbnail_url: Optional[str] = None absolute_featured_image_preview_url: Optional[str] = None class Config: model = Event model_fields = [ 'id', 'title', 'slug', 'description', 'featured_image', 'event_type', 'address', 'location', 'online_link', 'start_time', 'end_time', 'registration_start_date', 'registration_end_date', 'registration_success_markdown', 'capacity', 'price', 'status', 'created_at', 'updated_at' ] @staticmethod def resolve_absolute_featured_image_url(obj, context): request = context['request'] if obj.featured_image and hasattr(obj.featured_image, 'url'): return request.build_absolute_uri(obj.featured_image.url) return None @staticmethod def resolve_absolute_featured_image_thumbnail_url(obj, context): request = context["request"] url = derivative_url(obj.featured_image, THUMBNAIL_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_absolute_featured_image_preview_url(obj, context): request = context["request"] url = derivative_url(obj.featured_image, PREVIEW_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_registration_count(obj): return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count() @staticmethod def resolve_description_html(obj): return obj.description_html class EventListSchema(Schema): """Condensed event representation for list endpoints.""" id: int title: str slug: str featured_image: Optional[str] = None absolute_featured_image_url: Optional[str] = None absolute_featured_image_thumbnail_url: Optional[str] = None absolute_featured_image_preview_url: Optional[str] = None event_type: str start_time: datetime end_time: datetime registration_start_date: Optional[datetime] = None registration_end_date: Optional[datetime] = None capacity: Optional[int] = None price: Optional[float] = None status: str registration_count: int created_at: datetime @staticmethod def resolve_absolute_featured_image_url(obj, context): request = context['request'] if obj.featured_image and hasattr(obj.featured_image, 'url'): return request.build_absolute_uri(obj.featured_image.url) return None @staticmethod def resolve_absolute_featured_image_thumbnail_url(obj, context): request = context["request"] url = derivative_url(obj.featured_image, THUMBNAIL_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_absolute_featured_image_preview_url(obj, context): request = context["request"] url = derivative_url(obj.featured_image, PREVIEW_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_registration_count(obj): return obj.registrations.filter(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED]).count() class EventCreateSchema(Schema): """Payload for creating events via the API.""" title: str slug: Optional[str] = None description: str event_type: str address: Optional[str] = None location: Optional[str] = None online_link: Optional[str] = None start_time: datetime end_time: datetime registration_start_date: Optional[datetime] = None registration_end_date: Optional[datetime] = None capacity: Optional[int] = None price: Optional[float] = None status: str = "draft" registration_success_markdown: Optional[str] = None gallery_image_ids: Optional[List[int]] = [] class EventUpdateSchema(Schema): """Payload for updating events via the API.""" title: Optional[str] = None slug: Optional[str] = None description: Optional[str] = None event_type: Optional[str] = None address: Optional[str] = None location: Optional[str] = None online_link: Optional[str] = None start_time: Optional[datetime] = None end_time: Optional[datetime] = None registration_start_date: Optional[datetime] = None registration_end_date: Optional[datetime] = None capacity: Optional[int] = None price: Optional[float] = None status: Optional[str] = None registration_success_markdown: Optional[str] = None gallery_image_ids: Optional[List[int]] = None class RegistrationSchema(ModelSchema): """Schema describing a registration entry with event context.""" user: AuthorSchema event: EventListSchema discount_code: str | None = None class Config: model = Registration model_fields = [ 'id', 'status', 'registered_at', 'ticket_id', 'discount_amount', 'final_price', 'created_at', 'updated_at', ] @staticmethod def resolve_discount_code(obj): return obj.discount_code.code if obj.discount_code else None class AdminUserSchema(Schema): id: int username: str first_name: str last_name: str email: str mobile: Optional[str] = None profile_picture: Optional[str] = None profile_picture_thumbnail_url: Optional[str] = None profile_picture_preview_url: Optional[str] = None university: Optional[str] = None major: Optional[str] = None student_id: Optional[str] = None year_of_study: Optional[int] = None @staticmethod def resolve_profile_picture(obj, context): image = getattr(obj, "profile_picture", None) if not getattr(image, "name", None): return None request = context["request"] return request.build_absolute_uri(image.url) if hasattr(image, "url") else None @staticmethod def resolve_profile_picture_thumbnail_url(obj, context): image = getattr(obj, "profile_picture", None) if not getattr(image, "name", None): return None request = context["request"] url = derivative_url(image, THUMBNAIL_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_profile_picture_preview_url(obj, context): image = getattr(obj, "profile_picture", None) if not getattr(image, "name", None): return None request = context["request"] url = derivative_url(image, PREVIEW_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_university(obj): return obj.get_university_display() @staticmethod def resolve_major(obj): return obj.get_major_display() class PaymentAdminSchema(Schema): id: int authority: Optional[str] ref_id: Optional[str] card_pan: Optional[str] card_hash: Optional[str] status: int status_label: str base_amount: int discount_amount: int amount: int verified_at: Optional[datetime] created_at: datetime discount_code: Optional[str] user: AdminUserSchema @field_validator("discount_code", mode="before") def normalize_discount_code(cls, value): if value is None: return None if hasattr(value, "code"): return value.code return str(value) class RegistrationAdminSchema(Schema): id: int ticket_id: UUID status: str status_label: str registered_at: datetime final_price: Optional[int] discount_amount: Optional[int] user: AdminUserSchema payments: List[PaymentAdminSchema] class EventAdminDetailSchema(EventSchema): registrations: List[RegistrationAdminSchema] = [] @staticmethod def resolve_registrations(obj): return obj.registrations.select_related("user", "user__university", "user__major").prefetch_related( "payments__discount_code" ).order_by("-registered_at") class PaginatedRegistrationSchema(Schema): count: int next: Optional[str] = None previous: Optional[str] = None results: List[RegistrationAdminSchema] class RegistrationStatusUpdateSchema(Schema): status: str class RegisterationDetailSchema(Schema): """Detailed registration information with associated event metadata.""" event_image: Optional[str] event_title: str event_type: str ticket_id: UUID status: str registered_at: datetime success_markdown: Optional[str] class EventBriefSchema(Schema): """Minimal event representation used for nested responses.""" id: int title: str slug: str start_date: datetime end_date: Optional[datetime] = None location: Optional[str] = None price: int absolute_image_url: Optional[str] = None class MyEventRegistrationOut(Schema): """Registration information as returned to authenticated users.""" id: int created_at: datetime status: Literal["pending", "confirmed", "cancelled", "attended"] event: EventBriefSchema class RegistrationStatusOut(Schema): is_registered: bool class RegistrationCreateSchema(Schema): discount_code: Optional[str] = None