From bf7578a833a90f8c37e792e63b7423e146b58dc5 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Thu, 5 Mar 2020 12:23:42 +0100 Subject: [PATCH] Send emails when proposals are accepted/rejected, and when eventinstances are created/updated (#466) * fixup labels and help_text for the email field * add email and published columns to speaker proposal table, add published column to event proposal table * send email to user when a speaker or eventproposal is accepted or rejected. Send email to submitter and all speakers when an eventinstance is created, or when the time is updated (event is rescheduled). Fix a few other small things while here. --- .../templates/manage_proposals.html | 2 +- src/backoffice/views.py | 2 +- src/program/apps.py | 10 ++- src/program/email.py | 64 ++++++++++++++++++- src/program/forms.py | 53 +++++++++++++-- .../migrations/0083_auto_20200226_1853.py | 29 +++++++++ .../migrations/0084_auto_20200229_1801.py | 30 +++++++++ src/program/models.py | 20 +++++- src/program/signal_handlers.py | 25 +++++++- .../templates/emails/event_scheduled.html | 8 +++ .../templates/emails/event_scheduled.txt | 8 +++ .../emails/eventproposal_accepted.html | 11 ++++ .../emails/eventproposal_accepted.txt | 10 +++ .../emails/eventproposal_rejected.html | 11 ++++ .../emails/eventproposal_rejected.txt | 10 +++ .../emails/speakerproposal_accepted.html | 11 ++++ .../emails/speakerproposal_accepted.txt | 10 +++ .../emails/speakerproposal_rejected.html | 11 ++++ .../emails/speakerproposal_rejected.txt | 10 +++ .../includes/event_proposal_table.html | 8 +++ .../includes/speaker_proposal_table.html | 10 +++ src/utils/email.py | 6 +- 22 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 src/program/migrations/0083_auto_20200226_1853.py create mode 100644 src/program/migrations/0084_auto_20200229_1801.py create mode 100644 src/program/templates/emails/event_scheduled.html create mode 100644 src/program/templates/emails/event_scheduled.txt create mode 100644 src/program/templates/emails/eventproposal_accepted.html create mode 100644 src/program/templates/emails/eventproposal_accepted.txt create mode 100644 src/program/templates/emails/eventproposal_rejected.html create mode 100644 src/program/templates/emails/eventproposal_rejected.txt create mode 100644 src/program/templates/emails/speakerproposal_accepted.html create mode 100644 src/program/templates/emails/speakerproposal_accepted.txt create mode 100644 src/program/templates/emails/speakerproposal_rejected.html create mode 100644 src/program/templates/emails/speakerproposal_rejected.txt diff --git a/src/backoffice/templates/manage_proposals.html b/src/backoffice/templates/manage_proposals.html index 81d19e86..c812e4ec 100644 --- a/src/backoffice/templates/manage_proposals.html +++ b/src/backoffice/templates/manage_proposals.html @@ -69,7 +69,7 @@ {{ proposal.title }} {{ proposal.track }} {{ proposal.event_type }} - {% for speaker in proposal.speakers.all %} {% endfor %} + {% for speaker in proposal.speakers.all %} {% endfor %} {{ proposal.speaker|truefalseicon }} {{ proposal.user }} Manage diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 633d35f8..54c7132e 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -230,7 +230,7 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView """ - fields = [] + fields = ["reason"] def form_valid(self, form): """ diff --git a/src/program/apps.py b/src/program/apps.py index 7890bd8d..3bd2888b 100644 --- a/src/program/apps.py +++ b/src/program/apps.py @@ -1,18 +1,24 @@ from django.apps import AppConfig -from django.db.models.signals import m2m_changed, pre_save +from django.db.models.signals import m2m_changed, post_save, pre_save class ProgramConfig(AppConfig): name = "program" def ready(self): - from .models import Speaker + from .models import Speaker, EventInstance from .signal_handlers import ( check_speaker_event_camp_consistency, check_speaker_camp_change, + eventinstance_pre_save, + eventinstance_post_save, ) m2m_changed.connect( check_speaker_event_camp_consistency, sender=Speaker.events.through ) + pre_save.connect(check_speaker_camp_change, sender=Speaker) + pre_save.connect(eventinstance_pre_save, sender=EventInstance) + + post_save.connect(eventinstance_post_save, sender=EventInstance) diff --git a/src/program/email.py b/src/program/email.py index 4e0b3a65..7525f47e 100644 --- a/src/program/email.py +++ b/src/program/email.py @@ -1,7 +1,6 @@ import logging from django.core.exceptions import ObjectDoesNotExist - from teams.models import Team from utils.email import add_outgoing_email @@ -78,3 +77,66 @@ def add_eventproposal_updated_email(eventproposal): formatdict=formatdict, subject="Event proposal '%s' was just updated" % eventproposal.title, ) + + +def add_speakerproposal_rejected_email(speakerproposal): + formatdict = {"proposal": speakerproposal} + + return add_outgoing_email( + text_template="emails/speakerproposal_rejected.txt", + html_template="emails/speakerproposal_rejected.html", + to_recipients=speakerproposal.user.email, + formatdict=formatdict, + subject=f"Your {speakerproposal.camp.title} speaker proposal '{speakerproposal.name}' was rejected", + ) + + +def add_speakerproposal_accepted_email(speakerproposal): + formatdict = {"proposal": speakerproposal} + + return add_outgoing_email( + text_template="emails/speakerproposal_accepted.txt", + html_template="emails/speakerproposal_accepted.html", + to_recipients=speakerproposal.user.email, + formatdict=formatdict, + subject=f"Your {speakerproposal.camp.title} speaker proposal '{speakerproposal.name}' was accepted", + ) + + +def add_eventproposal_rejected_email(eventproposal): + formatdict = {"proposal": eventproposal} + + return add_outgoing_email( + text_template="emails/eventproposal_rejected.txt", + html_template="emails/eventproposal_rejected.html", + to_recipients=eventproposal.user.email, + formatdict=formatdict, + subject=f"Your {eventproposal.camp.title} event proposal '{eventproposal.title}' was rejected", + ) + + +def add_eventproposal_accepted_email(eventproposal): + formatdict = {"proposal": eventproposal} + + return add_outgoing_email( + text_template="emails/eventproposal_accepted.txt", + html_template="emails/eventproposal_accepted.html", + to_recipients=eventproposal.user.email, + formatdict=formatdict, + subject=f"Your {eventproposal.camp.title} event proposal '{eventproposal.title}' was accepted!", + ) + + +def add_event_scheduled_email(eventinstance, action): + formatdict = {"eventinstance": eventinstance, "action": action} + recipients = [speaker.email for speaker in eventinstance.event.speakers.all()] + recipients.append(eventinstance.event.proposal.user.email) + # loop over unique recipients and send an email to each + for rcpt in set(recipients): + return add_outgoing_email( + text_template="emails/event_scheduled.txt", + html_template="emails/event_scheduled.html", + to_recipients=rcpt, + formatdict=formatdict, + subject=f"Your {eventinstance.camp.title} event '{eventinstance.event.title}' has been {action}!", + ) diff --git a/src/program/forms.py b/src/program/forms.py index c0ac4f1d..ae15018f 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -13,11 +13,6 @@ class SpeakerProposalForm(forms.ModelForm): The SpeakerProposalForm. Takes an EventType in __init__ and changes fields accordingly. """ - email = forms.EmailField( - required=False, - help_text="The email of the speaker (defaults to the logged in user if empty).", - ) - class Meta: model = SpeakerProposal fields = [ @@ -43,6 +38,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of a debate guest. Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Guest Email" + self.fields[ + "email" + ].help_text = "The email for this guest. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Guest Biography" self.fields["biography"].help_text = "The biography of the guest." @@ -63,6 +64,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of the speaker. Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Speaker Email" + self.fields[ + "email" + ].help_text = "The email for this speaker. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Speaker Biography" self.fields["biography"].help_text = "The biography of the speaker." @@ -83,6 +90,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of the artist. Can be a real name or artist alias." + # fix label and help_text for the email field + self.fields["email"].label = "Artist Email" + self.fields[ + "email" + ].help_text = "The email for this artist. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Artist Description" self.fields["biography"].help_text = "The description of the artist." @@ -103,6 +116,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of the event host. Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Host Email" + self.fields[ + "email" + ].help_text = "The email for the host. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Host Biography" self.fields["biography"].help_text = "The biography of the host." @@ -123,6 +142,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of the speaker. Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Speaker Email" + self.fields[ + "email" + ].help_text = "The email for this speaker. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Speaker Biography" self.fields["biography"].help_text = "The biography of the speaker." @@ -142,6 +167,12 @@ class SpeakerProposalForm(forms.ModelForm): "The name of the workshop host. Can be a real name or an alias." ) + # fix label and help_text for the email field + self.fields["email"].label = "Host Email" + self.fields[ + "email" + ].help_text = "The email for the host. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Host Biography" self.fields["biography"].help_text = "The biography of the host." @@ -160,6 +191,12 @@ class SpeakerProposalForm(forms.ModelForm): self.fields["name"].label = "Host Name" self.fields["name"].help_text = "Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Host Email" + self.fields[ + "email" + ].help_text = "The email for the host. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Host Biography" self.fields["biography"].help_text = "The biography of the host." @@ -180,6 +217,12 @@ class SpeakerProposalForm(forms.ModelForm): "name" ].help_text = "The name of the meetup host. Can be a real name or an alias." + # fix label and help_text for the email field + self.fields["email"].label = "Host Email" + self.fields[ + "email" + ].help_text = "The email for the host. Will default to the logged-in users email if left empty." + # fix label and help_text for the biograpy field self.fields["biography"].label = "Host Biography" self.fields["biography"].help_text = "The biography of the host." diff --git a/src/program/migrations/0083_auto_20200226_1853.py b/src/program/migrations/0083_auto_20200226_1853.py new file mode 100644 index 00000000..30bbee5f --- /dev/null +++ b/src/program/migrations/0083_auto_20200226_1853.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.3 on 2020-02-26 17:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0082_eventinstance_uuid_not_null"), + ] + + operations = [ + migrations.AddField( + model_name="eventproposal", + name="reason", + field=models.TextField( + blank=True, + help_text="The reason this proposal was accepted or rejected.", + ), + ), + migrations.AddField( + model_name="speakerproposal", + name="reason", + field=models.TextField( + blank=True, + help_text="The reason this proposal was accepted or rejected.", + ), + ), + ] diff --git a/src/program/migrations/0084_auto_20200229_1801.py b/src/program/migrations/0084_auto_20200229_1801.py new file mode 100644 index 00000000..bdd9bf9d --- /dev/null +++ b/src/program/migrations/0084_auto_20200229_1801.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.3 on 2020-02-29 17:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0083_auto_20200226_1853"), + ] + + operations = [ + migrations.AlterField( + model_name="eventproposal", + name="allow_video_recording", + field=models.BooleanField( + default=False, + help_text="Recordings are made available under the CC BY-SA 4.0 license. Uncheck if you do not want the event recorded, or if you cannot accept the license.", + ), + ), + migrations.AlterField( + model_name="speakerproposal", + name="email", + field=models.EmailField( + blank=True, + help_text="The email of the speaker/artist/host. Defaults to the logged in users email if empty.", + max_length=150, + ), + ), + ] diff --git a/src/program/models.py b/src/program/models.py index b3409ddf..8d73ee24 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -14,6 +14,13 @@ from django.urls import reverse, reverse_lazy from django.utils.text import slugify from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel +from .email import ( + add_eventproposal_accepted_email, + add_eventproposal_rejected_email, + add_speakerproposal_accepted_email, + add_speakerproposal_rejected_email, +) + logger = logging.getLogger("bornhack.%s" % __name__) @@ -175,6 +182,10 @@ class UserSubmittedModel(CampRelatedModel): max_length=50, choices=PROPOSAL_STATUS_CHOICES, default=PROPOSAL_PENDING ) + reason = models.TextField( + blank=True, help_text="The reason this proposal was accepted or rejected.", + ) + def __str__(self): return "%s (submitted by: %s, status: %s)" % ( self.headline, @@ -214,8 +225,9 @@ class SpeakerProposal(UserSubmittedModel): ) email = models.EmailField( + blank=True, max_length=150, - help_text="The email of the speaker (defaults to the logged in user if empty).", + help_text="The email of the speaker/artist/host. Defaults to the logged in users email if empty.", ) biography = models.TextField( @@ -278,12 +290,14 @@ class SpeakerProposal(UserSubmittedModel): messages.success( request, "Speaker object %s has been created/updated" % speaker ) + add_speakerproposal_accepted_email(self) def mark_as_rejected(self, request): speakerproposalmodel = apps.get_model("program", "speakerproposal") self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED self.save() messages.success(request, "SpeakerProposal %s has been rejected" % self.name) + add_speakerproposal_rejected_email(self) class EventProposal(UserSubmittedModel): @@ -319,7 +333,7 @@ class EventProposal(UserSubmittedModel): allow_video_recording = models.BooleanField( default=False, - help_text="Uncheck to avoid video recording. Recordings are made available under the CC BY-SA 4.0 license. Uncheck if you can not accept this license.", + help_text="Recordings are made available under the CC BY-SA 4.0 license. Uncheck if you do not want the event recorded, or if you cannot accept the license.", ) duration = models.IntegerField( @@ -397,12 +411,14 @@ class EventProposal(UserSubmittedModel): Url.objects.create(url=url.url, urltype=url.urltype, event=event) messages.success(request, "Event object %s has been created/updated" % event) + add_eventproposal_accepted_email(self) def mark_as_rejected(self, request): eventproposalmodel = apps.get_model("program", "eventproposal") self.proposal_status = eventproposalmodel.PROPOSAL_REJECTED self.save() messages.success(request, "EventProposal %s has been rejected" % self.title) + add_eventproposal_rejected_email(self) ############################################################################### diff --git a/src/program/signal_handlers.py b/src/program/signal_handlers.py index 1bf37b0a..07671d55 100644 --- a/src/program/signal_handlers.py +++ b/src/program/signal_handlers.py @@ -2,6 +2,8 @@ import logging from django.core.exceptions import ValidationError +from .email import add_event_scheduled_email + logger = logging.getLogger("bornhack.%s" % __name__) @@ -29,8 +31,7 @@ def check_speaker_event_camp_consistency(sender, instance, **kwargs): if event.camp != instance.camp: raise ValidationError( { - "events": "The event (%s) belongs to a different camp (%s) than the event does (%s)" - % (event, event.camp, instance.camp) + "events": f"The event ({event}) belongs to a different camp ({event.camp}) than the speaker does ({instance.camp})" } ) @@ -44,3 +45,23 @@ def check_speaker_camp_change(sender, instance, **kwargs): "camp": "You cannot change the camp a speaker belongs to if the speaker is associated with one or more events." } ) + + +def eventinstance_pre_save(sender, instance, **kwargs): + """ Save the old instance.when value so we can later determine if it changed """ + try: + # get the old instance from the database, if we have one + instance.old_when = sender.objects.get(pk=instance.pk).when + except sender.DoesNotExist: + # nothing found in the DB with this pk, this is a new eventinstance + instance.old_when = instance.when + + +def eventinstance_post_save(sender, instance, created, **kwargs): + """ Send an email if this is a new eventinstance, or if the "when" field changed """ + if created: + add_event_scheduled_email(eventinstance=instance, action="scheduled") + else: + if instance.old_when != instance.when: + # date/time for this eventinstance changed, send a rescheduled email + add_event_scheduled_email(eventinstance=instance, action="rescheduled") diff --git a/src/program/templates/emails/event_scheduled.html b/src/program/templates/emails/event_scheduled.html new file mode 100644 index 00000000..32774c34 --- /dev/null +++ b/src/program/templates/emails/event_scheduled.html @@ -0,0 +1,8 @@ +Hello,
+
+The {{ eventinstance.camp.title }} event "{{ eventinstance.event.title }}" has been {{ action }} to begin {{ eventinstance.when.lower }} and end at {{ eventinstance.when.upper }}.
+
+Best regards,
+
+The BornHack Content Team
+
diff --git a/src/program/templates/emails/event_scheduled.txt b/src/program/templates/emails/event_scheduled.txt new file mode 100644 index 00000000..f990fdc5 --- /dev/null +++ b/src/program/templates/emails/event_scheduled.txt @@ -0,0 +1,8 @@ +Hello, + +The {{ eventinstance.camp.title }} event "{{ eventinstance.event.title }}" has been {% if rescheduled %}re{% endif %}scheduled to begin {{ eventinstance.when.lower }} and end at {{ eventinstance.when.upper }}. + +Best regards, + +The BornHack Content Team + diff --git a/src/program/templates/emails/eventproposal_accepted.html b/src/program/templates/emails/eventproposal_accepted.html new file mode 100644 index 00000000..6ed00022 --- /dev/null +++ b/src/program/templates/emails/eventproposal_accepted.html @@ -0,0 +1,11 @@ +{% load commonmark %} +Hello,
+
+We have reviewed your event proposal "{{ proposal.title }}" and have decided to accept it.
+
+{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %} +
+Best regards,
+
+The BornHack Content Team
+
diff --git a/src/program/templates/emails/eventproposal_accepted.txt b/src/program/templates/emails/eventproposal_accepted.txt new file mode 100644 index 00000000..8ad752ef --- /dev/null +++ b/src/program/templates/emails/eventproposal_accepted.txt @@ -0,0 +1,10 @@ +Hello, + +We have reviewed your event proposal "{{ proposal.title }}" and have decided to accept it. + +{% if proposal.reason %}{{ proposal.reason }}{% endif %} + +Best regards, + +The BornHack Content Team + diff --git a/src/program/templates/emails/eventproposal_rejected.html b/src/program/templates/emails/eventproposal_rejected.html new file mode 100644 index 00000000..41c8945c --- /dev/null +++ b/src/program/templates/emails/eventproposal_rejected.html @@ -0,0 +1,11 @@ +{% load commonmark %} +Hello,
+
+We have reviewed your event proposal "{{ proposal.title }}" and have decided to reject it.
+
+{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %} +
+Best regards,
+
+The BornHack Content Team
+
diff --git a/src/program/templates/emails/eventproposal_rejected.txt b/src/program/templates/emails/eventproposal_rejected.txt new file mode 100644 index 00000000..e42b60cc --- /dev/null +++ b/src/program/templates/emails/eventproposal_rejected.txt @@ -0,0 +1,10 @@ +Hello, + +We have reviewed your event proposal "{{ proposal.title }}" and have decided to reject it. + +{% if proposal.reason %}{{ proposal.reason }}{% endif %} + +Best regards, + +The BornHack Content Team + diff --git a/src/program/templates/emails/speakerproposal_accepted.html b/src/program/templates/emails/speakerproposal_accepted.html new file mode 100644 index 00000000..8a6304e3 --- /dev/null +++ b/src/program/templates/emails/speakerproposal_accepted.html @@ -0,0 +1,11 @@ +{% load commonmark %} +Hello,
+
+We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to accept it.
+
+{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %} +
+Best regards,
+
+The BornHack Content Team
+
diff --git a/src/program/templates/emails/speakerproposal_accepted.txt b/src/program/templates/emails/speakerproposal_accepted.txt new file mode 100644 index 00000000..ab2b5dd7 --- /dev/null +++ b/src/program/templates/emails/speakerproposal_accepted.txt @@ -0,0 +1,10 @@ +Hello, + +We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to accept it. + +{% if proposal.reason %}{{ proposal.reason }}{% endif %} + +Best regards, + +The BornHack Content Team + diff --git a/src/program/templates/emails/speakerproposal_rejected.html b/src/program/templates/emails/speakerproposal_rejected.html new file mode 100644 index 00000000..bc1c673f --- /dev/null +++ b/src/program/templates/emails/speakerproposal_rejected.html @@ -0,0 +1,11 @@ +{% load commonmark %} +Hello,
+
+We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to reject it.
+
+{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %} +
+Best regards,
+
+The BornHack Content Team
+
diff --git a/src/program/templates/emails/speakerproposal_rejected.txt b/src/program/templates/emails/speakerproposal_rejected.txt new file mode 100644 index 00000000..257ae2b5 --- /dev/null +++ b/src/program/templates/emails/speakerproposal_rejected.txt @@ -0,0 +1,10 @@ +Hello, + +We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to reject it. + +{% if proposal.reason %}{{ proposal.reason }}{% endif %} + +Best regards, + +The BornHack Content Team + diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 531423fa..d8cd6299 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -7,6 +7,7 @@ People Track Status + Published {% if request.resolver_match.app_name == "program" %} Available Actions {% endif %} @@ -30,6 +31,13 @@ {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} + + {% if eventproposal.event %} + Show Event + {% else %} + + {% endif %} + {% if request.resolver_match.app_name == "program" %} diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index 4c6cbbb4..e2589fbf 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -2,9 +2,11 @@ Name + Email Events URLs Status + Published {% if request.resolver_match.app_name == "program" %} Available Actions {% endif %} @@ -14,6 +16,7 @@ {% for speakerproposal in speakerproposals %} {{ speakerproposal.name }} + {{ speakerproposal.email }} {% if speakerproposal.eventproposals.all %} {% for ep in speakerproposal.eventproposals.all %} @@ -31,6 +34,13 @@ {% endfor %} {{ speakerproposal.proposal_status }} + + {% if speakerproposal.speaker %} + Show Speaker + {% else %} + + {% endif %} + {% if request.resolver_match.app_name == "program" %} diff --git a/src/utils/email.py b/src/utils/email.py index 1a715500..53eef7fa 100644 --- a/src/utils/email.py +++ b/src/utils/email.py @@ -86,10 +86,14 @@ def add_outgoing_email( if not isinstance(to_recipients, list): to_recipients = [to_recipients] - for recipient in to_recipients: + # loop over recipients and validate each + for recipient in to_recipients + cc_recipients + bcc_recipients: try: validate_email(recipient) except ValidationError: + logger.error( + f"There was a problem validating the email {recipient} - returning False" + ) return False email = OutgoingEmail.objects.create(