diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 7069286d..47ac59df 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -4,45 +4,61 @@ from django.template.loader import get_template from bookwyrm import models from bookwyrm.tasks import app +from bookwyrm.settings import DOMAIN + + +def email_data(): + """ fields every email needs """ + site = models.SiteSettings.objects.get() + if site.logo_small: + logo_path = "/images/{}".format(site.logo_small.url) + else: + logo_path = "/static/images/logo-small.png" + + return { + "site_name": site.name, + "logo": logo_path, + "domain": DOMAIN, + "user": None, + } def invite_email(invite_request): """ send out an invite code """ - site = models.SiteSettings.objects.get() - data = { - "site_name": site.name, - "invite_link": invite_request.invite.link, - } - send_email.delay(invite_request.email, "invite", data) + data = email_data() + data["invite_link"] = invite_request.invite.link + send_email.delay(invite_request.email, *format_email("invite", data)) def password_reset_email(reset_code): """ generate a password reset email """ - site = models.SiteSettings.objects.get() - data = { - "site_name": site.name, - "reset_link": reset_code.link, - } - send_email.delay(reset_code.user.email, "password_reset", data) + data = email_data() + data["reset_link"] = reset_code.link + data["user"] = reset_code.user.display_name + send_email.delay(reset_code.user.email, *format_email("password_reset", data)) -@app.task -def send_email(recipient, message_name, data): - """ use a task to send the email """ +def format_email(email_name, data): + """ render the email templates """ subject = ( - get_template("email/{}/subject.html".format(message_name)).render(data).strip() + get_template("email/{}/subject.html".format(email_name)).render(data).strip() ) html_content = ( - get_template("email/{}/html_content.html".format(message_name)) + get_template("email/{}/html_content.html".format(email_name)) .render(data) .strip() ) text_content = ( - get_template("email/{}/text_content.html".format(message_name)) + get_template("email/{}/text_content.html".format(email_name)) .render(data) .strip() ) + return (subject, html_content, text_content) + +@app.task +def send_email(recipient, subject, html_content, text_content): + """ use a task to send the email """ email = EmailMultiAlternatives(subject, text_content, None, [recipient]) email.attach_alternative(html_content, "text/html") email.send() diff --git a/bookwyrm/static/images/logo-small.png b/bookwyrm/static/images/logo-small.png index 10ea7a38..72f49ef7 100644 Binary files a/bookwyrm/static/images/logo-small.png and b/bookwyrm/static/images/logo-small.png differ diff --git a/bookwyrm/templates/email/html_layout.html b/bookwyrm/templates/email/html_layout.html new file mode 100644 index 00000000..02527ff5 --- /dev/null +++ b/bookwyrm/templates/email/html_layout.html @@ -0,0 +1,26 @@ +{% load i18n %} +
+
+
+ logo +
+
+ {{ site_name }}
+ {{ domain }}
+
+
+ +
+

+ {% if user %}{{ user }},{% else %}{% trans "Hi there," %}{% endif %} +

+ {% block content %}{% endblock %} +
+ +
+

{% blocktrans %}BookWyrm hosted on {{ site_name }}{% endblocktrans %}

+ {% if user %} +

{% trans "Email preference" %}

+ {% endif %} +
+
diff --git a/bookwyrm/templates/email/invite/html_content.html b/bookwyrm/templates/email/invite/html_content.html index 672d3407..358e23dc 100644 --- a/bookwyrm/templates/email/invite/html_content.html +++ b/bookwyrm/templates/email/invite/html_content.html @@ -1,2 +1,17 @@ +{% extends 'email/html_layout.html' %} {% load i18n %} -{% blocktrans %}Join {{ site_name }}{% endblocktrans %} + +{% block content %} +

+ {% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %} +

+ +{% trans "Join Now" as text %} +{% include 'email/snippets/action.html' with path=invite_link text=text %} + +

+ {% url 'code-of-conduct' as coc_path %} + {% url 'about' as about_path %} + {% blocktrans %}Learn more about this instance.{% endblocktrans %} +

+{% endblock %} diff --git a/bookwyrm/templates/email/invite/subject.html b/bookwyrm/templates/email/invite/subject.html index b3c4a141..efb8be5a 100644 --- a/bookwyrm/templates/email/invite/subject.html +++ b/bookwyrm/templates/email/invite/subject.html @@ -1,2 +1,2 @@ {% load i18n %} -{% blocktrans %}You're invited! Join {{ site_name }}{% endblocktrans %} +{% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %} diff --git a/bookwyrm/templates/email/invite/text_content.html b/bookwyrm/templates/email/invite/text_content.html index 7add9010..c3fcdc04 100644 --- a/bookwyrm/templates/email/invite/text_content.html +++ b/bookwyrm/templates/email/invite/text_content.html @@ -1,2 +1,10 @@ +{% extends 'email/text_layout.html' %} {% load i18n %} -{% blocktrans %}Join {{ site_name }}: {{ invite_link }}{% endblocktrans %} +{% block content %} +{% blocktrans %}You're invited to join {{ site_name }}! Click the link below to create an account.{% endblocktrans %} + +{{ invite_link }} + +{% trans "Learn more about this instance:" %} https://{{ domain }}{% url 'about' %} + +{% endblock %} diff --git a/bookwyrm/templates/email/password_reset/html_content.html b/bookwyrm/templates/email/password_reset/html_content.html index e3a006b0..eef0e5e5 100644 --- a/bookwyrm/templates/email/password_reset/html_content.html +++ b/bookwyrm/templates/email/password_reset/html_content.html @@ -1,2 +1,15 @@ +{% extends 'email/html_layout.html' %} {% load i18n %} -{% blocktrans %}Your password reset link is: {{ reset_link }}{% endblocktrans %} + +{% block content %} +

