Projet

Général

Profil

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

Valentin Deniaud, 03 août 2020 17:58

Télécharger (32,2 ko)

Voir les différences:

Subject: [PATCH 2/2] agendas: add email notifications for events (#44158)

 .../commands/send_email_notifications.py      |  66 ++++++++++
 .../migrations/0058_auto_20200803_1755.py     | 104 ++++++++++++++++
 chrono/agendas/models.py                      | 113 ++++++++++++++++++
 .../agendas/event_notification_body.html      |   1 +
 .../agendas/event_notification_body.txt       |   1 +
 chrono/manager/forms.py                       |  18 +++
 .../manager_agenda_notifications_form.html    |  34 ++++++
 .../manager_events_agenda_settings.html       |  22 ++++
 chrono/manager/urls.py                        |   5 +
 chrono/manager/views.py                       |  26 +++-
 chrono/settings.py                            |   2 +
 tests/settings.py                             |   2 +
 tests/test_agendas.py                         |  93 +++++++++++++-
 tests/test_import_export.py                   |  27 +++++
 tests/test_manager.py                         |  73 ++++++++++-
 15 files changed, 580 insertions(+), 7 deletions(-)
 create mode 100644 chrono/agendas/management/commands/send_email_notifications.py
 create mode 100644 chrono/agendas/migrations/0058_auto_20200803_1755.py
 create mode 100644 chrono/agendas/templates/agendas/event_notification_body.html
 create mode 100644 chrono/agendas/templates/agendas/event_notification_body.txt
 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
import copy
18
from urllib.parse import urljoin
19

  
20
from django.conf import settings
21
from django.core.mail import send_mail
22
from django.core.management.base import BaseCommand
23
from django.db.transaction import atomic
24
from django.template.loader import render_to_string
25
from django.utils import timezone
26
from django.utils.translation import ugettext_lazy as _
27

  
28
from chrono.agendas.models import Agenda
29

  
30

  
31
class Command(BaseCommand):
32
    EMAIL_SUBJECTS = {
33
        'almost_full': _('Alert: event "%s" is almost full (90%%)'),
34
        'full': _('Alert: event "%s" is full'),
35
        'cancelled': _('Alert: event "%s" is cancelled'),
36
    }
37
    help = 'Send email notifications'
38

  
39
    def handle(self, **options):
40
        agendas = Agenda.objects.filter(notifications_settings__isnull=False).select_related(
41
            'notifications_settings'
42
        )
43
        for agenda in agendas:
44
            for notification_type in agenda.notifications_settings.get_notification_types():
45
                recipients = notification_type.get_recipients()
46
                if not recipients:
47
                    continue
48

  
49
                status = notification_type.related_field
50
                filter_kwargs = {status: True, status + '_notification_timestamp__isnull': True}
51
                events = agenda.event_set.filter(**filter_kwargs)
52
                for event in events:
53
                    self.send_notification(event, status, recipients)
54

  
55
    def send_notification(self, event, status, recipients):
56
        subject = self.EMAIL_SUBJECTS[status] % event
57
        ctx = copy.copy(settings.TEMPLATE_VARS)
58
        ctx.update({'event_url': urljoin(settings.SITE_BASE_URL, event.get_absolute_view_url())})
59
        body = render_to_string('agendas/event_notification_body.txt', ctx)
60
        html_body = render_to_string('agendas/event_notification_body.html', ctx)
61

  
62
        timestamp = timezone.now()
63
        with atomic():
64
            setattr(event, status + '_notification_timestamp', timestamp)
65
            event.save()
66
            send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients, html_message=html_body)
chrono/agendas/migrations/0058_auto_20200803_1755.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-08-03 15:55
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', '0057_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',
93
            name='almost_full_notification_timestamp',
94
            field=models.DateTimeField(null=True),
95
        ),
96
        migrations.AddField(
97
            model_name='event',
98
            name='cancelled_notification_timestamp',
99
            field=models.DateTimeField(null=True),
100
        ),
101
        migrations.AddField(
102
            model_name='event', name='full_notification_timestamp', field=models.DateTimeField(null=True),
103
        ),
104
    ]
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
......
271 272
        }
272 273
        if self.kind == 'events':
