Projet

Général

Profil

0002-agendas-add-email-notifications-for-events-44158.patch

Valentin Deniaud, 16 juillet 2020 15:29

Télécharger (29,7 ko)

Voir les différences:

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
chrono/agendas/management/commands/send_email_notifications.py
1
# chrono - agendas system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from urllib.parse import urljoin
18

  
19
from django.conf import settings as django_settings
20
from django.core.mail import send_mail
21
from django.core.management.base import BaseCommand
22
from django.db.transaction import atomic
23
from django.utils.translation import ugettext_lazy as _
24

  
25
from chrono.agendas.models import AgendaNotificationsSettings
26

  
27

  
28
class Command(BaseCommand):
29
    help = 'Send email notifications'
30

  
31
    def handle(self, **options):
32
        notifications_settings = AgendaNotificationsSettings.objects.all()
33
        for settings in notifications_settings.select_related('agenda'):
34
            for setting, recipients in settings:
35
                if not recipients:
36
                    continue
37

  
38
                status = setting.replace('_event', '')
39
                events = settings.agenda.event_set.filter(**{status: True, 'was_' + status: False})
40
                for event in events:
41
                    self.send_notification(event, status, recipients)
42

  
43
    def send_notification(self, event, status, recipients):
44
        with atomic():
45
            setattr(event, 'was_' + status, True)
46
            event.save()
47
            subject = _('Alert: event "%(event)s" is %(status)s') % {
48
                'event': event,
49
                'status': status.replace('_', ' '),
50
            }
51
            body = _('You can view it here: %s.') % urljoin(
52
                django_settings.SITE_BASE_URL, event.get_absolute_view_url()
53
            )
54
            send_mail(subject, body, django_settings.DEFAULT_FROM_EMAIL, recipients)
chrono/agendas/migrations/0054_auto_20200716_1515.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-16 13:15
3
from __future__ import unicode_literals
4

  
5
import django.contrib.postgres.fields
6
from django.db import migrations, models
7
import django.db.models.deletion
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('agendas', '0053_event_almost_full'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='AgendaNotificationsSettings',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                (
25
                    'almost_full_event',
26
                    models.CharField(
27
                        blank=True,
28
                        choices=[
29
                            ('edit-role', 'Edit Role'),
30
                            ('view-role', 'View Role'),
31
                            ('use-email-field', 'Specify email addresses manually'),
32
                        ],
33
                        max_length=16,
34
                        verbose_name='Almost full event (90%)',
35
                    ),
36
                ),
37
                (
38
                    'almost_full_event_emails',
39
                    django.contrib.postgres.fields.ArrayField(
40
                        base_field=models.EmailField(max_length=254), blank=True, null=True, size=None
41
                    ),
42
                ),
43
                (
44
                    'full_event',
45
                    models.CharField(
46
                        blank=True,
47
                        choices=[
48
                            ('edit-role', 'Edit Role'),
49
                            ('view-role', 'View Role'),
50
                            ('use-email-field', 'Specify email addresses manually'),
51
                        ],
52
                        max_length=16,
53
                        verbose_name='Full event',
54
                    ),
55
                ),
56
                (
57
                    'full_event_emails',
58
                    django.contrib.postgres.fields.ArrayField(
59
                        base_field=models.EmailField(max_length=254), blank=True, null=True, size=None
60
                    ),
61
                ),
62
                (
63
                    'cancelled_event',
64
                    models.CharField(
65
                        blank=True,
66
                        choices=[
67
                            ('edit-role', 'Edit Role'),
68
                            ('view-role', 'View Role'),
69
                            ('use-email-field', 'Specify email addresses manually'),
70
                        ],
71
                        max_length=16,
72
                        verbose_name='Cancelled event',
73
                    ),
74
                ),
75
                (
76
                    'cancelled_event_emails',
77
                    django.contrib.postgres.fields.ArrayField(
78
                        base_field=models.EmailField(max_length=254), blank=True, null=True, size=None
79
                    ),
80
                ),
81
                (
82
                    'agenda',
83
                    models.OneToOneField(
84
                        on_delete=django.db.models.deletion.CASCADE,
85
                        related_name='notifications_settings',
86
                        to='agendas.Agenda',
87
                    ),
88
                ),
89
            ],
