extensions-website/emails/admin.py

187 lines
6.9 KiB
Python

import logging
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.auth import get_user_model
from django.template import Template, Context
from django.utils.html import format_html
from django.utils.safestring import mark_safe
import django.core.mail
from emails.models import Email
from emails.util import construct_email
logger = logging.getLogger(__name__)
User = get_user_model()
class NoAddDeleteMixin:
"""Disallow adding and deleting objects via the admin."""
def has_add_permission(self, *args, **kwargs):
"""Disallow adding new objects via the admin."""
return False
def has_delete_permission(self, *args, **kwargs):
"""Disallow removing objects via the admin."""
return False
@admin.register(Email)
class EmailAdmin(admin.ModelAdmin):
class Media:
css = {'all': (f'{settings.STATIC_URL}/emails/admin/email.css',)}
def rendered_html(self, obj) -> str:
"""Preview the HTML version of the email."""
# Escape " and & to avoid breaking srcdoc. Order of escaping is important.
body = obj.render_html().replace('&', '&').replace('"', '"')
context = Context({'body': body})
iframe_template = Template(
'<iframe sandbox style="width: 100%; height: 100vh;" srcdoc="{{ body|safe }}"></iframe>'
)
rendered: str = iframe_template.render(context)
return mark_safe(rendered)
rendered_html.allow_tags = True
rendered_html.short_description = "Preview"
def was_sent(self, obj):
"""Display yes/no icon sent status."""
return obj.date_sent is not None
was_sent.boolean = True
list_display = ['subject', 'from_email', 'to', 'was_sent']
list_filter = ['reply_to', 'base_html_template', 'date_sent']
readonly_fields = ['rendered_html', 'date_sent']
actions = ['send']
search_fields = ['to', 'subject']
def send(self, request, queryset):
"""Send an email. This is a custom admin action."""
for email in queryset:
try:
email.send()
msg = f'Message "{email.subject}" sent to {email.to}'
self.message_user(request, msg, messages.SUCCESS)
except Exception as e:
self.message_user(request, str(e), messages.ERROR)
send.short_description = "Send selected emails"
class EmailPreview(Email):
class Meta:
proxy = True
managed = False
def render_html(self):
"""Return already rendered HTML of the email."""
return self.html_message
@admin.register(EmailPreview)
class EmailPreviewAdmin(NoAddDeleteMixin, EmailAdmin):
save_as_continue = True
save_on_top = True
list_display = ['subject', 'from_email']
actions = []
def _get_email_sent_message(self, obj):
return f'Sent a test email "{obj.subject}" to {obj.to} from {obj.from_email}'
def get_object(self, request, object_id, from_field=None, fake_context=None):
"""Construct the Email on the fly from known email templates."""
if not fake_context:
fake_context = self._get_emails_with_fake_context(request)
context = fake_context[object_id]
mail_name = context.get('template', object_id)
email_body_html, email_body_txt, subject = construct_email(mail_name, context)
return EmailPreview(
id=object_id,
subject=subject,
from_email=settings.DEFAULT_FROM_EMAIL,
reply_to=settings.DEFAULT_REPLY_TO_EMAIL,
html_message=email_body_html,
message=email_body_txt,
)
def _get_emails_with_fake_context(self, request):
email_with_fake_context = {'feedback': {}}
if settings.DEBUG:
from common.tests.factories.notifications import construct_fake_notifications
fake_notifications = construct_fake_notifications()
for fake_notification in fake_notifications:
mail_name = fake_notification.original_template_name
email_with_fake_context[mail_name] = {
'template': fake_notification.template_name,
**fake_notification.get_template_context(),
}
return email_with_fake_context
def _get_emails_list(self, request):
emails = []
fake_context = self._get_emails_with_fake_context(request)
for mail_name in fake_context:
emails.append(self.get_object(request, object_id=mail_name, fake_context=fake_context))
return emails
def _changeform_view(self, request, object_id, form_url, extra_context):
"""Change title of the change view to make it clear it's not actually changing anything."""
template_name = object_id.replace('_5F', '_')
extra_context = {
**(extra_context or {}),
'title': format_html(
f'Previewing email rendered from the "<b>{template_name}</b>" template.'
'<br/>You can adjust the content of the email before sending it, '
'but these changes will not be persisted.'
),
}
return super()._changeform_view(request, object_id, form_url, extra_context=extra_context)
def save_model(self, request, obj, form, change):
"""Send an email instead, display a notification about it."""
for recipient in obj.to.split(','):
django.core.mail.send_mail(
obj.subject,
message=obj.message,
html_message=obj.html_message,
from_email=obj.from_email, # just use the configured default From-address.
recipient_list=[recipient.strip()],
fail_silently=False,
)
def construct_change_message(self, request, form, formsets, add=False):
"""Log the fact that an email was sent."""
obj = form.instance
return self._get_email_sent_message(obj)
def response_change(self, request, obj):
"""Override notification messages on change. Notify about successfully sent test email."""
response = super().response_change(request, obj)
# Clear the usual change messages: nothing was actually changed/saved, so they are confusing
list(messages.get_messages(request))
message = self._get_email_sent_message(obj)
self.message_user(request, message, messages.INFO)
return response
def get_changelist_instance(self, request):
"""Monkey-patch changelist replacing the queryset with the existing transactional emails."""
try:
cl = super().get_changelist_instance(request)
emails = self._get_emails_list(request)
cl.result_list = emails
cl.result_count = len(emails)
cl.can_show_all = True
cl.multi_page = False
cl.title = 'Select an email to preview it'
return cl
except Exception as e:
logger.exception(e)
raise