reminders: allow template syntax in message extra info (#61234)

This commit is contained in:
Valentin Deniaud 2022-02-14 16:10:35 +01:00
parent fc34aab1a8
commit 064c9a4ea3
7 changed files with 149 additions and 8 deletions

View File

@ -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:

View File

@ -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',
),
),

View File

@ -167,6 +167,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
@ -2866,7 +2883,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,
@ -2881,7 +2905,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):

View File

@ -11,7 +11,7 @@ You have a booking for event "{{ event }}", on {{ date }} at {{ time }}.
</p>
{% if email_extra_info %}
<p>{{ email_extra_info }}</p>
<p>{{ email_extra_info|force_escape|linebreaks }}</p>
{% endif %}
{% if booking.event.description %}

View File

@ -17,7 +17,7 @@ Your meeting "{{ meeting }}" is scheduled on {{ date }} at {{ time }}.
</p>
{% if email_extra_info %}
<p>{{ email_extra_info }}</p>
<p>{{ email_extra_info|force_escape|linebreaks }}</p>
{% endif %}
{% if booking.form_url %}

View File

@ -2553,6 +2553,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')
@ -2591,6 +2617,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')

View File

@ -1930,6 +1930,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 '<p>Go to Main Center.<br>Take your &quot;receipt&quot;.</p>' 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')