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