From ac3c84b8641a98e8d09b7a87d8faa06e32214a83 Mon Sep 17 00:00:00 2001
From: James Meakin <12661555+jmsmkn@users.noreply.github.com>
Date: Tue, 14 Jan 2025 08:19:37 +0100
Subject: [PATCH] Preview emails using the full page editor and standard email
rendering (#3778)
Adds the full page markdown editor for emails along with iframed
previews using the standard email rendering. As there are differences in
email clients and browsers a test email still needs to be sent out, but
at least the previews are isolated from the GC CSS/JS environment.
Closes #3682
---
app/grandchallenge/emails/forms.py | 24 ++-
.../emails/migrations/0001_initial.py | 7 +-
app/grandchallenge/emails/models.py | 25 +++-
.../js/emails/email_markdown_preview.mjs | 11 ++
.../templates/emails/email_body_update.html | 17 +++
.../emails/templates/emails/email_detail.html | 26 ++--
.../emails/templates/emails/email_form.html | 13 +-
.../email_full_page_markdown_widget.html | 15 ++
.../emails/templates/emails/email_list.html | 3 +-
.../emails/email_rendered_detail.html | 1 +
app/grandchallenge/emails/urls.py | 18 ++-
app/grandchallenge/emails/views.py | 67 +++++++--
app/grandchallenge/emails/widgets.py | 23 +++
app/tests/emails_tests/test_views.py | 139 ++++++++++++++++--
14 files changed, 333 insertions(+), 56 deletions(-)
create mode 100644 app/grandchallenge/emails/static/js/emails/email_markdown_preview.mjs
create mode 100644 app/grandchallenge/emails/templates/emails/email_body_update.html
create mode 100644 app/grandchallenge/emails/templates/emails/email_full_page_markdown_widget.html
create mode 100644 app/grandchallenge/emails/templates/emails/email_rendered_detail.html
create mode 100644 app/grandchallenge/emails/widgets.py
diff --git a/app/grandchallenge/emails/forms.py b/app/grandchallenge/emails/forms.py
index 9cfead375d..258d4d8e7d 100644
--- a/app/grandchallenge/emails/forms.py
+++ b/app/grandchallenge/emails/forms.py
@@ -1,15 +1,27 @@
from django import forms
from grandchallenge.core.forms import SaveFormInitMixin
-from grandchallenge.core.widgets import MarkdownEditorInlineWidget
from grandchallenge.emails.models import Email
+from grandchallenge.emails.widgets import MarkdownEditorEmailFullPageWidget
+from grandchallenge.subdomains.utils import reverse
-class EmailForm(SaveFormInitMixin, forms.ModelForm):
+class EmailMetadataForm(SaveFormInitMixin, forms.ModelForm):
class Meta:
model = Email
- fields = (
- "subject",
- "body",
+ fields = ("subject",)
+
+
+class EmailBodyForm(SaveFormInitMixin, forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.fields["body"].widget = MarkdownEditorEmailFullPageWidget(
+ preview_url=reverse(
+ "emails:rendered-detail", kwargs={"pk": self.instance.pk}
+ )
)
- widgets = {"body": MarkdownEditorInlineWidget}
+
+ class Meta:
+ model = Email
+ fields = ("body",)
diff --git a/app/grandchallenge/emails/migrations/0001_initial.py b/app/grandchallenge/emails/migrations/0001_initial.py
index 4a794e02fe..6642156694 100644
--- a/app/grandchallenge/emails/migrations/0001_initial.py
+++ b/app/grandchallenge/emails/migrations/0001_initial.py
@@ -23,12 +23,7 @@ class Migration(migrations.Migration):
),
),
("subject", models.CharField(max_length=1024)),
- (
- "body",
- models.TextField(
- help_text="Email body will be prepended with 'Dear [username],' and will end with 'Kind regards, Grand Challenge team' and a link to unsubscribe from the mailing list."
- ),
- ),
+ ("body", models.TextField()),
("sent", models.BooleanField(default=False)),
("sent_at", models.DateTimeField(blank=True, null=True)),
(
diff --git a/app/grandchallenge/emails/models.py b/app/grandchallenge/emails/models.py
index ad46dab767..b4de8b9311 100644
--- a/app/grandchallenge/emails/models.py
+++ b/app/grandchallenge/emails/models.py
@@ -1,15 +1,17 @@
+from django.contrib.sites.models import Site
from django.db import models
from django.urls import reverse
+from guardian.utils import get_anonymous_user
from grandchallenge.core.models import UUIDModel
+from grandchallenge.emails.emails import create_email_object
+from grandchallenge.profiles.models import EmailSubscriptionTypes
class Email(models.Model):
subject = models.CharField(max_length=1024)
- body = models.TextField(
- help_text="Email body will be prepended with 'Dear [username],' and will end with 'Kind regards, Grand Challenge team' and a link to unsubscribe from the mailing list."
- )
+ body = models.TextField()
sent = models.BooleanField(default=False)
sent_at = models.DateTimeField(blank=True, null=True)
status_report = models.JSONField(
@@ -25,6 +27,23 @@ class Meta:
def __str__(self):
return self.subject
+ @property
+ def rendered_body(self):
+ email = create_email_object(
+ recipient=get_anonymous_user(),
+ site=Site.objects.get_current(),
+ subject=self.subject,
+ markdown_message=self.body,
+ subscription_type=EmailSubscriptionTypes.SYSTEM,
+ connection=None,
+ )
+ alternatives = [
+ alternative
+ for alternative in email.alternatives
+ if alternative[1] == "text/html"
+ ]
+ return alternatives[0][0]
+
def get_absolute_url(self):
return reverse("emails:detail", kwargs={"pk": self.pk})
diff --git a/app/grandchallenge/emails/static/js/emails/email_markdown_preview.mjs b/app/grandchallenge/emails/static/js/emails/email_markdown_preview.mjs
new file mode 100644
index 0000000000..e3ad51072a
--- /dev/null
+++ b/app/grandchallenge/emails/static/js/emails/email_markdown_preview.mjs
@@ -0,0 +1,11 @@
+document.addEventListener("DOMContentLoaded", event => {
+ const iframes = document.querySelectorAll(".markdownx-preview");
+ for (const iframe of iframes) {
+ const observer = new MutationObserver(() => {
+ iframe.srcdoc = iframe.innerHTML;
+ });
+ observer.observe(iframe, {
+ childList: true,
+ });
+ }
+});
diff --git a/app/grandchallenge/emails/templates/emails/email_body_update.html b/app/grandchallenge/emails/templates/emails/email_body_update.html
new file mode 100644
index 0000000000..53039e6d7f
--- /dev/null
+++ b/app/grandchallenge/emails/templates/emails/email_body_update.html
@@ -0,0 +1,17 @@
+{% extends "emails/email_form.html" %}
+{% load crispy_forms_tags %}
+
+{% block container %}container-fluid{% endblock %}
+
+{% block content %}
+
+
Sent on {{ object.sent_at|date:"j N Y" }}
{% endif %}Dear user,
- {{ object.body|md2email_html }} -— Your Grand Challenge Team
-