0001-reminders-allow-template-syntax-in-message-extra-inf.patch
chrono/agendas/management/commands/send_booking_reminders.py | ||
---|---|---|
23 | 23 |
from django.core.management.base import BaseCommand |
24 | 24 |
from django.db.models import F |
25 | 25 |
from django.db.transaction import atomic |
26 |
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist |
|
26 | 27 |
from django.template.loader import render_to_string |
27 | 28 |
from django.utils import timezone, translation |
28 | 29 |
from django.utils.translation import ugettext_lazy as _ |
... | ... | |
88 | 89 |
'booking': booking, |
89 | 90 |
'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days, |
90 | 91 |
'date': booking.event.start_datetime, |
91 |
'email_extra_info': agenda.reminder_settings.email_extra_info, |
|
92 |
'sms_extra_info': agenda.reminder_settings.sms_extra_info, |
|
93 | 92 |
} |
94 | 93 |
ctx.update(settings.TEMPLATE_VARS) |
95 | 94 | |
95 |
for extra_info in ('email_extra_info', 'sms_extra_info'): |
|
96 |
try: |
|
97 |
ctx[extra_info] = Template(getattr(agenda.reminder_settings, extra_info)).render( |
|
98 |
Context({'booking': booking}, autoescape=False) |
|
99 |
) |
|
100 |
except (VariableDoesNotExist, TemplateSyntaxError): |
|
101 |
pass |
|
102 | ||
96 | 103 |
if msg_type == 'email': |
97 | 104 |
emails = set(booking.extra_emails) |
98 | 105 |
if booking.user_email: |
chrono/agendas/migrations/0062_auto_20200915_1401.py | ||
---|---|---|
3 | 3 |
import django.db.models.deletion |
4 | 4 |
from django.db import migrations, models |
5 | 5 | |
6 |
import chrono |
|
7 | ||
6 | 8 | |
7 | 9 |
class Migration(migrations.Migration): |
8 | 10 | |
... | ... | |
41 | 43 |
'email_extra_info', |
42 | 44 |
models.TextField( |
43 | 45 |
blank=True, |
44 |
help_text='Basic information such as event name, time and date are already included.', |
|
46 |
validators=[chrono.agendas.models.booking_template_validator], |
|
47 |
help_text=( |
|
48 |
'Basic information such as event name, time and date are already included. ' |
|
49 |
'Booking object can be accessed using standard template syntax. ' |
|
50 |
'This allows to access agenda name via {{ booking.event.agenda.label }}, ' |
|
51 |
'meeting type name via {{ booking.event.meeting_type.label }}, or any extra ' |
|
52 |
'parameter passed on booking creation via {{ booking.extra_data.xxx }}.' |
|
53 |
), |
|
45 | 54 |
verbose_name='Additional text to include in emails', |
46 | 55 |
), |
47 | 56 |
), |
... | ... | |
50 | 59 |
'sms_extra_info', |
51 | 60 |
models.TextField( |
52 | 61 |
blank=True, |
53 |
help_text='Basic information such as event name, time and date are already included.', |
|
62 |
validators=[chrono.agendas.models.booking_template_validator], |
|
63 |
help_text=( |
|
64 |
'Basic information such as event name, time and date are already included. ' |
|
65 |
'Booking object can be accessed using standard template syntax. ' |
|
66 |
'This allows to access agenda name via {{ booking.event.agenda.label }}, ' |
|
67 |
'meeting type name via {{ booking.event.meeting_type.label }}, or any extra ' |
|
68 |
'parameter passed on booking creation via {{ booking.extra_data.xxx }}.' |
|
69 |
), |
|
54 | 70 |
verbose_name='Additional text to include in SMS', |
55 | 71 |
), |
56 | 72 |
), |
chrono/agendas/models.py | ||
---|---|---|
154 | 154 |
raise ValidationError(_('syntax error: %s') % e) |
155 | 155 | |
156 | 156 | |
157 |
def booking_template_validator(value): |
|
158 |
example_event = Event( |
|
159 |
start_datetime=now(), |
|
160 |
publication_datetime=now(), |
|
161 |
recurrence_end_date=now().date(), |
|
162 |
places=1, |
|
163 |
duration=1, |
|
164 |
) |
|
165 |
example_booking = Booking(event=example_event) |
|
166 |
try: |
|
167 |
Template(value).render(Context({'booking': example_booking})) |
|
168 |
except TemplateSyntaxError as e: |
|
169 |
raise ValidationError(_('syntax error: %s') % e) |
|
170 |
except VariableDoesNotExist: |
|
171 |
pass |
|
172 | ||
173 | ||
157 | 174 |
class ICSError(Exception): |
158 | 175 |
pass |
159 | 176 | |
... | ... | |
2830 | 2847 |
email_extra_info = models.TextField( |
2831 | 2848 |
blank=True, |
2832 | 2849 |
verbose_name=_('Additional text to include in emails'), |
2833 |
help_text=_('Basic information such as event name, time and date are already included.'), |
|
2850 |
validators=[booking_template_validator], |
|
2851 |
help_text=_( |
|
2852 |
'Basic information such as event name, time and date are already included. ' |
|
2853 |
'Booking object can be accessed using standard template syntax. ' |
|
2854 |
'This allows to access agenda name via {{ booking.event.agenda.label }}, ' |
|
2855 |
'meeting type name via {{ booking.event.meeting_type.label }}, or any extra ' |
|
2856 |
'parameter passed on booking creation via {{ booking.extra_data.xxx }}.' |
|
2857 |
), |
|
2834 | 2858 |
) |
2835 | 2859 |
days_before_sms = models.IntegerField( |
2836 | 2860 |
null=True, |
... | ... | |
2845 | 2869 |
sms_extra_info = models.TextField( |
2846 | 2870 |
blank=True, |
2847 | 2871 |
verbose_name=_('Additional text to include in SMS'), |
2848 |
help_text=_('Basic information such as event name, time and date are already included.'), |
|
2872 |
validators=[booking_template_validator], |
|
2873 |
help_text=email_extra_info.help_text, |
|
2849 | 2874 |
) |
2850 | 2875 | |
2851 | 2876 |
def display_info(self): |
chrono/agendas/templates/agendas/events_reminder_body.html | ||
---|---|---|
11 | 11 |
</p> |
12 | 12 | |
13 | 13 |
{% if email_extra_info %} |
14 |
<p>{{ email_extra_info }}</p> |
|
14 |
<p>{{ email_extra_info|force_escape|linebreaks }}</p>
|
|
15 | 15 |
{% endif %} |
16 | 16 | |
17 | 17 |
{% if booking.event.description %} |
chrono/agendas/templates/agendas/meetings_reminder_body.html | ||
---|---|---|
17 | 17 |
</p> |
18 | 18 | |
19 | 19 |
{% if email_extra_info %} |
20 |
<p>{{ email_extra_info }}</p> |
|
20 |
<p>{{ email_extra_info|force_escape|linebreaks }}</p>
|
|
21 | 21 |
{% endif %} |
22 | 22 | |
23 | 23 |
{% if booking.form_url %} |
tests/manager/test_all.py | ||
---|---|---|
2899 | 2899 |
assert not 'Booking reminders' in resp.text |
2900 | 2900 | |
2901 | 2901 | |
2902 |
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC') |
|
2903 |
@pytest.mark.parametrize('extra_info_field', ('sms_extra_info', 'email_extra_info')) |
|
2904 |
def test_manager_reminders_templated_extra_info(app, admin_user, extra_info_field): |
|
2905 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2906 |
Desk.objects.create(agenda=agenda, slug='_exceptions_holder') |
|
2907 | ||
2908 |
login(app) |
|
2909 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
|
2910 |
resp = resp.click('Configure', href='reminder') |
|
2911 | ||
2912 |
extra_info = 'test {{ booking.extra_data.xxx }} {{ booking.event.label|default:booking.extra_data.yyy }}' |
|
2913 |
resp.form[extra_info_field] = extra_info |
|
2914 |
resp = resp.form.submit().follow() |
|
2915 |
assert getattr(agenda.reminder_settings, extra_info_field) == extra_info |
|
2916 | ||
2917 |
invalid_templates = [ |
|
2918 |
'{{ syntax error }}', |
|
2919 |
'{{ booking.label|invalidfilter }}', |
|
2920 |
] |
|
2921 |
for template in invalid_templates: |
|
2922 |
resp = app.get('/manage/agendas/%s/reminder' % agenda.id) |
|
2923 |
resp.form[extra_info_field] = template |
|
2924 |
resp = resp.form.submit() |
|
2925 |
assert 'syntax error' in resp.text |
|
2926 | ||
2927 | ||
2902 | 2928 |
def test_manager_reminders_preview(app, admin_user): |
2903 | 2929 |
agenda = Agenda.objects.create(label='Events', kind='events') |
2904 | 2930 |
Desk.objects.create(agenda=agenda, slug='_exceptions_holder') |
... | ... | |
2937 | 2963 |
in resp.text |
2938 | 2964 |
) |
2939 | 2965 | |
2966 |
# templates in extra info should not be interpreted |
|
2967 |
agenda.reminder_settings.sms_extra_info = '{{ booking.extra_data.xxx }}' |
|
2968 |
agenda.reminder_settings.email_extra_info = '{{ booking.extra_data.xxx }}' |
|
2969 |
agenda.reminder_settings.save() |
|
2970 | ||
2971 |
resp = resp.click('Return to settings') |
|
2972 |
resp = resp.click('Preview SMS') |
|
2973 |
assert '{{ booking.extra_data.xxx }}' in resp.text |
|
2974 | ||
2975 |
resp = resp.click('Return to settings') |
|
2976 |
resp = resp.click('Preview email') |
|
2977 |
assert '{{ booking.extra_data.xxx }}' in resp.text |
|
2978 | ||
2940 | 2979 | |
2941 | 2980 |
def test_manager_agenda_roles(app, admin_user, manager_user): |
2942 | 2981 |
agenda = Agenda.objects.create(label='Events', kind='events') |
tests/test_agendas.py | ||
---|---|---|
1925 | 1925 |
) |
1926 | 1926 | |
1927 | 1927 | |
1928 |
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC') |
|
1929 |
def test_agenda_reminders_templated_content(mailoutbox, freezer): |
|
1930 |
freezer.move_to('2020-01-01 14:00') |
|
1931 |
agenda = Agenda.objects.create(label='Main Center', kind='events') |
|
1932 |
AgendaReminderSettings.objects.create( |
|
1933 |
agenda=agenda, |
|
1934 |
days_before_email=1, |
|
1935 |
days_before_sms=1, |
|
1936 |
email_extra_info='Go to {{ booking.event.agenda.label }}.\nTake your {{ booking.extra_data.document_type }}.', |
|
1937 |
sms_extra_info='Take your {{ booking.extra_data.document_type }}.', |
|
1938 |
) |
|
1939 |
start_datetime = now() + datetime.timedelta(days=2) |
|
1940 |
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party') |
|
1941 | ||
1942 |
Booking.objects.create( |
|
1943 |
event=event, |
|
1944 |
user_email='t@test.org', |
|
1945 |
user_phone_number='+336123456789', |
|
1946 |
extra_data={'document_type': '"receipt"'}, |
|
1947 |
) |
|
1948 | ||
1949 |
freezer.move_to('2020-01-02 15:00') |
|
1950 |
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: |
|
1951 |
mock_response = mock.Mock(status_code=200) |
|
1952 |
mock_send.return_value = mock_response |
|
1953 |
call_command('send_booking_reminders') |
|
1954 | ||
1955 |
mail = mailoutbox[0] |
|
1956 |
assert 'Go to Main Center.\nTake your "receipt".' in mail.body |
|
1957 |
assert '<p>Go to Main Center.<br>Take your "receipt".</p>' in mail.alternatives[0][0] |
|
1958 | ||
1959 |
body = json.loads(mock_send.call_args[0][0].body.decode()) |
|
1960 |
assert 'Take your "receipt".' in body['message'] |
|
1961 | ||
1962 |
# in case of invalid template, send anyway |
|
1963 |
freezer.move_to('2020-01-01 14:00') |
|
1964 |
Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789') |
|
1965 |
agenda.reminder_settings.email_extra_info = 'Take your {{ syntax error }}' |
|
1966 |
agenda.reminder_settings.sms_extra_info = 'Take your {{ syntax error }}' |
|
1967 |
agenda.reminder_settings.save() |
|
1968 | ||
1969 |
freezer.move_to('2020-01-02 15:00') |
|
1970 |
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: |
|
1971 |
mock_response = mock.Mock(status_code=200) |
|
1972 |
mock_send.return_value = mock_response |
|
1973 |
call_command('send_booking_reminders') |
|
1974 | ||
1975 |
assert len(mailoutbox) == 2 |
|
1976 |
assert 'Take your' not in mailoutbox[1].body |
|
1977 | ||
1978 |
body = json.loads(mock_send.call_args[0][0].body.decode()) |
|
1979 |
assert 'Take your' not in body['message'] |
|
1980 | ||
1981 | ||
1928 | 1982 |
@override_settings(TIME_ZONE='UTC') |
1929 | 1983 |
def test_agenda_reminders_meetings(mailoutbox, freezer): |
1930 | 1984 |
freezer.move_to('2020-01-01 11:00') |
1931 |
- |