90
        ),
91
        migrations.AddField(
92
            model_name='event', name='was_almost_full', field=models.BooleanField(default=False),
93
        ),
94
        migrations.AddField(
95
            model_name='event', name='was_cancelled', field=models.BooleanField(default=False),
96
        ),
97
        migrations.AddField(model_name='event', name='was_full', field=models.BooleanField(default=False),),
98
    ]
chrono/agendas/models.py
29 29
import django
30 30
from django.conf import settings
31 31
from django.contrib.auth.models import Group
32
from django.contrib.postgres.fields import ArrayField
32 33
from django.core.exceptions import FieldDoesNotExist
33 34
from django.core.exceptions import ValidationError
34 35
from django.core.validators import MaxValueValidator
......
255 256
        }
256 257
        if self.kind == 'events':
257 258
            agenda['events'] = [x.export_json() for x in self.event_set.all()]
259
            if hasattr(self, 'notifications_settings'):
260
                agenda['notifications_settings'] = self.notifications_settings.export_json()
258 261
        elif self.kind == 'meetings':
259 262
            agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()]
260 263
            agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
......
269 272
        permissions = data.pop('permissions') or {}
270 273
        if data['kind'] == 'events':
271 274
            events = data.pop('events')
275
            notifications_settings = data.pop('notifications_settings', None)
272 276
        elif data['kind'] == 'meetings':
273 277
            meetingtypes = data.pop('meetingtypes')
274 278
            desks = data.pop('desks')
......
291 295
        if data['kind'] == 'events':
292 296
            if overwrite:
293 297
                Event.objects.filter(agenda=agenda).delete()
298
                AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
294 299
            for event_data in events:
295 300
                event_data['agenda'] = agenda
296 301
                Event.import_json(event_data).save()
302
            if notifications_settings:
303
                notifications_settings['agenda'] = agenda
304
                AgendaNotificationsSettings.import_json(notifications_settings).save()
297 305
        elif data['kind'] == 'meetings':
298 306
            if overwrite:
299 307
                MeetingType.objects.filter(agenda=agenda).delete()
......
758 766
    desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
759 767
    resources = models.ManyToManyField('Resource')
760 768

  
769
    # flags for detecting changes
770
    was_almost_full = models.BooleanField(default=False)
771
    was_full = models.BooleanField(default=False)
772
    was_cancelled = models.BooleanField(default=False)
773

  
761 774
    class Meta:
762 775
        ordering = ['agenda', 'start_datetime', 'duration', 'label']
763 776
        unique_together = ('agenda', 'slug')
......
864 877
    def get_absolute_url(self):
865 878
        return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id})
866 879

  
880
    def get_absolute_view_url(self):
881
        return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.id})
882

  
867 883
    @classmethod
868 884
    def import_json(cls, data):
869 885
        data['start_datetime'] = make_aware(
......
1378 1394
    def as_interval(self):
1379 1395
        '''Simplify insertion into IntervalSet'''
1380 1396
        return Interval(self.start_datetime, self.end_datetime)
1397

  
1398

  
1399
class AgendaNotificationsSettings(models.Model):
1400
    EMAIL_FIELD = 'use-email-field'
1401
    VIEW_ROLE = 'view-role'
1402
    EDIT_ROLE = 'edit-role'
1403

  
1404
    CHOICES = [
1405
        (EDIT_ROLE, _('Edit Role')),
1406
        (VIEW_ROLE, _('View Role')),
1407
        (EMAIL_FIELD, _('Specify email addresses manually')),
1408
    ]
1409

  
1410
    agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings')
1411

  
1412
    almost_full_event = models.CharField(
1413
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)')
1414
    )
