agendas: add booking reminders (#45293)

This commit is contained in:
Valentin Deniaud 2020-09-15 14:05:38 +02:00
parent 271cea7b44
commit ab32481a6b
21 changed files with 773 additions and 6 deletions

View File

@ -0,0 +1,134 @@
# chrono - agendas system
# Copyright (C) 2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
from urllib.parse import urljoin
from requests import RequestException
from smtplib import SMTPException
from django.conf import settings
from django.core.mail import send_mail
from django.core.management.base import BaseCommand
from django.db.models import F
from django.db.transaction import atomic
from django.template.loader import render_to_string
from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import Agenda, Booking
from chrono.utils.requests_wrapper import requests
SENDING_IN_PROGRESS = datetime(year=2, month=1, day=1)
class Command(BaseCommand):
help = 'Send booking reminders'
def handle(self, **options):
translation.activate(settings.LANGUAGE_CODE)
reminder_delta = F('event__agenda__reminder_settings__days') * timedelta(1)
starts_before = timezone.now() + reminder_delta
# 12 hours time window to run the command and send reminder, thus excluding old events
starts_after = timezone.now() + reminder_delta - timedelta(hours=12)
# prevent user who just booked from getting a reminder
created_before = timezone.now() - timedelta(hours=12)
bookings = Booking.objects.filter(
event__agenda__reminder_settings__days__isnull=False, # useless ?
cancellation_datetime__isnull=True,
creation_datetime__lte=created_before,
reminder_datetime__isnull=True,
event__start_datetime__lte=starts_before,
event__start_datetime__gte=starts_after,
).select_related('event', 'event__agenda', 'event__agenda__reminder_settings')
bookings_list = list(bookings)
bookings_pk = list(bookings.values_list('pk', flat=True))
bookings.update(reminder_datetime=SENDING_IN_PROGRESS)
try:
for booking in bookings_list:
self.send_reminder(booking)
finally:
Booking.objects.filter(pk__in=bookings_pk, reminder_datetime__lte=SENDING_IN_PROGRESS).update(
reminder_datetime=None
)
def send_reminder(self, booking):
agenda = booking.event.agenda
kind = agenda.kind
days = agenda.reminder_settings.days
ctx = {
'booking': booking,
'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days,
'date': booking.event.start_datetime,
'email_extra_info': agenda.reminder_settings.email_extra_info,
'sms_extra_info': agenda.reminder_settings.sms_extra_info,
}
ctx.update(getattr(settings, 'TEMPLATE_VARS', {}))
if booking.form_url:
ctx['form_url'] = urljoin(settings.SITE_BASE_URL, booking.form_url)
if agenda.reminder_settings.send_email:
self.send_email(booking, kind, ctx)
if agenda.reminder_settings.send_sms:
self.send_sms(booking, kind, ctx)
@staticmethod
def send_email(booking, kind, ctx):
if not booking.user_email:
return
subject = render_to_string('agendas/%s_reminder_subject.txt' % kind, ctx).strip()
body = render_to_string('agendas/%s_reminder_body.txt' % kind, ctx)
html_body = render_to_string('agendas/%s_reminder_body.html' % kind, ctx)
try:
with atomic():
send_mail(
subject, body, settings.DEFAULT_FROM_EMAIL, [booking.user_email], html_message=html_body
)
booking.reminder_datetime = timezone.now()
booking.save()
except SMTPException:
pass
@staticmethod
def send_sms(booking, kind, ctx):
if not booking.user_phone_number:
return
sms_url = getattr(settings, 'SMS_URL', '')
if not sms_url:
return
sms_from = settings.SMS_FROM
message = render_to_string('agendas/%s_reminder_message.txt' % kind, ctx).strip()
payload = {
'message': message,
'from': settings.SMS_FROM,
'to': [booking.user_phone_number],
}
try:
with atomic():
request = requests.post(sms_url, json=payload, remote_service='auto', timeout=10)
request.raise_for_status()
booking.reminder_datetime = timezone.now()
booking.save()
except RequestException:
pass

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2020-09-15 12:01
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agendas', '0061_auto_20200909_1752'),
]
operations = [
migrations.CreateModel(
name='AgendaReminderSettings',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
(
'days',
models.IntegerField(
blank=True,
choices=[
(None, 'Never'),
(1, 'One day before'),
(2, 'Two days before'),
(3, 'Three days before'),
],
null=True,
verbose_name='Send reminder',
),
),
('send_email', models.BooleanField(default=False, verbose_name='Notify by email')),
(
'email_extra_info',
models.TextField(
blank=True,
help_text='Basic information such as event name, time and date are already included',
verbose_name='Additional text to incude in emails',
),
),
('send_sms', models.BooleanField(default=False, verbose_name='Notify by SMS')),
(
'sms_extra_info',
models.TextField(
blank=True,
help_text='Basic information such as event name, time and date are already included',
verbose_name='Additional text to incude in SMS',
),
),
(
'agenda',
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='reminder_settings',
to='agendas.Agenda',
),
),
],
),
migrations.AddField(
model_name='booking', name='reminder_datetime', field=models.DateTimeField(null=True),
),
]

