init
This commit is contained in:
418
backend/events/admin.py
Normal file
418
backend/events/admin.py
Normal file
@@ -0,0 +1,418 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user