From 264dc851dc929f125454a9e7eba6b6df8c48920b Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Tue, 3 Oct 2017 23:15:00 +0200 Subject: [PATCH] manager: add support for remote ICS file with exceptions (#19070) --- chrono/agendas/models.py | 11 +++++ chrono/manager/forms.py | 8 +++- .../chrono/manager_import_exceptions.html | 4 +- chrono/manager/views.py | 5 +- requirements.txt | 1 + setup.py | 3 +- tests/test_agendas.py | 47 ++++++++++++++++++ tests/test_manager.py | 56 ++++++++++++++++++++++ 8 files changed, 129 insertions(+), 6 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 3a1c1a0..39f11d5 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -17,6 +17,7 @@ import datetime import vobject +import requests from django.contrib.auth.models import Group from django.core.exceptions import ValidationError @@ -417,6 +418,16 @@ class Desk(models.Model): in_two_weeks = self.get_exceptions_within_two_weeks() return self.timeperiodexception_set.count() == len(in_two_weeks) + def create_timeperiod_exceptions_from_remote_ics(self, url): + try: + response = requests.get(url) + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise ICSError(_('Error %s is returned while trying to get file.') % e.response.status_code) + except requests.exceptions.ConnectionError: + raise ICSError(_('URL is unreachable.')) + return self.create_timeperiod_exceptions_from_ics(response.text) + def create_timeperiod_exceptions_from_ics(self, data): try: parsed = vobject.readOne(data) diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index cc6e37c..c0cac95 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -170,5 +170,11 @@ class ExceptionsImportForm(forms.ModelForm): model = Desk fields = [] - ics_file = forms.FileField(label=_('ICS File'), + ics_file = forms.FileField(label=_('ICS File'), required=False, help_text=_('ICS file containing events which will be considered as exceptions')) + ics_url = forms.URLField(label=_('URL'), required=False) + + def clean(self): + cleaned_data = super(ExceptionsImportForm, self).clean() + if not cleaned_data['ics_file'] and not cleaned_data['ics_url']: + raise ValidationError(_('A file or an url should be filled.')) diff --git a/chrono/manager/templates/chrono/manager_import_exceptions.html b/chrono/manager/templates/chrono/manager_import_exceptions.html index d3eacc5..210d36b 100644 --- a/chrono/manager/templates/chrono/manager_import_exceptions.html +++ b/chrono/manager/templates/chrono/manager_import_exceptions.html @@ -11,12 +11,10 @@ {% endblock %} {% block content %} -
+

{% trans "You could upload a local file or specify an address to remote file." %}

{% csrf_token %} {{ form.as_p }} -

-

{% trans 'Cancel' %} diff --git a/chrono/manager/views.py b/chrono/manager/views.py index c5d4eae..0595628 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -396,7 +396,10 @@ class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView def form_valid(self, form): try: - exceptions = form.instance.create_timeperiod_exceptions_from_ics(form.cleaned_data['ics_file']) + if form.cleaned_data['ics_file']: + exceptions = form.instance.create_timeperiod_exceptions_from_ics(form.cleaned_data['ics_file']) + else: + exceptions = form.instance.create_timeperiod_exceptions_from_remote_ics(form.cleaned_data['ics_url']) except ICSError as e: form.add_error(None, unicode(e)) return self.form_invalid(form) diff --git a/requirements.txt b/requirements.txt index 536c4b8..9a35d86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ gadjo djangorestframework>=3.1, <3.7 django-jsonfield >= 0.9.3 intervaltree +requests diff --git a/setup.py b/setup.py index 8fefafb..bfcaa56 100644 --- a/setup.py +++ b/setup.py @@ -107,7 +107,8 @@ setup( 'djangorestframework>=3.1, <3.7', 'django-jsonfield >= 0.9.3', 'intervaltree', - 'vobject' + 'vobject', + 'requests' ], zip_safe=False, cmdclass={ diff --git a/tests/test_agendas.py b/tests/test_agendas.py index 7560a66..83fdf1c 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1,5 +1,7 @@ import pytest import datetime +import mock +import requests from django.utils.timezone import now, make_aware, localtime @@ -220,3 +222,48 @@ def test_timeexception_create_from_ics_with_no_events(): with pytest.raises(ICSError) as e: exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_NO_EVENTS) assert str(e.value) == "The file doesn't contain any events." + +@mock.patch('chrono.agendas.models.requests.get') +def test_timeperiodexception_creation_from_remote_ics(mocked_get): + agenda = Agenda(label=u'Test 8 agenda') + agenda.save() + desk = Desk(label='Test 8 desk', agenda=agenda) + desk.save() + mocked_response = mock.Mock() + mocked_response.text = ICS_SAMPLE + mocked_get.return_value = mocked_response + exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') + assert exceptions_count == 2 + +@mock.patch('chrono.agendas.models.requests.get') +def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get): + agenda = Agenda(label=u'Test 9 agenda') + agenda.save() + desk = Desk(label='Test 9 desk', agenda=agenda) + desk.save() + mocked_response = mock.Mock() + mocked_response.content.return_value = ICS_SAMPLE + mocked_get.return_value = mocked_response + def mocked_requests_connection_error(*args, **kwargs): + raise requests.exceptions.ConnectionError('unreachable') + mocked_get.side_effect = mocked_requests_connection_error + with pytest.raises(ICSError) as e: + exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') + assert str(e.value) == "URL is unreachable." + +@mock.patch('chrono.agendas.models.requests.get') +def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get): + agenda = Agenda(label=u'Test 10 agenda') + agenda.save() + desk = Desk(label='Test 10 desk', agenda=agenda) + desk.save() + mocked_response = mock.Mock() + mocked_response.status_code = 403 + mocked_get.return_value = mocked_response + def mocked_requests_http_forbidden_error(*args, **kwargs): + raise requests.exceptions.HTTPError(response=mocked_response) + mocked_get.side_effect = mocked_requests_http_forbidden_error + + with pytest.raises(ICSError) as e: + exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') + assert str(e.value) == "Error 403 is returned while trying to get file." diff --git a/tests/test_manager.py b/tests/test_manager.py index a39190e..620b5d0 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -5,6 +5,8 @@ from django.utils.timezone import make_aware, now, localtime import datetime import pytest from webtest import TestApp, Upload +import mock +import requests from chrono.wsgi import application @@ -840,6 +842,9 @@ def test_agenda_import_time_period_exception_from_ics(app, admin_user): resp = app.get('/manage/agendas/%d/' % agenda.pk) assert 'Import exceptions from .ics' in resp.content resp = resp.click('upload') + assert "You could upload a local file or specify an address to remote file." in resp + resp = resp.form.submit(status=200) + assert "A file or an url should be filled." in resp.body resp.form['ics_file'] = Upload('exceptions.ics', 'invalid content', 'text/calendar') resp = resp.form.submit(status=200) assert 'File format is invalid' in resp.content @@ -889,3 +894,54 @@ END:VCALENDAR""" assert TimePeriodException.objects.count() == 1 resp = resp.follow() assert 'An exception has been imported.' in resp.content + +@mock.patch('chrono.agendas.models.requests.get') +def test_agenda_import_time_period_exception_from_remote_ics(mocked_get, app, admin_user): + agenda = Agenda.objects.create(label='New Example', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='New Desk') + MeetingType(agenda=agenda, label='Bar').save() + login(app) + resp = app.get('/manage/agendas/%d/' % agenda.pk) + assert 'Import exceptions from .ics' not in resp.content + + TimePeriod.objects.create(weekday=1, desk=desk, + start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) + + resp = app.get('/manage/agendas/%d/' % agenda.pk) + resp = resp.click('upload') + + assert 'ics_file' in resp.form.fields + assert 'ics_url' in resp.form.fields + resp.form['ics_url'] = 'http://example.com/foo.ics' + mocked_response = mock.Mock() + mocked_get.return_value = mocked_response + def mocked_requests_connection_error(*args, **kwargs): + raise requests.exceptions.ConnectionError('unreachable') + mocked_get.side_effect = mocked_requests_connection_error + resp = resp.form.submit(status=200) + assert 'URL is unreachable.' in resp.content + +@mock.patch('chrono.agendas.models.requests.get') +def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_get, app, admin_user): + + agenda = Agenda.objects.create(label='New Example', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='New Desk') + MeetingType(agenda=agenda, label='Bar').save() + login(app) + resp = app.get('/manage/agendas/%d/' % agenda.pk) + assert 'Import exceptions from .ics' not in resp.content + + TimePeriod.objects.create(weekday=1, desk=desk, + start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) + + resp = app.get('/manage/agendas/%d/' % agenda.pk) + resp = resp.click('upload') + resp.form['ics_url'] = 'http://example.com/foo.ics' + mocked_response = mock.Mock() + mocked_response.status_code = 403 + mocked_get.return_value = mocked_response + def mocked_requests_http_forbidden_error(*args, **kwargs): + raise requests.exceptions.HTTPError(response=mocked_response) + mocked_get.side_effect = mocked_requests_http_forbidden_error + resp = resp.form.submit(status=200) + assert 'Error 403 is returned while trying to get' in resp.content -- 2.14.2