273 274
            agenda['events'] = [x.export_json() for x in self.event_set.all()]
275
            if hasattr(self, 'notifications_settings'):
276
                agenda['notifications_settings'] = self.notifications_settings.export_json()
274 277
        elif self.kind == 'meetings':
275 278
            agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()]
276 279
            agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
......
285 288
        permissions = data.pop('permissions') or {}
286 289
        if data['kind'] == 'events':
287 290
            events = data.pop('events')
291
            notifications_settings = data.pop('notifications_settings', None)
288 292
        elif data['kind'] == 'meetings':
289 293
            meetingtypes = data.pop('meetingtypes')
290 294
            desks = data.pop('desks')
......
312 316
        if data['kind'] == 'events':
313 317
            if overwrite:
314 318
                Event.objects.filter(agenda=agenda).delete()
319
                AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
315 320
            for event_data in events:
316 321
                event_data['agenda'] = agenda
317 322
                Event.import_json(event_data).save()
323
            if notifications_settings:
324
                notifications_settings['agenda'] = agenda
325
                AgendaNotificationsSettings.import_json(notifications_settings).save()
318 326
        elif data['kind'] == 'meetings':
319 327
            if overwrite:
320 328
                MeetingType.objects.filter(agenda=agenda).delete()
......
782 790
    desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
783 791
    resources = models.ManyToManyField('Resource')
784 792

  
793
    almost_full_notification_timestamp = models.DateTimeField(null=True)
794
    full_notification_timestamp = models.DateTimeField(null=True)
795
    cancelled_notification_timestamp = models.DateTimeField(null=True)
796

  
785 797
    class Meta:
786 798
        ordering = ['agenda', 'start_datetime', 'duration', 'label']
787 799
        unique_together = ('agenda', 'slug')
......
900 912
    def get_absolute_url(self):
901 913
        return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id})
902 914

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

  
903 918
    @classmethod
904 919
    def import_json(cls, data):
905 920
        data['start_datetime'] = make_aware(
......
1434 1449
    def as_interval(self):
1435 1450
        '''Simplify insertion into IntervalSet'''
1436 1451
        return Interval(self.start_datetime, self.end_datetime)
1452

  
1453

  
1454
class NotificationType:
1455
    def __init__(self, name, related_field, settings):
1456
        self.name = name
1457
        self.related_field = related_field
1458
        self.settings = settings
1459

  
1460
    def get_recipients(self):
1461
        choice = getattr(self.settings, self.name)
1462
        if not choice:
1463
            return []
1464

  
1465
        if choice == self.settings.EMAIL_FIELD:
1466
            return getattr(self.settings, self.name + '_emails')
1467

  
1468
        role = self.settings.get_role_from_choice(choice)
1469
        if not role or not hasattr(role, 'role'):
1470
            return []
1471
        emails = role.role.emails
1472
        if role.role.emails_to_members:
1473
            emails.extend(role.user_set.values_list('email', flat=True))
1474
        return emails
1475

  
1476
    @property
1477
    def display_value(self):
1478
        choice = getattr(self.settings, self.name)
1479
        if not choice:
1480
            return ''
1481

  
1482
        if choice == self.settings.EMAIL_FIELD:
1483
            emails = getattr(self.settings, self.name + '_emails')
1484
            return ', '.join(emails)
1485

  
1486
        role = self.settings.get_role_from_choice(choice)
1487
        if role:
1488
            display_name = getattr(self.settings, 'get_%s_display' % self.name)()
1489
            return '%s (%s)' % (display_name, role)
1490

  
1491
    @property
1492
    def label(self):
1493
        return self.settings._meta.get_field(self.name).verbose_name
1494

  
1495

  
1496
class AgendaNotificationsSettings(models.Model):
1497
    EMAIL_FIELD = 'use-email-field'
1498
    VIEW_ROLE = 'view-role'
1499
    EDIT_ROLE = 'edit-role'
1500

  
1501
    CHOICES = [
1502
        (EDIT_ROLE, _('Edit Role')),
1503
        (VIEW_ROLE, _('View Role')),
1504
        (EMAIL_FIELD, _('Specify email addresses manually')),
1505
    ]
1506

  
1507
    agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings')
1508

  
1509
    almost_full_event = models.CharField(
1510
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)')
1511
    )
