Projet

Général

Profil

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

Valentin Deniaud, 29 juillet 2020 11:51

Télécharger (30,8 ko)

Voir les différences:

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

 .../commands/send_email_notifications.py      |  58 ++++++++++
 .../migrations/0058_auto_20200729_1150.py     |  98 ++++++++++++++++
 chrono/agendas/models.py                      | 108 ++++++++++++++++++
 .../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       |  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 +++++++++++-
 14 files changed, 552 insertions(+), 7 deletions(-)
 create mode 100644 chrono/agendas/management/commands/send_email_notifications.py
 create mode 100644 chrono/agendas/migrations/0058_auto_20200729_1150.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
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.template.loader import render_to_string
24
from django.utils.translation import ugettext_lazy as _
25

  
26
from chrono.agendas.models import AgendaNotificationsSettings
27

  
28

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

  
37
    def handle(self, **options):
38
        notifications_settings = AgendaNotificationsSettings.objects.all()
39
        for settings in notifications_settings.select_related('agenda'):
40
            for setting, recipients in settings:
41
                if not recipients:
42
                    continue
43

  
44
                status = setting.replace('_event', '')
45
                events = settings.agenda.event_set.filter(**{status: True, 'was_' + status: False})
46
                for event in events:
47
                    self.send_notification(event, status, recipients)
48

  
49
    def send_notification(self, event, status, recipients):
50
        subject = self.EMAIL_SUBJECTS[status] % event
51
        ctx = {'event_url': urljoin(django_settings.SITE_BASE_URL, event.get_absolute_view_url())}
52
        body = render_to_string('agendas/event_notification_body.txt', ctx)
53
        html_body = render_to_string('agendas/event_notification_body.html', ctx)
54

  
55
        with atomic():
56
            setattr(event, 'was_' + status, True)
57
            event.save()
58
            send_mail(subject, body, django_settings.DEFAULT_FROM_EMAIL, recipients, html_message=html_body)
chrono/agendas/migrations/0058_auto_20200729_1150.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-29 09:50
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', 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
......
269 270
        }
270 271
        if self.kind == 'events':
271 272
            agenda['events'] = [x.export_json() for x in self.event_set.all()]
273
            if hasattr(self, 'notifications_settings'):
274
                agenda['notifications_settings'] = self.notifications_settings.export_json()
272 275
        elif self.kind == 'meetings':
273 276
            agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()]
274 277
            agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
......
283 286
        permissions = data.pop('permissions') or {}
284 287
        if data['kind'] == 'events':
285 288
            events = data.pop('events')
289
            notifications_settings = data.pop('notifications_settings', None)
286 290
        elif data['kind'] == 'meetings':
287 291
            meetingtypes = data.pop('meetingtypes')
288 292
            desks = data.pop('desks')
......
310 314
        if data['kind'] == 'events':
311 315
            if overwrite:
312 316
                Event.objects.filter(agenda=agenda).delete()
317
                AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
313 318
            for event_data in events:
314 319
                event_data['agenda'] = agenda
315 320
                Event.import_json(event_data).save()
321
            if notifications_settings:
322
                notifications_settings['agenda'] = agenda
323
                AgendaNotificationsSettings.import_json(notifications_settings).save()
316 324
        elif data['kind'] == 'meetings':
317 325
            if overwrite:
318 326
                MeetingType.objects.filter(agenda=agenda).delete()
......
780 788
    desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
781 789
    resources = models.ManyToManyField('Resource')
782 790

  
791
    # flags for detecting changes
792
    was_almost_full = models.BooleanField(default=False)
793
    was_full = models.BooleanField(default=False)
794
    was_cancelled = models.BooleanField(default=False)
795

  
783 796
    class Meta:
784 797
        ordering = ['agenda', 'start_datetime', 'duration', 'label']
785 798
        unique_together = ('agenda', 'slug')
......
898 911
    def get_absolute_url(self):
899 912
        return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id})
900 913

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

  
901 917
    @classmethod
902 918
    def import_json(cls, data):
903 919
        data['start_datetime'] = make_aware(
......
1432 1448
    def as_interval(self):
1433 1449
        '''Simplify insertion into IntervalSet'''
1434 1450
        return Interval(self.start_datetime, self.end_datetime)
1451

  
1452

  
1453
class AgendaNotificationsSettings(models.Model):
1454
    EMAIL_FIELD = 'use-email-field'
1455
    VIEW_ROLE = 'view-role'
1456
    EDIT_ROLE = 'edit-role'
1457

  
1458
    CHOICES = [
1459
        (EDIT_ROLE, _('Edit Role')),
1460
        (VIEW_ROLE, _('View Role')),
1461
        (EMAIL_FIELD, _('Specify email addresses manually')),
1462
    ]
1463

  
1464
    agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings')
1465

  
1466
    almost_full_event = models.CharField(
1467
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)')
1468
    )
