Projet

Général

Profil

0002-agendas-add-booking-reminder-mechanisms-45293.patch

Valentin Deniaud, 15 septembre 2020 14:25

Télécharger (41,7 ko)

Voir les différences:

Subject: [PATCH 2/3] agendas: add booking reminder mechanisms (#45293)

 .../commands/send_booking_reminders.py        | 138 ++++++++++
 .../migrations/0062_auto_20200915_1401.py     |  68 +++++
 chrono/agendas/models.py                      |  69 ++++-
 .../agendas/events_reminder_body.html         |  22 ++
 .../agendas/events_reminder_body.txt          |  14 +
 .../agendas/events_reminder_message.txt       |   3 +
 .../agendas/events_reminder_subject.txt       |   3 +
 .../agendas/meetings_reminder_body.html       |  24 ++
 .../agendas/meetings_reminder_body.txt        |  18 ++
 .../agendas/meetings_reminder_message.txt     |   3 +
 .../agendas/meetings_reminder_subject.txt     |   3 +
 chrono/manager/forms.py                       |  17 ++
 .../chrono/manager_agenda_reminder_form.html  |  22 ++
 .../chrono/manager_agenda_settings.html       |  16 ++
 .../manager_virtual_agenda_settings.html      |   2 +
 chrono/manager/urls.py                        |   5 +
 chrono/manager/views.py                       |  17 ++
 debian/chrono.cron.hourly                     |   1 +
 tests/test_agendas.py                         | 244 ++++++++++++++++++
 tests/test_import_export.py                   |  19 ++
 tests/test_manager.py                         |  47 +++-
 21 files changed, 750 insertions(+), 5 deletions(-)
 create mode 100644 chrono/agendas/management/commands/send_booking_reminders.py
 create mode 100644 chrono/agendas/migrations/0062_auto_20200915_1401.py
 create mode 100644 chrono/agendas/templates/agendas/events_reminder_body.html
 create mode 100644 chrono/agendas/templates/agendas/events_reminder_body.txt
 create mode 100644 chrono/agendas/templates/agendas/events_reminder_message.txt
 create mode 100644 chrono/agendas/templates/agendas/events_reminder_subject.txt
 create mode 100644 chrono/agendas/templates/agendas/meetings_reminder_body.html
 create mode 100644 chrono/agendas/templates/agendas/meetings_reminder_body.txt
 create mode 100644 chrono/agendas/templates/agendas/meetings_reminder_message.txt
 create mode 100644 chrono/agendas/templates/agendas/meetings_reminder_subject.txt
 create mode 100644 chrono/manager/templates/chrono/manager_agenda_reminder_form.html
chrono/agendas/management/commands/send_booking_reminders.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 datetime import datetime, timedelta
18
from urllib.parse import urljoin
19
from requests import RequestException
20
from smtplib import SMTPException
21

  
22
from django.conf import settings
23
from django.core.mail import send_mail
24
from django.core.management.base import BaseCommand
25
from django.db.models import F
26
from django.db.transaction import atomic
27
from django.template.loader import render_to_string
28
from django.utils import timezone, translation
29
from django.utils.translation import ugettext_lazy as _
30

  
31
from chrono.agendas.models import Agenda, Booking
32
from chrono.utils.requests_wrapper import requests
33

  
34
SENDING_IN_PROGRESS = datetime(year=2, month=1, day=1)
35

  
36

  
37
class Command(BaseCommand):
38
    help = 'Send booking reminders'
39

  
40
    def handle(self, **options):
41
        translation.activate(settings.LANGUAGE_CODE)
42

  
43
        reminder_delta = F('event__agenda__reminder_settings__days') * timedelta(1)
44
        starts_before = timezone.now() + reminder_delta
45
        # 12 hours time window to run the command and send reminder, thus excluding old events
46
        starts_after = timezone.now() + reminder_delta - timedelta(hours=12)
47
        # prevent user who just booked from getting a reminder
48
        created_before = timezone.now() - timedelta(hours=12)
49

  
50
        bookings = Booking.objects.filter(
51
            event__agenda__reminder_settings__days__isnull=False,  # useless ?
52
            cancellation_datetime__isnull=True,
53
            creation_datetime__lte=created_before,
54
            reminder_datetime__isnull=True,
55
            event__start_datetime__lte=starts_before,
56
            event__start_datetime__gte=starts_after,
57
        ).select_related('event', 'event__agenda', 'event__agenda__reminder_settings')
58

  
59
        bookings_list = list(bookings)
60
        bookings_pk = list(bookings.values_list('pk', flat=True))
61
        bookings.update(reminder_datetime=SENDING_IN_PROGRESS)
62

  
63
        try:
64
            for booking in bookings_list:
65
                self.send_reminder(booking)
66
        finally:
67
            Booking.objects.filter(pk__in=bookings_pk, reminder_datetime__lte=SENDING_IN_PROGRESS).update(
68
                reminder_datetime=None
69
            )
70

  
71
    def send_reminder(self, booking):
72
        agenda = booking.event.agenda
73
        kind = agenda.kind
74
        days = agenda.reminder_settings.days
75

  
76
        ctx = {
77
            'event': booking.event,
78
            'meeting': booking.user_display_label,
79
            'form_url': booking.form_url,
80
            'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days,
81
            'time': booking.event.start_datetime.strftime('%H:%M'),
82
            'date': booking.event.start_datetime.date().strftime('%A %d %B'),
83
            'date_short': booking.event.start_datetime.date().strftime('%d/%m'),
84
            'email_extra_info': agenda.reminder_settings.email_extra_info,
85
            'sms_extra_info': agenda.reminder_settings.sms_extra_info,
86
        }
87
        ctx.update(getattr(settings, 'TEMPLATE_VARS', {}))
88
        if booking.form_url:
89
            ctx['form_url'] = urljoin(settings.SITE_BASE_URL, booking.form_url)
90

  
91
        if agenda.reminder_settings.send_email:
92
            self.send_email(booking, kind, ctx)
93
        if agenda.reminder_settings.send_sms:
94
            self.send_sms(booking, kind, ctx)
95

  
96
    @staticmethod
97
    def send_email(booking, kind, ctx):
98
        if not booking.user_email:
99
            return
100

  
101
        subject = render_to_string('agendas/%s_reminder_subject.txt' % kind, ctx).strip()
102
        body = render_to_string('agendas/%s_reminder_body.txt' % kind, ctx)
103
        html_body = render_to_string('agendas/%s_reminder_body.html' % kind, ctx)
104
        try:
105
            with atomic():
106
                send_mail(
107
                    subject, body, settings.DEFAULT_FROM_EMAIL, [booking.user_email], html_message=html_body
108
                )
109
                booking.reminder_datetime = timezone.now()
110
                booking.save()
111
        except SMTPException:
112
            pass
113

  
114
    @staticmethod
115
    def send_sms(booking, kind, ctx):
116
        if not booking.user_phone_number:
117
            return
118

  
119
        sms_url = getattr(settings, 'SMS_URL', '')
120
        if not sms_url:
121
            return
122
        sms_from = settings.SMS_FROM
123

  
124
        message = render_to_string('agendas/%s_reminder_message.txt' % kind, ctx).strip()
125
        payload = {
126
            'message': message,
127
            'from': settings.SMS_FROM,
128
            'to': [booking.user_phone_number],
129
        }
130

  
131
        try:
132
            with atomic():
133
                request = requests.post(sms_url, json=payload, remote_service='auto', timeout=10)
134
                request.raise_for_status()
135
                booking.reminder_datetime = timezone.now()
136
                booking.save()
137
        except RequestException:
138
            pass
chrono/agendas/migrations/0062_auto_20200915_1401.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-09-15 12:01
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('agendas', '0061_auto_20200909_1752'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='AgendaReminderSettings',
18
            fields=[
19
                (
20
                    'id',
21
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22
                ),
23
                (
24
                    'days',
25
                    models.IntegerField(
26
                        blank=True,
27
                        choices=[
28
                            (None, 'Never'),
29
                            (1, 'One day before'),
30
                            (2, 'Two days before'),
31
                            (3, 'Three days before'),
32
                        ],
33
                        null=True,
34
                        verbose_name='Send reminder',
35
                    ),
36
                ),
37
                ('send_email', models.BooleanField(default=False, verbose_name='Notify by email')),
38
                (
39
                    'email_extra_info',
40
                    models.TextField(
41
                        blank=True,
42
                        help_text='Basic information such as event name, time and date are already included',
43
                        verbose_name='Additional text to incude in emails',
44
                    ),
45
                ),
46
                ('send_sms', models.BooleanField(default=False, verbose_name='Notify by SMS')),
47
                (
48
                    'sms_extra_info',
49
                    models.TextField(
50
                        blank=True,
51
                        help_text='Basic information such as event name, time and date are already included',
52
                        verbose_name='Additional text to incude in SMS',
53
                    ),
54
                ),
55
                (
56
                    'agenda',
57
                    models.OneToOneField(
58
                        on_delete=django.db.models.deletion.CASCADE,
59
                        related_name='reminder_settings',
60
                        to='agendas.Agenda',
61
                    ),
62
                ),
63
            ],
64
        ),
65
        migrations.AddField(
66
            model_name='booking', name='reminder_datetime', field=models.DateTimeField(null=True),
67
        ),
68
    ]
