225 lines
8.1 KiB
Python
225 lines
8.1 KiB
Python
from datetime import timedelta
|
|
from decimal import Decimal
|
|
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 import timezone
|
|
from django.utils.html import format_html
|
|
from django.utils.safestring import mark_safe
|
|
import anymail.exceptions
|
|
import django.core.mail
|
|
|
|
import looper.admin.mixins
|
|
import looper.models
|
|
import looper.taxes
|
|
|
|
from blog.models import Post
|
|
from common.queries import get_latest_trainings_and_production_lessons
|
|
from emails.models import Email
|
|
from emails.util import get_template_context, absolute_url
|
|
from subscriptions.tasks import _construct_subscription_mail
|
|
|
|
logger = logging.getLogger(__name__)
|
|
User = get_user_model()
|
|
iframe_template = Template(
|
|
'<iframe sandbox="allow-same-origin" style="min-width: 50vw;" srcdoc="{{ body|safe }}"'
|
|
'onload="this.style.height=(this.contentWindow.document.body.scrollHeight)+\'px\';">'
|
|
'</iframe>'
|
|
)
|
|
|
|
|
|
@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})
|
|
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', 'message', 'date_sent']
|
|
actions = ['send']
|
|
search_fields = ['to', 'subject']
|
|
|
|
def send(self, request, queryset):
|
|
"""Custom action for sending an email."""
|
|
for email in queryset:
|
|
try:
|
|
email.send()
|
|
msg = f'Message "{email.subject}" sent to {email.to}'
|
|
self.message_user(request, msg, messages.SUCCESS)
|
|
except anymail.exceptions.AnymailRequestsAPIError as e:
|
|
self.message_user(request, str(e), messages.ERROR)
|
|
|
|
send.short_description = "Send selected emails"
|
|
|
|
fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
'fields': (
|
|
('subject', 'to'),
|
|
('from_email', 'reply_to'),
|
|
('base_html_template', 'date_sent'),
|
|
)
|
|
},
|
|
),
|
|
('Message preview', {'fields': ('rendered_html', 'message')}),
|
|
('Message Source', {'fields': ('html_message',)}),
|
|
)
|
|
|
|
|
|
class SubscriptionEmailPreview(Email):
|
|
class Meta:
|
|
proxy = True
|
|
managed = False
|
|
|
|
def render_html(self):
|
|
"""These emails already have their HTML rendered."""
|
|
return self.html_message
|
|
|
|
|
|
@admin.register(SubscriptionEmailPreview)
|
|
class SubscriptionEmailPreviewAdmin(looper.admin.mixins.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):
|
|
"""Construct the Email on th fly from known subscription email templates."""
|
|
user = User(full_name='Jane Doe')
|
|
customer = looper.models.Customer(user=user)
|
|
user.customer = customer
|
|
now = timezone.now()
|
|
subscription = looper.models.Subscription(
|
|
id=1234567890,
|
|
customer=user.customer,
|
|
payment_method=looper.models.PaymentMethod(
|
|
method_type='cc',
|
|
gateway_id=1,
|
|
recognisable_name='Fake Credit Card payment method',
|
|
),
|
|
price=1000,
|
|
tax=160,
|
|
tax_type=looper.taxes.TaxType.VAT_CHARGE.value,
|
|
tax_rate=Decimal(19),
|
|
next_payment=now + timedelta(days=10),
|
|
)
|
|
order = looper.models.Order(subscription=subscription)
|
|
admin_url = absolute_url(
|
|
'admin:looper_subscription_change',
|
|
kwargs={'object_id': subscription.id},
|
|
)
|
|
context = {
|
|
'user': user,
|
|
'subscription': subscription,
|
|
'order': order,
|
|
# Also add context for the expired email
|
|
'latest_trainings': get_latest_trainings_and_production_lessons(),
|
|
'latest_posts': Post.objects.filter(is_published=True)[:5],
|
|
'admin_url': admin_url,
|
|
**get_template_context(),
|
|
}
|
|
mail_name = object_id
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
return SubscriptionEmailPreview(
|
|
id=mail_name,
|
|
subject=subject,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
reply_to=settings.DEFAULT_FROM_EMAIL,
|
|
html_message=email_body_html,
|
|
message=email_body_txt,
|
|
)
|
|
|
|
def _get_emails_list(self, request):
|
|
emails = []
|
|
for mail_name in (
|
|
'bank_transfer_required',
|
|
'subscription_activated',
|
|
'subscription_deactivated',
|
|
'payment_soft-failed',
|
|
'payment_paid',
|
|
'payment_failed',
|
|
'managed_notification',
|
|
'subscription_expired',
|
|
):
|
|
emails.append(self.get_object(request, object_id=mail_name))
|
|
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."""
|
|
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=[obj.to],
|
|
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
|