187 lines
6.9 KiB
Python
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
|