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.utils.translation import ugettext_lazy as _ |
|
24 | ||
25 |
from chrono.agendas.models import AgendaNotificationsSettings |
|
26 | ||
27 | ||
28 |
class Command(BaseCommand): |
|
29 |
help = 'Send email notifications' |
|
30 | ||
31 |
def handle(self, **options): |
|
32 |
notifications_settings = AgendaNotificationsSettings.objects.all() |
|
33 |
for settings in notifications_settings.select_related('agenda'): |
|
34 |
for setting, recipients in settings: |
|
35 |
if not recipients: |
|
36 |
continue |
|
37 | ||
38 |
status = setting.replace('_event', '') |
|
39 |
events = settings.agenda.event_set.filter(**{status: True, 'was_' + status: False}) |
|
40 |
for event in events: |
|
41 |
self.send_notification(event, status, recipients) |
|
42 | ||
43 |
def send_notification(self, event, status, recipients): |
|
44 |
with atomic(): |
|
45 |
setattr(event, 'was_' + status, True) |
|
46 |
event.save() |
|
47 |
subject = _('Alert: event "%(event)s" is %(status)s') % { |
|
48 |
'event': event, |
|
49 |
'status': status.replace('_', ' '), |
|
50 |
} |
|
51 |
body = _('You can view it here: %s.') % urljoin( |
|
52 |
django_settings.SITE_BASE_URL, event.get_absolute_view_url() |
|
53 |
) |
|
54 |
send_mail(subject, body, django_settings.DEFAULT_FROM_EMAIL, recipients) |
chrono/agendas/migrations/0054_auto_20200716_1515.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-07-16 13:15 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
import django.contrib.postgres.fields |
|
6 |
from django.db import migrations, models |
|
7 |
import django.db.models.deletion |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
dependencies = [ |
|
13 |
('agendas', '0053_event_almost_full'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.CreateModel( |
|
18 |
name='AgendaNotificationsSettings', |
|
19 |
fields=[ |
|
20 |
( |
|
21 |
'id', |
|
22 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
23 |
), |
|
24 |
( |
|
25 |
'almost_full_event', |
|
26 |
models.CharField( |
|
27 |
blank=True, |
|
28 |
choices=[ |
|
29 |
('edit-role', 'Edit Role'), |
|
30 |
('view-role', 'View Role'), |
|
31 |
('use-email-field', 'Specify email addresses manually'), |
|
32 |
], |
|
33 |
max_length=16, |
|
34 |
verbose_name='Almost full event (90%)', |
|
35 |
), |
|
36 |
), |
|
37 |
( |
|
38 |
'almost_full_event_emails', |
|
39 |
django.contrib.postgres.fields.ArrayField( |
|
40 |
base_field=models.EmailField(max_length=254), blank=True, null=True, size=None |
|
41 |
), |
|
42 |
), |
|
43 |
( |
|
44 |
'full_event', |
|
45 |
models.CharField( |
|
46 |
blank=True, |
|
47 |
choices=[ |
|
48 |
('edit-role', 'Edit Role'), |
|
49 |
('view-role', 'View Role'), |
|
50 |
('use-email-field', 'Specify email addresses manually'), |
|
51 |
], |
|
52 |
max_length=16, |
|
53 |
verbose_name='Full event', |
|
54 |
), |
|
55 |
), |
|
56 |
( |
|
57 |
'full_event_emails', |
|
58 |
django.contrib.postgres.fields.ArrayField( |
|
59 |
base_field=models.EmailField(max_length=254), blank=True, null=True, size=None |
|
60 |
), |
|
61 |
), |
|
62 |
( |
|
63 |
'cancelled_event', |
|
64 |
models.CharField( |
|
65 |
blank=True, |
|
66 |
choices=[ |
|
67 |
('edit-role', 'Edit Role'), |
|
68 |
('view-role', 'View Role'), |
|
69 |
('use-email-field', 'Specify email addresses manually'), |
|
70 |
], |
|
71 |
max_length=16, |
|
72 |
verbose_name='Cancelled event', |
|
73 |
), |
|
74 |
), |
|
75 |
( |
|
76 |
'cancelled_event_emails', |
|
77 |
django.contrib.postgres.fields.ArrayField( |
|
78 |
base_field=models.EmailField(max_length=254), blank=True, null=True, size=None |
|
79 |
), |
|
80 |
), |
|
81 |
( |
|
82 |
'agenda', |
|
83 |
models.OneToOneField( |
|
84 |
on_delete=django.db.models.deletion.CASCADE, |
|
85 |
related_name='notifications_settings', |
|
86 |
to='agendas.Agenda', |
|
87 |
), |
|
88 |
), |
|
89 |
], |
|
90 |
), |
|
91 |
migrations.AddField( |
|
92 |
model_name='event', name='was_almost_full', field=models.BooleanField(default=False), |
|
93 |
), |
|
94 |
migrations.AddField( |
|
95 |
model_name='event', name='was_cancelled', field=models.BooleanField(default=False), |
|
96 |
), |
|
97 |
migrations.AddField(model_name='event', name='was_full', field=models.BooleanField(default=False),), |
|
98 |
] |
chrono/agendas/models.py | ||
---|---|---|
29 | 29 |
import django |
30 | 30 |
from django.conf import settings |
31 | 31 |
from django.contrib.auth.models import Group |
32 |
from django.contrib.postgres.fields import ArrayField |
|
32 | 33 |
from django.core.exceptions import FieldDoesNotExist |
33 | 34 |
from django.core.exceptions import ValidationError |
34 | 35 |
from django.core.validators import MaxValueValidator |
... | ... | |
255 | 256 |
} |
256 | 257 |
if self.kind == 'events': |
257 | 258 |
agenda['events'] = [x.export_json() for x in self.event_set.all()] |
259 |
if hasattr(self, 'notifications_settings'): |
|
260 |
agenda['notifications_settings'] = self.notifications_settings.export_json() |
|
258 | 261 |
elif self.kind == 'meetings': |
259 | 262 |
agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()] |
260 | 263 |
agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()] |
... | ... | |
269 | 272 |
permissions = data.pop('permissions') or {} |
270 | 273 |
if data['kind'] == 'events': |
271 | 274 |
events = data.pop('events') |
275 |
notifications_settings = data.pop('notifications_settings', None) |
|
272 | 276 |
elif data['kind'] == 'meetings': |
273 | 277 |
meetingtypes = data.pop('meetingtypes') |
274 | 278 |
desks = data.pop('desks') |
... | ... | |
291 | 295 |
if data['kind'] == 'events': |
292 | 296 |
if overwrite: |
293 | 297 |
Event.objects.filter(agenda=agenda).delete() |
298 |
AgendaNotificationsSettings.objects.filter(agenda=agenda).delete() |
|
294 | 299 |
for event_data in events: |
295 | 300 |
event_data['agenda'] = agenda |
296 | 301 |
Event.import_json(event_data).save() |
302 |
if notifications_settings: |
|
303 |
notifications_settings['agenda'] = agenda |
|
304 |
AgendaNotificationsSettings.import_json(notifications_settings).save() |
|
297 | 305 |
elif data['kind'] == 'meetings': |
298 | 306 |
if overwrite: |
299 | 307 |
MeetingType.objects.filter(agenda=agenda).delete() |
... | ... | |
758 | 766 |
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE) |
759 | 767 |
resources = models.ManyToManyField('Resource') |
760 | 768 | |
769 |
# flags for detecting changes |
|
770 |
was_almost_full = models.BooleanField(default=False) |
|
771 |
was_full = models.BooleanField(default=False) |
|
772 |
was_cancelled = models.BooleanField(default=False) |
|
773 | ||
761 | 774 |
class Meta: |
762 | 775 |
ordering = ['agenda', 'start_datetime', 'duration', 'label'] |
763 | 776 |
unique_together = ('agenda', 'slug') |
... | ... | |
864 | 877 |
def get_absolute_url(self): |
865 | 878 |
return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) |
866 | 879 | |
880 |
def get_absolute_view_url(self): |
|
881 |
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) |
|
882 | ||
867 | 883 |
@classmethod |
868 | 884 |
def import_json(cls, data): |
869 | 885 |
data['start_datetime'] = make_aware( |
... | ... | |
1378 | 1394 |
def as_interval(self): |
1379 | 1395 |
'''Simplify insertion into IntervalSet''' |
1380 | 1396 |
return Interval(self.start_datetime, self.end_datetime) |
1397 | ||
1398 | ||
1399 |
class AgendaNotificationsSettings(models.Model): |
|
1400 |
EMAIL_FIELD = 'use-email-field' |
|
1401 |
VIEW_ROLE = 'view-role' |
|
1402 |
EDIT_ROLE = 'edit-role' |
|
1403 | ||
1404 |
CHOICES = [ |
|
1405 |
(EDIT_ROLE, _('Edit Role')), |
|
1406 |
(VIEW_ROLE, _('View Role')), |
|
1407 |
(EMAIL_FIELD, _('Specify email addresses manually')), |
|
1408 |
] |
|
1409 | ||
1410 |
agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings') |
|
1411 | ||
1412 |
almost_full_event = models.CharField( |
|
1413 |
max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)') |
|
1414 |
) |
|
1415 |
almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1416 | ||
1417 |
full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event')) |
|
1418 |
full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1419 | ||
1420 |
cancelled_event = models.CharField( |
|
1421 |
max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event') |
|
1422 |
) |
|
1423 |
cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1424 | ||
1425 |
def __iter__(self): |
|
1426 |
for field in self.get_setting_fields(): |
|
1427 |
yield (field.name, self.get_recipients(field.name)) |
|
1428 | ||
1429 |
@classmethod |
|
1430 |
def get_setting_fields(cls): |
|
1431 |
return [field for field in cls._meta.get_fields() if isinstance(field, models.CharField)] |
|
1432 | ||
1433 |
@classmethod |
|
1434 |
def get_email_field_names(cls): |
|
1435 |
return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)] |
|
1436 | ||
1437 |
def get_recipients(self, setting): |
|
1438 |
value = getattr(self, setting) |
|
1439 |
if not value: |
|
1440 |
return [] |
|
1441 | ||
1442 |
if value == self.EMAIL_FIELD: |
|
1443 |
return getattr(self, setting + '_emails') |
|
1444 | ||
1445 |
role = self.get_role_from_choice(value) |
|
1446 |
if not role or not hasattr(role, 'role'): |
|
1447 |
return [] |
|
1448 |
emails = role.role.emails |
|
1449 |
if role.role.emails_to_members: |
|
1450 |
emails.extend(role.user_set.values_list('email', flat=True)) |
|
1451 |
return emails |
|
1452 | ||
1453 |
@property |
|
1454 |
def display_info(self): |
|
1455 |
for field in self.get_setting_fields(): |
|
1456 |
choice = getattr(self, field.name) |
|
1457 |
if not choice: |
|
1458 |
continue |
|
1459 | ||
1460 |
if choice == self.EMAIL_FIELD: |
|
1461 |
emails = getattr(self, field.name + '_emails') |
|
1462 |
yield (field.verbose_name, ', '.join(emails)) |
|
1463 |
else: |
|
1464 |
role = self.get_role_from_choice(choice) |
|
1465 |
if role: |
|
1466 |
display_name = getattr(self, 'get_' + field.name + '_display')() |
|
1467 |
yield (field.verbose_name, '%s (%s)' % (display_name, role)) |
|
1468 | ||
1469 |
def get_role_from_choice(self, choice): |
|
1470 |
if choice == self.EDIT_ROLE: |
|
1471 |
return self.agenda.edit_role |
|
1472 |
elif choice == self.VIEW_ROLE: |
|
1473 |
return self.agenda.view_role |
|
1474 | ||
1475 |
@classmethod |
|
1476 |
def import_json(cls, data): |
|
1477 |
data = clean_import_data(cls, data) |
|
1478 |
return cls(**data) |
|
1479 | ||
1480 |
def export_json(self): |
|
1481 |
return { |
|
1482 |
'almost_full_event': self.almost_full_event, |
|
1483 |
'almost_full_event_emails': self.almost_full_event_emails, |
|
1484 |
'full_event': self.full_event, |
|
1485 |
'full_event_emails': self.full_event_emails, |
|
1486 |
'cancelled_event': self.cancelled_event, |
|
1487 |
'cancelled_event_emails': self.cancelled_event_emails, |
|
1488 |
} |
chrono/manager/forms.py | ||
---|---|---|
39 | 39 |
TimePeriodExceptionSource, |
40 | 40 |
VirtualMember, |
41 | 41 |
Resource, |
42 |
AgendaNotificationsSettings, |
|
42 | 43 |
WEEKDAYS_LIST, |
43 | 44 |
) |
44 | 45 | |
... | ... | |
449 | 450 |
class Meta: |
450 | 451 |
model = Booking |
451 | 452 |
fields = [] |
453 | ||
454 | ||
455 |
class AgendaNotificationsForm(forms.ModelForm): |
|
456 |
class Meta: |
|
457 |
model = AgendaNotificationsSettings |
|
458 |
fields = '__all__' |
|
459 |
widgets = { |
|
460 |
'agenda': forms.HiddenInput(), |
|
461 |
} |
|
462 | ||
463 |
def __init__(self, *args, **kwargs): |
|
464 |
super().__init__(*args, **kwargs) |
|
465 | ||
466 |
for email_field in AgendaNotificationsSettings.get_email_field_names(): |
|
467 |
self.fields[email_field].widget.attrs['size'] = 80 |
|
468 |
self.fields[email_field].label = '' |
|
469 |
self.fields[email_field].help_text = _('Enter a comma separated list of email addresses.') |
chrono/manager/templates/chrono/manager_agenda_notifications_form.html | ||
---|---|---|
1 |
{% extends "chrono/manager_agenda_view.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="">{% trans "Notification settings" %}</a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block appbar %} |
|
10 |
<h2>{% trans "Notification settings" %}</h2> |
|
11 |
{% endblock %} |
|
12 | ||
13 |
{% block content %} |
|
14 |
<form method="post" enctype="multipart/form-data"> |
|
15 |
{% csrf_token %} |
|
16 |
{{ form.as_p }} |
|
17 |
<div class="buttons"> |
|
18 |
<button class="submit-button">{% trans "Save" %}</button> |
|
19 |
<a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a> |
|
20 |
</div> |
|
21 | ||
22 |
<script> |
|
23 |
$('select').change(function(){ |
|
24 |
role_field_id = $(this).attr('id') |
|
25 |
email_field_id = '#' + role_field_id + '_emails' |
|
26 |
if ($(this).val() == 'use-email-field') |
|
27 |
$(email_field_id).parent('p').show(); |
|
28 |
else |
|
29 |
$(email_field_id).parent('p').hide(); |
|
30 |
}); |
|
31 |
$('select').trigger('change'); |
|
32 |
</script> |
|
33 |
</form> |
|
34 |
{% endblock %} |
chrono/manager/templates/chrono/manager_events_agenda_settings.html | ||
---|---|---|
30 | 30 |
</div> |
31 | 31 |
</div> |
32 | 32 | |
33 |
<div class="section"> |
|
34 |
<h3>{% trans "Notifications" %}</h3> |
|
35 |
<div> |
|
36 |
<ul> |
|
37 |
{% for setting, value in object.notifications_settings.display_info %} |
|
38 |
<li> |
|
39 |
{% blocktrans %} |
|
40 |
{{ setting }}: {{ value }} will be notified. |
|
41 |
{% endblocktrans %} |
|
42 |
</li> |
|
43 |
{% empty %} |
|
44 |
{% trans "Notifications are disabled for this agenda." %} |
|
45 |
{% endfor %} |
|
46 |
</ul> |
|
47 |
<a rel="popup" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a> |
|
48 |
</div> |
|
49 |
</div> |
|
50 | ||
33 | 51 |
{% endblock %} |
chrono/manager/urls.py | ||
---|---|---|
59 | 59 |
views.agenda_import_events, |
60 | 60 |
name='chrono-manager-agenda-import-events', |
61 | 61 |
), |
62 |
url( |
|
63 |
r'^agendas/(?P<pk>\d+)/notifications$', |
|
64 |
views.agenda_notifications_settings, |
|
65 |
name='chrono-manager-agenda-notifications-settings', |
|
66 |
), |
|
62 | 67 |
url( |
63 | 68 |
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$', |
64 | 69 |
views.event_view, |
chrono/manager/views.py | ||
---|---|---|
23 | 23 | |
24 | 24 |
from django.contrib import messages |
25 | 25 |
from django.core.exceptions import PermissionDenied |
26 |
from django.db.models import Q |
|
26 |
from django.db.models import Q, F
|
|
27 | 27 |
from django.db.models import Min, Max |
28 | 28 |
from django.forms import ValidationError |
29 | 29 |
from django.http import Http404, HttpResponse, HttpResponseRedirect |
... | ... | |
61 | 61 |
TimePeriodExceptionSource, |
62 | 62 |
VirtualMember, |
63 | 63 |
Resource, |
64 |
AgendaNotificationsSettings, |
|
64 | 65 |
) |
65 | 66 | |
66 | 67 |
from .forms import ( |
... | ... | |
85 | 86 |
AgendaResourceForm, |
86 | 87 |
AgendaDuplicateForm, |
87 | 88 |
BookingCancelForm, |
89 |
AgendaNotificationsForm, |
|
88 | 90 |
) |
89 | 91 |
from .utils import import_site |
90 | 92 | |
... | ... | |
1213 | 1215 |
agenda_import_events = AgendaImportEventsView.as_view() |
1214 | 1216 | |
1215 | 1217 | |
1218 |
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView): |
|
1219 |
template_name = 'chrono/manager_agenda_notifications_form.html' |
|
1220 |
model = AgendaNotificationsSettings |
|
1221 |
form_class = AgendaNotificationsForm |
|
1222 | ||
1223 |
def get_object(self): |
|
1224 |
try: |
|
1225 |
return self.agenda.notifications_settings |
|
1226 |
except AgendaNotificationsSettings.DoesNotExist: |
|
1227 |
# prevent old events from sending notifications |
|
1228 |
statuses = ('almost_full', 'full', 'cancelled') |
|
1229 |
kwargs = {'was_' + status: F(status) for status in statuses} |
|
1230 |
self.agenda.event_set.update(**kwargs) |
|
1231 |
return AgendaNotificationsSettings.objects.create(agenda=self.agenda) |
|
1232 | ||
1233 | ||
1234 |
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view() |
|
1235 | ||
1236 | ||
1216 | 1237 |
class EventDetailView(ViewableAgendaMixin, DetailView): |
1217 | 1238 |
model = Event |
1218 | 1239 |
pk_url_kwarg = 'event_pk' |
tests/settings.py | ||
---|---|---|
25 | 25 |
} |
26 | 26 |
}, |
27 | 27 |
} |
28 | ||
29 |
SITE_BASE_URL = 'https://example.com' |
tests/test_agendas.py | ||
---|---|---|
4 | 4 |
import requests |
5 | 5 | |
6 | 6 | |
7 |
from django.contrib.auth.models import Group |
|
7 |
from django.contrib.auth.models import Group, User
|
|
8 | 8 |
from django.core.files.base import ContentFile |
9 | 9 |
from django.core.management import call_command |
10 | 10 |
from django.utils.timezone import localtime, make_aware, now |
... | ... | |
21 | 21 |
TimePeriodException, |
22 | 22 |
TimePeriodExceptionSource, |
23 | 23 |
VirtualMember, |
24 |
AgendaNotificationsSettings, |
|
24 | 25 |
) |
25 | 26 | |
26 | 27 |
pytestmark = pytest.mark.django_db |
... | ... | |
987 | 988 | |
988 | 989 |
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists() |
989 | 990 |
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists() |
991 | ||
992 | ||
993 |
@mock.patch('django.contrib.auth.models.Group.role', create=True) |
|
994 |
@pytest.mark.parametrize( |
|
995 |
'emails_to_members,emails', |
|
996 |
[(False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']),], |
|
997 |
) |
|
998 |
def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox): |
|
999 |
group = Group.objects.create(name='group') |
|
1000 |
user = User.objects.create(username='user', email='user@entrouvert.com') |
|
1001 |
user.groups.add(group) |
|
1002 |
mocked_role.emails_to_members = emails_to_members |
|
1003 |
mocked_role.emails = emails |
|
1004 |
expected_recipients = emails |
|
1005 |
if emails_to_members: |
|
1006 |
expected_recipients.append(user.email) |
|
1007 |
expected_email_count = 1 if emails else 0 |
|
1008 | ||
1009 |
agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group) |
|
1010 | ||
1011 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1012 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1013 |
settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE |
|
1014 |
settings.save() |
|
1015 | ||
1016 |
# book 9/10 places to reach almost full state |
|
1017 |
for i in range(9): |
|
1018 |
Booking.objects.create(event=event) |
|
1019 |
event.refresh_from_db() |
|
1020 |
assert event.almost_full |
|
1021 | ||
1022 |
call_command('send_email_notifications') |
|
1023 |
assert len(mailoutbox) == expected_email_count |
|
1024 |
if mailoutbox: |
|
1025 |
assert mailoutbox[0].recipients() == expected_recipients |
|
1026 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full' |
|
1027 | ||
1028 |
# no new email on subsequent run |
|
1029 |
call_command('send_email_notifications') |
|
1030 |
assert len(mailoutbox) == expected_email_count |
|
1031 | ||
1032 | ||
1033 |
def test_agenda_notifications_email_list(mailoutbox): |
|
1034 |
agenda = Agenda.objects.create(label='Foo bar', kind='event') |
|
1035 | ||
1036 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1037 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1038 |
settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD |
|
1039 |
settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
1040 |
settings.save() |
|
1041 | ||
1042 |
for i in range(10): |
|
1043 |
Booking.objects.create(event=event) |
|
1044 |
event.refresh_from_db() |
|
1045 |
assert event.full |
|
1046 | ||
1047 |
call_command('send_email_notifications') |
|
1048 |
assert len(mailoutbox) == 1 |
|
1049 |
assert mailoutbox[0].recipients() == recipients |
|
1050 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is full' |
|
1051 |
assert mailoutbox[0].body == 'You can view it here: https://example.com/manage/agendas/%s/events/%s/.' % ( |
|
1052 |
agenda.pk, |
|
1053 |
event.pk, |
|
1054 |
) |
|
1055 | ||
1056 |
# no new email on subsequent run |
|
1057 |
call_command('send_email_notifications') |
|
1058 |
assert len(mailoutbox) == 1 |
|
1059 | ||
1060 | ||
1061 |
def test_agenda_notifications_cancelled(mailoutbox): |
|
1062 |
agenda = Agenda.objects.create(label='Foo bar', kind='event') |
|
1063 | ||
1064 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1065 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1066 |
settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD |
|
1067 |
settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
1068 |
settings.save() |
|
1069 | ||
1070 |
event.cancelled = True |
|
1071 |
event.save() |
|
1072 | ||
1073 |
call_command('send_email_notifications') |
|
1074 |
assert len(mailoutbox) == 1 |
|
1075 |
assert mailoutbox[0].recipients() == recipients |
|
1076 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled' |
|
1077 | ||
1078 |
# no new email on subsequent run |
|
1079 |
call_command('send_email_notifications') |
|
1080 |
assert len(mailoutbox) == 1 |
tests/test_import_export.py | ||
---|---|---|
26 | 26 |
AgendaImportError, |
27 | 27 |
MeetingType, |
28 | 28 |
VirtualMember, |
29 |
AgendaNotificationsSettings, |
|
29 | 30 |
) |
30 | 31 |
from chrono.manager.utils import import_site |
31 | 32 | |
... | ... | |
361 | 362 |
assert Desk.objects.exists() is True |
362 | 363 |
assert TimePeriod.objects.exists() is True |
363 | 364 |
assert TimePeriodException.objects.exists() is True |
365 | ||
366 | ||
367 |
def test_import_export_notification_settings(): |
|
368 |
agenda = Agenda.objects.create(label='Foo bar', kind='events') |
|
369 |
settings = AgendaNotificationsSettings.objects.create( |
|
370 |
agenda=agenda, |
|
371 |
almost_full_event=AgendaNotificationsSettings.EDIT_ROLE, |
|
372 |
full_event=AgendaNotificationsSettings.VIEW_ROLE, |
|
373 |
cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD, |
|
374 |
cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'], |
|
375 |
) |
|
376 |
output = get_output_of_command('export_site') |
|
377 |
payload = json.loads(output) |
|
378 | ||
379 |
agenda.delete() |
|
380 |
assert not AgendaNotificationsSettings.objects.exists() |
|
381 | ||
382 |
import_site(payload) |
|
383 |
agenda = Agenda.objects.first() |
|
384 |
AgendaNotificationsSettings.objects.get( |
|
385 |
agenda=agenda, |
|
386 |
almost_full_event=AgendaNotificationsSettings.EDIT_ROLE, |
|
387 |
full_event=AgendaNotificationsSettings.VIEW_ROLE, |
|
388 |
cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD, |
|
389 |
cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'], |
|
390 |
) |
tests/test_manager.py | ||
---|---|---|
9 | 9 |
import os |
10 | 10 | |
11 | 11 |
from django.contrib.auth.models import User, Group |
12 |
from django.core.management import call_command |
|
12 | 13 |
from django.db import connection |
13 | 14 |
from django.test.utils import CaptureQueriesContext |
14 | 15 |
from django.utils.encoding import force_text |
... | ... | |
47 | 48 | |
48 | 49 | |
49 | 50 |
@pytest.fixture |
50 |
def manager_user(): |
|
51 |
def managers_group(): |
|
52 |
group, _ = Group.objects.get_or_create(name='Managers') |
|
53 |
return group |
|
54 | ||
55 | ||
56 |
@pytest.fixture |
|
57 |
def manager_user(managers_group): |
|
51 | 58 |
try: |
52 | 59 |
user = User.objects.get(username='manager') |
53 | 60 |
except User.DoesNotExist: |
54 | 61 |
user = User.objects.create_user('manager', password='manager') |
55 |
group, created = Group.objects.get_or_create(name='Managers') |
|
56 |
if created: |
|
57 |
group.save() |
|
58 |
user.groups.set([group]) |
|
62 |
user.groups.set([managers_group]) |
|
59 | 63 |
return user |
60 | 64 | |
61 | 65 | |
... | ... | |
3496 | 3500 |
assert 'Cancelled' in resp.text |
3497 | 3501 |
assert '0 booked places' in resp.text |
3498 | 3502 |
assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2 |
3503 | ||
3504 | ||
3505 |
def test_agenda_notifications(app, admin_user, managers_group): |
|
3506 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
3507 | ||
3508 |
login(app) |
|
3509 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3510 | ||
3511 |
assert 'Notifications' in resp.text |
|
3512 |
assert 'Notifications are disabled' in resp.text |
|
3513 | ||
3514 |
resp = resp.click('Configure') |
|
3515 |
resp.form['almost_full_event'] = 'edit-role' |
|
3516 |
resp.form['full_event'] = 'view-role' |
|
3517 |
resp.form['cancelled_event'] = 'use-email-field' |
|
3518 |
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com' |
|
3519 |
resp = resp.form.submit().follow() |
|
3520 | ||
3521 |
settings = agenda.notifications_settings |
|
3522 |
assert settings.almost_full_event == 'edit-role' |
|
3523 |
assert settings.full_event == 'view-role' |
|
3524 |
assert settings.cancelled_event == 'use-email-field' |
|
3525 |
assert settings.cancelled_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
3526 | ||
3527 |
assert 'Cancelled event: hop@entrouvert.com, top@entrouvert.com will be notified' in resp.text |
|
3528 |
assert not 'Full event:' in resp.text |
|
3529 |
assert not 'Almost full event (90%):' in resp.text |
|
3530 | ||
3531 |
agenda.view_role = managers_group |
|
3532 |
agenda.edit_role = Group.objects.create(name='hop') |
|
3533 |
agenda.save() |
|
3534 | ||
3535 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3536 |
assert 'Almost full event (90%): Edit Role (hop) will be notified' in resp.text |
|
3537 |
assert 'Full event: View Role (Managers) will be notified' in resp.text |
|
3538 | ||
3539 | ||
3540 |
def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox): |
|
3541 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
3542 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Old event') |
|
3543 |
event.cancelled = True |
|
3544 |
event.save() |
|
3545 | ||
3546 |
login(app) |
|
3547 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3548 | ||
3549 |
resp = resp.click('Configure') |
|
3550 |
resp.form['cancelled_event'] = 'use-email-field' |
|
3551 |
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com' |
|
3552 |
resp.form.submit() |
|
3553 | ||
3554 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event') |
|
3555 |
event.cancelled = True |
|
3556 |
event.save() |
|
3557 | ||
3558 |
call_command('send_email_notifications') |
|
3559 |
# no notification is sent for old event |
|
3560 |
assert len(mailoutbox) == 1 |
|
3561 |
assert 'New event' in mailoutbox[0].subject |
|
3499 |
- |