initial commit
This commit is contained in:
0
apps/events/api/__init__.py
Normal file
0
apps/events/api/__init__.py
Normal file
247
apps/events/api/schemas.py
Normal file
247
apps/events/api/schemas.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""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
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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_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
|
||||
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_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
|
||||
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"
|
||||
gallery_image_ids: Optional[List[int]] = []
|
||||
|
||||
class EventUpdateSchema(Schema):
|
||||
"""Payload for updating events via the API."""
|
||||
title: 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
|
||||
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
|
||||
|
||||
|
||||
class PaymentAdminSchema(Schema):
|
||||
id: int
|
||||
authority: Optional[str]
|
||||
ref_id: 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").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
|
||||
370
apps/events/api/views.py
Normal file
370
apps/events/api/views.py
Normal file
@@ -0,0 +1,370 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q, Case, When, IntegerField
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
|
||||
from ninja import Router, Query
|
||||
from ninja.errors import HttpError
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from apps.events.api.schemas import (
|
||||
EventAdminDetailSchema,
|
||||
EventBriefSchema,
|
||||
EventCreateSchema,
|
||||
EventListSchema,
|
||||
EventSchema,
|
||||
EventUpdateSchema,
|
||||
MyEventRegistrationOut,
|
||||
PaginatedRegistrationSchema,
|
||||
RegisterationDetailSchema,
|
||||
RegistrationCreateSchema,
|
||||
RegistrationSchema,
|
||||
RegistrationStatusOut,
|
||||
RegistrationStatusUpdateSchema,
|
||||
)
|
||||
from core.authentication import jwt_auth
|
||||
from apps.events.models import Event, Registration
|
||||
from apps.payments.models import DiscountCode
|
||||
from core.api.schemas import ErrorSchema, MessageSchema
|
||||
|
||||
events_router = Router()
|
||||
|
||||
# Event endpoints
|
||||
@events_router.get("/", response=List[EventListSchema])
|
||||
def list_events(
|
||||
request,
|
||||
# status: Optional[str] = None,
|
||||
status: Optional[List[str]] = Query(None),
|
||||
event_type: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
limit: int = 20,
|
||||
offset: int = 0
|
||||
):
|
||||
"""List events with filtering and pagination"""
|
||||
queryset = Event.objects.filter(is_deleted=False).prefetch_related('gallery_images')
|
||||
|
||||
if status:
|
||||
if "," in status:
|
||||
parts = [s.strip() for s in status.split(",") if s.strip()]
|
||||
queryset = queryset.filter(status__in=parts)
|
||||
else:
|
||||
queryset = queryset.filter(status__in=status)
|
||||
if event_type:
|
||||
queryset = queryset.filter(event_type=event_type)
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(title__icontains=search) | Q(description__icontains=search)
|
||||
)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
published_first=Case(
|
||||
When(status='published', then=0),
|
||||
default=1,
|
||||
output_field=IntegerField()
|
||||
)
|
||||
).order_by('published_first', '-start_time', '-id')
|
||||
|
||||
events = queryset[offset:offset + limit]
|
||||
return events
|
||||
|
||||
@events_router.get("/{int:event_id}", response=EventSchema)
|
||||
def get_event(request, event_id: int):
|
||||
"""Get event details by ID"""
|
||||
event = get_object_or_404(
|
||||
Event.objects.prefetch_related('gallery_images'),
|
||||
id=event_id,
|
||||
is_deleted=False
|
||||
)
|
||||
return event
|
||||
|
||||
@events_router.get("/slug/{str:slug}", response=EventSchema)
|
||||
def get_event_by_slug(request, slug: str):
|
||||
"""Get event details by slug"""
|
||||
event = get_object_or_404(
|
||||
Event.objects.prefetch_related('gallery_images'),
|
||||
slug=slug,
|
||||
is_deleted=False
|
||||
)
|
||||
return event
|
||||
|
||||
@events_router.post("/", response=EventSchema)
|
||||
def create_event(request, payload: EventCreateSchema):
|
||||
"""Create a new event"""
|
||||
gallery_image_ids = payload.dict().pop('gallery_image_ids', [])
|
||||
event = Event.objects.create(**payload.dict(exclude={'gallery_image_ids'}))
|
||||
|
||||
if gallery_image_ids:
|
||||
event.gallery_images.set(gallery_image_ids)
|
||||
|
||||
return event
|
||||
|
||||
@events_router.put("/{int:event_id}", response=EventSchema)
|
||||
def update_event(request, event_id: int, payload: EventUpdateSchema):
|
||||
"""Update an existing event"""
|
||||
event = get_object_or_404(Event, id=event_id, is_deleted=False)
|
||||
|
||||
update_data = payload.dict(exclude_unset=True)
|
||||
gallery_image_ids = update_data.pop('gallery_image_ids', None)
|
||||
|
||||
for attr, value in update_data.items():
|
||||
setattr(event, attr, value)
|
||||
|
||||
if 'title' in update_data:
|
||||
event.slug = slugify(event.title)
|
||||
|
||||
event.save()
|
||||
|
||||
if gallery_image_ids is not None:
|
||||
event.gallery_images.set(gallery_image_ids)
|
||||
|
||||
return event
|
||||
|
||||
@events_router.delete("/{int:event_id}", response=MessageSchema)
|
||||
def delete_event(request, event_id: int):
|
||||
"""Soft delete an event"""
|
||||
event = get_object_or_404(Event, id=event_id, is_deleted=False)
|
||||
event.delete()
|
||||
return {"message": "Event deleted successfully"}
|
||||
|
||||
# Registration endpoints
|
||||
@events_router.get("/{int:event_id}/registrations", response=List[RegistrationSchema])
|
||||
def list_event_registrations(request, event_id: int, limit: int = 20, offset: int = 0):
|
||||
"""List registrations for a specific event"""
|
||||
event = get_object_or_404(Event, id=event_id, is_deleted=False)
|
||||
queryset = event.registrations.filter(is_deleted=False).select_related('user')
|
||||
|
||||
registrations = queryset[offset:offset + limit]
|
||||
return registrations
|
||||
|
||||
|
||||
@events_router.get("/{int:event_id}/admin-registrations", response={200: PaginatedRegistrationSchema, 403: ErrorSchema}, auth=jwt_auth)
|
||||
def list_event_registrations_admin(
|
||||
request,
|
||||
event_id: int,
|
||||
status: Optional[List[str]] = Query(None),
|
||||
university: Optional[str] = Query(None),
|
||||
major: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
limit: int = Query(20, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
"""List registrations with filters for admin dashboard"""
|
||||
user = request.auth
|
||||
if not (user.is_staff or user.is_superuser):
|
||||
return 403, {"error": "اجازه دسترسی ندارید."}
|
||||
|
||||
event = get_object_or_404(Event, id=event_id, is_deleted=False)
|
||||
qs = (
|
||||
event.registrations.filter(is_deleted=False)
|
||||
.select_related("user")
|
||||
.prefetch_related("payments__discount_code")
|
||||
.order_by("-registered_at")
|
||||
)
|
||||
|
||||
status_values = status or request.GET.getlist('status')
|
||||
if status_values:
|
||||
qs = qs.filter(status__in=status_values)
|
||||
|
||||
if university:
|
||||
qs = qs.filter(
|
||||
Q(user__university__code__icontains=university)
|
||||
| Q(user__university__name__icontains=university)
|
||||
)
|
||||
|
||||
if major:
|
||||
qs = qs.filter(
|
||||
Q(user__major__code__icontains=major)
|
||||
| Q(user__major__name__icontains=major)
|
||||
)
|
||||
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(user__username__icontains=search)
|
||||
| Q(user__email__icontains=search)
|
||||
| Q(user__first_name__icontains=search)
|
||||
| Q(user__last_name__icontains=search)
|
||||
)
|
||||
|
||||
total = qs.count()
|
||||
results = qs[offset : offset + limit]
|
||||
|
||||
return PaginatedRegistrationSchema(count=total, next=None, previous=None, results=list(results))
|
||||
|
||||
@events_router.post(
|
||||
"/{int:event_id}/register",
|
||||
response=RegistrationSchema,
|
||||
auth=jwt_auth,
|
||||
)
|
||||
def register_for_event(
|
||||
request,
|
||||
event_id: int,
|
||||
payload: RegistrationCreateSchema | None = None,
|
||||
):
|
||||
"""Register current user for an event"""
|
||||
event = get_object_or_404(Event, id=event_id, is_deleted=False)
|
||||
user = request.auth
|
||||
|
||||
if Registration.objects.filter(event=event, user=user, status=Registration.StatusChoices.CONFIRMED).exists():
|
||||
raise HttpError(400, "شما قبلا در این ایونت ثبتنام کردهاید.")
|
||||
|
||||
if event.registration_end_date and event.registration_end_date < timezone.now():
|
||||
raise HttpError(400, "مهلت ثبتنام به پایان رسیدهاست")
|
||||
|
||||
if event.registration_start_date and event.registration_start_date > timezone.now():
|
||||
raise HttpError(400, "زمان ثبتنام هنوز آغاز نشده است")
|
||||
|
||||
if not event.has_available_slots:
|
||||
raise HttpError(400, "ظرفیت شرکتکنندگان تکمیل است")
|
||||
|
||||
# Create or get existing registration
|
||||
discount_code = None
|
||||
if payload and payload.discount_code:
|
||||
discount_code = payload.discount_code
|
||||
elif request.GET.get("discount_code"):
|
||||
discount_code = request.GET.get("discount_code")
|
||||
|
||||
registration, created = Registration.objects.get_or_create(
|
||||
event=event,
|
||||
user=user,
|
||||
status=Registration.StatusChoices.PENDING,
|
||||
defaults={"final_price": event.price},
|
||||
)
|
||||
|
||||
if registration.status == Registration.StatusChoices.CONFIRMED:
|
||||
return HttpError(400, "شما قبلا در این ایونت ثبتنام کردهاید")
|
||||
|
||||
if registration.status == Registration.StatusChoices.CANCELLED:
|
||||
registration = Registration.objects.create(
|
||||
event=event,
|
||||
user=user,
|
||||
status=Registration.StatusChoices.PENDING,
|
||||
final_price=event.price,
|
||||
)
|
||||
elif not created and registration.final_price is None:
|
||||
registration.final_price = event.price
|
||||
registration.save(update_fields=["final_price"])
|
||||
|
||||
applied_code = None
|
||||
discount_amount = 0
|
||||
final_price = event.price
|
||||
fields_to_update = []
|
||||
|
||||
if discount_code:
|
||||
applied_code = DiscountCode.objects.filter(
|
||||
code=discount_code,
|
||||
applicable_events=event,
|
||||
is_active=True,
|
||||
).first()
|
||||
if not applied_code:
|
||||
raise HttpError(400, "UcO_ O<>OrU?UOU? U.O1O<31>O\"O<EFBFBD> U+UOO3O<33>")
|
||||
final_price, discount_amount = applied_code.calculate_discount(event, user)
|
||||
registration.discount_code = applied_code
|
||||
registration.discount_amount = discount_amount
|
||||
fields_to_update.extend(["discount_code", "discount_amount"])
|
||||
|
||||
if registration.final_price != final_price:
|
||||
registration.final_price = final_price
|
||||
fields_to_update.append("final_price")
|
||||
|
||||
if not event.price or final_price == 0:
|
||||
registration.status = Registration.StatusChoices.CONFIRMED
|
||||
fields_to_update.append("status")
|
||||
|
||||
if fields_to_update:
|
||||
registration.save(update_fields=list(set(fields_to_update)))
|
||||
|
||||
return registration
|
||||
|
||||
@events_router.put("/registrations/{int:registration_id}", response=RegistrationSchema, auth=jwt_auth)
|
||||
def update_registration_status(request, registration_id: int, payload: RegistrationStatusUpdateSchema):
|
||||
"""Update registration status"""
|
||||
user = request.auth
|
||||
|
||||
registration = get_object_or_404(Registration, id=registration_id, user=user, is_deleted=False)
|
||||
registration.status = payload.dict(exclude_unset=True).get('status')
|
||||
registration.full_clean()
|
||||
registration.save()
|
||||
|
||||
return registration
|
||||
|
||||
@events_router.delete("/registrations/{int:registration_id}", response=MessageSchema, auth=jwt_auth)
|
||||
def cancel_registration(request, registration_id: int):
|
||||
"""Cancel a registration"""
|
||||
user = request.auth
|
||||
|
||||
registration = get_object_or_404(Registration, id=registration_id, user=user, is_deleted=False)
|
||||
registration.delete()
|
||||
return {"message": "ثبتنام شما لغو شد :("}
|
||||
|
||||
@events_router.get("/registerations/verify/{UUID:ticket_id}", response=RegisterationDetailSchema, auth=jwt_auth)
|
||||
def verify_my_registration(request, ticket_id: UUID):
|
||||
try:
|
||||
reg = Registration.objects.select_related("event").get(ticket_id=ticket_id, user=request.auth)
|
||||
return {
|
||||
"event_image": request.build_absolute_uri(reg.event.featured_image.url) if reg.event.featured_image else None,
|
||||
"event_title": reg.event.title,
|
||||
"event_type": reg.event.get_event_type_display(),
|
||||
"ticket_id": reg.ticket_id,
|
||||
"status": reg.status,
|
||||
"registered_at": reg.registered_at,
|
||||
"success_markdown": reg.event.registration_success_markdown,
|
||||
}
|
||||
except Registration.DoesNotExist:
|
||||
raise HttpError(404, "registration not found")
|
||||
|
||||
|
||||
|
||||
@events_router.get("/my-registrations", response=List[MyEventRegistrationOut], auth=jwt_auth)
|
||||
def my_registrations(request):
|
||||
qs = (
|
||||
Registration.objects
|
||||
.filter(user=request.auth)
|
||||
.select_related("event")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
out: List[MyEventRegistrationOut] = []
|
||||
for r in qs:
|
||||
out.append(
|
||||
MyEventRegistrationOut(
|
||||
id=r.id,
|
||||
created_at=r.created_at,
|
||||
status=r.status,
|
||||
event=EventBriefSchema(
|
||||
id=r.event.id,
|
||||
title=r.event.title,
|
||||
slug=r.event.slug,
|
||||
start_date=r.event.start_time,
|
||||
end_date=r.event.end_time,
|
||||
location=r.event.location,
|
||||
price=r.event.price,
|
||||
absolute_image_url=request.build_absolute_uri(r.event.featured_image.url) if r.event.featured_image else None,
|
||||
),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
@events_router.get("/{event_id}/is-registered", response=RegistrationStatusOut, auth=jwt_auth)
|
||||
def is_registered(request, event_id: int):
|
||||
exists = Registration.objects.filter(
|
||||
user=request.auth,
|
||||
event_id=event_id,
|
||||
status=Registration.StatusChoices.CONFIRMED
|
||||
).exists()
|
||||
return {"is_registered": exists}
|
||||
@events_router.get("/{int:event_id}/admin-detail", response=EventAdminDetailSchema, auth=jwt_auth)
|
||||
def event_admin_detail(request, event_id: int):
|
||||
user = request.auth
|
||||
if not (user.is_staff or user.is_superuser):
|
||||
return 403, {"error": "اجازه دسترسی ندارید."}
|
||||
|
||||
event = get_object_or_404(
|
||||
Event.objects.prefetch_related(
|
||||
'gallery_images',
|
||||
'registrations__user',
|
||||
'registrations__payments__discount_code'
|
||||
),
|
||||
id=event_id,
|
||||
is_deleted=False,
|
||||
)
|
||||
return event
|
||||
Reference in New Issue
Block a user