1415
    almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1416

  
1417
    full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event'))
1418
    full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1419

  
1420
    cancelled_event = models.CharField(
1421
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event')
1422
    )
1423
    cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1424

  
1425
    def __iter__(self):
1426
        for field in self.get_setting_fields():
1427
            yield (field.name, self.get_recipients(field.name))
1428

  
1429
    @classmethod
1430
    def get_setting_fields(cls):
1431
        return [field for field in cls._meta.get_fields() if isinstance(field, models.CharField)]
1432

  
1433
    @classmethod
1434
    def get_email_field_names(cls):
1435
        return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)]
1436

  
1437
    def get_recipients(self, setting):
1438
        value = getattr(self, setting)
1439
        if not value:
1440
            return []
1441

  
1442
        if value == self.EMAIL_FIELD:
1443
            return getattr(self, setting + '_emails')
1444

  
1445
        role = self.get_role_from_choice(value)
1446
        if not role or not hasattr(role, 'role'):
1447
            return []
1448
        emails = role.role.emails
1449
        if role.role.emails_to_members:
1450
            emails.extend(role.user_set.values_list('email', flat=True))
1451
        return emails
1452

  
1453
    @property
1454
    def display_info(self):
1455
        for field in self.get_setting_fields():
1456
            choice = getattr(self, field.name)
1457
            if not choice:
1458
                continue
1459

  
1460
            if choice == self.EMAIL_FIELD:
1461
                emails = getattr(self, field.name + '_emails')
1462
                yield (field.verbose_name, ', '.join(emails))
1463
            else:
1464
                role = self.get_role_from_choice(choice)
1465
                if role:
1466
                    display_name = getattr(self, 'get_' + field.name + '_display')()
1467
                    yield (field.verbose_name, '%s (%s)' % (display_name, role))
1468

  
1469
    def get_role_from_choice(self, choice):
1470
        if choice == self.EDIT_ROLE:
1471
            return self.agenda.edit_role
1472
        elif choice == self.VIEW_ROLE:
1473
            return self.agenda.view_role
1474

  
1475
    @classmethod
1476
    def import_json(cls, data):
1477
        data = clean_import_data(cls, data)
1478
        return cls(**data)
1479

  
1480
    def export_json(self):
1481
        return {
1482
            'almost_full_event': self.almost_full_event,
1483
            'almost_full_event_emails': self.almost_full_event_emails,
1484
            'full_event': self.full_event,
1485
            'full_event_emails': self.full_event_emails,
1486
            'cancelled_event': self.cancelled_event,
1487
            'cancelled_event_emails': self.cancelled_event_emails,
1488
        }
chrono/manager/forms.py
39 39
    TimePeriodExceptionSource,
40 40
    VirtualMember,
41 41
    Resource,
42
    AgendaNotificationsSettings,
42 43
    WEEKDAYS_LIST,
43 44
)
44 45

  
......
449 450
    class Meta:
450 451
        model = Booking
451 452
        fields = []
453

  
454

  
455
class AgendaNotificationsForm(forms.ModelForm):
456
    class Meta:
457
        model = AgendaNotificationsSettings
458
        fields = '__all__'
459
        widgets = {
460
            'agenda': forms.HiddenInput(),
461
        }
462

  
463
    def __init__(self, *args, **kwargs):
464
        super().__init__(*args, **kwargs)
465

  
466
        for email_field in AgendaNotificationsSettings.get_email_field_names():
467
            self.fields[email_field].widget.attrs['size'] = 80
468
            self.fields[email_field].label = ''
469
            self.fields[email_field].help_text = _('Enter a comma separated list of email addresses.')
chrono/manager/templates/chrono/manager_agenda_notifications_form.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="">{% trans "Notification settings" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Notification settings" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Save" %}</button>
19
    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
20
  </div>
21

  
22
  <script>
