From 9dbf6b1d5c3b6cd40dbdb617b9c47b8ad0c74dc2 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 27 Jun 2022 15:20:09 +0200 Subject: [PATCH] manager: allow templated ICS URL (#66323) --- chrono/agendas/models.py | 7 ++++--- chrono/manager/forms.py | 19 +++++++++++++++++- tests/manager/test_exception.py | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 2a1edf4d..424931ec 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -2422,18 +2422,19 @@ class TimePeriodExceptionSource(models.Model): def _check_ics_content(self): if self.ics_url: + ics_url = Template(self.ics_url).render(Context(settings.TEMPLATE_VARS)) try: - response = requests.get(self.ics_url, proxies=settings.REQUESTS_PROXIES) + response = requests.get(ics_url, proxies=settings.REQUESTS_PROXIES) response.raise_for_status() except requests.HTTPError as e: raise ICSError( _('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).') - % {'url': self.ics_url, 'status_code': e.response.status_code} + % {'url': ics_url, 'status_code': e.response.status_code} ) except requests.RequestException as e: raise ICSError( _('Failed to retrieve remote calendar (%(url)s, %(exception)s).') - % {'url': self.ics_url, 'exception': e} + % {'url': ics_url, 'exception': e} ) try: # override response encoding received in HTTP headers as it may diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 358fdb42..dfb7e6a9 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -28,9 +28,11 @@ from django import forms from django.conf import settings from django.contrib.auth.models import Group from django.core.exceptions import FieldDoesNotExist +from django.core.validators import URLValidator from django.db import transaction from django.db.models import DurationField, ExpressionWrapper, F from django.forms import ValidationError, formset_factory +from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist from django.utils.encoding import force_text from django.utils.formats import date_format from django.utils.timezone import localtime, make_aware, now @@ -1195,8 +1197,9 @@ class ExceptionsImportForm(forms.ModelForm): required=False, help_text=_('ICS file containing events which will be considered as exceptions.'), ) - ics_url = forms.URLField( + ics_url = forms.CharField( label=_('URL'), + max_length=200, required=False, help_text=_('URL to remote calendar which will be synchronised hourly.'), ) @@ -1206,6 +1209,20 @@ class ExceptionsImportForm(forms.ModelForm): if not cleaned_data.get('ics_file') and not cleaned_data.get('ics_url'): raise forms.ValidationError(_('Please provide an ICS File or an URL.')) + def clean_ics_url(self): + url = self.cleaned_data['ics_url'] + if not url: + return url + + try: + url = Template(url).render(Context(settings.TEMPLATE_VARS)) + except (TemplateSyntaxError, VariableDoesNotExist) as e: + raise ValidationError(_('syntax error: %s') % e) + + URLValidator()(url) + + return self.cleaned_data['ics_url'] + class DeskExceptionsImportForm(ExceptionsImportForm): all_desks = forms.BooleanField(label=_('Apply exceptions on all desks of the agenda'), required=False) diff --git a/tests/manager/test_exception.py b/tests/manager/test_exception.py index 639aed65..697be6a6 100644 --- a/tests/manager/test_exception.py +++ b/tests/manager/test_exception.py @@ -772,6 +772,41 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mock assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text +@override_settings(TEMPLATE_VARS={'passerelle_url': 'https://passerelle.publik.love/'}) +@mock.patch('chrono.agendas.models.requests.get') +def test_agenda_import_time_period_exception_with_remote_ics_templated_url(mocked_get, app, admin_user): + agenda = Agenda.objects.create(label='New Example', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='New Desk') + login(app) + + resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk) + resp.form['ics_url'] = '{{ passerelle_url }}holidays/test/holidays.ics' + mocked_response = mock.Mock() + mocked_response.text = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//foo.bar//EN +BEGIN:VEVENT +DTSTART:20180101 +DTEND:20180101 +SUMMARY:New Year's Eve +END:VEVENT +END:VCALENDAR""" + mocked_get.return_value = mocked_response + resp = resp.form.submit(status=302) + assert TimePeriodException.objects.filter(desk=desk).count() == 1 + assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 1 + assert mocked_get.call_args.args[0] == 'https://passerelle.publik.love/holidays/test/holidays.ics' + + resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk) + resp.form['ics_url'] = '{{ unknown }}holidays/test/holidays.ics' + resp = resp.form.submit(status=200) + assert 'Enter a valid URL.' in resp.text + + resp.form['ics_url'] = '{{ { }}holidays/test/holidays.ics' + resp = resp.form.submit(status=200) + assert 'syntax error' in resp.text + + @mock.patch('chrono.agendas.models.requests.get') def test_agenda_import_time_period_exception_url_desk_all_desks(mocked_get, app, admin_user): agenda = Agenda.objects.create(label='Foo bar', kind='meetings')