Files
CE-Association-Website/backend/events/admin.py
Amirhossein Khalili 7a8ddeabed
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled
init
2026-05-18 11:34:07 +03:30

419 lines
16 KiB
Python

from django.contrib import admin, messages
from django.template.response import TemplateResponse
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.template.loader import render_to_string
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse_lazy
from import_export.admin import ImportExportModelAdmin
from utils.templatetags.jalali import jdate
from unfold.decorators import action as unfold_action
from utils.admin import SoftDeleteListFilter, BaseModelAdmin
from events.models import Event, Registration, EventEmailLog
from events.resources import EventResource, RegistrationResource
from events.tasks import (
queue_skyroom_credentials,
send_skyroom_credentials_individual_task,
send_event_reminder_task,
queue_event_announcement,
queue_invites_to_non_registered_users,
)
from events.admin_forms import AnnouncementForm
from events.tasks import _send_html_email
@admin.register(Event)
class EventAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = EventResource
list_display = (
'title', 'event_type', 'start_time_display', 'end_time_display', 'status',
'price_display', 'capacity_display', 'attendees_display', 'is_registration_open_display'
)
list_filter = (
'event_type', 'status', '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', 'status')
}),
('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',
'make_completed',
'restore_events',
]
actions_row = [
'action_send_announcement',
'action_send_reminder_now',
'action_send_skyroom_credentials',
'action_invite_other_users',
]
@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):
queryset.update(status=Event.StatusChoices.PUBLISHED)
self.message_user(request, f"Published {queryset.count()} events.")
@admin.action(description="Mark selected events as draft")
def make_draft(self, request, queryset):
queryset.update(status=Event.StatusChoices.DRAFT)
self.message_user(request, f"Marked {queryset.count()} events as draft.")
@admin.action(description="Mark selected events as cancelled")
def make_cancelled(self, request, queryset):
queryset.update(status=Event.StatusChoices.CANCELLED)
self.message_user(request, f"Cancelled {queryset.count()} events.")
@admin.action(description="Mark selected events as completed")
def make_completed(self, request, queryset):
queryset.update(status=Event.StatusChoices.COMPLETED)
self.message_user(request, f"Marked {queryset.count()} events as completed.")
@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.")
@unfold_action(description="Send Skyroom Credentials")
def action_send_skyroom_credentials(self, request, object_id: int):
event = Event.objects.get(pk=object_id)
queue_skyroom_credentials.delay(event.pk)
self.message_user(request, f"ارسال مشخصات اسکای‌روم برای رویداد '{event.title}' صف شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_event_changelist"))
@unfold_action(description="Send new Reminder")
def action_send_reminder_now(self, request, object_id: int):
event = Event.objects.get(pk=object_id)
send_event_reminder_task.delay(event.pk)
self.message_user(request, f"یادآوری برای رویداد '{event.title}' صف شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_event_changelist"))
@unfold_action(description="send new Announcement")
def action_send_announcement(self, request, object_id: int):
"""
این اکشن یک فرم می‌گیرد (عنوان/متن/وضعیت‌ها) و با تمپلیت Unfold نشان داده می‌شود.
"""
form = AnnouncementForm(request.POST or None)
event = Event.objects.get(pk=object_id)
if request.method == "POST" and form.is_valid():
subject = form.cleaned_data["subject"]
body_html = form.cleaned_data["body_html"]
statuses = form.cleaned_data["statuses"] or None
queue_event_announcement.delay(event.pk, subject, body_html, statuses=statuses)
self.message_user(request, f"اطلاعیه برای رویداد '{event.title}' صف شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_event_changelist"))
context = {
**self.admin_site.each_context(request),
"title": "ارسال اطلاعیه گروهی",
"opts": self.model._meta,
"form": form,
"action_name": "action_send_announcement",
"action_checkbox_name": ACTION_CHECKBOX_NAME,
}
return TemplateResponse(request, "forms/admin_announcement.html", context)
@unfold_action(description="Invite other users")
def action_invite_other_users(self, request, object_id: int):
event = Event.objects.get(pk=object_id)
queue_invites_to_non_registered_users.delay(event.pk)
self.message_user(request, f"دعوت برای شرکت در رویداد '{event.title}' صف شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_event_changelist"))
@admin.register(Registration)
class RegistrationAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = RegistrationResource
list_display = (
'user',
'event',
'status',
'registered_at',
'ticket_id',
'discount_code',
'discount_amount',
'final_price',
)
list_filter = (
'status',
'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_email_sent_at',
'cancellation_email_sent_at',
'discount_code',
'discount_amount',
'final_price',
'deleted_at',
)
fieldsets = (
(
'Registration Details',
{
'fields': (
'user',
'event',
'status',
'registered_at',
'ticket_id',
'confirmation_email_sent_at',
'cancellation_email_sent_at',
)
},
),
(
'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',
]
actions_row = [
'action_email_selected',
'action_send_skyroom_credentials',
]
@admin.action(description="Confirm selected registrations")
def confirm_registrations(self, request, queryset):
queryset.update(status=Registration.StatusChoices.CONFIRMED)
self.message_user(request, f"Confirmed {queryset.count()} registrations.")
@admin.action(description="Cancel selected registrations")
def cancel_registrations(self, request, queryset):
queryset.update(status=Registration.StatusChoices.CANCELLED)
self.message_user(request, f"Cancelled {queryset.count()} registrations.")
@admin.action(description="Mark selected registrations as attended")
def mark_attended(self, request, queryset):
queryset.update(status=Registration.StatusChoices.ATTENDED)
self.message_user(request, f"Marked {queryset.count()} registrations as attended.")
@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.")
@unfold_action(description="send email to registrated user")
def action_email_selected(self, request, object_id: int):
"""
همان فرم اطلاعیه را می‌گیرد و به افراد انتخاب‌شده ایمیل می‌زند.
برای نمایش فرم، از تمپلیت Unfold استفاده می‌کنیم.
"""
form = AnnouncementForm(request.POST or None)
registration = Registration.objects.get(id=object_id)
if request.method == "POST" and form.is_valid():
subject = form.cleaned_data["subject"]
body_html = form.cleaned_data["body_html"]
user = registration.user
ctx = {
"user": user,
"event": registration.event,
"body_html": body_html,
"event_url": f"{settings.FRONTEND_ROOT}events/{registration.event.slug}",
}
html = render_to_string("emails/event_announcement.html", ctx)
_send_html_email(subject, html, user.email)
self.message_user(request, f"ارسال ایمیل انجام شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_registration_changelist"))
context = {
**self.admin_site.each_context(request),
"title": "ارسال ایمیل به ثبت‌نام‌های انتخاب‌شده",
"form": AnnouncementForm(),
"opts": self.model._meta,
"action_name": "action_email_selected",
"action_checkbox_name": ACTION_CHECKBOX_NAME,
}
return TemplateResponse(request, "forms/admin_announcement.html", context)
@unfold_action(description="Send Skyroom Credentials")
def action_send_skyroom_credentials(self, request, object_id: int):
send_skyroom_credentials_individual_task.delay(object_id)
self.message_user(request, f"ارسال مشخصات اسکای‌روم به کاربر مربوطه صف شد.", messages.SUCCESS)
return redirect(reverse_lazy("admin:events_registration_changelist"))
from events.tasks import send_invite_to_user
@admin.register(EventEmailLog)
class EventEmailLogAdmin(BaseModelAdmin, ImportExportModelAdmin):
list_display = (
"id",
"event",
"user",
"user_email",
"kind",
"status",
"sent_at",
"created_at",
)
list_filter = (
"kind",
"status",
"event",
("sent_at", admin.EmptyFieldListFilter),
("error", admin.EmptyFieldListFilter),
SoftDeleteListFilter,
)
search_fields = (
"user__email",
"user__username",
"user__first_name",
"user__last_name",
"event__title",
)
autocomplete_fields = ("event", "user")
date_hierarchy = "created_at"
ordering = ("-created_at",)
list_per_page = 50
list_select_related = ("event", "user")
# چون این مدل برای ایدمپوتنسی حیاتی است، ویرایش دستی را محدود می‌کنیم
readonly_fields = (
"event",
"user",
"kind",
"status",
"error",
"sent_at",
"created_at",
"updated_at",
)
fields = readonly_fields
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return True
actions = BaseModelAdmin.actions + [
'resend_selected_emails'
]
@admin.display(description="Email", ordering="user__email")
def user_email(self, obj):
return obj.user.email or ""
@admin.action(description="ارسال مجدد ایمیل برای رکوردهای انتخاب‌شده")
def resend_selected_emails(self, request, queryset):
"""
رکوردهای SENT را اسکیپ می‌کند، بقیه را به وضعیت pending برمی‌گرداند
و تسک ارسال تکی را در صف می‌گذارد (ایدِمپوتنت).
"""
queued = 0
skipped = 0
for log in queryset.select_related("event", "user"):
if log.status == EventEmailLog.STATUS_SENT:
skipped += 1
continue
# برگرداندن به pending و پاک کردن خطا
if log.status != EventEmailLog.STATUS_PENDING or log.error:
log.status = EventEmailLog.STATUS_PENDING
log.error = ""
log.save(update_fields=["status", "error", "updated_at"])
# صف کردن تسک اتمی
send_invite_to_user.delay(log.event_id, log.user_id)
queued += 1
if queued:
self.message_user(
request,
"%(n)d مورد در صف ارسال قرار گرفت." % {"n": queued},
level=messages.SUCCESS,
)
if skipped:
self.message_user(
request,
"%(n)d مورد قبلاً ارسال شده بود و نادیده گرفته شد." % {"n": skipped},
level=messages.WARNING,
)