View File

@ -43,7 +43,7 @@ from django.utils.formats import date_format
from django.utils.module_loading import import_string
from django.utils.text import slugify
from django.utils.timezone import localtime, now, make_aware, make_naive, is_aware
from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.translation import ugettext_lazy as _, ugettext, ungettext
from jsonfield import JSONField
@ -286,6 +286,8 @@ class Agenda(models.Model):
},
'resources': [x.slug for x in self.resources.all()],
}
if hasattr(self, 'reminder_settings'):
agenda['reminder_settings'] = self.reminder_settings.export_json()
if self.kind == 'events':
agenda['events'] = [x.export_json() for x in self.event_set.all()]
if hasattr(self, 'notifications_settings'):
@ -302,6 +304,7 @@ class Agenda(models.Model):
def import_json(cls, data, overwrite=False):
data = data.copy()
permissions = data.pop('permissions') or {}
reminder_settings = data.pop('reminder_settings', None)
if data['kind'] == 'events':
events = data.pop('events')
notifications_settings = data.pop('notifications_settings', None)
@ -329,6 +332,11 @@ class Agenda(models.Model):
if not created:
for k, v in data.items():
setattr(agenda, k, v)
if overwrite:
AgendaReminderSettings.objects.filter(agenda=agenda).delete()
if reminder_settings:
reminder_settings['agenda'] = agenda
AgendaReminderSettings.import_json(reminder_settings).save()
if data['kind'] == 'events':
if overwrite:
Event.objects.filter(agenda=agenda).delete()
@ -993,6 +1001,7 @@ class Booking(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
extra_data = JSONField(null=True)
cancellation_datetime = models.DateTimeField(null=True)
reminder_datetime = models.DateTimeField(null=True)
in_waiting_list = models.BooleanField(default=False)
creation_datetime = models.DateTimeField(auto_now_add=True)
# primary booking is used to group multiple bookings together
@ -1680,3 +1689,61 @@ class AgendaNotificationsSettings(models.Model):
'cancelled_event': self.cancelled_event,
'cancelled_event_emails': self.cancelled_event_emails,
}
class AgendaReminderSettings(models.Model):
ONE_DAY_BEFORE = 1
TWO_DAYS_BEFORE = 2
THREE_DAYS_BEFORE = 3
CHOICES = [
(None, _('Never')),
(ONE_DAY_BEFORE, _('One day before')),
(TWO_DAYS_BEFORE, _('Two days before')),
(THREE_DAYS_BEFORE, _('Three days before')),
]
agenda = models.OneToOneField(Agenda, on_delete=models.CASCADE, related_name='reminder_settings')
days = models.IntegerField(null=True, blank=True, choices=CHOICES, verbose_name=_('Send reminder'))
send_email = models.BooleanField(default=False, verbose_name=_('Notify by email'))
email_extra_info = models.TextField(
blank=True,
verbose_name=_('Additional text to incude in emails'),
help_text=_('Basic information such as event name, time and date are already included.'),
)
send_sms = models.BooleanField(default=False, verbose_name=_('Notify by SMS'))
sms_extra_info = models.TextField(
blank=True,
verbose_name=_('Additional text to incude in SMS'),
help_text=_('Basic information such as event name, time and date are already included.'),
)
def display_info(self):
message = ungettext(
'Users will be reminded of their booking %(by_email_or_sms)s, one day in advance.',
'Users will be reminded of their booking %(by_email_or_sms)s, %(days)s days in advance.',
self.days,
)
if self.send_sms and self.send_email:
by = _('both by email and by SMS')
elif self.send_sms:
by = _('by SMS')
elif self.send_email:
by = _('by email')
return message % {'days': self.days, 'by_email_or_sms': by}
@classmethod
def import_json(cls, data):
data = clean_import_data(cls, data)
return cls(**data)
def export_json(self):
return {
'days': self.days,
'send_email': self.send_email,
'email_extra_info': self.email_extra_info,
'send_sms': self.send_sms,
'sms_extra_info': self.sms_extra_info,
}

