From 003089f6f5810a679861c9b44d4e73e23bd44bf2 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 16 Jul 2020 15:12:47 +0200 Subject: [PATCH 2/2] agendas: add email notifications for events (#44158) --- .../commands/send_email_notifications.py | 54 +++++++++ .../migrations/0054_auto_20200716_1515.py | 98 ++++++++++++++++ chrono/agendas/models.py | 108 ++++++++++++++++++ chrono/manager/forms.py | 18 +++ .../manager_agenda_notifications_form.html | 34 ++++++ .../manager_events_agenda_settings.html | 18 +++ chrono/manager/urls.py | 5 + chrono/manager/views.py | 23 +++- tests/settings.py | 2 + tests/test_agendas.py | 93 ++++++++++++++- tests/test_import_export.py | 27 +++++ tests/test_manager.py | 73 +++++++++++- 12 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 chrono/agendas/management/commands/send_email_notifications.py create mode 100644 chrono/agendas/migrations/0054_auto_20200716_1515.py create mode 100644 chrono/manager/templates/chrono/manager_agenda_notifications_form.html diff --git a/chrono/agendas/management/commands/send_email_notifications.py b/chrono/agendas/management/commands/send_email_notifications.py new file mode 100644 index 0000000..a524a67 --- /dev/null +++ b/chrono/agendas/management/commands/send_email_notifications.py @@ -0,0 +1,54 @@ +# chrono - agendas system +# Copyright (C) 2020 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from urllib.parse import urljoin + +from django.conf import settings as django_settings +from django.core.mail import send_mail +from django.core.management.base import BaseCommand +from django.db.transaction import atomic +from django.utils.translation import ugettext_lazy as _ + +from chrono.agendas.models import AgendaNotificationsSettings + + +class Command(BaseCommand): + help = 'Send email notifications' + + def handle(self, **options): + notifications_settings = AgendaNotificationsSettings.objects.all() + for settings in notifications_settings.select_related('agenda'): + for setting, recipients in settings: + if not recipients: + continue + + status = setting.replace('_event', '') + events = settings.agenda.event_set.filter(**{status: True, 'was_' + status: False}) + for event in events: + self.send_notification(event, status, recipients) + + def send_notification(self, event, status, recipients): + with atomic(): + setattr(event, 'was_' + status, True) + event.save() + subject = _('Alert: event "%(event)s" is %(status)s') % { + 'event': event, + 'status': status.replace('_', ' '), + } + body = _('You can view it here: %s.') % urljoin( + django_settings.SITE_BASE_URL, event.get_absolute_view_url() + ) + send_mail(subject, body, django_settings.DEFAULT_FROM_EMAIL, recipients) diff --git a/chrono/agendas/migrations/0054_auto_20200716_1515.py b/chrono/agendas/migrations/0054_auto_20200716_1515.py new file mode 100644 index 0000000..84945c1 --- /dev/null +++ b/chrono/agendas/migrations/0054_auto_20200716_1515.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-07-16 13:15 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0053_event_almost_full'), + ] + + operations = [ + migrations.CreateModel( + name='AgendaNotificationsSettings', + fields=[ + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ( + 'almost_full_event', + models.CharField( + blank=True, + choices=[ + ('edit-role', 'Edit Role'), + ('view-role', 'View Role'), + ('use-email-field', 'Specify email addresses manually'), + ], + max_length=16, + verbose_name='Almost full event (90%)', + ), + ), + ( + 'almost_full_event_emails', + django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, null=True, size=None + ), + ), + ( + 'full_event', + models.CharField( + blank=True, + choices=[ + ('edit-role', 'Edit Role'), + ('view-role', 'View Role'), + ('use-email-field', 'Specify email addresses manually'), + ], + max_length=16, + verbose_name='Full event', + ), + ), + ( + 'full_event_emails', + django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, null=True, size=None + ), + ), + ( + 'cancelled_event', + models.CharField( + blank=True, + choices=[ + ('edit-role', 'Edit Role'), + ('view-role', 'View Role'), + ('use-email-field', 'Specify email addresses manually'), + ], + max_length=16, + verbose_name='Cancelled event', + ), + ), + ( + 'cancelled_event_emails', + django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, null=True, size=None + ), + ), + ( + 'agenda', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='notifications_settings', + to='agendas.Agenda', + ), + ), + ], + ), + migrations.AddField( + model_name='event', name='was_almost_full', field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='event', name='was_cancelled', field=models.BooleanField(default=False), + ), + migrations.AddField(model_name='event', name='was_full', field=models.BooleanField(default=False),), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 7bda62f..c5fb2ef 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -29,6 +29,7 @@ import vobject import django from django.conf import settings from django.contrib.auth.models import Group +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator @@ -255,6 +256,8 @@ class Agenda(models.Model): } if self.kind == 'events': agenda['events'] = [x.export_json() for x in self.event_set.all()] + if hasattr(self, 'notifications_settings'): + agenda['notifications_settings'] = self.notifications_settings.export_json() elif self.kind == 'meetings': agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()] agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()] @@ -269,6 +272,7 @@ class Agenda(models.Model): permissions = data.pop('permissions') or {} if data['kind'] == 'events': events = data.pop('events') + notifications_settings = data.pop('notifications_settings', None) elif data['kind'] == 'meetings': meetingtypes = data.pop('meetingtypes') desks = data.pop('desks') @@ -291,9 +295,13 @@ class Agenda(models.Model): if data['kind'] == 'events': if overwrite: Event.objects.filter(agenda=agenda).delete() + AgendaNotificationsSettings.objects.filter(agenda=agenda).delete() for event_data in events: event_data['agenda'] = agenda Event.import_json(event_data).save() + if notifications_settings: + notifications_settings['agenda'] = agenda + AgendaNotificationsSettings.import_json(notifications_settings).save() elif data['kind'] == 'meetings': if overwrite: MeetingType.objects.filter(agenda=agenda).delete() @@ -758,6 +766,11 @@ class Event(models.Model): desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE) resources = models.ManyToManyField('Resource') + # flags for detecting changes + was_almost_full = models.BooleanField(default=False) + was_full = models.BooleanField(default=False) + was_cancelled = models.BooleanField(default=False) + class Meta: ordering = ['agenda', 'start_datetime', 'duration', 'label'] unique_together = ('agenda', 'slug') @@ -864,6 +877,9 @@ class Event(models.Model): def get_absolute_url(self): return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) + def get_absolute_view_url(self): + return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) + @classmethod def import_json(cls, data): data['start_datetime'] = make_aware( @@ -1378,3 +1394,95 @@ class TimePeriodException(models.Model): def as_interval(self): '''Simplify insertion into IntervalSet''' return Interval(self.start_datetime, self.end_datetime) + + +class AgendaNotificationsSettings(models.Model): + EMAIL_FIELD = 'use-email-field' + VIEW_ROLE = 'view-role' + EDIT_ROLE = 'edit-role' + + CHOICES = [ + (EDIT_ROLE, _('Edit Role')), + (VIEW_ROLE, _('View Role')), + (EMAIL_FIELD, _('Specify email addresses manually')), + ] + + agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings') + + almost_full_event = models.CharField( + max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)') + ) + almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) + + full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event')) + full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) + + cancelled_event = models.CharField( + max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event') + ) + cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True) + + def __iter__(self): + for field in self.get_setting_fields(): + yield (field.name, self.get_recipients(field.name)) + + @classmethod + def get_setting_fields(cls): + return [field for field in cls._meta.get_fields() if isinstance(field, models.CharField)] + + @classmethod + def get_email_field_names(cls): + return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)] + + def get_recipients(self, setting): + value = getattr(self, setting) + if not value: + return [] + + if value == self.EMAIL_FIELD: + return getattr(self, setting + '_emails') + + role = self.get_role_from_choice(value) + if not role or not hasattr(role, 'role'): + return [] + emails = role.role.emails + if role.role.emails_to_members: + emails.extend(role.user_set.values_list('email', flat=True)) + return emails + + @property + def display_info(self): + for field in self.get_setting_fields(): + choice = getattr(self, field.name) + if not choice: + continue + + if choice == self.EMAIL_FIELD: + emails = getattr(self, field.name + '_emails') + yield (field.verbose_name, ', '.join(emails)) + else: + role = self.get_role_from_choice(choice) + if role: + display_name = getattr(self, 'get_' + field.name + '_display')() + yield (field.verbose_name, '%s (%s)' % (display_name, role)) + + def get_role_from_choice(self, choice): + if choice == self.EDIT_ROLE: + return self.agenda.edit_role + elif choice == self.VIEW_ROLE: + return self.agenda.view_role + + @classmethod + def import_json(cls, data): + data = clean_import_data(cls, data) + return cls(**data) + + def export_json(self): + return { + 'almost_full_event': self.almost_full_event, + 'almost_full_event_emails': self.almost_full_event_emails, + 'full_event': self.full_event, + 'full_event_emails': self.full_event_emails, + 'cancelled_event': self.cancelled_event, + 'cancelled_event_emails': self.cancelled_event_emails, + } diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 3b10311..b94ce6b 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -39,6 +39,7 @@ from chrono.agendas.models import ( TimePeriodExceptionSource, VirtualMember, Resource, + AgendaNotificationsSettings, WEEKDAYS_LIST, ) @@ -449,3 +450,20 @@ class BookingCancelForm(forms.ModelForm): class Meta: model = Booking fields = [] + + +class AgendaNotificationsForm(forms.ModelForm): + class Meta: + model = AgendaNotificationsSettings + fields = '__all__' + widgets = { + 'agenda': forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for email_field in AgendaNotificationsSettings.get_email_field_names(): + self.fields[email_field].widget.attrs['size'] = 80 + self.fields[email_field].label = '' + self.fields[email_field].help_text = _('Enter a comma separated list of email addresses.') diff --git a/chrono/manager/templates/chrono/manager_agenda_notifications_form.html b/chrono/manager/templates/chrono/manager_agenda_notifications_form.html new file mode 100644 index 0000000..7716520 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_agenda_notifications_form.html @@ -0,0 +1,34 @@ +{% extends "chrono/manager_agenda_view.html" %} +{% load i18n %} + +{% block breadcrumb %} +{{ block.super }} +{% trans "Notification settings" %} +{% endblock %} + +{% block appbar %} +

{% trans "Notification settings" %}

+{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% trans 'Cancel' %} +
+ + +
+{% endblock %} diff --git a/chrono/manager/templates/chrono/manager_events_agenda_settings.html b/chrono/manager/templates/chrono/manager_events_agenda_settings.html index e9567aa..0752ca6 100644 --- a/chrono/manager/templates/chrono/manager_events_agenda_settings.html +++ b/chrono/manager/templates/chrono/manager_events_agenda_settings.html @@ -30,4 +30,22 @@ +
+

{% trans "Notifications" %}

+
+
    +{% for setting, value in object.notifications_settings.display_info %} +
  • + {% blocktrans %} + {{ setting }}: {{ value }} will be notified. + {% endblocktrans %} +
  • +{% empty %} +{% trans "Notifications are disabled for this agenda." %} +{% endfor %} +
+{% trans 'Configure' %} +
+
+ {% endblock %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index 53c98a8..fe1ca5e 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -59,6 +59,11 @@ urlpatterns = [ views.agenda_import_events, name='chrono-manager-agenda-import-events', ), + url( + r'^agendas/(?P\d+)/notifications$', + views.agenda_notifications_settings, + name='chrono-manager-agenda-notifications-settings', + ), url( r'^agendas/(?P\d+)/events/(?P\d+)/$', views.event_view, diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 0b33ab0..8babdbf 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -23,7 +23,7 @@ import uuid from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.db.models import Q +from django.db.models import Q, F from django.db.models import Min, Max from django.forms import ValidationError from django.http import Http404, HttpResponse, HttpResponseRedirect @@ -61,6 +61,7 @@ from chrono.agendas.models import ( TimePeriodExceptionSource, VirtualMember, Resource, + AgendaNotificationsSettings, ) from .forms import ( @@ -85,6 +86,7 @@ from .forms import ( AgendaResourceForm, AgendaDuplicateForm, BookingCancelForm, + AgendaNotificationsForm, ) from .utils import import_site @@ -1213,6 +1215,25 @@ class AgendaImportEventsView(ManagedAgendaMixin, FormView): agenda_import_events = AgendaImportEventsView.as_view() +class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView): + template_name = 'chrono/manager_agenda_notifications_form.html' + model = AgendaNotificationsSettings + form_class = AgendaNotificationsForm + + def get_object(self): + try: + return self.agenda.notifications_settings + except AgendaNotificationsSettings.DoesNotExist: + # prevent old events from sending notifications + statuses = ('almost_full', 'full', 'cancelled') + kwargs = {'was_' + status: F(status) for status in statuses} + self.agenda.event_set.update(**kwargs) + return AgendaNotificationsSettings.objects.create(agenda=self.agenda) + + +agenda_notifications_settings = AgendaNotificationsSettingsView.as_view() + + class EventDetailView(ViewableAgendaMixin, DetailView): model = Event pk_url_kwarg = 'event_pk' diff --git a/tests/settings.py b/tests/settings.py index d1ace55..cb105be 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,3 +25,5 @@ KNOWN_SERVICES = { } }, } + +SITE_BASE_URL = 'https://example.com' diff --git a/tests/test_agendas.py b/tests/test_agendas.py index d7d020f..a6bf174 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -4,7 +4,7 @@ import mock import requests -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.core.files.base import ContentFile from django.core.management import call_command from django.utils.timezone import localtime, make_aware, now @@ -21,6 +21,7 @@ from chrono.agendas.models import ( TimePeriodException, TimePeriodExceptionSource, VirtualMember, + AgendaNotificationsSettings, ) pytestmark = pytest.mark.django_db @@ -987,3 +988,93 @@ def test_agenda_virtual_duplicate(): assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists() assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists() + + +@mock.patch('django.contrib.auth.models.Group.role', create=True) +@pytest.mark.parametrize( + 'emails_to_members,emails', + [(False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']),], +) +def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox): + group = Group.objects.create(name='group') + user = User.objects.create(username='user', email='user@entrouvert.com') + user.groups.add(group) + mocked_role.emails_to_members = emails_to_members + mocked_role.emails = emails + expected_recipients = emails + if emails_to_members: + expected_recipients.append(user.email) + expected_email_count = 1 if emails else 0 + + agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group) + + event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') + settings = AgendaNotificationsSettings.objects.create(agenda=agenda) + settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE + settings.save() + + # book 9/10 places to reach almost full state + for i in range(9): + Booking.objects.create(event=event) + event.refresh_from_db() + assert event.almost_full + + call_command('send_email_notifications') + assert len(mailoutbox) == expected_email_count + if mailoutbox: + assert mailoutbox[0].recipients() == expected_recipients + assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full' + + # no new email on subsequent run + call_command('send_email_notifications') + assert len(mailoutbox) == expected_email_count + + +def test_agenda_notifications_email_list(mailoutbox): + agenda = Agenda.objects.create(label='Foo bar', kind='event') + + event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') + settings = AgendaNotificationsSettings.objects.create(agenda=agenda) + settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD + settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] + settings.save() + + for i in range(10): + Booking.objects.create(event=event) + event.refresh_from_db() + assert event.full + + call_command('send_email_notifications') + assert len(mailoutbox) == 1 + assert mailoutbox[0].recipients() == recipients + assert mailoutbox[0].subject == 'Alert: event "Hop" is full' + assert mailoutbox[0].body == 'You can view it here: https://example.com/manage/agendas/%s/events/%s/.' % ( + agenda.pk, + event.pk, + ) + + # no new email on subsequent run + call_command('send_email_notifications') + assert len(mailoutbox) == 1 + + +def test_agenda_notifications_cancelled(mailoutbox): + agenda = Agenda.objects.create(label='Foo bar', kind='event') + + event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') + settings = AgendaNotificationsSettings.objects.create(agenda=agenda) + settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD + settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] + settings.save() + + event.cancelled = True + event.save() + + call_command('send_email_notifications') + assert len(mailoutbox) == 1 + assert mailoutbox[0].recipients() == recipients + assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled' + + # no new email on subsequent run + call_command('send_email_notifications') + assert len(mailoutbox) == 1 diff --git a/tests/test_import_export.py b/tests/test_import_export.py index ef7a137..4303db2 100644 --- a/tests/test_import_export.py +++ b/tests/test_import_export.py @@ -26,6 +26,7 @@ from chrono.agendas.models import ( AgendaImportError, MeetingType, VirtualMember, + AgendaNotificationsSettings, ) from chrono.manager.utils import import_site @@ -361,3 +362,29 @@ def test_import_export_desk_unknown_fields(app, some_data, meetings_agenda): assert Desk.objects.exists() is True assert TimePeriod.objects.exists() is True assert TimePeriodException.objects.exists() is True + + +def test_import_export_notification_settings(): + agenda = Agenda.objects.create(label='Foo bar', kind='events') + settings = AgendaNotificationsSettings.objects.create( + agenda=agenda, + almost_full_event=AgendaNotificationsSettings.EDIT_ROLE, + full_event=AgendaNotificationsSettings.VIEW_ROLE, + cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD, + cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'], + ) + output = get_output_of_command('export_site') + payload = json.loads(output) + + agenda.delete() + assert not AgendaNotificationsSettings.objects.exists() + + import_site(payload) + agenda = Agenda.objects.first() + AgendaNotificationsSettings.objects.get( + agenda=agenda, + almost_full_event=AgendaNotificationsSettings.EDIT_ROLE, + full_event=AgendaNotificationsSettings.VIEW_ROLE, + cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD, + cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'], + ) diff --git a/tests/test_manager.py b/tests/test_manager.py index 9d176d6..d92cd5e 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -9,6 +9,7 @@ import mock import os from django.contrib.auth.models import User, Group +from django.core.management import call_command from django.db import connection from django.test.utils import CaptureQueriesContext from django.utils.encoding import force_text @@ -47,15 +48,18 @@ def simple_user(): @pytest.fixture -def manager_user(): +def managers_group(): + group, _ = Group.objects.get_or_create(name='Managers') + return group + + +@pytest.fixture +def manager_user(managers_group): try: user = User.objects.get(username='manager') except User.DoesNotExist: user = User.objects.create_user('manager', password='manager') - group, created = Group.objects.get_or_create(name='Managers') - if created: - group.save() - user.groups.set([group]) + user.groups.set([managers_group]) return user @@ -3496,3 +3500,62 @@ def test_event_cancellation(app, admin_user): assert 'Cancelled' in resp.text assert '0 booked places' in resp.text assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2 + + +def test_agenda_notifications(app, admin_user, managers_group): + agenda = Agenda.objects.create(label='Events', kind='events') + + login(app) + resp = app.get('/manage/agendas/%s/settings' % agenda.id) + + assert 'Notifications' in resp.text + assert 'Notifications are disabled' in resp.text + + resp = resp.click('Configure') + resp.form['almost_full_event'] = 'edit-role' + resp.form['full_event'] = 'view-role' + resp.form['cancelled_event'] = 'use-email-field' + resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com' + resp = resp.form.submit().follow() + + settings = agenda.notifications_settings + assert settings.almost_full_event == 'edit-role' + assert settings.full_event == 'view-role' + assert settings.cancelled_event == 'use-email-field' + assert settings.cancelled_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com'] + + assert 'Cancelled event: hop@entrouvert.com, top@entrouvert.com will be notified' in resp.text + assert not 'Full event:' in resp.text + assert not 'Almost full event (90%):' in resp.text + + agenda.view_role = managers_group + agenda.edit_role = Group.objects.create(name='hop') + agenda.save() + + resp = app.get('/manage/agendas/%s/settings' % agenda.id) + assert 'Almost full event (90%): Edit Role (hop) will be notified' in resp.text + assert 'Full event: View Role (Managers) will be notified' in resp.text + + +def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox): + agenda = Agenda.objects.create(label='Events', kind='events') + event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Old event') + event.cancelled = True + event.save() + + login(app) + resp = app.get('/manage/agendas/%s/settings' % agenda.id) + + resp = resp.click('Configure') + resp.form['cancelled_event'] = 'use-email-field' + resp.form['cancelled_event_emails'] = 'hop@entrouvert.com' + resp.form.submit() + + event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event') + event.cancelled = True + event.save() + + call_command('send_email_notifications') + # no notification is sent for old event + assert len(mailoutbox) == 1 + assert 'New event' in mailoutbox[0].subject -- 2.20.1