chrono/agendas/models.py
43 43
from django.utils.module_loading import import_string
44 44
from django.utils.text import slugify
45 45
from django.utils.timezone import localtime, now, make_aware, make_naive, is_aware
46
from django.utils.translation import ugettext_lazy as _, ugettext
46
from django.utils.translation import ugettext_lazy as _, ugettext, ungettext
47 47

  
48 48
from jsonfield import JSONField
49 49

  
......
286 286
            },
287 287
            'resources': [x.slug for x in self.resources.all()],
288 288
        }
289
        if hasattr(self, 'reminder_settings'):
290
            agenda['reminder_settings'] = self.reminder_settings.export_json()
289 291
        if self.kind == 'events':
290 292
            agenda['events'] = [x.export_json() for x in self.event_set.all()]
291 293
            if hasattr(self, 'notifications_settings'):
......
302 304
    def import_json(cls, data, overwrite=False):
303 305
        data = data.copy()
304 306
        permissions = data.pop('permissions') or {}
307
        reminder_settings = data.pop('reminder_settings', None)
305 308
        if data['kind'] == 'events':
306 309
            events = data.pop('events')
307 310
            notifications_settings = data.pop('notifications_settings', None)
......
329 332
        if not created:
330 333
            for k, v in data.items():
331 334
                setattr(agenda, k, v)