23
  $('select').change(function(){
24
    role_field_id = $(this).attr('id')
25
    email_field_id = '#' + role_field_id + '_emails'
26
    if ($(this).val() == 'use-email-field')
27
      $(email_field_id).parent('p').show();
28
    else
29
      $(email_field_id).parent('p').hide();
30
  });
31
  $('select').trigger('change');
32
  </script>
33
</form>
34
{% endblock %}
chrono/manager/templates/chrono/manager_events_agenda_settings.html
30 30
</div>
31 31
</div>
32 32

  
33
<div class="section">
34
<h3>{% trans "Notifications" %}</h3>
35
<div>
36
<ul>
37
{% for setting, value in object.notifications_settings.display_info %}
38
  <li>
39
  {% blocktrans %}
40
  {{ setting }}: {{ value }} will be notified.
41
  {% endblocktrans %}
42
  </li>
43
{% empty %}
44
{% trans "Notifications are disabled for this agenda." %}
45
{% endfor %}
46
</ul>
47
<a rel="popup" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>
48
</div>
49
</div>
50

  
33 51
{% endblock %}
chrono/manager/urls.py
59 59
        views.agenda_import_events,
60 60
        name='chrono-manager-agenda-import-events',
61 61
    ),
62
    url(
63
        r'^agendas/(?P<pk>\d+)/notifications$',
64
        views.agenda_notifications_settings,
65
        name='chrono-manager-agenda-notifications-settings',
66
    ),
62 67
    url(
63 68
        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$',
64 69
        views.event_view,
chrono/manager/views.py
23 23

  
24 24
from django.contrib import messages
25 25
from django.core.exceptions import PermissionDenied
26
from django.db.models import Q
26
from django.db.models import Q, F
27 27
from django.db.models import Min, Max
28 28
from django.forms import ValidationError
29 29
from django.http import Http404, HttpResponse, HttpResponseRedirect
......
61 61
    TimePeriodExceptionSource,
62 62
    VirtualMember,
63 63
    Resource,
64
    AgendaNotificationsSettings,
64 65
)
65 66

  
66 67
from .forms import (
......
85 86
    AgendaResourceForm,
86 87
    AgendaDuplicateForm,
87 88
    BookingCancelForm,
89
    AgendaNotificationsForm,
88 90
)
89 91
from .utils import import_site
90 92

  
......
1213 1215
agenda_import_events = AgendaImportEventsView.as_view()
1214 1216

  
1215 1217

  
1218
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView):
1219
    template_name = 'chrono/manager_agenda_notifications_form.html'
1220
    model = AgendaNotificationsSettings
1221
    form_class = AgendaNotificationsForm
1222

  
1223
    def get_object(self):
1224
        try:
1225
            return self.agenda.notifications_settings
1226
        except AgendaNotificationsSettings.DoesNotExist:
1227
            # prevent old events from sending notifications
1228
            statuses = ('almost_full', 'full', 'cancelled')
1229
            kwargs = {'was_' + status: F(status) for status in statuses}
1230
            self.agenda.event_set.update(**kwargs)
1231
            return AgendaNotificationsSettings.objects.create(agenda=self.agenda)
1232

  
1233

  
1234
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
1235

  
1236

  
1216 1237
class EventDetailView(ViewableAgendaMixin, DetailView):
1217 1238
    model = Event
1218 1239
    pk_url_kwarg = 'event_pk'
tests/settings.py
25 25
        }
26 26
    },
27 27
}
28

  
29
SITE_BASE_URL = 'https://example.com'
tests/test_agendas.py
4 4
import requests
5 5

  
6 6

  
7
from django.contrib.auth.models import Group
7
from django.contrib.auth.models import Group, User
8 8
from django.core.files.base import ContentFile
9 9
from django.core.management import call_command
10 10
from django.utils.timezone import localtime, make_aware, now
......
21 21
    TimePeriodException,
22 22
    TimePeriodExceptionSource,
23 23
    VirtualMember,
24
    AgendaNotificationsSettings,