View File

@ -0,0 +1,22 @@
{% extends "emails/body_base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Hi," %}</p>
<p>
{% blocktrans trimmed with event=booking.event date=date|date:"l j F" time=date|time %}
You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
{% endblocktrans %}
</p>
{% if email_extra_info %}
<p>{{ email_extra_info }}</p>
{% endif %}
{% if booking.form_url %}
{% with _("Edit or cancel booking") as button_label %}
{% include "emails/button-link.html" with url=booking.form_url label=button_label %}
{% endwith %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "emails/body_base.txt" %}
{% load i18n %}
{% block content %}{% autoescape off %}{% trans "Hi," %}
{% blocktrans trimmed with event=booking.event date=date|date:"l j F" time=date|time %}
You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
{% endblocktrans %}
{% if email_extra_info %}
{{ email_extra_info }}
{% endif %}
{% if booking.form_url %}
{% trans "If in need to cancel it, you can do so here:" %} {{ booking.form_url }}
{% endif %}
{% endautoescape %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% load i18n %}
{% blocktrans trimmed with event=booking.event date=date|date:"d/m" time=date|time %}
Reminder: you have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
{% endblocktrans %}{% if sms_extra_info %} {{ sms_extra_info }}{% endif %}

View File

@ -0,0 +1,6 @@
{% extends "emails/subject.txt" %}
{% load i18n %}
{% block email-subject %}{% autoescape off %}{% blocktrans trimmed with time=date|time %}
Reminder for your booking {{ in_x_days }} at {{ time }}
{% endblocktrans %}{% endautoescape %}{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "emails/body_base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Hi," %}</p>
<p>
{% with date=date|date:"l j F" time=date|time %}
{% if booking.user_display_label %}
{% blocktrans trimmed with meeting=booking.user_display_label %}
Your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.
{% endblocktrans %}
{% else %}
{% blocktrans %}You have a meeting scheduled on {{ date }} at {{ time }}.{% endblocktrans %}
{% endif %}
{% endwith %}
</p>
{% if email_extra_info %}
<p>{{ email_extra_info }}</p>
{% endif %}
{% if booking.form_url %}
{% with _("Edit or cancel meeting") as button_label %}
{% include "emails/button-link.html" with url=booking.form_url label=button_label %}
{% endwith %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "emails/body_base.txt" %}
{% load i18n %}
{% block content %}{% autoescape off %}{% trans "Hi," %}
{% with date=date|date:"l j F" time=date|time %}
{% if booking.user_display_label %}
{% blocktrans trimmed with meeting=booking.user_display_label %}
Your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.
{% endblocktrans %}
{% else %}
{% blocktrans %}You have a meeting scheduled on {{ date }} at {{ time }}.{% endblocktrans %}
{% endif %}
{% endwith %}
{% if email_extra_info %}{{ email_extra_info }}{% endif %}
{% if booking.form_url %}
{% trans "If in need to cancel it, you can do so here:" %} {{ booking.form_url }}
{% endif %}
{% endautoescape %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% load i18n %}
{% if label %}{% blocktrans trimmed with meeting=booking.user_display_label date=date|date:"d/m" time=date|time %}
Reminder: your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.
{% endblocktrans %}{% else %}{% blocktrans trimmed with date=date|date:"d/m" time=date|time %}
Reminder: you have a meeting scheduled on {{ date }} at {{ time }}.
{% endblocktrans %}{% endif %}{% if sms_extra_info %} {{ sms_extra_info }}{% endif %}

