Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email sender feature #3414

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions website/thaliawebsite/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""Special forms."""
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
from django.utils.translation import gettext_lazy as _

from utils.snippets import send_email


class AuthenticationForm(BaseAuthenticationForm):
Expand All @@ -10,3 +15,87 @@ def clean(self):
if "username" in self.cleaned_data:
self.cleaned_data["username"] = self.cleaned_data["username"].lower()
super().clean()


class EmailSenderForm(forms.Form):
recipients = forms.CharField(
label=_("Recipients"),
help_text=_("Enter multiple email addresses separated by commas."),
required=True,
)
cc_recipients = forms.CharField(
label=_("CC recipients (optional)"),
help_text=_("Enter multiple email addresses separated by commas."),
required=False,
)
bcc_recipients = forms.CharField(
label=_("BCC recipients (optional)"),
help_text=_(
"Enter multiple email addresses separated by commas. Emails will always be sent to %s as well."
)
% settings.DEFAULT_FROM_EMAIL,
required=False,
)

reply_to = forms.CharField(
label=_("Reply to-addresses (optional)"),
help_text=_(
"Enter multiple email addresses separated by commas. Will be set as reply-to email address. "
"Emails will always be sent from %s."
)
% settings.DEFAULT_FROM_EMAIL,
required=False,
)

subject = forms.CharField(label=_("Subject"))
message = forms.CharField(
label=_("Message body"),
help_text=_("Supports or simple HTML (but be careful with that)."),
widget=forms.Textarea,
)

def clean_recipients(self):
return self.email_address_cleaner("recipients")

def clean_bcc_recipients(self):
return self.email_address_cleaner("bcc_recipients")

def clean_cc_recipients(self):
return self.email_address_cleaner("cc_recipients")

def clean_reply_to(self):
return self.email_address_cleaner("reply_to")

def email_address_cleaner(self, field_name):
recipients = self.cleaned_data[field_name].split(",")
recipients = [r.strip() for r in recipients]

if recipients == [""]:
return None

# validate that all recipients are valid email addresses
for recipient in recipients:
if not forms.EmailField().clean(recipient):
raise forms.ValidationError(
_("Invalid email address: %(email)s"), params={"email": recipient}
)

return recipients

def send(self):
txt_template = "email/email_sender_email.txt"
html_template = "email/email_sender_email.html"

bcc = self.cleaned_data["bcc_recipients"] or []
bcc += settings.DEFAULT_FROM_EMAIL

send_email(
to=self.cleaned_data["recipients"],
subject=self.cleaned_data["subject"],
txt_template=txt_template,
context={"message": self.cleaned_data["message"]},
html_template=html_template,
bcc=bcc,
cc=self.cleaned_data["cc_recipients"],
reply_to=self.cleaned_data["reply_to"],
)
33 changes: 33 additions & 0 deletions website/thaliawebsite/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.1 on 2023-10-02 18:47

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="FunctionalPermissions",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
],
options={
"permissions": (
("email_sender", "Send emails using the email sender"),
),
"managed": False,
"default_permissions": (),
},
),
]
13 changes: 13 additions & 0 deletions website/thaliawebsite/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.db import models


class FunctionalPermissions(models.Model):
"""Custom auxiliary model to define functional non-model permissions."""

class Meta:
managed = False # Don't create a table for this model
default_permissions = () # disable "add", "change", "delete" and "view" default permissions
permissions = (("email_sender", "Send emails using the email sender"),)

def __str__(self):
return "Functional permissions"
8 changes: 8 additions & 0 deletions website/thaliawebsite/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@
{% trans "Site administration" %}
</a>
</li>
{% if perms.thaliawebsite.email_sender %}
<li>
<a class="dropdown-item"
href="{% url 'email-sender' %}">
{% trans "Email sender" %}
</a>
</li>
{% endif %}
{% endif %}
<li>
<a class="dropdown-item"
Expand Down
12 changes: 12 additions & 0 deletions website/thaliawebsite/templates/email/email_sender_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "email/html_email.html" %}

{% block content %}
{{ message|safe }}
{% endblock %}
{% block regards %}
With kind regards,
<br><br>
Study Association Thalia
<br>
{% endblock %}
{% block disclaimer %}{% endblock %}
4 changes: 4 additions & 0 deletions website/thaliawebsite/templates/email/email_sender_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{ message|striptags }}

With kind regards,
Study Association Thalia
2 changes: 2 additions & 0 deletions website/thaliawebsite/templates/email/html_email.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
{% endblock %}
</div>
</div>
{% block disclaimer %}
<p style="text-align: center;margin: 20px;"><em>This email was automatically generated.</em></p>
{% endblock %}
</body>
</html>
17 changes: 17 additions & 0 deletions website/thaliawebsite/templates/email_sender.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "small_page.html" %}
{% load i18n static django_bootstrap5 alert %}

{% block title %}{% trans "Send email" %} — {{ block.super }}{% endblock %}
{% block opengraph_title %}{% trans "Send email" %} — {{ block.super }}{% endblock %}

{% block page_title %}{% trans "Send email" %}{% endblock %}

{% block page_content %}
<form method="post" class="col-12">
{% csrf_token %}
{% for field in form %}
{% bootstrap_field field %}
{% endfor %}
<input type="submit" value="{% trans 'Send email' %}" class="btn btn-primary float-end"/>
</form>
{% endblock %}
2 changes: 2 additions & 0 deletions website/thaliawebsite/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from thabloid.sitemaps import sitemap as thabloid_sitemap
from thaliawebsite.forms import AuthenticationForm
from thaliawebsite.views import (
EmailSenderView,
IndexView,
RateLimitedLoginView,
RateLimitedPasswordResetView,
Expand Down Expand Up @@ -160,6 +161,7 @@
]
),
),
path("send-email/", EmailSenderView.as_view(), name="email-sender"),
# Apps
path("", include("singlepages.urls")),
path("", include("merchandise.urls")),
Expand Down
25 changes: 23 additions & 2 deletions website/thaliawebsite/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
"""General views for the website."""

from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.views import LoginView, PasswordResetView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic import ListView, TemplateView
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, ListView, TemplateView
from django.views.generic.base import View

from django_ratelimit.decorators import ratelimit

from thaliawebsite.forms import EmailSenderForm


class IndexView(TemplateView):
template_name = "index.html"
Expand Down Expand Up @@ -85,3 +90,19 @@ def admin_unauthorized_view(request):
raise PermissionDenied("You are not allowed to access the administration page.")
else:
return redirect(request.GET.get("next", "/"))


@method_decorator(staff_member_required, name="dispatch")
@method_decorator(
permission_required("thaliawebsite.email_sender"),
name="dispatch",
)
class EmailSenderView(FormView):
template_name = "email_sender.html"
form_class = EmailSenderForm
success_url = reverse_lazy("send-email")

def form_valid(self, form):
form.send()
messages.success(self.request, _("Email sent successfully."))
return super().form_valid(form)
4 changes: 4 additions & 0 deletions website/utils/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ def send_email(
context: dict,
html_template: Optional[str] = None,
bcc: Optional[list[str]] = None,
cc: Optional[list[str]] = None,
reply_to: Optional[list[str]] = None,
from_email: Optional[str] = None,
connection=None,
) -> None:
Expand All @@ -221,6 +223,8 @@ def send_email(
body=txt_message,
to=to,
bcc=bcc,
cc=cc,
reply_to=reply_to,
from_email=from_email,
connection=connection,
)
Expand Down