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.
This commit is contained in:
parent
7e2981e855
commit
bf7578a833
|
@ -69,7 +69,7 @@
|
||||||
<td>{{ proposal.title }}</td>
|
<td>{{ proposal.title }}</td>
|
||||||
<td>{{ proposal.track }}</td>
|
<td>{{ proposal.track }}</td>
|
||||||
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> {{ proposal.event_type }}</td>
|
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> {{ proposal.event_type }}</td>
|
||||||
<td>{% for speaker in proposal.speakers.all %}<i class="fas fa-user" data-toggle="tooltip" title="{{ speaker.name }}"></i> {% endfor %}</td>
|
<td>{% for speaker in proposal.speakers.all %}<i class="fas fa-user{% if speaker.proposal_status == "approved" %} text-success{% elif speaker.proposal_status == "rejected" %} text-danger{% endif %}" data-toggle="tooltip" title="{{ speaker.name }} ({{ speaker.proposal_status }})"></i> {% endfor %}</td>
|
||||||
<td class="text-center">{{ proposal.speaker|truefalseicon }}</td>
|
<td class="text-center">{{ proposal.speaker|truefalseicon }}</td>
|
||||||
<td>{{ proposal.user }}</td>
|
<td>{{ proposal.user }}</td>
|
||||||
<td><a href="{% url 'backoffice:eventproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
<td><a href="{% url 'backoffice:eventproposal_manage' camp_slug=camp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
|
||||||
|
|
|
@ -230,7 +230,7 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi
|
||||||
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
|
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fields = []
|
fields = ["reason"]
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
from django.apps import AppConfig
|
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):
|
class ProgramConfig(AppConfig):
|
||||||
name = "program"
|
name = "program"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .models import Speaker
|
from .models import Speaker, EventInstance
|
||||||
from .signal_handlers import (
|
from .signal_handlers import (
|
||||||
check_speaker_event_camp_consistency,
|
check_speaker_event_camp_consistency,
|
||||||
check_speaker_camp_change,
|
check_speaker_camp_change,
|
||||||
|
eventinstance_pre_save,
|
||||||
|
eventinstance_post_save,
|
||||||
)
|
)
|
||||||
|
|
||||||
m2m_changed.connect(
|
m2m_changed.connect(
|
||||||
check_speaker_event_camp_consistency, sender=Speaker.events.through
|
check_speaker_event_camp_consistency, sender=Speaker.events.through
|
||||||
)
|
)
|
||||||
|
|
||||||
pre_save.connect(check_speaker_camp_change, sender=Speaker)
|
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)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from teams.models import Team
|
from teams.models import Team
|
||||||
from utils.email import add_outgoing_email
|
from utils.email import add_outgoing_email
|
||||||
|
|
||||||
|
@ -78,3 +77,66 @@ def add_eventproposal_updated_email(eventproposal):
|
||||||
formatdict=formatdict,
|
formatdict=formatdict,
|
||||||
subject="Event proposal '%s' was just updated" % eventproposal.title,
|
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}!",
|
||||||
|
)
|
||||||
|
|
|
@ -13,11 +13,6 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
The SpeakerProposalForm. Takes an EventType in __init__ and changes fields accordingly.
|
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:
|
class Meta:
|
||||||
model = SpeakerProposal
|
model = SpeakerProposal
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -43,6 +38,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of a debate guest. Can be a real name or an alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Guest Biography"
|
self.fields["biography"].label = "Guest Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the guest."
|
self.fields["biography"].help_text = "The biography of the guest."
|
||||||
|
@ -63,6 +64,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of the speaker. Can be a real name or an alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Speaker Biography"
|
self.fields["biography"].label = "Speaker Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the speaker."
|
self.fields["biography"].help_text = "The biography of the speaker."
|
||||||
|
@ -83,6 +90,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of the artist. Can be a real name or artist alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Artist Description"
|
self.fields["biography"].label = "Artist Description"
|
||||||
self.fields["biography"].help_text = "The description of the artist."
|
self.fields["biography"].help_text = "The description of the artist."
|
||||||
|
@ -103,6 +116,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of the event host. Can be a real name or an alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Host Biography"
|
self.fields["biography"].label = "Host Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the host."
|
self.fields["biography"].help_text = "The biography of the host."
|
||||||
|
@ -123,6 +142,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of the speaker. Can be a real name or an alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Speaker Biography"
|
self.fields["biography"].label = "Speaker Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the speaker."
|
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."
|
"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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Host Biography"
|
self.fields["biography"].label = "Host Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the host."
|
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"].label = "Host Name"
|
||||||
self.fields["name"].help_text = "Can be a real name or an alias."
|
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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Host Biography"
|
self.fields["biography"].label = "Host Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the host."
|
self.fields["biography"].help_text = "The biography of the host."
|
||||||
|
@ -180,6 +217,12 @@ class SpeakerProposalForm(forms.ModelForm):
|
||||||
"name"
|
"name"
|
||||||
].help_text = "The name of the meetup host. Can be a real name or an alias."
|
].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
|
# fix label and help_text for the biograpy field
|
||||||
self.fields["biography"].label = "Host Biography"
|
self.fields["biography"].label = "Host Biography"
|
||||||
self.fields["biography"].help_text = "The biography of the host."
|
self.fields["biography"].help_text = "The biography of the host."
|
||||||
|
|
29
src/program/migrations/0083_auto_20200226_1853.py
Normal file
29
src/program/migrations/0083_auto_20200226_1853.py
Normal file
|
@ -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.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
30
src/program/migrations/0084_auto_20200229_1801.py
Normal file
30
src/program/migrations/0084_auto_20200229_1801.py
Normal file
|
@ -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 <b>CC BY-SA 4.0</b> 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,6 +14,13 @@ from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
|
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__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,6 +182,10 @@ class UserSubmittedModel(CampRelatedModel):
|
||||||
max_length=50, choices=PROPOSAL_STATUS_CHOICES, default=PROPOSAL_PENDING
|
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):
|
def __str__(self):
|
||||||
return "%s (submitted by: %s, status: %s)" % (
|
return "%s (submitted by: %s, status: %s)" % (
|
||||||
self.headline,
|
self.headline,
|
||||||
|
@ -214,8 +225,9 @@ class SpeakerProposal(UserSubmittedModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
|
blank=True,
|
||||||
max_length=150,
|
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(
|
biography = models.TextField(
|
||||||
|
@ -278,12 +290,14 @@ class SpeakerProposal(UserSubmittedModel):
|
||||||
messages.success(
|
messages.success(
|
||||||
request, "Speaker object %s has been created/updated" % speaker
|
request, "Speaker object %s has been created/updated" % speaker
|
||||||
)
|
)
|
||||||
|
add_speakerproposal_accepted_email(self)
|
||||||
|
|
||||||
def mark_as_rejected(self, request):
|
def mark_as_rejected(self, request):
|
||||||
speakerproposalmodel = apps.get_model("program", "speakerproposal")
|
speakerproposalmodel = apps.get_model("program", "speakerproposal")
|
||||||
self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED
|
self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED
|
||||||
self.save()
|
self.save()
|
||||||
messages.success(request, "SpeakerProposal %s has been rejected" % self.name)
|
messages.success(request, "SpeakerProposal %s has been rejected" % self.name)
|
||||||
|
add_speakerproposal_rejected_email(self)
|
||||||
|
|
||||||
|
|
||||||
class EventProposal(UserSubmittedModel):
|
class EventProposal(UserSubmittedModel):
|
||||||
|
@ -319,7 +333,7 @@ class EventProposal(UserSubmittedModel):
|
||||||
|
|
||||||
allow_video_recording = models.BooleanField(
|
allow_video_recording = models.BooleanField(
|
||||||
default=False,
|
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 <b>CC BY-SA 4.0</b> license. Uncheck if you do not want the event recorded, or if you cannot accept the license.",
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = models.IntegerField(
|
duration = models.IntegerField(
|
||||||
|
@ -397,12 +411,14 @@ class EventProposal(UserSubmittedModel):
|
||||||
Url.objects.create(url=url.url, urltype=url.urltype, event=event)
|
Url.objects.create(url=url.url, urltype=url.urltype, event=event)
|
||||||
|
|
||||||
messages.success(request, "Event object %s has been created/updated" % event)
|
messages.success(request, "Event object %s has been created/updated" % event)
|
||||||
|
add_eventproposal_accepted_email(self)
|
||||||
|
|
||||||
def mark_as_rejected(self, request):
|
def mark_as_rejected(self, request):
|
||||||
eventproposalmodel = apps.get_model("program", "eventproposal")
|
eventproposalmodel = apps.get_model("program", "eventproposal")
|
||||||
self.proposal_status = eventproposalmodel.PROPOSAL_REJECTED
|
self.proposal_status = eventproposalmodel.PROPOSAL_REJECTED
|
||||||
self.save()
|
self.save()
|
||||||
messages.success(request, "EventProposal %s has been rejected" % self.title)
|
messages.success(request, "EventProposal %s has been rejected" % self.title)
|
||||||
|
add_eventproposal_rejected_email(self)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from .email import add_event_scheduled_email
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,8 +31,7 @@ def check_speaker_event_camp_consistency(sender, instance, **kwargs):
|
||||||
if event.camp != instance.camp:
|
if event.camp != instance.camp:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{
|
{
|
||||||
"events": "The event (%s) belongs to a different camp (%s) than the event does (%s)"
|
"events": f"The event ({event}) belongs to a different camp ({event.camp}) than the speaker does ({instance.camp})"
|
||||||
% (event, event.camp, 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."
|
"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")
|
||||||
|
|
8
src/program/templates/emails/event_scheduled.html
Normal file
8
src/program/templates/emails/event_scheduled.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Hello,<br>
|
||||||
|
<br>
|
||||||
|
The {{ eventinstance.camp.title }} event "{{ eventinstance.event.title }}" has been {{ action }} to begin {{ eventinstance.when.lower }} and end at {{ eventinstance.when.upper }}.<br>
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Content Team<br>
|
||||||
|
<br>
|
8
src/program/templates/emails/event_scheduled.txt
Normal file
8
src/program/templates/emails/event_scheduled.txt
Normal file
|
@ -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
|
||||||
|
|
11
src/program/templates/emails/eventproposal_accepted.html
Normal file
11
src/program/templates/emails/eventproposal_accepted.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
Hello,<br>
|
||||||
|
<br>
|
||||||
|
We have reviewed your event proposal "{{ proposal.title }}" and have decided to accept it.<br>
|
||||||
|
<br>
|
||||||
|
{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %}
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Content Team<br>
|
||||||
|
<br>
|
10
src/program/templates/emails/eventproposal_accepted.txt
Normal file
10
src/program/templates/emails/eventproposal_accepted.txt
Normal file
|
@ -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
|
||||||
|
|
11
src/program/templates/emails/eventproposal_rejected.html
Normal file
11
src/program/templates/emails/eventproposal_rejected.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
Hello,<br>
|
||||||
|
<br>
|
||||||
|
We have reviewed your event proposal "{{ proposal.title }}" and have decided to reject it.<br>
|
||||||
|
<br>
|
||||||
|
{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %}
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Content Team<br>
|
||||||
|
<br>
|
10
src/program/templates/emails/eventproposal_rejected.txt
Normal file
10
src/program/templates/emails/eventproposal_rejected.txt
Normal file
|
@ -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
|
||||||
|
|
11
src/program/templates/emails/speakerproposal_accepted.html
Normal file
11
src/program/templates/emails/speakerproposal_accepted.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
Hello,<br>
|
||||||
|
<br>
|
||||||
|
We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to accept it.<br>
|
||||||
|
<br>
|
||||||
|
{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %}
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Content Team<br>
|
||||||
|
<br>
|
10
src/program/templates/emails/speakerproposal_accepted.txt
Normal file
10
src/program/templates/emails/speakerproposal_accepted.txt
Normal file
|
@ -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
|
||||||
|
|
11
src/program/templates/emails/speakerproposal_rejected.html
Normal file
11
src/program/templates/emails/speakerproposal_rejected.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load commonmark %}
|
||||||
|
Hello,<br>
|
||||||
|
<br>
|
||||||
|
We have reviewed your speaker proposal "{{ proposal.name }}" and have decided to reject it.<br>
|
||||||
|
<br>
|
||||||
|
{% if proposal.reason %}{{ proposal.reason|untrustedcommonmark }}{% endif %}
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Content Team<br>
|
||||||
|
<br>
|
10
src/program/templates/emails/speakerproposal_rejected.txt
Normal file
10
src/program/templates/emails/speakerproposal_rejected.txt
Normal file
|
@ -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
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<th>People</th>
|
<th>People</th>
|
||||||
<th>Track</th>
|
<th>Track</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Published</th>
|
||||||
{% if request.resolver_match.app_name == "program" %}
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<th class='text-right'>Available Actions</th>
|
<th class='text-right'>Available Actions</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -30,6 +31,13 @@
|
||||||
</span></td>
|
</span></td>
|
||||||
<td><span class="h4">{{ eventproposal.track.name }}</span></td>
|
<td><span class="h4">{{ eventproposal.track.name }}</span></td>
|
||||||
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
||||||
|
<td>
|
||||||
|
{% if eventproposal.event %}
|
||||||
|
<a href="{% url 'program:event_detail' camp_slug=camp.slug event_slug=eventproposal.event.slug %}" class="btn btn-default btn-sm"><i class="fas fa-{{ eventproposal.event_type.icon }} fa-lg" style="color: {{ eventproposal.event_type.color }};"></i> Show Event</a>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% if request.resolver_match.app_name == "program" %}
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<td class='text-right'>
|
<td class='text-right'>
|
||||||
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
<th class="text-center">Events</th>
|
<th class="text-center">Events</th>
|
||||||
<th class="text-center">URLs</th>
|
<th class="text-center">URLs</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Published</th>
|
||||||
{% if request.resolver_match.app_name == "program" %}
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<th class="text-right">Available Actions</th>
|
<th class="text-right">Available Actions</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -14,6 +16,7 @@
|
||||||
{% for speakerproposal in speakerproposals %}
|
{% for speakerproposal in speakerproposals %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="h4">{{ speakerproposal.name }}</span></td>
|
<td><span class="h4">{{ speakerproposal.name }}</span></td>
|
||||||
|
<td><span class="h4">{{ speakerproposal.email }}</span></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if speakerproposal.eventproposals.all %}
|
{% if speakerproposal.eventproposals.all %}
|
||||||
{% for ep in speakerproposal.eventproposals.all %}
|
{% for ep in speakerproposal.eventproposals.all %}
|
||||||
|
@ -31,6 +34,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
|
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
|
||||||
|
<td>
|
||||||
|
{% if speakerproposal.speaker %}
|
||||||
|
<a href="{% url 'program:speaker_detail' camp_slug=camp.slug slug=speakerproposal.speaker.slug %}" class="btn btn-default btn-sm"><i class="fas fa-user"></i> Show Speaker</a>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% if request.resolver_match.app_name == "program" %}
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
|
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
|
|
|
@ -86,10 +86,14 @@ def add_outgoing_email(
|
||||||
if not isinstance(to_recipients, list):
|
if not isinstance(to_recipients, list):
|
||||||
to_recipients = [to_recipients]
|
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:
|
try:
|
||||||
validate_email(recipient)
|
validate_email(recipient)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
|
logger.error(
|
||||||
|
f"There was a problem validating the email {recipient} - returning False"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
email = OutgoingEmail.objects.create(
|
email = OutgoingEmail.objects.create(
|
||||||
|
|
Loading…
Reference in a new issue