schon/engine/vibes_auth/admin.py
2026-01-26 03:23:41 +03:00

448 lines
14 KiB
Python

from typing import Any
from django.contrib import admin, messages
from django.contrib.admin import register
from django.contrib.auth.admin import (
GroupAdmin as BaseGroupAdmin,
)
from django.contrib.auth.admin import (
UserAdmin as BaseUserAdmin,
)
from django.contrib.auth.models import Group as BaseGroup
from django.contrib.auth.models import Permission
from django.core.exceptions import PermissionDenied
from django.db.models import Prefetch, QuerySet
from django.http import HttpRequest
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from rest_framework_simplejwt.token_blacklist.admin import (
BlacklistedTokenAdmin as BaseBlacklistedTokenAdmin,
)
from rest_framework_simplejwt.token_blacklist.admin import (
OutstandingTokenAdmin as BaseOutstandingTokenAdmin,
)
from rest_framework_simplejwt.token_blacklist.models import (
BlacklistedToken as BaseBlacklistedToken,
)
from rest_framework_simplejwt.token_blacklist.models import (
OutstandingToken as BaseOutstandingToken,
)
from unfold.admin import ModelAdmin, TabularInline
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.forms import AdminPasswordChangeForm, UserCreationForm
from engine.core.admin import ActivationActionsMixin
from engine.core.models import Order
from engine.payments.models import Balance
from engine.vibes_auth.emailing.choices import CampaignStatus
from engine.vibes_auth.emailing.models import (
CampaignRecipient,
EmailCampaign,
EmailImage,
EmailTemplate,
)
from engine.vibes_auth.emailing.tasks import (
prepare_campaign_recipients,
send_campaign_email_batch,
)
from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.models import (
BlacklistedToken,
ChatMessage,
ChatThread,
Group,
OutstandingToken,
ThreadStatus,
User,
)
class BalanceInline(TabularInline):
model = Balance
can_delete = False
extra = 0
verbose_name = _("balance")
verbose_name_plural = _("balance")
is_navtab = True
icon = "fa-solid fa-wallet"
class OrderInline(TabularInline):
model = Order
extra = 0
verbose_name = _("order")
verbose_name_plural = _("orders")
is_navtab = True
icon = "fa-solid fa-cart-shopping"
class UserAdmin(ActivationActionsMixin, BaseUserAdmin, ModelAdmin):
inlines = (BalanceInline, OrderInline)
fieldsets = (
(None, {"fields": ("email", "password")}),
(
_("personal info"),
{"fields": ("first_name", "last_name", "phone_number", "avatar")},
),
(
_("permissions"),
{
"fields": (
"is_active",
"is_verified",
"is_subscribed",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
(_("important dates"), {"fields": ("last_login", "date_joined")}),
(_("additional info"), {"fields": ("language", "attributes")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2"),
},
),
)
list_display = ("email", "phone_number", "is_verified", "is_active", "is_staff")
search_fields = ("email", "phone_number")
list_filter = (
"is_verified",
"is_active",
"is_staff",
"is_superuser",
"is_subscribed",
)
ordering = ("email",)
readonly_fields = ("password",)
form = UserForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm
def get_queryset(self, request: HttpRequest) -> QuerySet[User]:
qs = super().get_queryset(request)
return qs.prefetch_related(
"groups", "payments_balance", "orders"
).prefetch_related(
Prefetch(
"user_permissions",
queryset=Permission.objects.select_related("content_type"),
)
)
def save_model( # ty: ignore[invalid-method-override]
self, request: HttpRequest, obj: Any, form: UserForm, change: Any
) -> None:
if form.cleaned_data.get("attributes") is None:
obj.attributes = None
if (
form.cleaned_data.get("is_superuser", False)
and not request.user.is_superuser # ty: ignore[possibly-missing-attribute]
):
raise PermissionDenied(_("You cannot jump over your head!"))
super().save_model(request, obj, form, change) # ty: ignore[invalid-argument-type]
# noinspection PyUnusedLocal
@register(ChatThread)
class ChatThreadAdmin(ModelAdmin):
list_display = (
"uuid",
"user",
"email",
"assigned_to",
"status",
"last_message_at",
"is_active",
"created",
"modified",
)
list_filter = (
"status",
"is_active",
("assigned_to", admin.EmptyFieldListFilter),
)
search_fields = ("uuid", "email", "user__email", "user__username")
autocomplete_fields = ("user", "assigned_to")
actions = (
"close_threads",
"open_threads",
"delete_selected",
)
readonly_fields = ("created", "modified")
@admin.action(description=_("Close selected threads"))
def close_threads(self, request, queryset):
queryset.update(status=ThreadStatus.CLOSED)
@admin.action(description=_("Open selected threads"))
def open_threads(self, request, queryset):
queryset.update(status=ThreadStatus.OPEN)
@register(ChatMessage)
class ChatMessageAdmin(admin.ModelAdmin):
list_display = ("uuid", "thread", "sender_type", "sender_user", "sent_at")
list_filter = ("sender_type",)
search_fields = ("uuid", "thread__uuid", "sender_user__email")
autocomplete_fields = ("thread", "sender_user")
readonly_fields = ("created", "modified")
@register(EmailImage)
class EmailImageAdmin(ActivationActionsMixin, ModelAdmin):
list_display = ("name", "image_preview", "alt_text", "is_active", "created")
list_filter = ("is_active",)
search_fields = ("name", "alt_text")
readonly_fields = ("uuid", "created", "modified", "image_preview_large")
fieldsets = (
(None, {"fields": ("name", "image", "alt_text")}),
(_("Preview"), {"fields": ("image_preview_large",)}),
(_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}),
)
@admin.display(description=_("preview"))
def image_preview(self, obj: EmailImage) -> str:
if obj.image:
return format_html(
'<img src="{}" style="max-height: 50px; max-width: 100px;" />',
obj.image.url,
)
return "-"
@admin.display(description=_("image preview"))
def image_preview_large(self, obj: EmailImage) -> str:
if obj.image:
return format_html(
'<img src="{}" style="max-height: 300px; max-width: 500px;" />',
obj.image.url,
)
return "-"
@register(EmailTemplate)
class EmailTemplateAdmin(ActivationActionsMixin, ModelAdmin):
list_display = ("name", "slug", "subject", "is_active", "modified")
list_filter = ("is_active",)
search_fields = ("name", "slug", "subject")
prepopulated_fields = {"slug": ("name",)}
readonly_fields = ("uuid", "created", "modified")
fieldsets = (
(None, {"fields": ("name", "slug", "subject")}),
(_("Content"), {"fields": ("html_content", "plain_content")}),
(
_("Documentation"),
{
"fields": ("available_variables",),
"classes": ("collapse",),
},
),
(_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}),
)
def formfield_for_dbfield(self, db_field, request, **kwargs):
if db_field.name == "html_content":
kwargs["widget"] = WysiwygWidget
return super().formfield_for_dbfield(db_field, request, **kwargs)
class CampaignRecipientInline(TabularInline):
model = CampaignRecipient
extra = 0
readonly_fields = (
"user",
"status",
"sent_at",
"opened_at",
"clicked_at",
"error_message",
)
can_delete = False
max_num = 0 # Prevent adding new recipients via inline
def has_add_permission(self, request, obj=None):
return False
@register(EmailCampaign)
class EmailCampaignAdmin(ActivationActionsMixin, ModelAdmin):
list_display = (
"name",
"template",
"status",
"scheduled_at",
"sent_at",
"stats_display",
"is_active",
)
list_filter = ("status", "is_active", "template")
search_fields = ("name",)
readonly_fields = (
"uuid",
"created",
"modified",
"sent_at",
"total_recipients",
"sent_count",
"failed_count",
"opened_count",
"clicked_count",
)
autocomplete_fields = ("template",)
inlines = (CampaignRecipientInline,)
actions = [
"delete_selected",
"activate_selected",
"deactivate_selected",
"prepare_recipients",
"send_campaign_now",
"cancel_campaign",
]
fieldsets = (
(None, {"fields": ("name", "template", "status")}),
(_("Scheduling"), {"fields": ("scheduled_at", "sent_at")}),
(
_("Statistics"),
{
"fields": (
"total_recipients",
"sent_count",
"failed_count",
"opened_count",
"clicked_count",
)
},
),
(_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}),
)
@admin.display(description=_("stats"))
def stats_display(self, obj: EmailCampaign) -> str:
return (
f"{obj.sent_count}/{obj.total_recipients} sent, {obj.opened_count} opened"
)
@admin.action(description=_("Prepare recipients (subscribed users)"))
def prepare_recipients(
self, request: HttpRequest, queryset: QuerySet[EmailCampaign]
) -> None:
for campaign in queryset:
if campaign.status != CampaignStatus.DRAFT:
self.message_user(
request,
_("Campaign '{}' is not in draft status.").format(campaign.name),
level=messages.WARNING,
)
continue
prepare_campaign_recipients.delay(str(campaign.uuid))
self.message_user(
request,
_("Preparing recipients for campaign '{}'.").format(campaign.name),
level=messages.SUCCESS,
)
@admin.action(description=_("Send campaign now"))
def send_campaign_now(
self, request: HttpRequest, queryset: QuerySet[EmailCampaign]
) -> None:
for campaign in queryset:
if campaign.status not in (CampaignStatus.DRAFT, CampaignStatus.SCHEDULED):
self.message_user(
request,
_("Campaign '{}' cannot be sent (status: {}).").format(
campaign.name, campaign.status
),
level=messages.WARNING,
)
continue
if campaign.total_recipients == 0:
self.message_user(
request,
_(
"Campaign '{}' has no recipients. Prepare recipients first."
).format(campaign.name),
level=messages.WARNING,
)
continue
campaign.status = CampaignStatus.SENDING
campaign.save(update_fields=["status", "modified"])
send_campaign_email_batch.delay(str(campaign.uuid))
self.message_user(
request,
_("Started sending campaign '{}'.").format(campaign.name),
level=messages.SUCCESS,
)
@admin.action(description=_("Cancel campaign"))
def cancel_campaign(
self, request: HttpRequest, queryset: QuerySet[EmailCampaign]
) -> None:
updated = queryset.filter(
status__in=(CampaignStatus.DRAFT, CampaignStatus.SCHEDULED)
).update(status=CampaignStatus.CANCELLED, modified=timezone.now())
self.message_user(
request,
_("{} campaign(s) cancelled.").format(updated),
level=messages.SUCCESS,
)
@register(CampaignRecipient)
class CampaignRecipientAdmin(ModelAdmin):
list_display = (
"campaign",
"user",
"status",
"sent_at",
"opened_at",
"clicked_at",
)
list_filter = ("status", "campaign")
search_fields = ("user__email", "campaign__name")
readonly_fields = (
"uuid",
"campaign",
"user",
"tracking_id",
"created",
"modified",
)
autocomplete_fields = ("campaign", "user")
def has_add_permission(self, request):
return False # Recipients are added via campaign preparation
class GroupAdmin(BaseGroupAdmin, ModelAdmin):
pass
class BlacklistedTokenAdmin(BaseBlacklistedTokenAdmin, ModelAdmin):
pass
class OutstandingTokenAdmin(BaseOutstandingTokenAdmin, ModelAdmin):
pass
admin.site.register(User, UserAdmin)
admin.site.unregister(BaseGroup)
admin.site.register(Group, GroupAdmin)
admin.site.unregister(BaseBlacklistedToken)
admin.site.register(BlacklistedToken, BlacklistedTokenAdmin)
admin.site.unregister(BaseOutstandingToken)
admin.site.register(OutstandingToken, OutstandingTokenAdmin)