feat: replace email flows with in-app notifications
Add recipient notification detail and filtering, a queued admin composer, and contextual automatic titles. Retire email delivery, obsolete auth routes, newsletter data, and related templates and settings.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
from django import forms
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
from simplemde.widgets import SimpleMDEEditor
|
||||
|
||||
from apps.users.models import User, University, Major
|
||||
from apps.users.resources import UserResource
|
||||
from apps.users.tasks import send_verification_email
|
||||
from apps.notifications.tasks import send_manual_notification
|
||||
from core.admin import SoftDeleteListFilter, BaseModelAdmin
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ class UserAdminForm(forms.ModelForm):
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ManualNotificationForm(forms.Form):
|
||||
title = forms.CharField(max_length=200, label="عنوان")
|
||||
digest = forms.CharField(max_length=500, label="توضیح کوتاه", widget=forms.Textarea(attrs={"rows": 3}))
|
||||
body_markdown = forms.CharField(required=False, label="متن کامل (Markdown)", widget=SimpleMDEEditor())
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin, BaseModelAdmin, ImportExportModelAdmin):
|
||||
form = UserAdminForm
|
||||
@@ -41,16 +47,10 @@ class UserAdmin(BaseUserAdmin, BaseModelAdmin, ImportExportModelAdmin):
|
||||
}),
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
|
||||
('Email Verification', {
|
||||
'fields': ('is_email_verified', 'email_verification_token', 'email_verification_sent_at')
|
||||
}),
|
||||
('Email identity', {'fields': ('is_email_verified',)}),
|
||||
('Mobile Verification', {
|
||||
'fields': ('is_mobile_verified',)
|
||||
}),
|
||||
('Password Reset', {
|
||||
'fields': ('password_reset_token', 'password_reset_token_expires_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Soft Delete', {
|
||||
'fields': ('is_deleted', 'deleted_at'),
|
||||
'classes': ('collapse',)
|
||||
@@ -66,50 +66,44 @@ class UserAdmin(BaseUserAdmin, BaseModelAdmin, ImportExportModelAdmin):
|
||||
),
|
||||
)
|
||||
|
||||
readonly_fields = ('email_verification_token', 'email_verification_sent_at', 'deleted_at',
|
||||
'password_reset_token', 'password_reset_token_expires_at')
|
||||
readonly_fields = ('deleted_at',)
|
||||
|
||||
actions = BaseModelAdmin.actions + [
|
||||
'verify_emails',
|
||||
'resend_verification_email',
|
||||
]
|
||||
|
||||
@admin.action(description='Verify selected user emails')
|
||||
def verify_emails(self, request, queryset):
|
||||
queryset.update(is_email_verified=True)
|
||||
self.message_user(request, f'Verified {queryset.count()} user emails.')
|
||||
actions = BaseModelAdmin.actions + ['send_notification']
|
||||
|
||||
@admin.action(description="Resend verification email")
|
||||
def resend_verification_email(self, request, queryset):
|
||||
qs = queryset.filter(is_email_verified=False).exclude(email__isnull=True).exclude(email="")
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if not request.user.has_perm('users.send_notification'):
|
||||
actions.pop('send_notification', None)
|
||||
return actions
|
||||
|
||||
total = queryset.count()
|
||||
to_send = qs.count()
|
||||
skipped = total - to_send
|
||||
sent = failed = 0
|
||||
|
||||
for user in qs:
|
||||
try:
|
||||
user.regenerate_verification_token()
|
||||
user.email_verification_sent_at = timezone.now()
|
||||
user.save(update_fields=["email_verification_sent_at"])
|
||||
|
||||
verification_url = f"{settings.FRONTEND_ROOT}verify-email/{user.email_verification_token}"
|
||||
send_verification_email.delay(user.id, verification_url)
|
||||
sent += 1
|
||||
except Exception as exc:
|
||||
failed += 1
|
||||
|
||||
if sent:
|
||||
self.message_user(request, f"ایمیل تأیید برای {sent} کاربر ارسال شد.", level=messages.SUCCESS)
|
||||
if skipped:
|
||||
self.message_user(
|
||||
request,
|
||||
f"{skipped} کاربر کنار گذاشته شدند (یا قبلاً تأیید شدهاند یا ایمیل ندارند).",
|
||||
level=messages.WARNING,
|
||||
@admin.action(description="Send in-app notification to selected users")
|
||||
def send_notification(self, request, queryset):
|
||||
if not request.user.has_perm('users.send_notification'):
|
||||
self.message_user(request, "اجازه ارسال اعلان ندارید.", level=messages.ERROR)
|
||||
return None
|
||||
form = ManualNotificationForm(request.POST if 'confirm_send' in request.POST else None)
|
||||
user_ids = list(queryset.filter(is_active=True, is_deleted=False).values_list('pk', flat=True))
|
||||
if not user_ids:
|
||||
self.message_user(request, "کاربر فعال برای ارسال انتخاب نشده است.", level=messages.WARNING)
|
||||
return None
|
||||
if 'confirm_send' in request.POST and form.is_valid() and user_ids:
|
||||
send_manual_notification.delay(
|
||||
user_ids,
|
||||
form.cleaned_data['title'],
|
||||
form.cleaned_data['digest'],
|
||||
form.cleaned_data['body_markdown'] or None,
|
||||
)
|
||||
if failed:
|
||||
self.message_user(request, f"ارسال برای {failed} کاربر با خطا مواجه شد.", level=messages.ERROR)
|
||||
self.message_user(request, f"ارسال اعلان برای {len(user_ids)} کاربر در صف قرار گرفت.")
|
||||
return None
|
||||
return TemplateResponse(request, 'admin/users/send_notification.html', {
|
||||
**self.admin_site.each_context(request),
|
||||
'title': 'ارسال اعلان',
|
||||
'opts': self.model._meta,
|
||||
'form': form,
|
||||
'selected_ids': user_ids,
|
||||
'selected_count': len(user_ids),
|
||||
'action_checkbox_name': ACTION_CHECKBOX_NAME,
|
||||
})
|
||||
|
||||
|
||||
@admin.register(University)
|
||||
|
||||
Reference in New Issue
Block a user