Skip to content

Commit

Permalink
Add a dashboard with email previews (#92)
Browse files Browse the repository at this point in the history
Introducing a way to access a dashboard with all available email classes
used in the project.
---------

Co-authored-by: Johannes Maron <[email protected]>
  • Loading branch information
amureki and codingjoe authored Sep 27, 2024
1 parent d4503dc commit 62d8b87
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 2 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,53 @@ EMAIL_BACKEND = "emark.backends.ConsoleEmailBackend"

The `ConsoleEmailBackend` will only print the plain text version of the email.

### Email Dashboard

Django eMark comes with a simple email dashboard to preview your templates.

To enable the dashboard, add the app to your `INSTALLED_APPS` setting

```python
# settings.py
INSTALLED_APPS = [
# ...
"emark",
"emark.contrib.dashboard", # needs to be added before Django's admin app
# ...
"django.contrib.admin", # required for the dashboard
# ...
]
```

and add the following to your `urls.py`:

```python
# urls.py
from django.urls import include, path


urlpatterns = [
# … other urls
path("emark/", include([
path("", include("emark.urls")),
path("dashboard/", include("emark.contrib.dashboard.urls")),
])),
]
```

Next you need to register the email classes you want to preview in the dashboard:

```python
# myapp/emails.py
from emark.message import MarkdownEmail
from emark.contrib import dashboard

@dashboard.register
class MyMessage(MarkdownEmail):
subject = "Hello World"
template_name = "myapp/email.md"
```

## Credits

- Django eMark uses modified version of [Responsive HTML Email Template](https://github.com/leemunroe/responsive-html-email-template/) as a base template
Expand Down
Empty file added emark/contrib/__init__.py
Empty file.
27 changes: 27 additions & 0 deletions emark/contrib/dashboard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""This module is used to register the email classes for the emark dashboard.
The following is an example of how to use this module:
```python
from emark.messages import MarkdownEmail
from emark.contrib import dashboard
@dashboard.register
class MyEmail(MarkdownEmail):
template = "my_app/emails/my_email.md"
```
"""

from emark.message import MarkdownEmail

_registry: dict[str, type[MarkdownEmail]] = {}


__all__ = ["register"]


def register(cls: type[MarkdownEmail], key: str = None) -> type[MarkdownEmail]:
"""Register a MarkdownEmail with the registry."""
_registry[key or f"{cls.__module__}{cls.__qualname__}"] = cls
return cls
6 changes: 6 additions & 0 deletions emark/contrib/dashboard/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class EmarkDashboardAppConfig(AppConfig):
name = "emark.contrib.dashboard"
label = "emark_dashboard"
9 changes: 9 additions & 0 deletions emark/contrib/dashboard/templates/admin/base_site.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "admin/base_site.html" %}
{% block branding %}
{{ block.super }}
{% url 'emark-dashboard:dashboard' as dashboard_url %}
{% if request.path != dashboard_url %}
<a href="{% url 'emark-dashboard:dashboard' %}"
style="align-self: center">eMark</a>
{% endif %}
{% endblock %}
16 changes: 16 additions & 0 deletions emark/contrib/dashboard/templates/emark/dashboard/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "admin/base_site.html" %}
{% block content %}
<h2>eMark Dashboard</h2>
{% regroup emails by app_label as emails_by_app_label %}
{% for app_label in emails_by_app_label %}
<h3>{{ app_label.grouper }}</h3>
<ul>
{% for email in app_label.list %}
<li>
<a href="{{ email.detail_url }}">{{ email.name }}</a>
{% if email.doc %}-- {{ email.doc }}{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}
18 changes: 18 additions & 0 deletions emark/contrib/dashboard/templates/emark/dashboard/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo;
<a href="{% url 'emark-dashboard:dashboard' %}">{{ title }}</a>
&rsaquo; {{ subtitle }}
</div>
{% endblock %}
{% block content %}
<div id="preview"></div>
<script>
const iFrame = document.getElementById('preview');
const shadow = iFrame.attachShadow({ mode: "open" });
shadow.innerHTML = `{{ email.preview|safe }}`;
</script>
{% endblock %}
22 changes: 22 additions & 0 deletions emark/contrib/dashboard/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.urls import include, path

from . import views

app_name = "emark-dashboard"

urlpatterns = [
path("", staff_member_required(views.DashboardView.as_view()), name="dashboard"),
path(
"<str:email_class>/",
include(
[
path(
"preview",
staff_member_required(views.EmailPreviewView.as_view()),
name="email-preview",
)
]
),
),
]
58 changes: 58 additions & 0 deletions emark/contrib/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.http import Http404
from django.urls import reverse
from django.views.generic import TemplateView

from ...message import MarkdownEmail
from . import _registry


def serialize_email_class(email_class: type[MarkdownEmail], path: str) -> {str: str}:
return {
"app_label": email_class.__module__.split(".")[0],
"class": email_class,
"name": email_class.__name__,
"doc": email_class.__doc__ or "",
"detail_url": reverse("emark-dashboard:email-preview", args=[path]),
"preview": email_class.render_preview,
}


class DashboardView(TemplateView):
"""Show a dashboard of registered email classes."""

template_name = "emark/dashboard/dashboard.html"

def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"site_header": "eMark",
"site_title": "eMark",
"title": "Dashboard",
"emails": [
serialize_email_class(klass, path) for path, klass in _registry.items()
],
}


class EmailPreviewView(TemplateView):
"""Render a preview of a single email template."""

template_name = "emark/dashboard/preview.html"

def get(self, request, *args, **kwargs):
key = kwargs["email_class"]
try:
self.email_class = _registry[key]
except KeyError as e:
raise Http404() from e
return super().get(request, *args, **kwargs)

def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"site_header": "eMark",
"site_title": "eMark",
"title": "Dashboard",
"subtitle": self.email_class.__name__,
"email": serialize_email_class(
self.email_class, self.kwargs["email_class"]
),
}
17 changes: 17 additions & 0 deletions emark/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,20 @@ 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) # noqa: S308
template = loader.get_template(cls.base_html_template)
return template.render(context)
Empty file.
Empty file.
87 changes: 87 additions & 0 deletions tests/testapp/contrib/dashboard/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import pytest
from django.http import Http404
from emark.contrib.dashboard import _registry, views
from emark.message import MarkdownEmail


class MarkdownEmailTest(MarkdownEmail):
template = "template.md"


def test_serialize_email_class():
assert views.serialize_email_class(email_class=MarkdownEmailTest, path="path") == {
"app_label": "tests",
"class": MarkdownEmailTest,
"detail_url": "/emark/dashboard/path/preview",
"doc": "",
"name": "MarkdownEmailTest",
"preview": MarkdownEmailTest.render_preview,
}


class TestDashboardView:

def test_get_context_data(self, rf):
request = rf.get("/emark/dashboard/")
view = views.DashboardView()
view.setup(request)
context = view.get_context_data()
assert context == {
"site_header": "eMark",
"site_title": "eMark",
"title": "Dashboard",
"emails": [],
"view": view,
}

def test_render(self, admin_client):
response = admin_client.get("/emark/dashboard/")
assert response.status_code == 200


class TestEmailPreviewView:

def test_get__404(self, rf):
request = rf.get("/emark/dashboard/path/preview")
view = views.EmailPreviewView()
with pytest.raises(Http404):
view.dispatch(request, email_class="path")

def test_get(self, rf):
view = views.EmailPreviewView()
view.request = rf.get("/emark/dashboard/tests.MarkdownEmailTest/preview")
view.kwargs = {"email_class": "tests.MarkdownEmailTest"}
_registry["MarkdownEmailTest"] = MarkdownEmailTest
view.get(view.request, email_class="MarkdownEmailTest")
assert view.email_class == MarkdownEmailTest
del _registry["MarkdownEmailTest"]

def test_get_context_data(self):
view = views.EmailPreviewView()
view.kwargs = {"email_class": "MarkdownEmailTest"}
view.email_class = MarkdownEmailTest
context = view.get_context_data()
_registry["MarkdownEmailTest"] = MarkdownEmailTest
assert context == {
"view": view,
"site_header": "eMark",
"site_title": "eMark",
"title": "Dashboard",
"subtitle": "MarkdownEmailTest",
"email": {
"app_label": "tests",
"class": MarkdownEmailTest,
"name": "MarkdownEmailTest",
"doc": "",
"detail_url": "/emark/dashboard/MarkdownEmailTest/preview",
"preview": MarkdownEmailTest.render_preview,
},
}

del _registry["MarkdownEmailTest"]

def test_render(self, admin_client):
_registry["MarkdownEmailTest"] = MarkdownEmailTest
response = admin_client.get("/emark/dashboard/MarkdownEmailTest/preview")
assert response.status_code == 200
del _registry["MarkdownEmailTest"]
3 changes: 2 additions & 1 deletion tests/testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@
# Application definition

INSTALLED_APPS = [
"emark",
"emark.contrib.dashboard",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
"emark",
"tests.testapp",
]

Expand Down
10 changes: 9 additions & 1 deletion tests/testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
from django.urls import include, path

urlpatterns = [
path("emark/", include("emark.urls")),
path(
"emark/",
include(
[
path("", include("emark.urls")),
path("dashboard/", include("emark.contrib.dashboard.urls")),
]
),
),
path("admin/", admin.site.urls),
]

0 comments on commit 62d8b87

Please sign in to comment.