1469
    almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1470

  
1471
    full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event'))
1472
    full_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1473

  
1474
    cancelled_event = models.CharField(
1475
        max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event')
1476
    )
1477
    cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True)
1478

  
1479
    def __iter__(self):
1480
        for field in self.get_setting_fields():
1481
            yield (field.name, self.get_recipients(field.name))
1482

  
1483
    @classmethod
1484
    def get_setting_fields(cls):
1485
        return [field for field in cls._meta.get_fields() if isinstance(field, models.CharField)]
1486

  
1487
    @classmethod
1488
    def get_email_field_names(cls):
1489
        return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)]
1490

  
1491
    def get_recipients(self, setting):
1492
        value = getattr(self, setting)
1493
        if not value:
1494
            return []
1495

  
1496
        if value == self.EMAIL_FIELD:
1497
            return getattr(self, setting + '_emails')
1498

  
1499
        role = self.get_role_from_choice(value)
1500
        if not role or not hasattr(role, 'role'):
1501
            return []
1502
        emails = role.role.emails
1503
        if role.role.emails_to_members:
1504
            emails.extend(role.user_set.values_list('email', flat=True))
1505
        return emails
1506

  
1507
    @property
1508
    def display_info(self):
1509
        for field in self.get_setting_fields():
1510
            choice = getattr(self, field.name)
1511
            if not choice:
1512
                continue
1513

  
1514
            if choice == self.EMAIL_FIELD:
1515
                emails = getattr(self, field.name + '_emails')
1516
                yield (field.verbose_name, ', '.join(emails))
1517
            else:
1518
                role = self.get_role_from_choice(choice)
1519
                if role:
1520
                    display_name = getattr(self, 'get_' + field.name + '_display')()
1521
                    yield (field.verbose_name, '%s (%s)' % (display_name, role))
1522

  
1523
    def get_role_from_choice(self, choice):
1524
        if choice == self.EDIT_ROLE:
1525
            return self.agenda.edit_role
1526
        elif choice == self.VIEW_ROLE:
1527
            return self.agenda.view_role
1528

  
1529
    @classmethod
1530
    def import_json(cls, data):
1531
        data = clean_import_data(cls, data)
1532
        return cls(**data)
1533

  
1534
    def export_json(self):
1535
        return {
1536
            'almost_full_event': self.almost_full_event,
1537
            'almost_full_event_emails': self.almost_full_event_emails,
1538
            'full_event': self.full_event,
1539
            'full_event_emails': self.full_event_emails,
1540
            'cancelled_event': self.cancelled_event,
1541
            'cancelled_event_emails': self.cancelled_event_emails,
1542
        }
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 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
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
            kwargs = {'was_' + status: F(status) for status in statuses}
1356
            self.agenda.event_set.update(**kwargs)
1357
            return AgendaNotificationsSettings.objects.create(agenda=self.agenda)
1358

  
1359

  
1360
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
1361

  
1362

  
1342 1363
class EventDetailView(ViewableAgendaMixin, DetailView):
1343 1364
    model = Event
1344 1365
    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
......
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

  
......
3737 3741
    assert 'Cancelled' in resp.text
3738 3742
    assert '0/10 bookings' in resp.text
3739 3743
    assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2
3744

  
3745

  
3746
def test_agenda_notifications(app, admin_user, managers_group):
3747
    agenda = Agenda.objects.create(label='Events', kind='events')
3748

  
3749
    login(app)
3750
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3751

  
3752
    assert 'Notifications' in resp.text
3753
    assert 'Notifications are disabled' in resp.text
3754

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

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

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

  
3772
    agenda.view_role = managers_group
3773
    agenda.edit_role = Group.objects.create(name='hop')
3774
    agenda.save()
3775

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

  
3780

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

  
3787
    login(app)
3788
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3789

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

  
3795
    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event')
3796
    event.cancelled = True
3797
    event.save()
3798

  
3799
    call_command('send_email_notifications')
3800
    # no notification is sent for old event
3801
    assert len(mailoutbox) == 1
3802
    assert 'New event' in mailoutbox[0].subject
3740
-