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(
| |