335
        if overwrite:
336
            AgendaReminderSettings.objects.filter(agenda=agenda).delete()
337
        if reminder_settings:
338
            reminder_settings['agenda'] = agenda
339
            AgendaReminderSettings.import_json(reminder_settings).save()
332 340
        if data['kind'] == 'events':
333 341
            if overwrite:
334 342
                Event.objects.filter(agenda=agenda).delete()
......
993 1001
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
994 1002
    extra_data = JSONField(null=True)
995 1003
    cancellation_datetime = models.DateTimeField(null=True)
1004
    reminder_datetime = models.DateTimeField(null=True)
996 1005
    in_waiting_list = models.BooleanField(default=False)
997 1006
    creation_datetime = models.DateTimeField(auto_now_add=True)
998 1007
    # primary booking is used to group multiple bookings together
......
1676 1685
            'cancelled_event': self.cancelled_event,
1677 1686
            'cancelled_event_emails': self.cancelled_event_emails,
1678 1687
        }
1688

  
1689

  
1690
class AgendaReminderSettings(models.Model):
1691
    ONE_DAY_BEFORE = 1
1692
    TWO_DAYS_BEFORE = 2
1693
    THREE_DAYS_BEFORE = 3
1694

  
1695
    CHOICES = [
1696
        (None, _('Never')),
1697
        (ONE_DAY_BEFORE, _('One day before')),
1698
        (TWO_DAYS_BEFORE, _('Two days before')),
1699
        (THREE_DAYS_BEFORE, _('Three days before')),
1700
    ]
1701

  
1702
    agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='reminder_settings')
1703
    days = models.IntegerField(null=True, blank=True, choices=CHOICES, verbose_name=_('Send reminder'))
1704
    send_email = models.BooleanField(default=False, verbose_name=_('Notify by email'))
1705
    email_extra_info = models.TextField(
1706
        blank=True,
1707
        verbose_name=_('Additional text to incude in emails'),
1708
        help_text=_('Basic information such as event name, time and date are already included'),
1709
    )
1710
    send_sms = models.BooleanField(default=False, verbose_name=_('Notify by SMS'))
1711
    sms_extra_info = models.TextField(
1712
        blank=True,
1713
        verbose_name=_('Additional text to incude in SMS'),
1714
        help_text=_('Basic information such as event name, time and date are already included'),
1715
    )
1716

  
1717
    def display_info(self):
1718
        message = ungettext(
1719
            'Users will be reminded of their booking %(by_email_or_sms)s, one day in advance.',
1720
            'Users will be reminded of their booking %(by_email_or_sms)s, %(days)s days in advance.',
1721
            self.days,
1722
        )
1723

  
1724
        if self.send_sms and self.send_email:
1725
            by = _('both by email and by SMS')
1726
        elif self.send_sms:
1727
            by = _('by SMS')
1728
        elif self.send_email:
1729
            by = _('by email')
1730

  
1731
        return message % {'days': self.days, 'by_email_or_sms': by}
1732

  
1733
    @classmethod
1734
    def import_json(cls, data):
1735
        data = clean_import_data(cls, data)
1736
        return cls(**data)
1737

  
1738
    def export_json(self):
1739
        return {
1740
            'days': self.days,
1741
            'send_email': self.send_email,
1742
            'email_extra_info': self.email_extra_info,
1743
            'send_sms': self.send_sms,
1744
            'sms_extra_info': self.sms_extra_info,
1745
        }
