0002-agendas-add-email-notifications-for-events-44158.patch
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 |
- |