View File

@ -0,0 +1,6 @@
{% extends "emails/subject.txt" %}
{% load i18n %}
{% block email-subject %}{% autoescape off %}{% blocktrans trimmed with time=date|time %}
Reminder for your meeting {{ in_x_days }} at {{ time }}
{% endblocktrans %}{% endautoescape %}{% endblock %}

View File

@ -20,6 +20,7 @@ import csv
import datetime
from django import forms
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist
from django.forms import ValidationError
@ -41,6 +42,7 @@ from chrono.agendas.models import (
Resource,
Category,
AgendaNotificationsSettings,
AgendaReminderSettings,
WEEKDAYS_LIST,
)
@ -541,3 +543,21 @@ class AgendaNotificationsForm(forms.ModelForm):
for role_field in AgendaNotificationsSettings.get_role_field_names():
field = self.fields[role_field]
field.choices = self.update_choices(field.choices, settings)
class AgendaReminderForm(forms.ModelForm):
class Meta:
model = AgendaReminderSettings
exclude = ['agenda']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not hasattr(settings, 'SMS_URL'):
del self.fields['send_sms']
del self.fields['sms_extra_info']
def clean(self):
cleaned_data = super().clean()
if cleaned_data['days'] and not (cleaned_data['send_email'] or cleaned_data.get('send_sms')):
raise ValidationError(_('Select at least one notification medium.'))
return cleaned_data

View File