chrono/agendas/templates/agendas/events_reminder_body.html
1
{% extends "emails/body_base.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<p>{% trans "Hi," %}</p>
6

  
7
<p>
8
{% blocktrans trimmed %}
9
You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
10
{% endblocktrans %}
11
</p>
12

  
13
{% if email_extra_info %}
14
<p>{{ email_extra_info }}</p>
15
{% endif %}
16

  
17
{% if form_url %}
18
{% with _("Edit or cancel booking") as button_label %}
19
{% include "emails/button-link.html" with url=form_url label=button_label %}
20
{% endwith %}
21
{% endif %}
22
{% endblock %}
chrono/agendas/templates/agendas/events_reminder_body.txt
1
{% extends "emails/body_base.txt" %}
2
{% load i18n %}
3

  
4
{% block content %}{% autoescape off %}{% blocktrans %}Hi,
5

  
6
You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.{% endblocktrans %}
7
{% if email_extra_info %}
8
{{ email_extra_info }}
9
{% endif %}
10
{% if form_url %}
11
{% trans "If in need to cancel it, you can do so here:" %} {{ form_url }}
12
{% endif %}
13
{% endautoescape %}
14
{% endblock %}
chrono/agendas/templates/agendas/events_reminder_message.txt
1
{% load i18n %}
2

  
3
{% blocktrans %}Reminder: you have a booking for event "{{ event }}", on {{ date_short }} at {{ time }}.{% endblocktrans %}{% if sms_extra_info %} {{ sms_extra_info }}{% endif %}
chrono/agendas/templates/agendas/events_reminder_subject.txt
1
{% extends "emails/subject.txt" %}
2
{% block email-subject %}{% autoescape off %}Reminder for your booking {{ in_x_days }} at {{ time }}{% endautoescape %}{% endblock %}
3

  
chrono/agendas/templates/agendas/meetings_reminder_body.html
1
{% extends "emails/body_base.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<p>{% trans "Hi," %}</p>
6

  
7
<p>
8
{% if meeting %}
9
{% blocktrans %}Your meeting "{{ meeting }}" is scheduled {{ in_x_days }} at {{ time }}.{% endblocktrans %}
10
{% else %}
11
{% blocktrans %}You have a meeting scheduled on {{ date }} at {{ time }}.{% endblocktrans %}
12
{% endif %}
13
</p>
14

  
15
{% if email_extra_info %}
16
<p>{{ email_extra_info }}</p>
17
{% endif %}
18

  
19
{% if form_url %}
20
{% with _("Edit or cancel meeting") as button_label %}
21
{% include "emails/button-link.html" with url=form_url label=button_label %}
22
{% endwith %}
23
{% endif %}
24
{% endblock %}
chrono/agendas/templates/agendas/meetings_reminder_body.txt
1
{% extends "emails/body_base.txt" %}
2
{% load i18n %}
3

  
4
{% block content %}{% autoescape off %}{% trans "Hi," %}
5

  
6
{% if meeting %}
7
{% blocktrans %}Your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.{% endblocktrans %}
8
{% else %}
9
{% blocktrans %}You have a meeting scheduled on {{ date }} at {{ time }}.{% endblocktrans %}
10
{% endif %}
11

  
12
{% if email_extra_info %}{{ email_extra_info }}{% endif %}
13

  
14
{% if form_url %}
15
{% trans "If in need to cancel it, you can do so here:" %} {{ form_url }}
16
{% endif %}
17
{% endautoescape %}
18
{% endblock %}
chrono/agendas/templates/agendas/meetings_reminder_message.txt
1
{% load i18n %}
2

  
3
{% if label %}{% blocktrans %}Reminder: your meeting "{{ meeting }}" is scheduled on {{ date_short }} at {{ time }}.{% endblocktrans %}{% else %}{% blocktrans %}Reminder: you have a meeting scheduled on {{ date_short }} at {{ time }}.{% endblocktrans %}{% endif %}{% if sms_extra_info %} {{ sms_extra_info }}{% endif %}
chrono/agendas/templates/agendas/meetings_reminder_subject.txt
1
{% extends "emails/subject.txt" %}
2
{% block email-subject %}{% autoescape off %}Reminder for your meeting {{ in_x_days }} at {{ time }}{% endautoescape %}{% endblock %}
3

  
chrono/manager/forms.py
20 20
import datetime
21 21

  
22 22
from django import forms
23
from django.conf import settings
23 24
from django.contrib.auth.models import Group
24 25
from django.core.exceptions import FieldDoesNotExist
25 26
from django.forms import ValidationError
......
41 42
    Resource,
42 43
    Category,
43 44
    AgendaNotificationsSettings,
45
    AgendaReminderSettings,
44 46
    WEEKDAYS_LIST,
45 47
)
46 48

  
......
526 528
            self.fields[email_field].widget.attrs['size'] = 80
527 529
            self.fields[email_field].label = ''
528 530
            self.fields[email_field].help_text = _('Enter a comma separated list of email addresses.')
531

  
532

  
533
class AgendaReminderForm(forms.ModelForm):
534
    class Meta:
535
        model = AgendaReminderSettings
536
        exclude = ['agenda']
537

  
538
    def clean(self):
539
        cleaned_data = super().clean()
540
        if cleaned_data['days'] and not (cleaned_data['send_sms'] or cleaned_data['send_email']):
541
            raise ValidationError(_('Select at least one notification medium.'))
542

  
543
        if cleaned_data['send_sms'] and not hasattr(settings, 'SMS_URL'):
544
            raise ValidationError(_('SMS are unavailable on this instance.'))
545
        return cleaned_data
chrono/manager/templates/chrono/manager_agenda_reminder_form.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="">{% trans "Reminder settings" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Reminder 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
</form>
22
{% endblock %}
chrono/manager/templates/chrono/manager_agenda_settings.html
34 34
{% block agenda-settings %}
35 35
{% endblock %}
36 36

  
37
{% block agenda-reminder %}
38
<div class="section">
39
<h3>{% trans "Booking reminders" %}</h3>
40
<div>
41
<p>
42
{% if not agenda.reminder_settings or not agenda.reminder_settings.days %}
43
{% trans "Reminders are disabled for this agenda." %}
44
{% else %}
45
{{ agenda.reminder_settings.display_info }}
46
{% endif %}
47
</p>
48
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-reminder-settings' pk=object.id %}">{% trans "Configure" %}</a>
49
</div>
50
</div>
51
{% endblock %}
52

  
37 53
{% block agenda-permissions %}
38 54
<div class="section">
39 55
<h3>{% trans "Permissions" %}</h3>
chrono/manager/templates/chrono/manager_virtual_agenda_settings.html
76 76
</div>
77 77
{% endif %}
78 78

  
79
{% block agenda-reminder %}
80
{% endblock %}
79 81
{% endblock %}
chrono/manager/urls.py
78 78
        views.agenda_notifications_settings,
79 79
        name='chrono-manager-agenda-notifications-settings',
80 80
    ),
81
    url(
82
        r'^agendas/(?P<pk>\d+)/reminder$',
83
        views.agenda_reminder_settings,
84
        name='chrono-manager-agenda-reminder-settings',
85
    ),
81 86
    url(
82 87
        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$',
83 88
        views.event_view,
chrono/manager/views.py
64 64
    Category,
65 65
    EventCancellationReport,
66 66
    AgendaNotificationsSettings,
67
    AgendaReminderSettings,
67 68
)
68 69

  
69 70
from .forms import (
......
92 93
    BookingCancelForm,
93 94
    EventCancelForm,
94 95
    AgendaNotificationsForm,
96
    AgendaReminderForm,
95 97
)
96 98
from .utils import import_site
97 99

  
......
1371 1373
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
1372 1374

  
1373 1375

  
1376
class AgendaReminderSettingsView(ManagedAgendaMixin, UpdateView):
1377
    template_name = 'chrono/manager_agenda_reminder_form.html'
1378
    model = AgendaReminderSettings
1379
    form_class = AgendaReminderForm
1380

  
1381
    def get_object(self):
1382
        try:
1383
            return self.agenda.reminder_settings
1384
        except AgendaReminderSettings.DoesNotExist:
1385
            return AgendaReminderSettings.objects.create(agenda=self.agenda)
1386

  
1387

  
1388
agenda_reminder_settings = AgendaReminderSettingsView.as_view()
1389

  
1390

  
1374 1391
class EventDetailView(ViewableAgendaMixin, DetailView):
1375 1392
    model = Event
1376 1393
    pk_url_kwarg = 'event_pk'
debian/chrono.cron.hourly
2 2

  
3 3
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command clearsessions --all-tenants
4 4
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command sync_desks_timeperiod_exceptions --all-tenants
5
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command send_booking_reminders --all-tenants
tests/test_agendas.py
1 1
import pytest
2 2
import datetime
3
import json
3 4
import mock
4 5
import requests
6
import smtplib
5 7

  
6 8

  
7 9
from django.contrib.auth.models import Group, User
......
25 27
    VirtualMember,
26 28
    EventCancellationReport,
27 29
    AgendaNotificationsSettings,
30
    AgendaReminderSettings,
28 31
)
29 32

  
30 33
pytestmark = pytest.mark.django_db
......
1238 1241
    # no new email on subsequent run
