From c8310fe4a6d86018648f8e70d39381cef989b1c8 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sat, 23 Nov 2024 00:53:36 -0500 Subject: [PATCH 1/4] Support email blasts to ticket holders --- backend/clubs/views.py | 69 +++++++++++++++++++++++++++++ backend/templates/emails/blast.html | 25 +++++++++++ 2 files changed, 94 insertions(+) create mode 100644 backend/templates/emails/blast.html diff --git a/backend/clubs/views.py b/backend/clubs/views.py index b425af8f1..9d148ed7a 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -3039,6 +3039,75 @@ 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 + "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) + + 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): """ diff --git a/backend/templates/emails/blast.html b/backend/templates/emails/blast.html new file mode 100644 index 000000000..09545581e --- /dev/null +++ b/backend/templates/emails/blast.html @@ -0,0 +1,25 @@ + +{% extends 'emails/base.html' %} + +{% block content %} +

Message from {{ sender }}

+ +

+ The following message was sent by {{ sender }}: +

+ +

{{ content }}

+ +

+ If you have any questions, please respond to this email or contact the sender. +

+{% endblock %} From bf9aeea64ac3dd2a457fe1fa3fb5776057ed09d5 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sat, 23 Nov 2024 00:53:44 -0500 Subject: [PATCH 2/4] Add tests --- backend/tests/clubs/test_ticketing.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py index f1dbce0d6..9d731475d 100644 --- a/backend/tests/clubs/test_ticketing.py +++ b/backend/tests/clubs/test_ticketing.py @@ -399,6 +399,35 @@ 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_get_tickets_information_no_tickets(self): # Delete all the tickets Ticket.objects.all().delete() From df498d0ddae8647b2acb2920bc0ba8ad84d26225 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Sun, 24 Nov 2024 22:57:40 -0800 Subject: [PATCH 3/4] Fix permissioning and add 400 response --- backend/clubs/permissions.py | 7 ++++++- backend/clubs/views.py | 18 ++++++++++++++++++ backend/tests/clubs/test_ticketing.py | 9 +++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/clubs/permissions.py b/backend/clubs/permissions.py index 664c0775c..ce634fc9a 100644 --- a/backend/clubs/permissions.py +++ b/backend/clubs/permissions.py @@ -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) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 9d148ed7a..9ff22ae0e 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -3067,6 +3067,17 @@ def email_blast(self, request, *args, **kwargs): 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: @@ -3088,6 +3099,13 @@ def email_blast(self, request, *args, **kwargs): 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}", diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py index 9d731475d..8f047b9d8 100644 --- a/backend/tests/clubs/test_ticketing.py +++ b/backend/tests/clubs/test_ticketing.py @@ -428,6 +428,15 @@ def test_email_blast(self): ) 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() From b4c6656e67188d38fd2019c7c51c98a5572b7671 Mon Sep 17 00:00:00 2001 From: aviupadhyayula Date: Mon, 25 Nov 2024 12:13:07 -0800 Subject: [PATCH 4/4] Address nits --- backend/clubs/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 9ff22ae0e..6d03c9607 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -3099,7 +3099,7 @@ def email_blast(self, request, *args, **kwargs): officer_emails = event.club.get_officer_emails() emails = list(holder_emails) + list(officer_emails) - content = request.data.get("content").strip() + content = request.data.get("content", "").strip() if not content: return Response( {"detail": "Content must be specified"},