Files
guilan-ace-backend/apps/events/api/schemas.py

452 lines
15 KiB
Python

"""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 django.db.models import Q
from django.utils import timezone
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 Meta:
model = Gallery
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 Meta:
model = Event
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):
now = timezone.now()
return obj.registrations.filter(is_deleted=False).filter(
Q(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED])
| Q(status=Registration.StatusChoices.PENDING, checkout_expires_at__gt=now)
).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
address: 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
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):
if hasattr(obj, "browse_registration_count"):
return obj.browse_registration_count
now = timezone.now()
return obj.registrations.filter(is_deleted=False).filter(
Q(status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED])
| Q(status=Registration.StatusChoices.PENDING, checkout_expires_at__gt=now)
).count()
class PaginatedEventBrowseSchema(Schema):
count: int
page: int
page_size: int
total_pages: int
price_ceiling: int
results: List[EventListSchema]
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 Meta:
model = Registration
fields = [
'id',
'status',
'registered_at',
'checkout_started_at',
'checkout_expires_at',
'cancellation_reason',
'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
ticket_id: UUID
created_at: datetime
status: Literal["pending", "confirmed", "cancelled", "attended"]
event: EventBriefSchema
checkout_state: Literal["none", "active", "legacy_pending", "expired", "unresolved", "cancelled"] = "none"
checkout_expires_at: Optional[datetime] = None
payment_id: Optional[int] = None
payment_status: Optional[Literal["initiated", "pending", "paid", "failed", "canceled"]] = None
refund_status: Optional[Literal["none", "pending", "succeeded", "manual_review"]] = None
can_resume: bool = False
class PaginatedMyEventRegistrationsSchema(Schema):
count: int
page: int
page_size: int
total_pages: int
results: List[MyEventRegistrationOut]
class RegistrationStatusOut(Schema):
is_registered: bool
checkout_state: Literal[
"none", "confirmed", "active", "legacy_pending", "expired", "unresolved", "cancelled"
] = "none"
registration_id: Optional[int] = None
registration_status: Optional[str] = None
ticket_id: Optional[UUID] = None
checkout_expires_at: Optional[datetime] = None
payment_id: Optional[int] = None
payment_status: Optional[Literal["initiated", "pending", "paid", "failed", "canceled"]] = None
refund_status: Optional[Literal["none", "pending", "succeeded", "manual_review"]] = None
can_resume: bool = False
can_cancel: bool = False
class RegistrationHubEventOut(Schema):
id: int
title: str
slug: str
description: str
event_type: str
address: Optional[str] = None
location: 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: int
status: str
registration_count: int
image_url: Optional[str] = None
registration_success_markdown: Optional[str] = None
class RegistrationHubRegistrationOut(Schema):
ticket_id: UUID
status: str
registered_at: datetime
checkout_started_at: Optional[datetime] = None
checkout_expires_at: Optional[datetime] = None
cancellation_reason: str = ""
final_price: Optional[int] = None
discount_amount: int = 0
class RegistrationHubPaymentOut(Schema):
id: int
status: Literal["initiated", "pending", "paid", "failed", "canceled"]
verification_state: Literal[
"awaiting_verification", "checking", "retry_scheduled", "manual_review",
"verified", "failed", "canceled",
]
refund_status: Literal["none", "pending", "succeeded", "manual_review"]
base_amount: int
discount_amount: int
amount: int
ref_id: Optional[str] = None
created_at: datetime
gateway_paid_at: Optional[datetime] = None
class RegistrationHubOut(Schema):
state: Literal[
"ready", "active_checkout", "legacy_pending", "verification_pending",
"manual_review", "paid_success", "free_success", "failed", "cancelled",
"expired", "refund_pending", "refund_manual_review", "refund_succeeded",
]
event: RegistrationHubEventOut
registration: Optional[RegistrationHubRegistrationOut] = None
payment: Optional[RegistrationHubPaymentOut] = None
can_register: bool = False
can_resume: bool = False
can_cancel: bool = False
class RegistrationCreateSchema(Schema):
discount_code: Optional[str] = None