+ {% blocktrans %}You requested to reset your {{ site_name }} password. Click the link below to set a new password and log in to your account.{% endblocktrans %} +

+ +{% trans "Reset Password" as text %} +{% include 'email/snippets/action.html' with path=reset_link text=text %} + +

+ {% trans "If you didn't request to reset your password, you can ignore this email." %} +

+{% endblock %} diff --git a/bookwyrm/templates/email/password_reset/subject.html b/bookwyrm/templates/email/password_reset/subject.html index b216a6c2..88680140 100644 --- a/bookwyrm/templates/email/password_reset/subject.html +++ b/bookwyrm/templates/email/password_reset/subject.html @@ -1,2 +1,2 @@ {% load i18n %} -{% blocktrans %}Reset your password on {{ site_name }}{% endblocktrans %} +{% blocktrans %}Reset your {{ site_name }} password{% endblocktrans %} diff --git a/bookwyrm/templates/email/password_reset/text_content.html b/bookwyrm/templates/email/password_reset/text_content.html index e3a006b0..b5cf754e 100644 --- a/bookwyrm/templates/email/password_reset/text_content.html +++ b/bookwyrm/templates/email/password_reset/text_content.html @@ -1,2 +1,9 @@ +{% extends 'email/text_layout.html' %} {% load i18n %} -{% blocktrans %}Your password reset link is: {{ reset_link }}{% endblocktrans %} +{% block content %} +{% blocktrans %}You requested to reset your {{ site_name }} password. Click the link below to set a new password and log in to your account.{% endblocktrans %} + +{{ reset_link }} + +{% trans "If you didn't request to reset your password, you can ignore this email." %} +{% endblock %} diff --git a/bookwyrm/templates/email/preview.html b/bookwyrm/templates/email/preview.html new file mode 100644 index 00000000..66d856c0 --- /dev/null +++ b/bookwyrm/templates/email/preview.html @@ -0,0 +1,19 @@ + + +
+ Subject: {% include subject_path %} +
+
+ + Html email: +
+ {% include html_content_path %} +
+
+ + Text email: +
+ {% include text_content_path %} +
+ + diff --git a/bookwyrm/templates/email/snippets/action.html b/bookwyrm/templates/email/snippets/action.html new file mode 100644 index 00000000..56feb9ef --- /dev/null +++ b/bookwyrm/templates/email/snippets/action.html @@ -0,0 +1,5 @@ +

+ + {{ text }} + +

diff --git a/bookwyrm/templates/email/text_layout.html b/bookwyrm/templates/email/text_layout.html new file mode 100644 index 00000000..cd0444f1 --- /dev/null +++ b/bookwyrm/templates/email/text_layout.html @@ -0,0 +1,3 @@ +{% load i18n %} +{% if user %}{{ user.display_name }},{% else %}{% trans "Hi there," %}{% endif %} +{% block content %}{% endblock %} diff --git a/bookwyrm/templates/password_reset_request.html b/bookwyrm/templates/password_reset_request.html index 97191afa..5d877442 100644 --- a/bookwyrm/templates/password_reset_request.html +++ b/bookwyrm/templates/password_reset_request.html @@ -8,7 +8,9 @@

{% trans "Reset Password" %}

- {% if message %}

{{ message }}

{% endif %} + + {% if message %}

{{ message }}

{% endif %} +

{% trans "A link to reset your password will be sent to your email address" %}

{% csrf_token %} @@ -16,6 +18,9 @@
+ {% if error %} +

{{ error }}

