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.
202 lines
8.7 KiB
Python
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.')
|