1512
    almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1513

  
1514
    full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event'))
1515
    full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1516

  
1517
    cancelled_event = models.CharField(
1518
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event')
1519
    )
1520
    cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1521

  
1522
    @classmethod
1523
    def get_email_field_names(cls):
1524
        return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)]
1525

  
1526
    def get_notification_types(self):
1527
        for field in ['almost_full_event', 'full_event', 'cancelled_event']:
1528
            yield NotificationType(name=field, related_field=field.replace('_event', ''), settings=self)
1529

  
1530
    def get_role_from_choice(self, choice):
1531
        if choice == self.EDIT_ROLE:
1532
            return self.agenda.edit_role
1533
        elif choice == self.VIEW_ROLE:
1534
            return self.agenda.view_role
1535

  
1536
    @classmethod
1537
    def import_json(cls, data):
1538
        data = clean_import_data(cls, data)
1539
        return cls(**data)
1540

  
1541
    def export_json(self):
1542
        return {
1543
            'almost_full_event': self.almost_full_event,
1544
            'almost_full_event_emails': self.almost_full_event_emails,
1545
            'full_event': self.full_event,
1546
            'full_event_emails': self.full_event_emails,
1547
            'cancelled_event': self.cancelled_event,
1548
            'cancelled_event_emails': self.cancelled_event_emails,
1549
        }
chrono/agendas/templates/agendas/event_notification_body.html
1
You can view it <a href="{{ event_url }}">here</a>.
chrono/agendas/templates/agendas/event_notification_body.txt
1
You can view it here: {{ event_url }}.
chrono/manager/forms.py
40 40
    VirtualMember,
41 41
    Resource,
42 42
    Category,
43
    AgendaNotificationsSettings,
43 44
    WEEKDAYS_LIST,
44 45
)
45 46

  
......
473 474
    class Meta:
474 475
        model = Booking
475 476
        fields = []
477

  
478

  
479
class AgendaNotificationsForm(forms.ModelForm):
480
    class Meta:
481
        model = AgendaNotificationsSettings
482
        fields = '__all__'
483
        widgets = {
484
            'agenda': forms.HiddenInput(),
485
        }
486

  
487
    def __init__(self, *args, **kwargs):
488
        super().__init__(*args, **kwargs)
489

  
490
        for email_field in AgendaNotificationsSettings.get_email_field_names():
491
            self.fields[email_field].widget.attrs['size'] = 80
492
            self.fields[email_field].label = ''
493
            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 notification_type in object.notifications_settings.get_notification_types %}
38
  {% with display_value=notification_type.display_value label=notification_type.label %}
39
  {% if display_value %}
40
  <li>
41
  {% blocktrans %}
42
  {{ label }}: {{ display_value }} will be notified.
43
  {% endblocktrans %}
44
  </li>
45
  {% endif %}
46
  {% endwith %}
47
{% empty %}
48
{% trans "Notifications are disabled for this agenda." %}
49
{% endfor %}
50
</ul>
51
<a rel="popup" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>
52
</div>
53
</div>
54

  
33 55
{% endblock %}
chrono/manager/urls.py
73 73
        views.agenda_import_events,
74 74
        name='chrono-manager-agenda-import-events',
75 75
    ),
76
    url(
77
        r'^agendas/(?P<pk>\d+)/notifications$',
78
        views.agenda_notifications_settings,
79
        name='chrono-manager-agenda-notifications-settings',
80
    ),
76 81
    url(
77 82
        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$',
78 83
        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.http import Http404, HttpResponse, HttpResponseRedirect
29 29
from django.shortcuts import get_object_or_404
......
62 62
    VirtualMember,
63 63
    Resource,
64 64
    Category,
65
    AgendaNotificationsSettings,
65 66
)
66 67

  
67 68
from .forms import (
......
88 89
    CategoryAddForm,
89 90
    CategoryEditForm,
90 91
    BookingCancelForm,
92
    AgendaNotificationsForm,
91 93
)
92 94
from .utils import import_site
93 95

  
......
1339 1341
agenda_import_events = AgendaImportEventsView.as_view()
1340 1342

  
1341 1343

  
1344
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView):
1345
    template_name = 'chrono/manager_agenda_notifications_form.html'