1239 1242
    call_command('send_email_notifications')
1240 1243
    assert len(mailoutbox) == 1
1244

  
1245

  
1246
def test_agenda_reminders(mailoutbox, freezer):
1247
    agenda = Agenda.objects.create(label='Events', kind='events')
1248

  
1249
    # add some old event with booking
1250
    freezer.move_to('2019-01-01')
1251
    old_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
1252
    Booking.objects.create(event=old_event, user_email='old@test.org')
1253

  
1254
    # no reminder configured
1255
    call_command('send_booking_reminders')
1256
    assert len(mailoutbox) == 0
1257

  
1258
    # move to present day
1259
    freezer.move_to('2020-01-01 14:00')
1260
    # configure reminder the day before
1261
    AgendaReminderSettings.objects.create(agenda=agenda, days=1, send_email=True)
1262
    # event starts in 2 days
1263
    start_datetime = now() + datetime.timedelta(days=2)
1264
    event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
1265

  
1266
    for i in range(5):
1267
        booking = Booking.objects.create(event=event, user_email='t@test.org')
1268
    # extra booking with no email, should be ignored
1269
    booking = Booking.objects.create(event=event)
1270

  
1271
    freezer.move_to('2020-01-02 10:00')
1272
    # not time to send reminders yet
1273
    call_command('send_booking_reminders')
