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 |
|
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 import timezone |
|
25 |
from django.utils.translation import ugettext_lazy as _ |
|
26 | ||
27 |
from chrono.agendas.models import Agenda |
|
28 | ||
29 | ||
30 |
class Command(BaseCommand): |
|
31 |
EMAIL_SUBJECTS = { |
|
32 |
'almost_full': _('Alert: event "%s" is almost full (90%%)'), |
|
33 |
'full': _('Alert: event "%s" is full'), |
|
34 |
'cancelled': _('Alert: event "%s" is cancelled'), |
|
35 |
} |
|
36 |
help = 'Send email notifications' |
|
37 | ||
38 |
def handle(self, **options): |
|
39 |
agendas = Agenda.objects.filter(notifications_settings__isnull=False).select_related( |
|
40 |
'notifications_settings' |
|
41 |
) |
|
42 |
for agenda in agendas: |
|
43 |
for notification_type in agenda.notifications_settings.get_notification_types(): |
|
44 |
recipients = notification_type.get_recipients() |
|
45 |
if not recipients: |
|
46 |
continue |
|
47 | ||
48 |
status = notification_type.related_field |
|
49 |
filter_kwargs = {status: True, status + '_notification_timestamp__isnull': True} |
|
50 |
events = agenda.event_set.filter(**filter_kwargs) |
|
51 |
for event in events: |
|
52 |
self.send_notification(event, status, recipients) |
|
53 | ||
54 |
def send_notification(self, event, status, recipients): |
|
55 |
subject = self.EMAIL_SUBJECTS[status] % event |
|
56 |
ctx = {'event': event, 'event_url': urljoin(settings.SITE_BASE_URL, event.get_absolute_view_url())} |
|
57 |
ctx.update(settings.TEMPLATE_VARS) |
|
58 |
body = render_to_string('agendas/event_notification_body.txt', ctx) |
|
59 |
html_body = render_to_string('agendas/event_notification_body.html', ctx) |
|
60 | ||
61 |
timestamp = timezone.now() |
|
62 |
with atomic(): |
|
63 |
setattr(event, status + '_notification_timestamp', timestamp) |
|
64 |
event.save() |
|
65 |
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients, html_message=html_body) |
chrono/agendas/migrations/0058_auto_20200812_1211.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-08-12 10:11 |
|
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(blank=True, null=True), |
|
95 |
), |
|
96 |
migrations.AddField( |
|
97 |
model_name='event', |
|
98 |
name='cancelled_notification_timestamp', |
|
99 |
field=models.DateTimeField(blank=True, null=True), |
|
100 |
), |
|
101 |
migrations.AddField( |
|
102 |
model_name='event', |
|
103 |
name='full_notification_timestamp', |
|
104 |
field=models.DateTimeField(blank=True, null=True), |
|
105 |
), |
|
106 |
] |
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() |
... | ... | |
783 | 791 |
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE) |
784 | 792 |
resources = models.ManyToManyField('Resource') |
785 | 793 | |
794 |
almost_full_notification_timestamp = models.DateTimeField(null=True, blank=True) |
|
795 |
full_notification_timestamp = models.DateTimeField(null=True, blank=True) |
|
796 |
cancelled_notification_timestamp = models.DateTimeField(null=True, blank=True) |
|
797 | ||
786 | 798 |
class Meta: |
787 | 799 |
ordering = ['agenda', 'start_datetime', 'duration', 'label'] |
788 | 800 |
unique_together = ('agenda', 'slug') |
... | ... | |
908 | 920 |
def get_absolute_url(self): |
909 | 921 |
return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) |
910 | 922 | |
923 |
def get_absolute_view_url(self): |
|
924 |
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.id}) |
|
925 | ||
911 | 926 |
@classmethod |
912 | 927 |
def import_json(cls, data): |
913 | 928 |
data['start_datetime'] = make_aware( |
... | ... | |
1474 | 1489 | |
1475 | 1490 |
class Meta: |
1476 | 1491 |
ordering = ['-timestamp'] |
1492 | ||
1493 | ||
1494 |
class NotificationType: |
|
1495 |
def __init__(self, name, related_field, settings): |
|
1496 |
self.name = name |
|
1497 |
self.related_field = related_field |
|
1498 |
self.settings = settings |
|
1499 | ||
1500 |
def get_recipients(self): |
|
1501 |
choice = getattr(self.settings, self.name) |
|
1502 |
if not choice: |
|
1503 |
return [] |
|
1504 | ||
1505 |
if choice == self.settings.EMAIL_FIELD: |
|
1506 |
return getattr(self.settings, self.name + '_emails') |
|
1507 | ||
1508 |
role = self.settings.get_role_from_choice(choice) |
|
1509 |
if not role or not hasattr(role, 'role'): |
|
1510 |
return [] |
|
1511 |
emails = role.role.emails |
|
1512 |
if role.role.emails_to_members: |
|
1513 |
emails.extend(role.user_set.values_list('email', flat=True)) |
|
1514 |
return emails |
|
1515 | ||
1516 |
@property |
|
1517 |
def display_value(self): |
|
1518 |
choice = getattr(self.settings, self.name) |
|
1519 |
if not choice: |
|
1520 |
return '' |
|
1521 | ||
1522 |
if choice == self.settings.EMAIL_FIELD: |
|
1523 |
emails = getattr(self.settings, self.name + '_emails') |
|
1524 |
return ', '.join(emails) |
|
1525 | ||
1526 |
role = self.settings.get_role_from_choice(choice) |
|
1527 |
if role: |
|
1528 |
display_name = getattr(self.settings, 'get_%s_display' % self.name)() |
|
1529 |
return '%s (%s)' % (display_name, role) |
|
1530 | ||
1531 |
@property |
|
1532 |
def label(self): |
|
1533 |
return self.settings._meta.get_field(self.name).verbose_name |
|
1534 | ||
1535 | ||
1536 |
class AgendaNotificationsSettings(models.Model): |
|
1537 |
EMAIL_FIELD = 'use-email-field' |
|
1538 |
VIEW_ROLE = 'view-role' |
|
1539 |
EDIT_ROLE = 'edit-role' |
|
1540 | ||
1541 |
CHOICES = [ |
|
1542 |
(EDIT_ROLE, _('Edit Role')), |
|
1543 |
(VIEW_ROLE, _('View Role')), |
|
1544 |
(EMAIL_FIELD, _('Specify email addresses manually')), |
|
1545 |
] |
|
1546 | ||
1547 |
agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='notifications_settings') |
|
1548 | ||
1549 |
almost_full_event = models.CharField( |
|
1550 |
max_length=16, blank=True, choices=CHOICES, verbose_name=_('Almost full event (90%)') |
|
1551 |
) |
|
1552 |
almost_full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1553 | ||
1554 |
full_event = models.CharField(max_length=16, blank=True, choices=CHOICES, verbose_name=_('Full event')) |
|
1555 |
full_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1556 | ||
1557 |
cancelled_event = models.CharField( |
|
1558 |
max_length=16, blank=True, choices=CHOICES, verbose_name=_('Cancelled event') |
|
1559 |
) |
|
1560 |
cancelled_event_emails = ArrayField(models.EmailField(), blank=True, null=True) |
|
1561 | ||
1562 |
@classmethod |
|
1563 |
def get_email_field_names(cls): |
|
1564 |
return [field.name for field in cls._meta.get_fields() if isinstance(field, ArrayField)] |
|
1565 | ||
1566 |
def get_notification_types(self): |
|
1567 |
for field in ['almost_full_event', 'full_event', 'cancelled_event']: |
|
1568 |
yield NotificationType(name=field, related_field=field.replace('_event', ''), settings=self) |
|
1569 | ||
1570 |
def get_role_from_choice(self, choice): |
|
1571 |
if choice == self.EDIT_ROLE: |
|
1572 |
return self.agenda.edit_role |
|
1573 |
elif choice == self.VIEW_ROLE: |
|
1574 |
return self.agenda.view_role |
|
1575 | ||
1576 |
@classmethod |
|
1577 |
def import_json(cls, data): |
|
1578 |
data = clean_import_data(cls, data) |
|
1579 |
return cls(**data) |
|
1580 | ||
1581 |
def export_json(self): |
|
1582 |
return { |
|
1583 |
'almost_full_event': self.almost_full_event, |
|
1584 |
'almost_full_event_emails': self.almost_full_event_emails, |
|
1585 |
'full_event': self.full_event, |
|
1586 |
'full_event_emails': self.full_event_emails, |
|
1587 |
'cancelled_event': self.cancelled_event, |
|
1588 |
'cancelled_event_emails': self.cancelled_event_emails, |
|
1589 |
} |
chrono/agendas/templates/agendas/event_notification_body.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% autoescape off %} |
|
3 |
{% blocktrans %} |
|
4 |
Hi, |
|
5 | ||
6 |
You have been notified because the status of event "{{ event }}" has changed. |
|
7 |
{% endblocktrans %} |
|
8 |
<a href="{{ event_url }}">{% trans "View event" %}</a>. |
chrono/agendas/templates/agendas/event_notification_body.txt | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% autoescape off %} |
|
3 |
{% blocktrans %} |
|
4 |
Hi, |
|
5 | ||
6 |
You have been notified because the status of event "{{ event }}" has changed. |
|
7 |
You can view it here: {{ event_url }}. |
|
8 |
{% endblocktrans %} |
chrono/manager/forms.py | ||
---|---|---|
40 | 40 |
VirtualMember, |
41 | 41 |
Resource, |
42 | 42 |
Category, |
43 |
AgendaNotificationsSettings, |
|
43 | 44 |
WEEKDAYS_LIST, |
44 | 45 |
) |
45 | 46 | |
... | ... | |
508 | 509 |
class Meta: |
509 | 510 |
model = Event |
510 | 511 |
fields = [] |
512 | ||
513 | ||
514 |
class AgendaNotificationsForm(forms.ModelForm): |
|
515 |
class Meta: |
|
516 |
model = AgendaNotificationsSettings |
|
517 |
fields = '__all__' |
|
518 |
widgets = { |
|
519 |
'agenda': forms.HiddenInput(), |
|
520 |
} |
|
521 | ||
522 |
def __init__(self, *args, **kwargs): |
|
523 |
super().__init__(*args, **kwargs) |
|
524 | ||
525 |
for email_field in AgendaNotificationsSettings.get_email_field_names(): |
|
526 |
self.fields[email_field].widget.attrs['size'] = 80 |
|
527 |
self.fields[email_field].label = '' |
|
528 |
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 |
... | ... | |
63 | 63 |
Resource, |
64 | 64 |
Category, |
65 | 65 |
EventCancellationReport, |
66 |
AgendaNotificationsSettings, |
|
66 | 67 |
) |
67 | 68 | |
68 | 69 |
from .forms import ( |
... | ... | |
90 | 91 |
CategoryEditForm, |
91 | 92 |
BookingCancelForm, |
92 | 93 |
EventCancelForm, |
94 |
AgendaNotificationsForm, |
|
93 | 95 |
) |
94 | 96 |
from .utils import import_site |
95 | 97 | |
... | ... | |
1345 | 1347 |
agenda_import_events = AgendaImportEventsView.as_view() |
1346 | 1348 | |
1347 | 1349 | |
1350 |
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView): |
|
1351 |
template_name = 'chrono/manager_agenda_notifications_form.html' |
|
1352 |
model = AgendaNotificationsSettings |
|
1353 |
form_class = AgendaNotificationsForm |
|
1354 | ||
1355 |
def get_object(self): |
|
1356 |
try: |
|
1357 |
return self.agenda.notifications_settings |
|
1358 |
except AgendaNotificationsSettings.DoesNotExist: |
|
1359 |
# prevent old events from sending notifications |
|
1360 |
statuses = ('almost_full', 'full', 'cancelled') |
|
1361 |
timestamp = now() |
|
1362 |
for status in statuses: |
|
1363 |
filter_kwargs = {status: True} |
|
1364 |
update_kwargs = {status + '_notification_timestamp': timestamp} |
|
1365 |
self.agenda.event_set.filter(**filter_kwargs).update(**update_kwargs) |
|
1366 |
return AgendaNotificationsSettings.objects.create(agenda=self.agenda) |
|
1367 | ||
1368 | ||
1369 |
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view() |
|
1370 | ||
1371 | ||
1348 | 1372 |
class EventDetailView(ViewableAgendaMixin, DetailView): |
1349 | 1373 |
model = Event |
1350 | 1374 |
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 |
... | ... | |
23 | 23 |
TimePeriodExceptionSource, |
24 | 24 |
VirtualMember, |
25 | 25 |
EventCancellationReport, |
26 |
AgendaNotificationsSettings, |
|
26 | 27 |
) |
27 | 28 | |
28 | 29 |
pytestmark = pytest.mark.django_db |
... | ... | |
1075 | 1076 |
freezer.move_to('2020-03-01') |
1076 | 1077 |
call_command('cancel_events') |
1077 | 1078 |
assert not EventCancellationReport.objects.exists() |
1079 | ||
1080 | ||
1081 |
@mock.patch('django.contrib.auth.models.Group.role', create=True) |
|
1082 |
@pytest.mark.parametrize( |
|
1083 |
'emails_to_members,emails', |
|
1084 |
[(False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']),], |
|
1085 |
) |
|
1086 |
def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox): |
|
1087 |
group = Group.objects.create(name='group') |
|
1088 |
user = User.objects.create(username='user', email='user@entrouvert.com') |
|
1089 |
user.groups.add(group) |
|
1090 |
mocked_role.emails_to_members = emails_to_members |
|
1091 |
mocked_role.emails = emails |
|
1092 |
expected_recipients = emails |
|
1093 |
if emails_to_members: |
|
1094 |
expected_recipients.append(user.email) |
|
1095 |
expected_email_count = 1 if emails else 0 |
|
1096 | ||
1097 |
agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group) |
|
1098 | ||
1099 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1100 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1101 |
settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE |
|
1102 |
settings.save() |
|
1103 | ||
1104 |
# book 9/10 places to reach almost full state |
|
1105 |
for i in range(9): |
|
1106 |
Booking.objects.create(event=event) |
|
1107 |
event.refresh_from_db() |
|
1108 |
assert event.almost_full |
|
1109 | ||
1110 |
call_command('send_email_notifications') |
|
1111 |
assert len(mailoutbox) == expected_email_count |
|
1112 |
if mailoutbox: |
|
1113 |
assert mailoutbox[0].recipients() == expected_recipients |
|
1114 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full (90%)' |
|
1115 | ||
1116 |
# no new email on subsequent run |
|
1117 |
call_command('send_email_notifications') |
|
1118 |
assert len(mailoutbox) == expected_email_count |
|
1119 | ||
1120 | ||
1121 |
def test_agenda_notifications_email_list(mailoutbox): |
|
1122 |
agenda = Agenda.objects.create(label='Foo bar', kind='event') |
|
1123 | ||
1124 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1125 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1126 |
settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD |
|
1127 |
settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
1128 |
settings.save() |
|
1129 | ||
1130 |
for i in range(10): |
|
1131 |
Booking.objects.create(event=event) |
|
1132 |
event.refresh_from_db() |
|
1133 |
assert event.full |
|
1134 | ||
1135 |
call_command('send_email_notifications') |
|
1136 |
assert len(mailoutbox) == 1 |
|
1137 |
assert mailoutbox[0].recipients() == recipients |
|
1138 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is full' |
|
1139 |
assert ( |
|
1140 |
'view it here: https://example.com/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk,) |
|
1141 |
in mailoutbox[0].body |
|
1142 |
) |
|
1143 | ||
1144 |
# no new email on subsequent run |
|
1145 |
call_command('send_email_notifications') |
|
1146 |
assert len(mailoutbox) == 1 |
|
1147 | ||
1148 | ||
1149 |
def test_agenda_notifications_cancelled(mailoutbox): |
|
1150 |
agenda = Agenda.objects.create(label='Foo bar', kind='event') |
|
1151 | ||
1152 |
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') |
|
1153 |
settings = AgendaNotificationsSettings.objects.create(agenda=agenda) |
|
1154 |
settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD |
|
1155 |
settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
1156 |
settings.save() |
|
1157 | ||
1158 |
event.cancelled = True |
|
1159 |
event.save() |
|
1160 | ||
1161 |
call_command('send_email_notifications') |
|
1162 |
assert len(mailoutbox) == 1 |
|
1163 |
assert mailoutbox[0].recipients() == recipients |
|
1164 |
assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled' |
|
1165 | ||
1166 |
# no new email on subsequent run |
|
1167 |
call_command('send_email_notifications') |
|
1168 |
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 | ||
---|---|---|
49 | 49 | |
50 | 50 | |
51 | 51 |
@pytest.fixture |
52 |
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): |
|
53 | 59 |
try: |
54 | 60 |
user = User.objects.get(username='manager') |
55 | 61 |
except User.DoesNotExist: |
56 | 62 |
user = User.objects.create_user('manager', password='manager') |
57 |
group, created = Group.objects.get_or_create(name='Managers') |
|
58 |
if created: |
|
59 |
group.save() |
|
60 |
user.groups.set([group]) |
|
63 |
user.groups.set([managers_group]) |
|
61 | 64 |
return user |
62 | 65 | |
63 | 66 | |
... | ... | |
3809 | 3812 |
resp = resp.click('Cancel', href='/cancel') |
3810 | 3813 |
assert 'event has bookings with no callback url configured' in resp.text |
3811 | 3814 |
assert 'Proceed with cancellation' not in resp.text |
3815 | ||
3816 | ||
3817 |
def test_agenda_notifications(app, admin_user, managers_group): |
|
3818 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
3819 | ||
3820 |
login(app) |
|
3821 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3822 | ||
3823 |
assert 'Notifications' in resp.text |
|
3824 |
assert 'Notifications are disabled' in resp.text |
|
3825 | ||
3826 |
resp = resp.click('Configure') |
|
3827 |
resp.form['almost_full_event'] = 'edit-role' |
|
3828 |
resp.form['full_event'] = 'view-role' |
|
3829 |
resp.form['cancelled_event'] = 'use-email-field' |
|
3830 |
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com' |
|
3831 |
resp = resp.form.submit().follow() |
|
3832 | ||
3833 |
settings = agenda.notifications_settings |
|
3834 |
assert settings.almost_full_event == 'edit-role' |
|
3835 |
assert settings.full_event == 'view-role' |
|
3836 |
assert settings.cancelled_event == 'use-email-field' |
|
3837 |
assert settings.cancelled_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com'] |
|
3838 | ||
3839 |
assert 'Cancelled event: hop@entrouvert.com, top@entrouvert.com will be notified' in resp.text |
|
3840 |
assert not 'Full event:' in resp.text |
|
3841 |
assert not 'Almost full event (90%):' in resp.text |
|
3842 | ||
3843 |
agenda.view_role = managers_group |
|
3844 |
agenda.edit_role = Group.objects.create(name='hop') |
|
3845 |
agenda.save() |
|
3846 | ||
3847 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3848 |
assert 'Almost full event (90%): Edit Role (hop) will be notified' in resp.text |
|
3849 |
assert 'Full event: View Role (Managers) will be notified' in resp.text |
|
3850 | ||
3851 | ||
3852 |
def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox): |
|
3853 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
3854 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Old event') |
|
3855 |
event.cancelled = True |
|
3856 |
event.save() |
|
3857 | ||
3858 |
login(app) |
|
3859 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
3860 | ||
3861 |
resp = resp.click('Configure') |
|
3862 |
resp.form['cancelled_event'] = 'use-email-field' |
|
3863 |
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com' |
|
3864 |
resp.form.submit() |
|
3865 | ||
3866 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='New event') |
|
3867 |
event.cancelled = True |
|
3868 |
event.save() |
|
3869 | ||
3870 |
call_command('send_email_notifications') |
|
3871 |
# no notification is sent for old event |
|
3872 |
assert len(mailoutbox) == 1 |
|
3873 |
assert 'New event' in mailoutbox[0].subject |
|
3812 |
- |