diff --git a/src/program/email.py b/src/program/email.py new file mode 100644 index 00000000..661ba9c6 --- /dev/null +++ b/src/program/email.py @@ -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 diff --git a/src/program/models.py b/src/program/models.py index 947238aa..b395ef20 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -1,10 +1,15 @@ import uuid import os +import icalendar +import CommonMark +import logging + from datetime import timedelta from django.contrib.postgres.fields import DateTimeRangeField from django.contrib import messages from django.db import models +from django.dispatch import receiver from django.utils.text import slugify from django.conf import settings from django.core.exceptions import ValidationError @@ -13,11 +18,11 @@ from django.core.files.storage import FileSystemStorage from django.urls import reverse from django.apps import apps from django.core.files.base import ContentFile - -import icalendar -import CommonMark +from django.db.models.signals import post_save 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): @@ -80,12 +85,14 @@ class UserSubmittedModel(CampRelatedModel): PROPOSAL_PENDING = 'pending' PROPOSAL_APPROVED = 'approved' PROPOSAL_REJECTED = 'rejected' + PROPOSAL_MODIFIED_AFTER_APPROVAL = 'modified after approval' PROPOSAL_STATUSES = [ PROPOSAL_DRAFT, PROPOSAL_PENDING, PROPOSAL_APPROVED, - PROPOSAL_REJECTED + PROPOSAL_REJECTED, + PROPOSAL_MODIFIED_AFTER_APPROVAL ] PROPOSAL_STATUS_CHOICES = [ @@ -93,6 +100,7 @@ class UserSubmittedModel(CampRelatedModel): (PROPOSAL_PENDING, 'Pending approval'), (PROPOSAL_APPROVED, 'Approved'), (PROPOSAL_REJECTED, 'Rejected'), + (PROPOSAL_MODIFIED_AFTER_APPROVAL, 'Modified after approval'), ] proposal_status = models.CharField( @@ -265,6 +273,21 @@ class EventProposal(UserSubmittedModel): 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) + ) + ############################################################################### diff --git a/src/program/templates/emails/new_eventproposal.html b/src/program/templates/emails/new_eventproposal.html new file mode 100644 index 00000000..b2611c02 --- /dev/null +++ b/src/program/templates/emails/new_eventproposal.html @@ -0,0 +1,10 @@ +Hello!
+
+Event {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}. +
+
+More info here. +
+Best regards,
+
+The BornHack Team
diff --git a/src/program/templates/emails/new_eventproposal.txt b/src/program/templates/emails/new_eventproposal.txt new file mode 100644 index 00000000..aef5618c --- /dev/null +++ b/src/program/templates/emails/new_eventproposal.txt @@ -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 diff --git a/src/program/templates/emails/new_speakerproposal.html b/src/program/templates/emails/new_speakerproposal.html new file mode 100644 index 00000000..277f5be8 --- /dev/null +++ b/src/program/templates/emails/new_speakerproposal.html @@ -0,0 +1,10 @@ +Hello!
+
+Speaker {{ proposal.name }} was just submitted as a new proposal for {{ proposal.camp }}. +
+
+More info here. +
+Best regards,
+
+The BornHack Team
diff --git a/src/program/templates/emails/new_speakerproposal.txt b/src/program/templates/emails/new_speakerproposal.txt new file mode 100644 index 00000000..9078b823 --- /dev/null +++ b/src/program/templates/emails/new_speakerproposal.txt @@ -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 diff --git a/src/program/templates/emails/update_eventproposal.html b/src/program/templates/emails/update_eventproposal.html new file mode 100644 index 00000000..c701d535 --- /dev/null +++ b/src/program/templates/emails/update_eventproposal.html @@ -0,0 +1,10 @@ +Hello!
+
+Event {{ proposal.name }} for {{ proposal.camp }} was just updated! +
+
+More info here. +
+Best regards,
+
+The BornHack Team
diff --git a/src/program/templates/emails/update_eventproposal.txt b/src/program/templates/emails/update_eventproposal.txt new file mode 100644 index 00000000..f0c87ce3 --- /dev/null +++ b/src/program/templates/emails/update_eventproposal.txt @@ -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 diff --git a/src/program/templates/emails/update_speakerproposal.html b/src/program/templates/emails/update_speakerproposal.html new file mode 100644 index 00000000..838e9b6e --- /dev/null +++ b/src/program/templates/emails/update_speakerproposal.html @@ -0,0 +1,10 @@ +Hello!
+
+Speaker {{ proposal.name }} for {{ proposal.camp }} was just updated! +
+
+More info here. +
+Best regards,
+
+The BornHack Team
diff --git a/src/program/templates/emails/update_speakerproposal.txt b/src/program/templates/emails/update_speakerproposal.txt new file mode 100644 index 00000000..c5d2e46a --- /dev/null +++ b/src/program/templates/emails/update_speakerproposal.txt @@ -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 diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index d1ebb644..7ac15b6b 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -24,11 +24,7 @@ Proposals | {{ block.super }} Details {% if not camp.read_only %} - {% if speakerproposal.proposal_status == "approved" %} - Modify - {% else %} - Modify - {% endif %} + Modify {% if speakerproposal.proposal_status == "pending" or speakerproposal.proposal_status == "approved" %} Submit {% else %} diff --git a/src/program/views.py b/src/program/views.py index f1e5becd..523e6b43 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -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.edit import CreateView, UpdateView @@ -8,14 +10,26 @@ from django.http import Http404, HttpResponse from django.utils.decorators import method_decorator from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages -from django.shortcuts import redirect from django.urls import reverse import icalendar 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 +import logging +logger = logging.getLogger("bornhack.%s" % __name__) ############## ical calendar ######################################################## @@ -84,7 +98,7 @@ class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposa 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 fields = ['name', 'biography', 'picture_small', 'picture_large'] template_name = 'speakerproposal_form.html' @@ -96,6 +110,15 @@ class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwn 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.") 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) @@ -149,7 +172,7 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, CreateProposalM return context -class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureUnapprovedProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView): +class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EnsureCFSOpenMixin, UpdateView): model = models.EventProposal fields = ['title', 'abstract', 'event_type', 'speakers'] template_name = 'eventproposal_form.html' @@ -161,6 +184,15 @@ class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsP 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.") 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)