fix #133
An email is now sent when a new speaker- or eventproposal is created and when any such is updated.
This commit is contained in:
parent
a67f9ee4a5
commit
fcd85f680a
102
src/program/email.py
Normal file
102
src/program/email.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
from utils.email import add_outgoing_email
|
||||||
|
from teams.models import Team
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def add_new_speakerproposal_email(speakerproposal):
|
||||||
|
formatdict = {
|
||||||
|
'proposal': speakerproposal
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_team = Team.objects.get(
|
||||||
|
camp=speakerproposal.camp, name='Content'
|
||||||
|
)
|
||||||
|
|
||||||
|
return add_outgoing_email(
|
||||||
|
text_template='emails/new_speakerproposal.txt',
|
||||||
|
html_template='emails/new_speakerproposal.html',
|
||||||
|
to_recipients=content_team.mailing_list,
|
||||||
|
formatdict=formatdict,
|
||||||
|
subject='New speaker proposal for {}'.format(
|
||||||
|
speakerproposal.camp.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist as e:
|
||||||
|
logger.info('There is no team with name Content: {}'.format(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_new_eventproposal_email(eventproposal):
|
||||||
|
formatdict = {
|
||||||
|
'proposal': eventproposal
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_team = Team.objects.get(
|
||||||
|
camp=eventproposal.camp, name='Content'
|
||||||
|
)
|
||||||
|
|
||||||
|
return add_outgoing_email(
|
||||||
|
text_template='emails/new_eventproposal.txt',
|
||||||
|
html_template='emails/new_eventproposal.html',
|
||||||
|
to_recipients=content_team.mailing_list,
|
||||||
|
formatdict=formatdict,
|
||||||
|
subject='New event proposal for {}'.format(
|
||||||
|
eventproposal.camp.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist as e:
|
||||||
|
logger.info('There is no team with name Content: {}'.format(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_speakerproposal_updated_email(speakerproposal):
|
||||||
|
formatdict = {
|
||||||
|
'proposal': speakerproposal
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_team = Team.objects.get(
|
||||||
|
camp=speakerproposal.camp, name='Content'
|
||||||
|
)
|
||||||
|
|
||||||
|
return add_outgoing_email(
|
||||||
|
text_template='emails/update_speakerproposal.txt',
|
||||||
|
html_template='emails/update_speakerproposal.html',
|
||||||
|
to_recipients=content_team.mailing_list,
|
||||||
|
formatdict=formatdict,
|
||||||
|
subject='Updated speaker proposal for {}'.format(
|
||||||
|
speakerproposal.camp.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist as e:
|
||||||
|
logger.info('There is no team with name Content: {}'.format(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_eventproposal_updated_email(eventproposal):
|
||||||
|
formatdict = {
|
||||||
|
'proposal': eventproposal
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_team = Team.objects.get(
|
||||||
|
camp=eventproposal.camp, name='Content'
|
||||||
|
)
|
||||||
|
|
||||||
|
return add_outgoing_email(
|
||||||
|
text_template='emails/update_eventproposal.txt',
|
||||||
|
html_template='emails/update_eventproposal.html',
|
||||||
|
to_recipients=content_team.mailing_list,
|
||||||
|
formatdict=formatdict,
|
||||||
|
subject='New event proposal for {}'.format(
|
||||||
|
eventproposal.camp.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist as e:
|
||||||
|
logger.info('There is no team with name Content: {}'.format(e))
|
||||||
|
return False
|
|
@ -1,10 +1,15 @@
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
import icalendar
|
||||||
|
import CommonMark
|
||||||
|
import logging
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.postgres.fields import DateTimeRangeField
|
from django.contrib.postgres.fields import DateTimeRangeField
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -13,11 +18,11 @@ from django.core.files.storage import FileSystemStorage
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db.models.signals import post_save
|
||||||
import icalendar
|
|
||||||
import CommonMark
|
|
||||||
|
|
||||||
from utils.models import CreatedUpdatedModel, CampRelatedModel
|
from utils.models import CreatedUpdatedModel, CampRelatedModel
|
||||||
|
from .email import add_new_speakerproposal_email, add_new_eventproposal_email
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
class CustomUrlStorage(FileSystemStorage):
|
class CustomUrlStorage(FileSystemStorage):
|
||||||
|
@ -80,12 +85,14 @@ class UserSubmittedModel(CampRelatedModel):
|
||||||
PROPOSAL_PENDING = 'pending'
|
PROPOSAL_PENDING = 'pending'
|
||||||
PROPOSAL_APPROVED = 'approved'
|
PROPOSAL_APPROVED = 'approved'
|
||||||
PROPOSAL_REJECTED = 'rejected'
|
PROPOSAL_REJECTED = 'rejected'
|
||||||
|
PROPOSAL_MODIFIED_AFTER_APPROVAL = 'modified after approval'
|
||||||
|
|
||||||
PROPOSAL_STATUSES = [
|
PROPOSAL_STATUSES = [
|
||||||
PROPOSAL_DRAFT,
|
PROPOSAL_DRAFT,
|
||||||
PROPOSAL_PENDING,
|
PROPOSAL_PENDING,
|
||||||
PROPOSAL_APPROVED,
|
PROPOSAL_APPROVED,
|
||||||
PROPOSAL_REJECTED
|
PROPOSAL_REJECTED,
|
||||||
|
PROPOSAL_MODIFIED_AFTER_APPROVAL
|
||||||
]
|
]
|
||||||
|
|
||||||
PROPOSAL_STATUS_CHOICES = [
|
PROPOSAL_STATUS_CHOICES = [
|
||||||
|
@ -93,6 +100,7 @@ class UserSubmittedModel(CampRelatedModel):
|
||||||
(PROPOSAL_PENDING, 'Pending approval'),
|
(PROPOSAL_PENDING, 'Pending approval'),
|
||||||
(PROPOSAL_APPROVED, 'Approved'),
|
(PROPOSAL_APPROVED, 'Approved'),
|
||||||
(PROPOSAL_REJECTED, 'Rejected'),
|
(PROPOSAL_REJECTED, 'Rejected'),
|
||||||
|
(PROPOSAL_MODIFIED_AFTER_APPROVAL, 'Modified after approval'),
|
||||||
]
|
]
|
||||||
|
|
||||||
proposal_status = models.CharField(
|
proposal_status = models.CharField(
|
||||||
|
@ -265,6 +273,21 @@ class EventProposal(UserSubmittedModel):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=EventProposal)
|
||||||
|
@receiver(post_save, sender=SpeakerProposal)
|
||||||
|
def notify_content_team(sender, created, instance, **kwargs):
|
||||||
|
if created and isinstance(instance, SpeakerProposal):
|
||||||
|
if not add_new_speakerproposal_email(instance):
|
||||||
|
logger.error(
|
||||||
|
'Error adding speaker proposal email to outgoing queue for {}'.format(instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
if created and isinstance(instance, EventProposal):
|
||||||
|
if not add_new_eventproposal_email(instance):
|
||||||
|
logger.error(
|
||||||
|
'Error adding event proposal email to outgoing queue for {}'.format(instance)
|
||||||
|
)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
10
src/program/templates/emails/new_eventproposal.html
Normal file
10
src/program/templates/emails/new_eventproposal.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!<br>
|
||||||
|
<br>
|
||||||
|
Event {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
More info <a href="https://bornhack.dk/admin/program/eventproposal/{{ proposal.uuid }}/change/">here</a>.
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Team<br>
|
10
src/program/templates/emails/new_eventproposal.txt
Normal file
10
src/program/templates/emails/new_eventproposal.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!
|
||||||
|
|
||||||
|
Event {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}.
|
||||||
|
|
||||||
|
More info: https://bornhack.dk/admin/program/speakerproposal/{{ proposal.uuid }}/change/
|
||||||
|
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The BornHack Team
|
10
src/program/templates/emails/new_speakerproposal.html
Normal file
10
src/program/templates/emails/new_speakerproposal.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!<br>
|
||||||
|
<br>
|
||||||
|
Speaker {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
More info <a href="https://bornhack.dk/admin/program/speakerproposal/{{ proposal.uuid }}/change/">here</a>.
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Team<br>
|
10
src/program/templates/emails/new_speakerproposal.txt
Normal file
10
src/program/templates/emails/new_speakerproposal.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!
|
||||||
|
|
||||||
|
Speaker {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}.
|
||||||
|
|
||||||
|
More info: https://bornhack.dk/admin/program/speakerproposal/{{ proposal.uuid }}/change/
|
||||||
|
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The BornHack Team
|
10
src/program/templates/emails/update_eventproposal.html
Normal file
10
src/program/templates/emails/update_eventproposal.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!<br>
|
||||||
|
<br>
|
||||||
|
Event {{ proposal.name }} for {{ proposal.camp }} was just updated!
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
More info <a href="https://bornhack.dk/admin/program/eventproposal/{{ proposal.uuid }}/change/">here</a>.
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Team<br>
|
10
src/program/templates/emails/update_eventproposal.txt
Normal file
10
src/program/templates/emails/update_eventproposal.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!
|
||||||
|
|
||||||
|
Event {{ proposal.name }} for {{ proposal.camp }} was just updated!
|
||||||
|
|
||||||
|
More info: https://bornhack.dk/admin/program/eventproposal/{{ proposal.uuid }}/change/
|
||||||
|
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The BornHack Team
|
10
src/program/templates/emails/update_speakerproposal.html
Normal file
10
src/program/templates/emails/update_speakerproposal.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!<br>
|
||||||
|
<br>
|
||||||
|
Speaker {{ proposal.name }} for {{ proposal.camp }} was just updated!
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
More info <a href="https://bornhack.dk/admin/program/speakerproposal/{{ proposal.uuid }}/change/">here</a>.
|
||||||
|
<br>
|
||||||
|
Best regards,<br>
|
||||||
|
<br>
|
||||||
|
The BornHack Team<br>
|
10
src/program/templates/emails/update_speakerproposal.txt
Normal file
10
src/program/templates/emails/update_speakerproposal.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Hello!
|
||||||
|
|
||||||
|
Speaker {{ proposal.name }} was just submitted as a new speaker proposal for {{ proposal.camp }}.
|
||||||
|
|
||||||
|
More info: https://bornhack.dk/admin/program/speakerproposal/{{ proposal.uuid }}/change/
|
||||||
|
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The BornHack Team
|
|
@ -24,11 +24,7 @@ Proposals | {{ block.super }}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Details</a>
|
<a href="{% url 'speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Details</a>
|
||||||
{% if not camp.read_only %}
|
{% if not camp.read_only %}
|
||||||
{% if speakerproposal.proposal_status == "approved" %}
|
<a href="{% url 'speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Modify</a>
|
||||||
<a href="{% url 'speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs btn-disabled" disabled>Modify</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs">Modify</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if speakerproposal.proposal_status == "pending" or speakerproposal.proposal_status == "approved" %}
|
{% if speakerproposal.proposal_status == "pending" or speakerproposal.proposal_status == "approved" %}
|
||||||
<a href="{% url 'speakerproposal_submit' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs btn-disabled" disabled>Submit</a>
|
<a href="{% url 'speakerproposal_submit' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-xs btn-disabled" disabled>Submit</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import datetime, os
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from django.views.generic import ListView, TemplateView, DetailView, View
|
from django.views.generic import ListView, TemplateView, DetailView, View
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
|
@ -8,14 +10,26 @@ from django.http import Http404, HttpResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import icalendar
|
import icalendar
|
||||||
|
|
||||||
from camps.mixins import CampViewMixin
|
from camps.mixins import CampViewMixin
|
||||||
from .mixins import CreateProposalMixin, EnsureUnapprovedProposalMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, PictureViewMixin, EnsureCFSOpenMixin
|
from .mixins import (
|
||||||
|
CreateProposalMixin,
|
||||||
|
EnsureUnapprovedProposalMixin,
|
||||||
|
EnsureUserOwnsProposalMixin,
|
||||||
|
EnsureWritableCampMixin,
|
||||||
|
PictureViewMixin,
|
||||||
|
EnsureCFSOpenMixin
|
||||||
|
)
|
||||||
|
from .email import (
|
||||||
|
add_speakerproposal_updated_email,
|
||||||
|
add_eventproposal_updated_email
|
||||||
|
)
|
||||||
from . import models
|
from . import models
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
############## ical calendar ########################################################
|
############## ical calendar ########################################################
|
||||||
|
@ -84,7 +98,7 @@ class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposa
|
||||||
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
return reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})
|
||||||
|
|
||||||
|
|
||||||
class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureUnapprovedProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
||||||
model = models.SpeakerProposal
|
model = models.SpeakerProposal
|
||||||
fields = ['name', 'biography', 'picture_small', 'picture_large']
|
fields = ['name', 'biography', 'picture_small', 'picture_large']
|
||||||
template_name = 'speakerproposal_form.html'
|
template_name = 'speakerproposal_form.html'
|
||||||
|
@ -96,6 +110,15 @@ class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwn
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
||||||
messages.warning(self.request, "Your speaker proposal has been reverted to status draft. Please submit it again when you are ready.")
|
messages.warning(self.request, "Your speaker proposal has been reverted to status draft. Please submit it again when you are ready.")
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
||||||
|
|
||||||
|
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
||||||
|
messages.warning(self.request, "Your speaker proposal has been set to modified after approval. Please await approval of the changes.")
|
||||||
|
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_MODIFIED_AFTER_APPROVAL
|
||||||
|
if not add_speakerproposal_updated_email(form.instance):
|
||||||
|
logger.error(
|
||||||
|
'Unable to add update email to queue for speaker: {}'.format(form.instance)
|
||||||
|
)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,7 +172,7 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposalM
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureUnapprovedProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView):
|
||||||
model = models.EventProposal
|
model = models.EventProposal
|
||||||
fields = ['title', 'abstract', 'event_type', 'speakers']
|
fields = ['title', 'abstract', 'event_type', 'speakers']
|
||||||
template_name = 'eventproposal_form.html'
|
template_name = 'eventproposal_form.html'
|
||||||
|
@ -161,6 +184,15 @@ class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsP
|
||||||
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_PENDING:
|
||||||
messages.warning(self.request, "Your event proposal has been reverted to status draft. Please submit it again when you are ready.")
|
messages.warning(self.request, "Your event proposal has been reverted to status draft. Please submit it again when you are ready.")
|
||||||
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_DRAFT
|
||||||
|
|
||||||
|
if form.instance.proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED:
|
||||||
|
messages.warning(self.request, "Your event proposal has been set to status modified after approval. Please await approval of the changes.")
|
||||||
|
form.instance.proposal_status = models.UserSubmittedModel.PROPOSAL_MODIFIED_AFTER_APPROVAL
|
||||||
|
if not add_eventproposal_updated_email(form.instance):
|
||||||
|
logger.error(
|
||||||
|
'Unable to add update email to queue for event: {}'.format(form.instance)
|
||||||
|
)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue