Skip to content

Commit

Permalink
Add support for (ticket) email blasts (#749)
Browse files Browse the repository at this point in the history
* Support email blasts to ticket holders

* Add tests

* Fix permissioning and add 400 response

* Address nits
  • Loading branch information
aviupadhyayula authored Nov 25, 2024
1 parent e0fc84f commit 56b9394
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
7 changes: 6 additions & 1 deletion backend/clubs/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,12 @@ def has_object_permission(self, request, view, obj):

if not old_type == FAIR_TYPE and new_type == FAIR_TYPE:
return False
elif view.action in ["buyers", "create_tickets", "issue_tickets"]:
elif view.action in [
"buyers",
"create_tickets",
"issue_tickets",
"email_blast",
]:
if not request.user.is_authenticated:
return False
membership = find_membership_helper(request.user, obj.club)
Expand Down
87 changes: 87 additions & 0 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3039,6 +3039,93 @@ def issue_tickets(self, request, *args, **kwargs):
{"success": True, "detail": f"Issued {len(tickets)} tickets", "errors": []}
)

@action(detail=True, methods=["post"])
def email_blast(self, request, *args, **kwargs):
"""
Send an email blast to all users holding tickets.
---
requestBody:
content:
application/json:
schema:
type: object
properties:
content:
type: string
description: The content of the email blast to send
required:
- content
responses:
"200":
description: Email blast was sent successfully
content:
application/json:
schema:
type: object
properties:
detail:
type: string
description: A message indicating how many
recipients received the blast
"400":
description: Content field was empty or missing
content:
application/json:
schema:
type: object
properties:
detail:
type: string
description: Error message indicating content
was not provided
"404":
description: Event not found
content:
application/json:
schema:
type: object
properties:
detail:
type: string
description: Error message indicating event was
not found
---
"""
event = self.get_object()

holder_emails = Ticket.objects.filter(
event=event, owner__isnull=False
).values_list("owner__email", flat=True)
officer_emails = event.club.get_officer_emails()
emails = list(holder_emails) + list(officer_emails)

content = request.data.get("content", "").strip()
if not content:
return Response(
{"detail": "Content must be specified"},
status=status.HTTP_400_BAD_REQUEST,
)

send_mail_helper(
name="blast",
subject=f"Update on {event.name} from {event.club.name}",
emails=emails,
context={
"sender": event.club.name,
"content": request.data.get("content"),
"reply_emails": event.club.get_officer_emails(),
},
)

return Response(
{
"detail": (
f"Blast sent to {len(holder_emails)} ticket holders "
f"and {len(officer_emails)} officers"
)
}
)

@action(detail=True, methods=["post"])
def upload(self, request, *args, **kwargs):
"""
Expand Down
25 changes: 25 additions & 0 deletions backend/templates/emails/blast.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!-- TYPES:
sender:
type: string
content:
type: string
reply_emails:
type: array
items:
type: string
-->
{% extends 'emails/base.html' %}

{% block content %}
<h2>Message from {{ sender }}</h2>

<p style="font-size: 1.2em">
The following message was sent by <b>{{ sender }}</b>:
</p>

<p style="font-size: 1.2em; padding: 8px; border-left: 5px solid #ccc; white-space: pre-wrap;">{{ content }}</p>

<p style="font-size: 1.2em">
If you have any questions, please respond to this email or <a href="mailto:{% for email in reply_emails %}{% if not forloop.first %},{% endif %}{{ email }}{% endfor %}">contact the sender</a>.
</p>
{% endblock %}
38 changes: 38 additions & 0 deletions backend/tests/clubs/test_ticketing.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,44 @@ def test_issue_tickets_insufficient_quantity(self):
Ticket.objects.filter(type="normal", holder__isnull=False).count(), 0
)

def test_email_blast(self):
Membership.objects.create(
person=self.user1, club=self.club1, role=Membership.ROLE_OFFICER
)
self.client.login(username=self.user1.username, password="test")

ticket1 = self.tickets1[0]
ticket1.owner = self.user2
ticket1.save()

resp = self.client.post(
reverse("club-events-email-blast", args=(self.club1.code, self.event1.pk)),
{"content": "Test email blast content"},
format="json",
)

self.assertEqual(resp.status_code, 200, resp.content)

self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]

self.assertIn(self.user2.email, email.to)
self.assertIn(self.user1.email, email.to)

self.assertEqual(
email.subject, f"Update on {self.event1.name} from {self.club1.name}"
)
self.assertIn("Test email blast content", email.body)

def test_email_blast_empty_content(self):
self.client.login(username=self.user1.username, password="test")
resp = self.client.post(
reverse("club-events-email-blast", args=(self.club1.code, self.event1.pk)),
{"content": ""},
format="json",
)
self.assertEqual(resp.status_code, 400, resp.content)

def test_get_tickets_information_no_tickets(self):
# Delete all the tickets
Ticket.objects.all().delete()
Expand Down

0 comments on commit 56b9394

Please sign in to comment.