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 |
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 |
- |