24 25
)
25 26

  
26 27
pytestmark = pytest.mark.django_db
......
987 988

  
988 989
    assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists()
989 990
    assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists()
991

  
992

  
993
@mock.patch('django.contrib.auth.models.Group.role', create=True)
994
@pytest.mark.parametrize(
995
    'emails_to_members,emails',
996
    [(False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']),],
997
)
998
def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox):
999
    group = Group.objects.create(name='group')
1000
    user = User.objects.create(username='user', email='user@entrouvert.com')
1001
    user.groups.add(group)
1002
    mocked_role.emails_to_members = emails_to_members
1003
    mocked_role.emails = emails
1004
    expected_recipients = emails
1005
    if emails_to_members:
1006
        expected_recipients.append(user.email)
1007
    expected_email_count = 1 if emails else 0
1008

  
1009
    agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group)
1010

  
1011
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1012
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1013
    settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE
1014
    settings.save()
1015

  
1016
    # book 9/10 places to reach almost full state
1017
    for i in range(9):
1018
        Booking.objects.create(event=event)
1019
    event.refresh_from_db()
1020
    assert event.almost_full
1021

  
1022
    call_command('send_email_notifications')
1023
    assert len(mailoutbox) == expected_email_count
1024
    if mailoutbox:
1025
        assert mailoutbox[0].recipients() == expected_recipients
1026
        assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full'
1027

  
1028
    # no new email on subsequent run
1029
    call_command('send_email_notifications')
1030
    assert len(mailoutbox) == expected_email_count
1031

  
1032

  
1033
def test_agenda_notifications_email_list(mailoutbox):
1034
    agenda = Agenda.objects.create(label='Foo bar', kind='event')
1035

  
1036
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1037
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1038
    settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD
1039
    settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
1040
    settings.save()
1041

  
1042
    for i in range(10):
1043
        Booking.objects.create(event=event)
1044
    event.refresh_from_db()
1045
    assert event.full
1046

  
1047
    call_command('send_email_notifications')
1048
    assert len(mailoutbox) == 1
1049
    assert mailoutbox[0].recipients() == recipients
1050
    assert mailoutbox[0].subject == 'Alert: event "Hop" is full'
1051
    assert mailoutbox[0].body == 'You can view it here: https://example.com/manage/agendas/%s/events/%s/.' % (
1052
        agenda.pk,
1053
        event.pk,
1054
    )
1055

  
1056
    # no new email on subsequent run
1057
    call_command('send_email_notifications')
1058
    assert len(mailoutbox) == 1
1059

  
1060

  
1061
def test_agenda_notifications_cancelled(mailoutbox):
1062
    agenda = Agenda.objects.create(label='Foo bar', kind='event')
1063

  
1064
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1065
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1066
    settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD
1067
    settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
1068
    settings.save()
1069

  
1070
    event.cancelled = True
1071
    event.save()
1072

  
1073
    call_command('send_email_notifications')
1074
    assert len(mailoutbox) == 1
1075
    assert mailoutbox[0].recipients() == recipients
1076
    assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled'
1077

  
1078
    # no new email on subsequent run
1079
    call_command('send_email_notifications')
1080
    assert len(mailoutbox) == 1
tests/test_import_export.py
26 26
    AgendaImportError,
27 27
    MeetingType,
28 28
    VirtualMember,
29
    AgendaNotificationsSettings,
29 30
)
30 31
from chrono.manager.utils import import_site
31 32

  
......
361 362
    assert Desk.objects.exists() is True
362 363
    assert TimePeriod.objects.exists() is True
363 364
    assert TimePeriodException.objects.exists() is True
365

  
366

  
367
def test_import_export_notification_settings():
368
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
369
    settings = AgendaNotificationsSettings.objects.create(
370
        agenda=agenda,
371
        almost_full_event=AgendaNotificationsSettings.EDIT_ROLE,
372
        full_event=AgendaNotificationsSettings.VIEW_ROLE,
373
        cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
374
        cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
375
    )
