From 41324467cfcfc60bb5195e327a1bcd34f9015c98 Mon Sep 17 00:00:00 2001 From: Rust Saiargaliev Date: Wed, 14 Aug 2024 16:40:45 +0200 Subject: [PATCH] Add dashboard with emails preview --- emark/conf.py | 1 + emark/message.py | 18 ++++++++ emark/templates/emark/dashboard.html | 17 ++++++++ emark/templates/emark/preview.html | 10 +++++ emark/urls.py | 2 + emark/utils.py | 7 ++++ emark/views.py | 63 +++++++++++++++++++++++++++- 7 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 emark/templates/emark/dashboard.html create mode 100644 emark/templates/emark/preview.html diff --git a/emark/conf.py b/emark/conf.py index a0f8f6e..eb4f243 100644 --- a/emark/conf.py +++ b/emark/conf.py @@ -10,6 +10,7 @@ def get_settings(): { "UTM_PARAMS": {"utm_source": "website", "utm_medium": "email"}, "DOMAIN": None, + "DASHBOARD_HIDDEN_CLASSES": [], **getattr(settings, "EMARK", {}), }, ) diff --git a/emark/message.py b/emark/message.py index 3ca2329..27f70ac 100644 --- a/emark/message.py +++ b/emark/message.py @@ -253,3 +253,21 @@ def render(self, tracking_uuid=None): ) self.body = self.get_body(self.html) self.attach_alternative(self.html, "text/html") + + @classmethod + def render_preview(cls): + """Return a preview of the email.""" + + markdown_string = loader.get_template(cls.template).template.source + context = {} + html_message = markdown.markdown( + markdown_string, + extensions=[ + "markdown.extensions.meta", + "markdown.extensions.tables", + "markdown.extensions.extra", + ], + ) + context["markdown_string"] = mark_safe(html_message) + template = loader.get_template(cls.base_html_template) + return template.render(context) diff --git a/emark/templates/emark/dashboard.html b/emark/templates/emark/dashboard.html new file mode 100644 index 0000000..fd21611 --- /dev/null +++ b/emark/templates/emark/dashboard.html @@ -0,0 +1,17 @@ +

+ Dashboard +

+{% regroup emails by app_label as emails_by_app_label %} +{% for app_label in emails_by_app_label %} +

+ {{ app_label.grouper }} +

+ +{% endfor %} diff --git a/emark/templates/emark/preview.html b/emark/templates/emark/preview.html new file mode 100644 index 0000000..18da9f0 --- /dev/null +++ b/emark/templates/emark/preview.html @@ -0,0 +1,10 @@ +
+    Dashboard > {{ email_name }} {% if email_doc %}-- {{ email_doc }}{% endif %} +
+ +{{ email_preview|safe }} diff --git a/emark/urls.py b/emark/urls.py index eabc463..c2f6767 100644 --- a/emark/urls.py +++ b/emark/urls.py @@ -7,4 +7,6 @@ path("/", views.EmailDetailView.as_view(), name="email-detail"), path("/click", views.EmailClickView.as_view(), name="email-click"), path("/open", views.EmailOpenView.as_view(), name="email-open"), + path("dashboard/", views.DashboardView.as_view(), name="email-dashboard"), + path("dashboard//", views.EmailPreviewView.as_view(), name="email-preview"), ] diff --git a/emark/utils.py b/emark/utils.py index 2fd8f91..d5ebca1 100644 --- a/emark/utils.py +++ b/emark/utils.py @@ -122,3 +122,10 @@ def __str__(self) -> str: # sanitize all wide vertical or horizontal spaces text = self.DOUBLE_NEWLINE.sub("\n\n", text.strip()) return self.DOUBLE_SPACE.sub(" ", text) + + +def get_subclasses(cls): + """Return all subclasses of a class, without the base classes.""" + return set(cls.__subclasses__()).union( + [s for c in cls.__subclasses__() for s in get_subclasses(c)] + ) diff --git a/emark/views.py b/emark/views.py index 942ea32..f8eabbb 100644 --- a/emark/views.py +++ b/emark/views.py @@ -2,11 +2,15 @@ from django import http from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.http import Http404 from django.http.request import split_domain_port, validate_host +from django.urls import reverse from django.views import View +from django.views.generic import TemplateView from django.views.generic.detail import SingleObjectMixin -from . import models +from . import conf, message, models, utils # white 1x1 pixel JPEG in bytes: # @@ -93,3 +97,60 @@ def get(self, request, *args, **kwargs): "Cache-Control": "no-cache, no-store, must-revalidate", }, ) + + +class DashboardView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): + """Show a dashboard of available email classes.""" + + template_name = "emark/dashboard.html" + + def test_func(self): + return self.request.user.is_staff + + def get_emails(self): + hidden_classes = [message.MarkdownEmail.__name__, *conf.get_settings().DASHBOARD_HIDDEN_CLASSES] + emails = [ + { + "app_label": email_class.__module__.split(".")[0], + "class_name": email_class.__name__, + "doc": email_class.__doc__ or "", + "detail_url": reverse("emark:email-preview", args=[email_class.__name__]), + } + for email_class in utils.get_subclasses(message.MarkdownEmail) + if email_class.__name__ not in hidden_classes + ] + return sorted(emails, key=lambda email: (email["app_label"], email["class_name"])) + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "emails": self.get_emails(), + } + + +class EmailPreviewView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): + """Render a preview of the email.""" + + template_name = "emark/preview.html" + + def test_func(self): + return self.request.user.is_staff + + def dispatch(self, request, *args, **kwargs): + self.email_class = self.get_email_class(kwargs["email_class"]) + if not self.email_class: + raise Http404() + return super().dispatch(request, *args, **kwargs) + + def get_email_class(self, email_class): + for cl in utils.get_subclasses(message.MarkdownEmail): + if cl.__name__ == email_class: + return cl + return None + + def get_context_data(self, **kwargs): + email = self.email_class + return super().get_context_data(**kwargs) | { + "email_preview": email.render_preview(), + "email_name": email.__name__, + "email_doc": email.__doc__ + }