+ {% endif %}
diff --git a/bookwyrm/tests/test_emailing.py b/bookwyrm/tests/test_emailing.py new file mode 100644 index 00000000..5d7d4894 --- /dev/null +++ b/bookwyrm/tests/test_emailing.py @@ -0,0 +1,51 @@ +""" test creating emails """ +from unittest.mock import patch + +from django.test import TestCase +from django.test.client import RequestFactory +import responses + +from bookwyrm import emailing, models + + +@patch("bookwyrm.emailing.send_email.delay") +class Emailing(TestCase): + """ every response to a get request, html or json """ + + def setUp(self): + """ we need basic test data and mocks """ + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + models.SiteSettings.objects.create() + + def test_invite_email(self, email_mock): + """ load the invite email """ + invite_request = models.InviteRequest.objects.create( + email="test@email.com", + invite=models.SiteInvite.objects.create(user=self.local_user), + ) + + emailing.invite_email(invite_request) + + self.assertEqual(email_mock.call_count, 1) + args = email_mock.call_args[0] + self.assertEqual(args[0], "test@email.com") + self.assertEqual(args[1], "You're invited to join BookWyrm!") + self.assertEqual(len(args), 4) + + def test_password_reset_email(self, email_mock): + """ load the password reset email """ + reset = models.PasswordReset.objects.create(user=self.local_user) + emailing.password_reset_email(reset) + + self.assertEqual(email_mock.call_count, 1) + args = email_mock.call_args[0] + self.assertEqual(args[0], "mouse@mouse.mouse") + self.assertEqual(args[1], "Reset your BookWyrm password") + self.assertEqual(len(args), 4) diff --git a/bookwyrm/tests/test_goodreads_import.py b/bookwyrm/tests/test_goodreads_import.py index 080ccd15..a62cfdd2 100644 --- a/bookwyrm/tests/test_goodreads_import.py +++ b/bookwyrm/tests/test_goodreads_import.py @@ -9,7 +9,6 @@ import responses from bookwyrm import models, importer from bookwyrm.goodreads_import import GoodreadsImporter -from bookwyrm import importer from bookwyrm.settings import DOMAIN @@ -17,8 +16,8 @@ class GoodreadsImport(TestCase): """ importing from goodreads csv """ def setUp(self): - self.importer = GoodreadsImporter() """ use a test csv """ + self.importer = GoodreadsImporter() datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") self.csv = open(datafile, "r", encoding=self.importer.encoding) self.user = models.User.objects.create_user( diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index f67f5538..53a9bcbd 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -42,7 +42,8 @@ class PasswordViews(TestCase): request = self.factory.post("", {"email": "aa@bb.ccc"}) view = views.PasswordResetRequest.as_view() resp = view(request) - self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.status_code, 200) + resp.render() request = self.factory.post("", {"email": "mouse@mouse.com"}) with patch("bookwyrm.emailing.send_email.delay"): diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 30c1bba4..4aced6fe 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -48,6 +48,11 @@ urlpatterns = [ ), # admin re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"), + re_path( + r"^settings/email-preview", + views.site.email_preview, + name="settings-email-preview", + ), re_path( r"^settings/federation", views.Federation.as_view(), name="settings-federation" ), @@ -87,8 +92,8 @@ urlpatterns = [ ), re_path(r"^report/?$", views.make_report, name="report"), # landing pages - re_path(r"^about/?$", views.About.as_view()), - path("", views.Home.as_view()), + re_path(r"^about/?$", views.About.as_view(), name="about"), + path("", views.Home.as_view(), name="landing"), re_path(r"^discover/?$", views.Discover.as_view()), re_path(r"^notifications/?$", views.Notifications.as_view()), # feeds diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index e853d16b..2926b9d7 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -5,6 +5,7 @@ from django.core.exceptions import PermissionDenied from django.shortcuts import redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ from django.views import View from bookwyrm import models @@ -28,7 +29,8 @@ class PasswordResetRequest(View): try: user = models.User.objects.get(email=email) except models.User.DoesNotExist: - return redirect("/password-reset") + data = {"error": _("No user with that email address was found.")} + return TemplateResponse(request, "password_reset_request.html", data) # remove any existing password reset cods for this user models.PasswordReset.objects.filter(user=user).all().delete() @@ -36,7 +38,7 @@ class PasswordResetRequest(View): # create a new reset code code = models.PasswordReset.objects.create(user=user) password_reset_email(code) - data = {"message": "Password reset link sent to %s" % email} + data = {"message": _("A password reset link sent to %s" % email)} return TemplateResponse(request, "password_reset_request.html", data) diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py index ce64e6e0..e5897660 100644 --- a/bookwyrm/views/site.py +++ b/bookwyrm/views/site.py @@ -5,7 +5,7 @@ from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View -from bookwyrm import forms, models +from bookwyrm import emailing, forms, models # pylint: disable= no-self-use @@ -33,3 +33,17 @@ class Site(View): form.save() return redirect("settings-site") + + +@login_required +@permission_required("bookwyrm.edit_instance_settings", raise_exception=True) +def email_preview(request): + """ for development, renders and example email template """ + template = request.GET.get("email") + data = emailing.email_data() + data["subject_path"] = "email/{}/subject.html".format(template) + data["html_content_path"] = "email/{}/html_content.html".format(template) + data["text_content_path"] = "email/{}/text_content.html".format(template) + data["reset_link"] = "https://example.com/link" + data["invite_link"] = "https://example.com/link" + return TemplateResponse(request, "email/preview.html", data)