376
    output = get_output_of_command('export_site')
377
    payload = json.loads(output)
378

  
379
    agenda.delete()
380
    assert not AgendaNotificationsSettings.objects.exists()
381

  
382
    import_site(payload)
383
    agenda = Agenda.objects.first()
384
    AgendaNotificationsSettings.objects.get(
385
        agenda=agenda,
386
        almost_full_event=AgendaNotificationsSettings.EDIT_ROLE,
387
        full_event=AgendaNotificationsSettings.VIEW_ROLE,
388
        cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
389
        cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
390
    )
tests/test_manager.py
9 9
import os
10 10

  
11 11
from django.contrib.auth.models import User, Group
12
from django.core.management import call_command
12 13
from django.db import connection
13 14
from django.test.utils import CaptureQueriesContext
14 15
from django.utils.encoding import force_text
......
47 48

  
48 49

  
49 50
@pytest.fixture
50
def manager_user():
51
def managers_group():
52
    group, _ = Group.objects.get_or_create(name='Managers')
53
    return group
54

  
55

  
56
@pytest.fixture
57
def manager_user(managers_group):
51 58
    try:
52 59
        user = User.objects.get(username='manager')
53 60
    except User.DoesNotExist:
54 61
        user = User.objects.create_user('manager', password='manager')
55
    group, created = Group.objects.get_or_create(name='Managers')
56
    if created:
57
        group.save()
58
    user.groups.set([group])
62
    user.groups.set([managers_group])
59 63
    return user
60 64

  
61 65

  
......
3496 3500
    assert 'Cancelled' in resp.text
3497 3501
    assert '0 booked places' in resp.text
3498 3502
    assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2
3503

  
3504

  
3505
def test_agenda_notifications(app, admin_user, managers_group):
3506
    agenda = Agenda.objects.create(label='Events', kind='events')
3507

  
3508
    login(app)
3509
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3510

  
3511
    assert 'Notifications' in resp.text
3512
    assert 'Notifications are disabled' in resp.text
3513

  
3514
    resp = resp.click('Configure')
3515
    resp.form['almost_full_event'] = 'edit-role'
3516
    resp.form['full_event'] = 'view-role'
3517
    resp.form['cancelled_event'] = 'use-email-field'
3518
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com'
3519
    resp = resp.form.submit().follow()
3520

  
3521
    settings = agenda.notifications_settings
3522
    assert settings.almost_full_event == 'edit-role'
3523
    assert settings.full_event == 'view-role'
3524
    assert settings.cancelled_event == 'use-email-field'
3525
    assert settings.cancelled_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com']
3526

  
3527
    assert 'Cancelled event: hop@entrouvert.com, top@entrouvert.com will be notified' in resp.text
3528
    assert not 'Full event:' in resp.text
3529
    assert not 'Almost full event (90%):' in resp.text
3530

  
3531
    agenda.view_role = managers_group
3532
    agenda.edit_role = Group.objects.create(name='hop')
3533
    agenda.save()
3534

  
3535
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3536
    assert 'Almost full event (90%): Edit Role (hop) will be notified' in resp.text
3537
    assert 'Full event: View Role (Managers) will be notified' in resp.text
3538

  
3539

  
3540
def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox):
3541
    agenda = Agenda.objects.create(label='Events', kind='events')
3542
    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Old event')
3543
    event.cancelled = True
3544
    event.save()
3545

  
3546
    login(app)
3547
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3548

  
3549
    resp = resp.click('Configure')
3550
    resp.form['cancelled_event'] = 'use-email-field'
3551
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com'
3552
    resp.form.submit()
3553

  
3554
    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event')
3555
    event.cancelled = True
3556
    event.save()
3557

  
3558
    call_command('send_email_notifications')
3559
    # no notification is sent for old event
3560
    assert len(mailoutbox) == 1
3561
    assert 'New event' in mailoutbox[0].subject
3499
-