From 02dec79d43c073c5a69aa8e1ca27b971e048ee9b Mon Sep 17 00:00:00 2001
From: Valentin Deniaud
Date: Mon, 14 Feb 2022 16:10:35 +0100
Subject: [PATCH 1/3] reminders: allow template syntax in message extra info
(#61234)
---
.../commands/send_booking_reminders.py | 11 +++-
.../migrations/0062_auto_20200915_1401.py | 20 ++++++-
chrono/agendas/models.py | 29 +++++++++-
.../agendas/events_reminder_body.html | 2 +-
.../agendas/meetings_reminder_body.html | 2 +-
tests/manager/test_all.py | 39 ++++++++++++++
tests/test_agendas.py | 54 +++++++++++++++++++
7 files changed, 149 insertions(+), 8 deletions(-)
diff --git a/chrono/agendas/management/commands/send_booking_reminders.py b/chrono/agendas/management/commands/send_booking_reminders.py
index 61c48ca1..0902ef62 100644
--- a/chrono/agendas/management/commands/send_booking_reminders.py
+++ b/chrono/agendas/management/commands/send_booking_reminders.py
@@ -23,6 +23,7 @@ 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 import Context, Template, TemplateSyntaxError, VariableDoesNotExist
from django.template.loader import render_to_string
from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _
@@ -88,11 +89,17 @@ class Command(BaseCommand):
'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(settings.TEMPLATE_VARS)
+ for extra_info in ('email_extra_info', 'sms_extra_info'):
+ try:
+ ctx[extra_info] = Template(getattr(agenda.reminder_settings, extra_info)).render(
+ Context({'booking': booking}, autoescape=False)
+ )
+ except (VariableDoesNotExist, TemplateSyntaxError):
+ pass
+
if msg_type == 'email':
emails = set(booking.extra_emails)
if booking.user_email:
diff --git a/chrono/agendas/migrations/0062_auto_20200915_1401.py b/chrono/agendas/migrations/0062_auto_20200915_1401.py
index bd4b7963..943de3e0 100644
--- a/chrono/agendas/migrations/0062_auto_20200915_1401.py
+++ b/chrono/agendas/migrations/0062_auto_20200915_1401.py
@@ -3,6 +3,8 @@
import django.db.models.deletion
from django.db import migrations, models
+import chrono
+
class Migration(migrations.Migration):
@@ -41,7 +43,14 @@ class Migration(migrations.Migration):
'email_extra_info',
models.TextField(
blank=True,
- help_text='Basic information such as event name, time and date are already included.',
+ validators=[chrono.agendas.models.booking_template_validator],
+ help_text=(
+ 'Basic information such as event name, time and date are already included. '
+ 'Booking object can be accessed using standard template syntax. '
+ 'This allows to access agenda name via {{ booking.event.agenda.label }}, '
+ 'meeting type name via {{ booking.event.meeting_type.label }}, or any extra '
+ 'parameter passed on booking creation via {{ booking.extra_data.xxx }}.'
+ ),
verbose_name='Additional text to include in emails',
),
),
@@ -50,7 +59,14 @@ class Migration(migrations.Migration):
'sms_extra_info',
models.TextField(
blank=True,
- help_text='Basic information such as event name, time and date are already included.',
+ validators=[chrono.agendas.models.booking_template_validator],
+ help_text=(
+ 'Basic information such as event name, time and date are already included. '
+ 'Booking object can be accessed using standard template syntax. '
+ 'This allows to access agenda name via {{ booking.event.agenda.label }}, '
+ 'meeting type name via {{ booking.event.meeting_type.label }}, or any extra '
+ 'parameter passed on booking creation via {{ booking.extra_data.xxx }}.'
+ ),
verbose_name='Additional text to include in SMS',
),
),
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 15acda8a..f3875f78 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -154,6 +154,23 @@ def event_template_validator(value):
raise ValidationError(_('syntax error: %s') % e)
+def booking_template_validator(value):
+ example_event = Event(
+ start_datetime=now(),
+ publication_datetime=now(),
+ recurrence_end_date=now().date(),
+ places=1,
+ duration=1,
+ )
+ example_booking = Booking(event=example_event)
+ try:
+ Template(value).render(Context({'booking': example_booking}))
+ except TemplateSyntaxError as e:
+ raise ValidationError(_('syntax error: %s') % e)
+ except VariableDoesNotExist:
+ pass
+
+
class ICSError(Exception):
pass
@@ -2830,7 +2847,14 @@ class AgendaReminderSettings(models.Model):
email_extra_info = models.TextField(
blank=True,
verbose_name=_('Additional text to include in emails'),
- help_text=_('Basic information such as event name, time and date are already included.'),
+ validators=[booking_template_validator],
+ help_text=_(
+ 'Basic information such as event name, time and date are already included. '
+ 'Booking object can be accessed using standard template syntax. '
+ 'This allows to access agenda name via {{ booking.event.agenda.label }}, '
+ 'meeting type name via {{ booking.event.meeting_type.label }}, or any extra '
+ 'parameter passed on booking creation via {{ booking.extra_data.xxx }}.'
+ ),
)
days_before_sms = models.IntegerField(
null=True,
@@ -2845,7 +2869,8 @@ class AgendaReminderSettings(models.Model):
sms_extra_info = models.TextField(
blank=True,
verbose_name=_('Additional text to include in SMS'),
- help_text=_('Basic information such as event name, time and date are already included.'),
+ validators=[booking_template_validator],
+ help_text=email_extra_info.help_text,
)
def display_info(self):
diff --git a/chrono/agendas/templates/agendas/events_reminder_body.html b/chrono/agendas/templates/agendas/events_reminder_body.html
index 25390e06..3ac1620a 100644
--- a/chrono/agendas/templates/agendas/events_reminder_body.html
+++ b/chrono/agendas/templates/agendas/events_reminder_body.html
@@ -11,7 +11,7 @@ You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
{% if email_extra_info %}
-{{ email_extra_info }}
+{{ email_extra_info|force_escape|linebreaks }}
{% endif %}
{% if booking.event.description %}
diff --git a/chrono/agendas/templates/agendas/meetings_reminder_body.html b/chrono/agendas/templates/agendas/meetings_reminder_body.html
index 2d9889b8..ecedee40 100644
--- a/chrono/agendas/templates/agendas/meetings_reminder_body.html
+++ b/chrono/agendas/templates/agendas/meetings_reminder_body.html
@@ -17,7 +17,7 @@ Your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.
{% if email_extra_info %}
-{{ email_extra_info }}
+{{ email_extra_info|force_escape|linebreaks }}
{% endif %}
{% if booking.form_url %}
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index eb2b7aa4..ba732597 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -2899,6 +2899,32 @@ def test_manager_reminders(app, admin_user):
assert not 'Booking reminders' in resp.text
+@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC')
+@pytest.mark.parametrize('extra_info_field', ('sms_extra_info', 'email_extra_info'))
+def test_manager_reminders_templated_extra_info(app, admin_user, extra_info_field):
+ agenda = Agenda.objects.create(label='Events', kind='events')
+ Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
+
+ login(app)
+ resp = app.get('/manage/agendas/%s/settings' % agenda.id)
+ resp = resp.click('Configure', href='reminder')
+
+ extra_info = 'test {{ booking.extra_data.xxx }} {{ booking.event.label|default:booking.extra_data.yyy }}'
+ resp.form[extra_info_field] = extra_info
+ resp = resp.form.submit().follow()
+ assert getattr(agenda.reminder_settings, extra_info_field) == extra_info
+
+ invalid_templates = [
+ '{{ syntax error }}',
+ '{{ booking.label|invalidfilter }}',
+ ]
+ for template in invalid_templates:
+ resp = app.get('/manage/agendas/%s/reminder' % agenda.id)
+ resp.form[extra_info_field] = template
+ resp = resp.form.submit()
+ assert 'syntax error' in resp.text
+
+
def test_manager_reminders_preview(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
@@ -2937,6 +2963,19 @@ def test_manager_reminders_preview(app, admin_user):
in resp.text
)
+ # templates in extra info should not be interpreted
+ agenda.reminder_settings.sms_extra_info = '{{ booking.extra_data.xxx }}'
+ agenda.reminder_settings.email_extra_info = '{{ booking.extra_data.xxx }}'
+ agenda.reminder_settings.save()
+
+ resp = resp.click('Return to settings')
+ resp = resp.click('Preview SMS')
+ assert '{{ booking.extra_data.xxx }}' in resp.text
+
+ resp = resp.click('Return to settings')
+ resp = resp.click('Preview email')
+ assert '{{ booking.extra_data.xxx }}' in resp.text
+
def test_manager_agenda_roles(app, admin_user, manager_user):
agenda = Agenda.objects.create(label='Events', kind='events')
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index 286c7c01..61794d90 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -1925,6 +1925,60 @@ def test_agenda_reminders_sms_content(freezer):
)
+@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC')
+def test_agenda_reminders_templated_content(mailoutbox, freezer):
+ freezer.move_to('2020-01-01 14:00')
+ agenda = Agenda.objects.create(label='Main Center', kind='events')
+ AgendaReminderSettings.objects.create(
+ agenda=agenda,
+ days_before_email=1,
+ days_before_sms=1,
+ email_extra_info='Go to {{ booking.event.agenda.label }}.\nTake your {{ booking.extra_data.document_type }}.',
+ sms_extra_info='Take your {{ booking.extra_data.document_type }}.',
+ )
+ start_datetime = now() + datetime.timedelta(days=2)
+ event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
+
+ Booking.objects.create(
+ event=event,
+ user_email='t@test.org',
+ user_phone_number='+336123456789',
+ extra_data={'document_type': '"receipt"'},
+ )
+
+ 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')
+
+ mail = mailoutbox[0]
+ assert 'Go to Main Center.\nTake your "receipt".' in mail.body
+ assert 'Go to Main Center.
Take your "receipt".
' in mail.alternatives[0][0]
+
+ body = json.loads(mock_send.call_args[0][0].body.decode())
+ assert 'Take your "receipt".' in body['message']
+
+ # in case of invalid template, send anyway
+ freezer.move_to('2020-01-01 14:00')
+ Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789')
+ agenda.reminder_settings.email_extra_info = 'Take your {{ syntax error }}'
+ agenda.reminder_settings.sms_extra_info = 'Take your {{ syntax error }}'
+ agenda.reminder_settings.save()
+
+ 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 len(mailoutbox) == 2
+ assert 'Take your' not in mailoutbox[1].body
+
+ body = json.loads(mock_send.call_args[0][0].body.decode())
+ assert 'Take your' not in body['message']
+
+
@override_settings(TIME_ZONE='UTC')
def test_agenda_reminders_meetings(mailoutbox, freezer):
freezer.move_to('2020-01-01 11:00')
--
2.30.2