1346
    model = AgendaNotificationsSettings
1347
    form_class = AgendaNotificationsForm
1348

  
1349
    def get_object(self):
1350
        try:
1351
            return self.agenda.notifications_settings
1352
        except AgendaNotificationsSettings.DoesNotExist:
1353
            # prevent old events from sending notifications
1354
            statuses = ('almost_full', 'full', 'cancelled')
1355
            timestamp = now()
1356
            for status in statuses:
1357
                filter_kwargs = {status: True}
1358
                update_kwargs = {status + '_notification_timestamp': timestamp}
1359
                self.agenda.event_set.filter(**filter_kwargs).update(**update_kwargs)
1360
            return AgendaNotificationsSettings.objects.create(agenda=self.agenda)
1361

  
1362

  
1363
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
1364

  
1365

  
1342 1366
class EventDetailView(ViewableAgendaMixin, DetailView):
1343 1367
    model = Event
1344 1368
    pk_url_kwarg = 'event_pk'
chrono/settings.py
166 166
# we use 28s by default: timeout just before web server, which is usually 30s
167 167
REQUESTS_TIMEOUT = 28
168 168

  
169
TEMPLATE_VARS = {}
170

  
169 171
local_settings_file = os.environ.get(
170 172
    'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
171 173
)
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
......
22 22
    TimePeriodException,
23 23
    TimePeriodExceptionSource,
24 24
    VirtualMember,
25
    AgendaNotificationsSettings,
25 26
)
26 27

  
27 28
pytestmark = pytest.mark.django_db
......
1008 1009

  
1009 1010
    assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists()
1010 1011
    assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists()
1012

  
1013

  
1014
@mock.patch('django.contrib.auth.models.Group.role', create=True)
1015
@pytest.mark.parametrize(
1016
    'emails_to_members,emails',
1017
    [(False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']),],
1018
)
1019
def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox):
1020
    group = Group.objects.create(name='group')
1021
    user = User.objects.create(username='user', email='user@entrouvert.com')
1022
    user.groups.add(group)
1023
    mocked_role.emails_to_members = emails_to_members
1024
    mocked_role.emails = emails
1025
    expected_recipients = emails
1026
    if emails_to_members:
1027
        expected_recipients.append(user.email)
1028
    expected_email_count = 1 if emails else 0
1029

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

  
1032
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1033
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1034
    settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE
1035
    settings.save()
1036

  
1037
    # book 9/10 places to reach almost full state
1038
    for i in range(9):
1039
        Booking.objects.create(event=event)
1040
    event.refresh_from_db()
1041
    assert event.almost_full
1042

  
1043
    call_command('send_email_notifications')
1044
    assert len(mailoutbox) == expected_email_count
1045
    if mailoutbox:
1046
        assert mailoutbox[0].recipients() == expected_recipients
1047
        assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full (90%)'
1048

  
1049
    # no new email on subsequent run
1050
    call_command('send_email_notifications')
1051
    assert len(mailoutbox) == expected_email_count
1052

  
1053

  
1054
def test_agenda_notifications_email_list(mailoutbox):
1055
    agenda = Agenda.objects.create(label='Foo bar', kind='event')
1056

  
1057
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1058
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1059
    settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD
1060
    settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
1061
    settings.save()
1062

  
1063
    for i in range(10):
1064
        Booking.objects.create(event=event)
1065
    event.refresh_from_db()
1066
    assert event.full
1067

  
1068
    call_command('send_email_notifications')
1069
    assert len(mailoutbox) == 1
1070
    assert mailoutbox[0].recipients() == recipients
1071
    assert mailoutbox[0].subject == 'Alert: event "Hop" is full'
1072
    assert (
1073
        'view it here: https://example.com/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk,)
1074
        in mailoutbox[0].body
1075
    )
1076

  
1077
    # no new email on subsequent run
1078
    call_command('send_email_notifications')
1079
    assert len(mailoutbox) == 1
1080

  
1081

  
1082
def test_agenda_notifications_cancelled(mailoutbox):
1083
    agenda = Agenda.objects.create(label='Foo bar', kind='event')
1084

  
1085
    event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
1086
    settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
1087
    settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD
1088
    settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
1089
    settings.save()
1090

  
1091
    event.cancelled = True
1092
    event.save()
1093

  
1094
    call_command('send_email_notifications')
1095
    assert len(mailoutbox) == 1
1096
    assert mailoutbox[0].recipients() == recipients
1097
    assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled'
1098

  
1099
    # no new email on subsequent run
1100
    call_command('send_email_notifications')
1101
    assert len(mailoutbox) == 1
tests/test_import_export.py
28 28
    AgendaImportError,
29 29
    MeetingType,
30 30
    VirtualMember,
31
    AgendaNotificationsSettings,
31 32
)
32 33
from chrono.manager.utils import import_site
33 34

  
......
456 457
    with pytest.raises(AgendaImportError) as excinfo:
457 458
        import_site(payload)
458 459
    assert str(excinfo.value) == 'Bad slug format "meeting-type&"'
460

  
461

  
462
def test_import_export_notification_settings():
463
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
464
    settings = AgendaNotificationsSettings.objects.create(
465
        agenda=agenda,
466
        almost_full_event=AgendaNotificationsSettings.EDIT_ROLE,
467
        full_event=AgendaNotificationsSettings.VIEW_ROLE,
468
        cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
469
        cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
470
    )
471
    output = get_output_of_command('export_site')
472
    payload = json.loads(output)
473

  
474
    agenda.delete()
475
    assert not AgendaNotificationsSettings.objects.exists()
476

  
477
    import_site(payload)
478
    agenda = Agenda.objects.first()
479
    AgendaNotificationsSettings.objects.get(
480
        agenda=agenda,
481
        almost_full_event=AgendaNotificationsSettings.EDIT_ROLE,
482
        full_event=AgendaNotificationsSettings.VIEW_ROLE,
483
        cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
484
        cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
485
    )
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
......
48 49

  
49 50

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

  
56

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

  
62 66

  
......
3734 3738
    assert 'Cancelled' in resp.text
3735 3739
    assert '0/10 bookings' in resp.text
3736 3740
    assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2
3741

  
3742

  
3743
def test_agenda_notifications(app, admin_user, managers_group):
3744
    agenda = Agenda.objects.create(label='Events', kind='events')
3745

  
3746
    login(app)
3747
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3748

  
3749
    assert 'Notifications' in resp.text
3750
    assert 'Notifications are disabled' in resp.text
3751

  
3752
    resp = resp.click('Configure')
3753
    resp.form['almost_full_event'] = 'edit-role'
3754
    resp.form['full_event'] = 'view-role'
3755
    resp.form['cancelled_event'] = 'use-email-field'
3756
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com'
3757
    resp = resp.form.submit().follow()
3758

  
3759
    settings = agenda.notifications_settings
3760
    assert settings.almost_full_event == 'edit-role'
3761
    assert settings.full_event == 'view-role'
3762
    assert settings.cancelled_event == 'use-email-field'
3763
    assert settings.cancelled_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com']
3764

  
3765
    assert 'Cancelled event: hop@entrouvert.com, top@entrouvert.com will be notified' in resp.text
3766
    assert not 'Full event:' in resp.text
3767
    assert not 'Almost full event (90%):' in resp.text
3768

  
3769
    agenda.view_role = managers_group
3770
    agenda.edit_role = Group.objects.create(name='hop')
3771
    agenda.save()
3772

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

  
3777

  
3778
def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox):
3779
    agenda = Agenda.objects.create(label='Events', kind='events')
3780
    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Old event')
3781
    event.cancelled = True
3782
    event.save()
3783

  
3784
    login(app)
3785
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3786

  
3787
    resp = resp.click('Configure')
3788
    resp.form['cancelled_event'] = 'use-email-field'
3789
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com'
3790
    resp.form.submit()
3791

  
3792
    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event')
3793
    event.cancelled = True
3794
    event.save()
3795

  
3796
    call_command('send_email_notifications')
3797
    # no notification is sent for old event
3798
    assert len(mailoutbox) == 1
3799
    assert 'New event' in mailoutbox[0].subject
3737
-