1274
    assert len(mailoutbox) == 0
1275

  
1276
    # one of the booking is cancelled
1277
    Booking.objects.filter(user_email='t@test.org').first().cancel()
1278

  
1279
    freezer.move_to('2020-01-02 15:00')
1280
    call_command('send_booking_reminders')
1281
    assert len(mailoutbox) == 4
1282
    mailoutbox.clear()
1283

  
1284
    call_command('send_booking_reminders')
1285
    assert len(mailoutbox) == 0
1286

  
1287
    # booking is placed the day of the event, notfication should no be sent
1288
    freezer.move_to('2020-01-03 08:00')
1289
    booking = Booking.objects.create(event=event, user_email='t@test.org')
1290
    call_command('send_booking_reminders')
1291
    assert len(mailoutbox) == 0
1292

  
1293

  
1294
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO')
1295
def test_agenda_reminders_sms(freezer):
1296
    freezer.move_to('2020-01-01 14:00')
1297
    agenda = Agenda.objects.create(label='Events', kind='events')
1298
    AgendaReminderSettings.objects.create(agenda=agenda, days=1, send_sms=True)
1299
    start_datetime = now() + datetime.timedelta(days=2)
1300
    event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
1301

  
1302
    for i in range(5):
1303
        booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
1304
    booking = Booking.objects.create(event=event)
1305

  
1306
    freezer.move_to('2020-01-02 15:00')
1307
    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
1308
        mock_response = mock.Mock(status_code=200)
1309
        mock_send.return_value = mock_response
1310
        call_command('send_booking_reminders')
1311

  
1312
    assert mock_send.call_count == 5
1313
    body = json.loads(mock_send.call_args[0][0].body)
1314
    assert body['from'] == 'EO'
1315
    assert body['to'] == ['+336123456789']
1316

  
1317

  
1318
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO')
1319
def test_agenda_reminders_retry(freezer):
1320
    freezer.move_to('2020-01-01 14:00')
1321
    agenda = Agenda.objects.create(label='Events', kind='events')
1322
    settings = AgendaReminderSettings.objects.create(agenda=agenda, days=1)
1323
    start_datetime = now() + datetime.timedelta(days=2)
1324
    event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
1325

  
1326
    settings.send_email = True
1327
    settings.save()
1328
    booking = Booking.objects.create(event=event, user_email='t@test.org')
1329
    freezer.move_to('2020-01-02 15:00')
1330

  
1331
    def send_mail_error(*args, **kwargs):
1332
        raise smtplib.SMTPException
1333

  
1334
    with mock.patch('chrono.agendas.management.commands.send_booking_reminders.send_mail') as mock_send:
1335
        mock_send.return_value = None
1336
        mock_send.side_effect = send_mail_error
1337
        call_command('send_booking_reminders')
1338
        assert mock_send.call_count == 1
1339
        booking.refresh_from_db()
1340
        assert not booking.reminder_datetime
1341

  
1342
        mock_send.side_effect = None
1343
        call_command('send_booking_reminders')
1344
        assert mock_send.call_count == 2
1345
        booking.refresh_from_db()
1346
        assert booking.reminder_datetime
1347

  
1348
    settings.send_email = False
1349
    settings.send_sms = True
1350
    settings.save()
1351
    freezer.move_to('2020-01-01 14:00')
1352
    booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
1353
    freezer.move_to('2020-01-02 15:00')
1354

  
1355
    def mocked_requests_connection_error(*args, **kwargs):
1356
        raise requests.ConnectionError('unreachable')
1357

  
1358
    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
1359
        mock_send.side_effect = mocked_requests_connection_error
1360
        mock_response = mock.Mock(status_code=200)
1361
        mock_send.return_value = mock_response
1362
        call_command('send_booking_reminders')
1363
        assert mock_send.call_count == 1
1364
        booking.refresh_from_db()
1365
        assert not booking.reminder_datetime
1366

  
1367
        mock_send.side_effect = None
