From 89e1563989e99c4e226c25df848e63d5a45dbfcf Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 9 Oct 2017 11:01:10 +0200 Subject: [PATCH] agenda: add remote timeperiods url for desks (#19070) Add command to synchronize time periods. --- .../0020_desk_timeperiod_exceptions_remote_url.py | 19 ++++++++++ chrono/agendas/models.py | 2 + chrono/manager/forms.py | 20 ++++++++-- .../commands/sync_desks_timeperiod_exceptions.py | 33 +++++++++++++++++ tests/test_agendas.py | 23 ++++++++++++ tests/test_manager.py | 43 ++++++++++++++++++++++ 6 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 chrono/agendas/migrations/0020_desk_timeperiod_exceptions_remote_url.py create mode 100644 chrono/manager/management/commands/sync_desks_timeperiod_exceptions.py diff --git a/chrono/agendas/migrations/0020_desk_timeperiod_exceptions_remote_url.py b/chrono/agendas/migrations/0020_desk_timeperiod_exceptions_remote_url.py new file mode 100644 index 0000000..a19524c --- /dev/null +++ b/chrono/agendas/migrations/0020_desk_timeperiod_exceptions_remote_url.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0019_timeperiodexception'), + ] + + operations = [ + migrations.AddField( + model_name='desk', + name='timeperiod_exceptions_remote_url', + field=models.URLField(null=True, verbose_name='URL to fetch time period exceptions from', blank=True), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 39f11d5..71d829e 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -359,6 +359,8 @@ class Desk(models.Model): agenda = models.ForeignKey(Agenda) label = models.CharField(_('Label'), max_length=150) slug = models.SlugField(_('Identifier'), max_length=150) + timeperiod_exceptions_remote_url = models.URLField(_('URL to fetch time period exceptions from'), + null=True, blank=True) def __unicode__(self): return self.label diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index c0cac95..b0a01bd 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -16,6 +16,7 @@ import csv import datetime +import requests from django import forms from django.forms import ValidationError @@ -77,22 +78,33 @@ class TimePeriodForm(forms.ModelForm): exclude = [] -class NewDeskForm(forms.ModelForm): +class DeskForm(forms.ModelForm): class Meta: model = Desk widgets = { 'agenda': forms.HiddenInput(), } - exclude = ['slug'] + exclude = [] + + def clean_timeperiod_exceptions_remote_url(self): + if not self.cleaned_data['timeperiod_exceptions_remote_url']: + return + try: + response = requests.get(self.cleaned_data['timeperiod_exceptions_remote_url']) + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise ValidationError(_('Error %s is returned while trying to get file.') % e.response.status_code) + except requests.exceptions.ConnectionError: + raise ValidationError(_('URL is unreachable.')) -class DeskForm(forms.ModelForm): +class NewDeskForm(DeskForm): class Meta: model = Desk widgets = { 'agenda': forms.HiddenInput(), } - exclude = [] + exclude = ['slug'] class TimePeriodExceptionForm(forms.ModelForm): diff --git a/chrono/manager/management/commands/sync_desks_timeperiod_exceptions.py b/chrono/manager/management/commands/sync_desks_timeperiod_exceptions.py new file mode 100644 index 0000000..bd8328f --- /dev/null +++ b/chrono/manager/management/commands/sync_desks_timeperiod_exceptions.py @@ -0,0 +1,33 @@ +# chrono - agendas system +# Copyright (C) 2016-2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys +import logging + +from django.core.management.base import BaseCommand +from chrono.agendas.models import Desk, ICSError + + +class Command(BaseCommand): + help = 'Synchronize time period exceptions from desks remote ics' + + def handle(self, **options): + logger = logging.getLogger(__name__) + for desk in Desk.objects.filter(timeperiod_exceptions_remote_url__isnull=False): + try: + desk.create_timeperiod_exceptions_from_remote_ics(desk.timeperiod_exceptions_remote_url) + except ICSError as e: + logger.warning(u'unable to create timeperiod exceptions for "%s": %s' % (desk, e)) diff --git a/tests/test_agendas.py b/tests/test_agendas.py index 83fdf1c..eab4d97 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -2,8 +2,10 @@ import pytest import datetime import mock import requests +import logging from django.utils.timezone import now, make_aware, localtime +from django.core.management import call_command from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, Desk, TimePeriodException, ICSError) @@ -267,3 +269,24 @@ def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get): 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." + + +@mock.patch('chrono.agendas.models.requests.get') +def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, caplog): + agenda = Agenda(label=u'Test 11 agenda') + agenda.save() + desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics') + 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 + call_command('sync_desks_timeperiod_exceptions') + records = caplog.records() + assert len(records) == 1 + for record in records: + assert record.name == 'chrono.manager.management.commands.sync_desks_timeperiod_exceptions' + assert record.levelno == logging.WARNING + assert record.getMessage() == 'unable to create timeperiod exceptions for "Test 11 desk": Error 403 is returned while trying to get file.' diff --git a/tests/test_manager.py b/tests/test_manager.py index 620b5d0..8dec0f6 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -701,6 +701,49 @@ def test_meetings_agenda_add_desk(app, admin_user): assert 'Desk A' in resp.text assert 'Desk B' in resp.text +@mock.patch('chrono.manager.forms.requests.get') +def test_meetings_agenda_add_desk_with_non_existing_exceptions_url(mocked_get, app, admin_user): + app = login(app) + resp = app.get('/manage/', status=200) + resp = resp.click('New') + resp.form['label'] = 'Foo bar' + resp.form['kind'] = 'meetings' + resp = resp.form.submit() + agenda = Agenda.objects.get(slug='foo-bar') + resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) + 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.click('New Desk') + resp.form['label'] = 'Desk A' + resp.form['timeperiod_exceptions_remote_url'] = 'http://nowhere.com/unknown.ics' + resp = resp.form.submit(status=200) + assert 'Error 403 is returned while trying to get file.' in resp.text + +@mock.patch('chrono.manager.forms.requests.get') +def test_meetings_agenda_add_desk_with_unreachable_exceptions_url(mocked_get, app, admin_user): + app = login(app) + resp = app.get('/manage/', status=200) + resp = resp.click('New') + resp.form['label'] = 'Foo bar' + resp.form['kind'] = 'meetings' + resp = resp.form.submit() + agenda = Agenda.objects.get(slug='foo-bar') + resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) + 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.click('New Desk') + resp.form['label'] = 'Desk A' + resp.form['timeperiod_exceptions_remote_url'] = 'http://nowhere.com/unknown.ics' + resp = resp.form.submit(status=200) + assert 'URL is unreachable.' in resp.text + def test_meetings_agenda_delete_desk(app, admin_user): app = login(app) -- 2.14.2