@ -0,0 +1,22 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="">{% trans "Reminder settings" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Reminder settings" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -34,6 +34,22 @@
{% block agenda-settings %}
{% endblock %}
{% block agenda-reminder %}
<div class="section">
<h3>{% trans "Booking reminders" %}</h3>
<div>
<p>
{% if not agenda.reminder_settings or not agenda.reminder_settings.days %}
{% trans "Reminders are disabled for this agenda." %}
{% else %}
{{ agenda.reminder_settings.display_info }}
{% endif %}
</p>
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-reminder-settings' pk=object.id %}">{% trans "Configure" %}</a>
</div>
</div>
{% endblock %}
{% block agenda-permissions %}
<div class="section">
<h3>{% trans "Permissions" %}</h3>

View File

@ -76,4 +76,6 @@
</div>
{% endif %}
{% block agenda-reminder %}
{% endblock %}
{% endblock %}

View File

@ -78,6 +78,11 @@ urlpatterns = [
views.agenda_notifications_settings,
name='chrono-manager-agenda-notifications-settings',
),
url(
r'^agendas/(?P<pk>\d+)/reminder$',
views.agenda_reminder_settings,
name='chrono-manager-agenda-reminder-settings',
),
url(
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$',
views.event_view,

View File

@ -64,6 +64,7 @@ from chrono.agendas.models import (
Category,
EventCancellationReport,
AgendaNotificationsSettings,
AgendaReminderSettings,
)
from .forms import (
@ -92,6 +93,7 @@ from .forms import (
BookingCancelForm,
EventCancelForm,
AgendaNotificationsForm,
AgendaReminderForm,
)
from .utils import import_site
@ -1371,6 +1373,21 @@ class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView):
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
class AgendaReminderSettingsView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_agenda_reminder_form.html'
model = AgendaReminderSettings
form_class = AgendaReminderForm
def get_object(self):
try:
return self.agenda.reminder_settings
except AgendaReminderSettings.DoesNotExist:
return AgendaReminderSettings.objects.create(agenda=self.agenda)
agenda_reminder_settings = AgendaReminderSettingsView.as_view()
class EventDetailView(ViewableAgendaMixin, DetailView):
model = Event
pk_url_kwarg = 'event_pk'

View File

@ -2,3 +2,4 @@
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command clearsessions --all-tenants
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command sync_desks_timeperiod_exceptions --all-tenants
/sbin/runuser -u chrono /usr/bin/chrono-manage -- tenant_command send_booking_reminders --all-tenants

View File

@ -1,7 +1,9 @@
import pytest
import datetime
import json
import mock
import requests
import smtplib
from django.contrib.auth.models import Group, User
@ -25,6 +27,7 @@ from chrono.agendas.models import (
VirtualMember,
EventCancellationReport,
AgendaNotificationsSettings,
AgendaReminderSettings,
)
pytestmark = pytest.mark.django_db
@ -1238,3 +1241,246 @@ def test_agenda_notifications_cancelled(mailoutbox):
# no new email on subsequent run
call_command('send_email_notifications')
assert len(mailoutbox) == 1
def test_agenda_reminders(mailoutbox, freezer):
agenda = Agenda.objects.create(label='Events', kind='events')
# add some old event with booking
freezer.move_to('2019-01-01')
old_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
Booking.objects.create(event=old_event, user_email='old@test.org')
# no reminder configured
call_command('send_booking_reminders')
assert len(mailoutbox) == 0
# move to present day
freezer.move_to('2020-01-01 14:00')
# configure reminder the day before
AgendaReminderSettings.objects.create(agenda=agenda, days=1, send_email=True)
# event starts in 2 days
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
for i in range(5):
booking = Booking.objects.create(event=event, user_email='t@test.org')
# extra booking with no email, should be ignored
booking = Booking.objects.create(event=event)
freezer.move_to('2020-01-02 10:00')
# not time to send reminders yet
call_command('send_booking_reminders')
assert len(mailoutbox) == 0
# one of the booking is cancelled
Booking.objects.filter(user_email='t@test.org').first().cancel()
freezer.move_to('2020-01-02 15:00')
call_command('send_booking_reminders')
assert len(mailoutbox) == 4
mailoutbox.clear()
call_command('send_booking_reminders')
assert len(mailoutbox) == 0
# booking is placed the day of the event, notfication should no be sent
freezer.move_to('2020-01-03 08:00')
booking = Booking.objects.create(event=event, user_email='t@test.org')
call_command('send_booking_reminders')
assert len(mailoutbox) == 0
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO')
def test_agenda_reminders_sms(freezer):
freezer.move_to('2020-01-01 14:00')
agenda = Agenda.objects.create(label='Events', kind='events')
AgendaReminderSettings.objects.create(agenda=agenda, days=1, send_sms=True)
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
for i in range(5):
booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
booking = Booking.objects.create(event=event)
freezer.move_to('2020-01-02 15:00')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
call_command('send_booking_reminders')
assert mock_send.call_count == 5
body = json.loads(mock_send.call_args[0][0].body.decode())
assert body['from'] == 'EO'
assert body['to'] == ['+336123456789']
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO')
def test_agenda_reminders_retry(freezer):
freezer.move_to('2020-01-01 14:00')
agenda = Agenda.objects.create(label='Events', kind='events')
settings = AgendaReminderSettings.objects.create(agenda=agenda, days=1)
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
settings.send_email = True
settings.save()
booking = Booking.objects.create(event=event, user_email='t@test.org')
freezer.move_to('2020-01-02 15:00')
def send_mail_error(*args, **kwargs):
raise smtplib.SMTPException
with mock.patch('chrono.agendas.management.commands.send_booking_reminders.send_mail') as mock_send:
mock_send.return_value = None
mock_send.side_effect = send_mail_error
call_command('send_booking_reminders')
assert mock_send.call_count == 1
booking.refresh_from_db()
assert not booking.reminder_datetime
mock_send.side_effect = None
call_command('send_booking_reminders')
assert mock_send.call_count == 2
booking.refresh_from_db()
assert booking.reminder_datetime
settings.send_email = False
settings.send_sms = True
settings.save()
freezer.move_to('2020-01-01 14:00')
booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
freezer.move_to('2020-01-02 15:00')
def mocked_requests_connection_error(*args, **kwargs):
raise requests.ConnectionError('unreachable')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
mock_send.side_effect = mocked_requests_connection_error
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
call_command('send_booking_reminders')
assert mock_send.call_count == 1
booking.refresh_from_db()
assert not booking.reminder_datetime
mock_send.side_effect = None
call_command('send_booking_reminders')
assert mock_send.call_count == 2
booking.refresh_from_db()
assert booking.reminder_datetime
# when both sms and email are to be sent, only one is necessary to consider reminder successful
settings.send_email = True
settings.save()
freezer.move_to('2020-01-01 14:00')
booking = Booking.objects.create(event=event, user_phone_number='+336123456789', user_email='t@test.org')
freezer.move_to('2020-01-02 15:00')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send, mock.patch(
'chrono.agendas.management.commands.send_booking_reminders.send_mail'
) as mock_send_mail:
mock_send.side_effect = mocked_requests_connection_error
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
mock_send_mail.return_value = None
call_command('send_booking_reminders')
assert mock_send.call_count == 1
assert mock_send_mail.call_count == 1
booking.refresh_from_db()
assert booking.reminder_datetime
call_command('send_booking_reminders')
assert mock_send.call_count == 1
assert mock_send_mail.call_count == 1
@override_settings(TIME_ZONE='UTC')
def test_agenda_reminders_email_content(mailoutbox, freezer):
freezer.move_to('2020-01-01 14:00')
agenda = Agenda.objects.create(label='Events', kind='events')
settings = AgendaReminderSettings.objects.create(
agenda=agenda, days=1, send_email=True, email_extra_info='Do no forget ID card.'
)
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
booking = Booking.objects.create(event=event, user_email='t@test.org')
freezer.move_to('2020-01-02 15:00')
call_command('send_booking_reminders')
mail = mailoutbox[0]
assert mail.subject == 'Reminder for your booking tomorrow at 2 p.m.'
mail_bodies = (mail.body, mail.alternatives[0][0])
for body in mail_bodies:
assert 'Hi,' in body
assert 'You have a booking for event "Pool party", on Friday 3 January at 2 p.m..' in body
assert 'Do no forget ID card.' in body
assert not 'cancel' in body
mailoutbox.clear()
freezer.move_to('2020-01-01 14:00')
booking = Booking.objects.create(event=event, user_email='t@test.org', form_url='https://example.org/')
freezer.move_to('2020-01-02 15:00')
call_command('send_booking_reminders')
mail = mailoutbox[0]
assert 'If in need to cancel it, you can do so here: https://example.org/' in mail.body
assert 'Edit or cancel booking' in mail.alternatives[0][0]
assert 'href="https://example.org/"' in mail.alternatives[0][0]
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO', TIME_ZONE='UTC')
def test_agenda_reminders_sms_content(freezer):
freezer.move_to('2020-01-01 14:00')
agenda = Agenda.objects.create(label='Events', kind='events')
AgendaReminderSettings.objects.create(
agenda=agenda, days=1, send_sms=True, sms_extra_info='Do no forget ID card.'
)
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
freezer.move_to('2020-01-02 15:00')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
call_command('send_booking_reminders')
body = json.loads(mock_send.call_args[0][0].body.decode())
assert (
body['message']
== 'Reminder: you have a booking for event "Pool party", on 03/01 at 2 p.m.. Do no forget ID card.'
)
@override_settings(TIME_ZONE='UTC')
def test_agenda_reminders_meetings(mailoutbox, freezer):
freezer.move_to('2020-01-01 11:00')
agenda = Agenda.objects.create(label='Events', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk')
meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30)
timeperiod = TimePeriod.objects.create(
desk=desk, weekday=now().weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
)
AgendaReminderSettings.objects.create(agenda=agenda, days=2, send_email=True)
event = Event.objects.create(
agenda=agenda,
places=1,
desk=desk,
meeting_type=meetingtype,
start_datetime=now() + datetime.timedelta(days=5), # 06/01
)
Booking.objects.create(event=event, user_email='t@test.org', user_display_label='Birth certificate')
freezer.move_to('2020-01-04 15:00')
call_command('send_booking_reminders')
assert len(mailoutbox) == 1
mail = mailoutbox[0]
assert mail.subject == 'Reminder for your meeting in 2 days at 11 a.m.'
assert 'Your meeting "Birth certificate" is scheduled on Monday 6 January at 11 a.m..' in mail.body

View File

@ -29,6 +29,7 @@ from chrono.agendas.models import (
MeetingType,
VirtualMember,
AgendaNotificationsSettings,
AgendaReminderSettings,
)
from chrono.manager.utils import import_site
@ -483,3 +484,21 @@ def test_import_export_notification_settings():
cancelled_event=AgendaNotificationsSettings.EMAIL_FIELD,
cancelled_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
)
def test_import_export_reminder_settings():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
settings = AgendaReminderSettings.objects.create(
agenda=agenda, days=2, send_email=True, send_sms=False, email_extra_info='test',
)
output = get_output_of_command('export_site')
payload = json.loads(output)
agenda.delete()
assert not AgendaReminderSettings.objects.exists()
import_site(payload)
agenda = Agenda.objects.first()
AgendaReminderSettings.objects.get(
agenda=agenda, days=2, send_email=True, send_sms=False, email_extra_info='test',
)

View File

@ -979,7 +979,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user):
app = login(app)
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/agendas/%s/settings' % agenda.pk)
assert len(ctx.captured_queries) == 8
assert len(ctx.captured_queries) == 9
def test_agenda_resources(app, admin_user):
@ -3910,11 +3910,11 @@ def test_agenda_notifications(app, admin_user, managers_group):
assert 'Notifications' in resp.text
assert 'Notifications are disabled' in resp.text
resp = resp.click('Configure')
resp = resp.click('Configure', href='notifications')
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Notifications are disabled' in resp.text
resp = resp.click('Configure')
resp = resp.click('Configure', href='notifications')
resp.form['cancelled_event'] = 'use-email-field'
resp = resp.form.submit().follow()
assert 'Notifications are disabled' in resp.text
@ -3922,7 +3922,7 @@ def test_agenda_notifications(app, admin_user, managers_group):
agenda.view_role = managers_group
agenda.save()
resp = resp.click('Configure')
resp = resp.click('Configure', href='notifications')
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com, top@entrouvert.com'
resp.form['almost_full_event'] = 'edit-role'
option = resp.form['almost_full_event'].selectedIndex
@ -3958,7 +3958,7 @@ def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox):
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
resp = resp.click('Configure')
resp = resp.click('Configure', href='notifications')
resp.form['cancelled_event'] = 'use-email-field'
resp.form['cancelled_event_emails'] = 'hop@entrouvert.com'
resp.form.submit()
@ -3971,3 +3971,41 @@ def test_agenda_notifications_no_old_events(app, admin_user, mailoutbox):
# no notification is sent for old event
assert len(mailoutbox) == 1
assert 'New event' in mailoutbox[0].subject
def test_manager_reminders(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Booking reminders' in resp.text
assert 'Reminders are disabled' in resp.text
resp = resp.click('Configure', href='reminder')
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Reminders are disabled' in resp.text
resp = resp.click('Configure', href='reminder')
assert not 'SMS' in resp.text
resp.form['days'] = 3
resp.form['send_email'] = True
resp.form['email_extra_info'] = 'test'
resp = resp.form.submit().follow()
assert 'Users will be reminded of their booking by email, 3 days in advance.' in resp.text
with override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_FROM='EO'):
resp = resp.click('Configure', href='reminder')
resp.form['send_sms'] = True
resp = resp.form.submit().follow()
assert 'Users will be reminded of their booking both by email and by SMS, 3 days in advance.' in resp.text
agenda = Agenda.objects.create(label='Meetings', kind='meetings')
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Booking reminders' in resp.text
agenda = Agenda.objects.create(label='Virtual', kind='virtual')
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert not 'Booking reminders' in resp.text