1368
        call_command('send_booking_reminders')
1369
        assert mock_send.call_count == 2
1370
        booking.refresh_from_db()
1371
        assert booking.reminder_datetime
1372

  
1373
    # when both sms and email are to be sent, only one is necessary to consider reminder successful
1374
    settings.send_email = True
1375
    settings.save()
1376
    freezer.move_to('2020-01-01 14:00')
1377
    booking = Booking.objects.create(event=event, user_phone_number='+336123456789', user_email='t@test.org')
1378
    freezer.move_to('2020-01-02 15:00')
1379

  
1380
    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send, mock.patch(
1381
        'chrono.agendas.management.commands.send_booking_reminders.send_mail'
1382
    ) as mock_send_mail:
1383
        mock_send.side_effect = mocked_requests_connection_error
1384
        mock_response = mock.Mock(status_code=200)
1385
        mock_send.return_value = mock_response
1386
        mock_send_mail.return_value = None
1387
        call_command('send_booking_reminders')
1388

  
1389
        assert mock_send.call_count == 1
1390
        assert mock_send_mail.call_count == 1
1391
        booking.refresh_from_db()
1392
        assert booking.reminder_datetime
1393

  
1394
        call_command('send_booking_reminders')
1395
        assert mock_send.call_count == 1
1396
        assert mock_send_mail.call_count == 1
1397

  
1398

  
1399
def test_agenda_reminders_email_content(mailoutbox, freezer):
1400
    freezer.move_to('2020-01-01 14:00')
1401
    agenda = Agenda.objects.create(label='Events', kind='events')
1402
    settings = AgendaReminderSettings.objects.create(
1403
        agenda=agenda, days=1, send_email=True, email_extra_info='Do no forget ID card.'
1404
    )
1405
    start_datetime = now() + datetime.timedelta(days=2)
1406
    event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
1407

  
1408
    booking = Booking.objects.create(event=event, user_email='t@test.org')
1409

  
1410
    freezer.move_to('2020-01-02 15:00')
1411
    call_command('send_booking_reminders')
1412

  
1413
    mail = mailoutbox[0]
1414
    assert mail.subject == 'Reminder for your booking tomorrow at 14:00'
1415
    mail_bodies = (mail.body, mail.alternatives[0][0])
1416
    for body in mail_bodies:
1417
        assert 'Hi,' in body
1418
        assert 'You have a booking for event "Pool party", on Friday 03 January at 14:00.' in body
1419
        assert 'Do no forget ID card.' in body
1420
        assert not 'cancel' in body
1421
    mailoutbox.clear()
1422

  
1423
    freezer.move_to('2020-01-01 14:00')
1424
    booking = Booking.objects.create(event=event, user_email='t@test.org', form_url='https://example.org/')
1425
    freezer.move_to('2020-01-02 15:00')
1426
    call_command('send_booking_reminders')
1427

  
1428
    mail = mailoutbox[0]
1429
    assert 'If in need to cancel it, you can do so here: https://example.org/' in mail.body
1430
    assert 'Edit or cancel booking' in mail.alternatives[0][0]
1431
    assert 'href="https://example.org/"' in mail.alternatives[0][0]
1432

  
1433

  
1434
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO')
1435
def test_agenda_reminders_sms_content(freezer):
1436
    freezer.move_to('2020-01-01 14:00')
1437
    agenda = Agenda.objects.create(label='Events', kind='events')
1438
    AgendaReminderSettings.objects.create(
1439
        agenda=agenda, days=1, send_sms=True, sms_extra_info='Do no forget ID card.'
1440
    )
1441
    start_datetime = now() + datetime.timedelta(days=2)
1442
    event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
1443

  
1444
    booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
1445

  
1446
    freezer.move_to('2020-01-02 15:00')
1447
    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
1448
        mock_response = mock.Mock(status_code=200)
1449
        mock_send.return_value = mock_response
1450
        call_command('send_booking_reminders')
1451

  
1452
    body = json.loads(mock_send.call_args[0][0].body)
1453
    assert (
1454
        body['message']
1455
        == 'Reminder: you have a booking for event "Pool party", on 03/01 at 14:00. Do no forget ID card.'
1456
    )
1457

  
1458

  
1459
def test_agenda_reminders_meetings(mailoutbox, freezer):
1460
    freezer.move_to('2020-01-01 11:00')
1461
    agenda = Agenda.objects.create(label='Events', kind='meetings')
1462
    desk = Desk.objects.create(agenda=agenda, label='Desk')
1463
    meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30)
1464
    timeperiod = TimePeriod.objects.create(
1465
        desk=desk, weekday=now().weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
1466
    )
1467
    AgendaReminderSettings.objects.create(agenda=agenda, days=2, send_email=True)
