Skip to content

Commit 31f4097

Browse files
Merge pull request #245 from RSE-Sheffield/feat/243-mass-email
Send invite email to multiple recipients
2 parents 252eaa9 + b14ec07 commit 31f4097

File tree

8 files changed

+49
-32
lines changed

8 files changed

+49
-32
lines changed

SORT/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def cast_to_boolean(obj: Any) -> bool:
6565
"django_extensions",
6666
"debug_toolbar",
6767
"qr_code",
68+
"crispy_forms",
69+
"crispy_bootstrap5",
6870
# apps created by FA:
6971
"home",
7072
"survey",
@@ -254,3 +256,8 @@ def cast_to_boolean(obj: Any) -> bool:
254256
"Midwives": "sort_only_config_midwives.json",
255257
"NMAHPs": "sort_only_config_nmahps.json",
256258
}
259+
260+
# Crispy enables Bootstrap styling on Django forms
261+
# https://django-crispy-forms.readthedocs.io/en/latest/install.html
262+
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
263+
CRISPY_TEMPLATE_PACK = "bootstrap5"

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ pytest==8.3.4
3131
pytest-django==4.9.0
3232
django-qr-code==4.1.0
3333
factory-boy==3.3.3
34+
django-crispy-forms==2.4
35+
crispy-bootstrap5==2025.4

survey/forms.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@
33
from django.forms import BaseFormSet, formset_factory
44
from strenum import StrEnum
55

6+
from .validators.email_list_validator import EmailListValidator
7+
68

79
class InvitationForm(forms.Form):
8-
email = forms.EmailField(
9-
label="Participant Email",
10-
max_length=100,
10+
email = forms.CharField(
11+
label="Participant Emails",
12+
help_text="Please enter a list of email addresses",
1113
required=True,
12-
validators=[EmailValidator()],
14+
validators=[EmailListValidator()],
15+
widget=forms.Textarea(attrs=dict(rows=6)),
16+
)
17+
message = forms.CharField(
18+
label="Message",
19+
help_text="(Optional) Additional message for the participants",
20+
required=False,
21+
widget=forms.Textarea(attrs=dict(rows=3)),
1322
)
1423

1524

survey/templates/invitations/send_invitation.html

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,17 @@
11
{% extends 'base_manager.html' %}
2-
2+
{% load crispy_forms_tags %}
33
{% block content %}
44
<div class="container mx-auto px-4 py-8 mt-4">
55
<div>
66
<h1>Invitations</h1>
77
<p class="subtitle">Use the form below to send out invitations to your survey participants.</p>
88
</div>
9-
10-
119
<div class="card custom-card shadow-sm my-4">
1210
<div class="card-body">
13-
1411
<form method="post">
1512
{% csrf_token %}
16-
17-
<div class="mb-3">
18-
<label for="id_email" class="form-label">Email Address</label>
19-
<input type="email" name="email" id="id_email" class="form-control"
20-
placeholder="Enter email address" required>
21-
<small class="form-text text-muted">Enter the participant's email address.</small>
22-
</div>
23-
24-
<div class="mb-3">
25-
<label for="id_message" class="form-label">Message (Optional)</label>
26-
<textarea name="message" id="id_message" class="form-control" rows="3"
27-
placeholder="Write your message here..."></textarea>
28-
<small class="form-text text-muted">Add any additional messages.</small>
29-
</div>
30-
31-
<div class="text-center">
32-
<button type="submit" class="btn btn-primary" style="background-color: #6933AD; border: none;">
33-
Send
34-
</button>
35-
</div>
13+
{{ form | crispy }}
14+
<button type="submit" class="btn btn-primary">Send</button>
3615
</form>
3716
</div>
3817

survey/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,5 @@
9898
views.SurveyLinkInvalidView.as_view(),
9999
name="survey_link_invalid",
100100
),
101-
path("invite/<int:pk>", views.InvitationView.as_view(), name="invite"),
101+
path("survey/<int:pk>/invite", views.InvitationView.as_view(), name="invite"),
102102
]

survey/validators/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.core.validators import EmailValidator
2+
3+
4+
class EmailListValidator(EmailValidator):
5+
"""
6+
Validate a sequence of email addresses.
7+
"""
8+
message = "Enter valid email addresses."
9+
10+
@classmethod
11+
def clean(cls, value):
12+
"""
13+
Separate strings by commas or whitespace.
14+
"""
15+
# Replace commas with whitespace
16+
return [email.strip() for email in value.replace(",", " ").lower().split()]
17+
18+
def __call__(self, value):
19+
for email in self.clean(value):
20+
super().__call__(email)

survey/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ class InvitationView(FormView):
529529
form_class = InvitationForm
530530

531531
def form_valid(self, form):
532-
email = form.cleaned_data["email"]
532+
recipient_list = tuple(form.cleaned_data["email"].replace(",", " ").split())
533533
message = form.data["message"]
534534
survey = Survey.objects.get(pk=self.kwargs["pk"])
535535
# Generate the survey link with the token
@@ -541,12 +541,12 @@ def form_valid(self, form):
541541
subject="Your SORT Survey Invitation",
542542
message=f"Click here to start the SORT survey:\n{survey_link}\n\n{message}",
543543
from_email=settings.DEFAULT_FROM_EMAIL,
544-
recipient_list=[email],
544+
recipient_list=recipient_list,
545545
fail_silently=False,
546546
)
547547

548548
# Show success message
549-
messages.success(self.request, f"Invitation sent to {email}.")
549+
messages.success(self.request, f"Invitation sent to {len(recipient_list)} recipients.")
550550
return super().form_valid(form)
551551

552552
def get_success_url(self):

0 commit comments

Comments
 (0)