Files
guilan-ace-backend/apps/events/admin.py
Amirhossein Khalili 6c5fadef2f feat: derive event lifecycle status from dates and flags
Replace persisted event statuses with publication and cancellation flags, and compute public lifecycle labels from event timing and registration capacity. Keep filters, registration checks, and profile responses aligned with the derived status.
2026-10-03 12:46:16 +03:30

202 lines
8.7 KiB
Python

from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from django.utils import timezone
from core.admin import BaseModelAdmin, SoftDeleteListFilter
from core.templatetags.jalali import jdate
from apps.events.models import Event, Registration
from apps.events.resources import EventResource, RegistrationResource
from apps.events.services import verify_registration_attendance
from apps.events.checkout import cancel_event_checkouts
@admin.register(Event)
class EventAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = EventResource
list_display = (
'title', 'event_type', 'start_time_display', 'end_time_display', 'status_display',
'price_display', 'capacity_display', 'attendees_display', 'is_registration_open_display',
)
list_filter = (
'event_type', 'is_published', 'is_cancelled', 'is_deleted', 'start_time', 'end_time',
'registration_start_date', 'registration_end_date', SoftDeleteListFilter,
)
search_fields = ('title', 'description', 'address')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'start_time'
filter_horizontal = ('gallery_images',)
fieldsets = (
('Event Details', {'fields': ('title', 'slug', 'description', 'featured_image')}),
('Timing & Type', {'fields': ('start_time', 'end_time', 'event_type')}),
('Publication', {'fields': ('is_published', 'is_cancelled')}),
('Location & Online', {
'fields': ('address', 'location', 'online_link'),
'description': 'For On-Site or Hybrid events, provide address and select on map. For Online events, provide a link.',
}),
('Registration & Pricing', {
'fields': ('capacity', 'price', 'registration_start_date', 'registration_end_date', 'registration_success_markdown'),
'description': 'Leave capacity blank for unlimited. Leave price blank for free events.',
}),
('Gallery', {'fields': ('gallery_images',), 'description': 'Add images related to this event from the Gallery app.'}),
('Soft Delete', {'fields': ('is_deleted', 'deleted_at'), 'classes': ('collapse',)}),
)
readonly_fields = ('deleted_at',)
actions = BaseModelAdmin.actions + [
'make_published', 'make_draft', 'make_cancelled', 'restore_events',
]
@admin.display(description='Status')
def status_display(self, obj):
return obj.status_label
@admin.display(description='Price')
def price_display(self, obj):
return obj.price if obj.price is not None else 'رایگان'
@admin.display(description='Start')
def start_time_display(self, obj):
return jdate(obj.start_time)
@admin.display(description='End')
def end_time_display(self, obj):
return jdate(obj.end_time)
@admin.display(description='Capacity')
def capacity_display(self, obj):
return obj.capacity if obj.capacity is not None else 'نامحدود'
@admin.display(description='Attendees')
def attendees_display(self, obj):
return obj.current_attendees_count
@admin.display(description='Open', boolean=True)
def is_registration_open_display(self, obj):
return obj.is_registration_open
@admin.action(description='Mark selected events as published')
def make_published(self, request, queryset):
count = queryset.update(is_published=True)
self.message_user(request, f'Published {count} events.')
@admin.action(description='Mark selected events as draft')
def make_draft(self, request, queryset):
count = queryset.update(is_published=False)
self.message_user(request, f'Marked {count} events as draft.')
@admin.action(description='Mark selected events as cancelled')
def make_cancelled(self, request, queryset):
count = 0
for event in queryset:
if not event.is_cancelled:
event.is_cancelled = True
event.save(update_fields=['is_cancelled', 'updated_at'])
cancel_event_checkouts(event.pk)
count += 1
self.message_user(request, f'Cancelled {count} events.')
@admin.action(description='Restore selected events')
def restore_events(self, request, queryset):
for event in queryset:
event.restore()
self.message_user(request, f'Restored {queryset.count()} events.')
def save_model(self, request, obj, form, change):
was_cancelled = None
if change and obj.pk:
was_cancelled = Event.all_objects.filter(pk=obj.pk).values_list("is_cancelled", flat=True).first()
super().save_model(request, obj, form, change)
if was_cancelled is False and obj.is_cancelled:
cancel_event_checkouts(obj.pk)
@admin.register(Registration)
class RegistrationAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = RegistrationResource
list_display = (
'user', 'event', 'status', 'registered_at', 'checkout_expires_at',
'cancellation_reason', 'ticket_id',
'discount_code', 'discount_amount', 'final_price',
)
list_filter = (
'status', 'cancellation_reason', 'event', 'is_deleted', 'registered_at',
SoftDeleteListFilter,
)
search_fields = (
'user__username', 'user__email', 'user__first_name', 'user__last_name',
'event__title', 'ticket_id',
)
readonly_fields = (
'ticket_id', 'registered_at', 'confirmation_notification_sent_at',
'cancellation_notification_sent_at', 'discount_code', 'discount_amount',
'final_price', 'checkout_started_at', 'checkout_expires_at',
'cancellation_reason', 'deleted_at', 'attendance_verified_at',
'attendance_verified_by', 'attendance_verification_source',
)
fieldsets = (
('Registration Details', {'fields': (
'user', 'event', 'status', 'registered_at', 'ticket_id',
'confirmation_notification_sent_at', 'cancellation_notification_sent_at',
'checkout_started_at', 'checkout_expires_at', 'cancellation_reason',
)}),
('Attendance verification', {'fields': (
'attendance_verified_at', 'attendance_verified_by', 'attendance_verification_source',
)}),
('Pricing & Discount', {
'fields': ('discount_code', 'discount_amount', 'final_price'),
'classes': ('collapse',),
}),
('Soft Delete', {'fields': ('is_deleted', 'deleted_at'), 'classes': ('collapse',)}),
)
actions = BaseModelAdmin.actions + [
'confirm_registrations', 'cancel_registrations', 'mark_attended', 'restore_registrations',
]
@admin.action(description='Confirm selected registrations')
def confirm_registrations(self, request, queryset):
count = 0
for registration in queryset:
registration.status = Registration.StatusChoices.CONFIRMED
registration.save(update_fields=['status'])
count += 1
self.message_user(request, f'Confirmed {count} registrations.')
@admin.action(description='Cancel selected registrations')
def cancel_registrations(self, request, queryset):
count = 0
for registration in queryset:
registration.status = Registration.StatusChoices.CANCELLED
registration.save(update_fields=['status'])
count += 1
self.message_user(request, f'Cancelled {count} registrations.')
@admin.action(description='Mark selected registrations as attended')
def mark_attended(self, request, queryset):
count = 0
for registration in queryset:
verify_registration_attendance(
registration,
verifier=request.user,
source=Registration.AttendanceVerificationSource.STAFF_ADMIN,
)
count += 1
self.message_user(request, f'Marked {count} registrations as attended.')
def save_model(self, request, obj, form, change):
previous_status = None
if change and obj.pk:
previous_status = Registration.all_objects.filter(pk=obj.pk).values_list("status", flat=True).first()
if (
obj.status == Registration.StatusChoices.ATTENDED
and (previous_status != Registration.StatusChoices.ATTENDED or not obj.attendance_verified_at)
):
obj.attendance_verified_by = request.user
obj.attendance_verified_at = timezone.now()
obj.attendance_verification_source = Registration.AttendanceVerificationSource.STAFF_ADMIN
super().save_model(request, obj, form, change)
@admin.action(description='Restore selected registrations')
def restore_registrations(self, request, queryset):
for registration in queryset:
registration.restore()
self.message_user(request, f'Restored {queryset.count()} registrations.')