1468

  
1469
    event = Event.objects.create(
1470
        agenda=agenda,
1471
        places=1,
1472
        desk=desk,
1473
        meeting_type=meetingtype,
1474
        start_datetime=now() + datetime.timedelta(days=5),  # 06/01
1475
    )
1476
    Booking.objects.create(event=event, user_email='t@test.org', user_display_label='Birth certificate')
1477

  
1478
    freezer.move_to('2020-01-04 15:00')
1479
    call_command('send_booking_reminders')
1480
    assert len(mailoutbox) == 1
1481

  
1482
    mail = mailoutbox[0]
1483
    assert mail.subject == 'Reminder for your meeting in 2 days at 11:00'
1484
    assert 'Your meeting "Birth certificate" is scheduled on Monday 06 January at 11:00.' in mail.body
tests/test_import_export.py
29 29
    MeetingType,
30 30
    VirtualMember,
31 31
    AgendaNotificationsSettings,
32
    AgendaReminderSettings,
32 33
)
33 34
from chrono.manager.utils import import_site
34 35

  
......
483 484
        cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
484 485
        cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
485 486
    )
487

  
488

  
489
def test_import_export_reminder_settings():
490
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
491
    settings = AgendaReminderSettings.objects.create(
492
        agenda=agenda, days=2, send_email=True, send_sms=False, email_extra_info='test',
493
    )
494
    output = get_output_of_command('export_site')
495
    payload = json.loads(output)
496

  
497
    agenda.delete()
498
    assert not AgendaReminderSettings.objects.exists()
499

  
500
    import_site(payload)
501
    agenda = Agenda.objects.first()
502
    AgendaReminderSettings.objects.get(
503
        agenda=agenda, days=2, send_email=True, send_sms=False, email_extra_info='test',
504
    )
tests/test_manager.py
3910 3910
    assert 'Notifications' in resp.text
3911 3911
    assert 'Notifications are disabled' in resp.text
3912 3912

  
3913
    resp = resp.click('Configure')
3913
    resp = resp.click('Configure', href='notifications')
3914 3914
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3915 3915
    assert 'Notifications are disabled' in resp.text
3916 3916

  
3917
    resp = resp.click('Configure')
3917
    resp = resp.click('Configure', href='notifications')
3918 3918
    resp.form['cancelled_event'] = 'use-email-field'
3919 3919
    resp = resp.form.submit().follow()
3920 3920
    assert 'Notifications are disabled' in resp.text
3921 3921

  
3922
    resp = resp.click('Configure')
3922
    resp = resp.click('Configure', href='notifications')
3923 3923
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com'
3924 3924
    resp.form['almost_full_event'] = 'edit-role'
3925 3925
    resp.form['full_event'] = 'view-role'
......
3953 3953
    login(app)
3954 3954
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3955 3955

  
3956
    resp = resp.click('Configure')
3956
    resp = resp.click('Configure', href='notifications')
3957 3957
    resp.form['cancelled_event'] = 'use-email-field'
3958 3958
    resp.form['cancelled_event_emails'] = 'hop@entrouvert.com'
3959 3959
    resp.form.submit()
......
3966 3966
    # no notification is sent for old event
3967 3967
    assert len(mailoutbox) == 1
3968 3968
    assert 'New event' in mailoutbox[0].subject
3969

  
3970

  
3971
def test_manager_reminders(app, admin_user):
3972
    agenda = Agenda.objects.create(label='Events', kind='events')
3973

  
3974
    login(app)
3975
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3976

  
3977
    assert 'Booking reminders' in resp.text
3978
    assert 'Reminders are disabled' in resp.text
3979

  
3980
    resp = resp.click('Configure', href='reminder')
3981
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3982
    assert 'Reminders are disabled' in resp.text
3983

  
3984
    resp = resp.click('Configure', href='reminder')
3985
    resp.form['days'] = 3
3986
    resp.form['send_email'] = True
3987
    resp.form['email_extra_info'] = 'test'
3988
    resp = resp.form.submit().follow()
3989

  
3990
    assert 'Users will be reminded of their booking by email, 3 days in advance.' in resp.text
3991

  
3992
    resp = resp.click('Configure', href='reminder')
3993
    resp.form['send_sms'] = True
3994
    resp = resp.form.submit()
3995
    assert 'SMS are unavailable on this instance.' in resp.text
3996

  
3997
    with override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO'):
3998
        resp = resp.form.submit().follow()
3999
    assert 'Users will be reminded of their booking both by email and by SMS, 3 days in advance.' in resp.text
4000

  
4001
    agenda = Agenda.objects.create(label='Meetings', kind='meetings')
4002
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
4003
    assert 'Booking reminders' in resp.text
4004

  
4005
    agenda = Agenda.objects.create(label='Virtual', kind='virtual')
4006
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
4007
    assert not 'Booking reminders' in resp.text
3969
-