0001-agenda-add-remote-timeperiods-url-for-desks-19070.patch
chrono/agendas/migrations/0020_desk_timeperiod_exceptions_remote_url.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('agendas', '0019_timeperiodexception'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='desk', |
|
16 |
name='timeperiod_exceptions_remote_url', |
|
17 |
field=models.URLField(null=True, verbose_name='URL to fetch time period exceptions from', blank=True), |
|
18 |
), |
|
19 |
] |
chrono/agendas/models.py | ||
---|---|---|
359 | 359 |
agenda = models.ForeignKey(Agenda) |
360 | 360 |
label = models.CharField(_('Label'), max_length=150) |
361 | 361 |
slug = models.SlugField(_('Identifier'), max_length=150) |
362 |
timeperiod_exceptions_remote_url = models.URLField(_('URL to fetch time period exceptions from'), |
|
363 |
null=True, blank=True) |
|
362 | 364 | |
363 | 365 |
def __unicode__(self): |
364 | 366 |
return self.label |
chrono/manager/forms.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import csv |
18 | 18 |
import datetime |
19 |
import requests |
|
19 | 20 | |
20 | 21 |
from django import forms |
21 | 22 |
from django.forms import ValidationError |
... | ... | |
77 | 78 |
exclude = [] |
78 | 79 | |
79 | 80 | |
80 |
class NewDeskForm(forms.ModelForm):
|
|
81 |
class DeskForm(forms.ModelForm): |
|
81 | 82 |
class Meta: |
82 | 83 |
model = Desk |
83 | 84 |
widgets = { |
84 | 85 |
'agenda': forms.HiddenInput(), |
85 | 86 |
} |
86 |
exclude = ['slug'] |
|
87 |
exclude = [] |
|
88 | ||
89 |
def clean_timeperiod_exceptions_remote_url(self): |
|
90 |
if not self.cleaned_data['timeperiod_exceptions_remote_url']: |
|
91 |
return |
|
92 |
try: |
|
93 |
response = requests.get(self.cleaned_data['timeperiod_exceptions_remote_url']) |
|
94 |
response.raise_for_status() |
|
95 |
except requests.exceptions.HTTPError as e: |
|
96 |
raise ValidationError(_('Error %s is returned while trying to get file.') % e.response.status_code) |
|
97 |
except requests.exceptions.ConnectionError: |
|
98 |
raise ValidationError(_('URL is unreachable.')) |
|
87 | 99 | |
88 | 100 | |
89 |
class DeskForm(forms.ModelForm):
|
|
101 |
class NewDeskForm(DeskForm):
|
|
90 | 102 |
class Meta: |
91 | 103 |
model = Desk |
92 | 104 |
widgets = { |
93 | 105 |
'agenda': forms.HiddenInput(), |
94 | 106 |
} |
95 |
exclude = [] |
|
107 |
exclude = ['slug']
|
|
96 | 108 | |
97 | 109 | |
98 | 110 |
class TimePeriodExceptionForm(forms.ModelForm): |
chrono/manager/management/commands/sync_desks_timeperiod_exceptions.py | ||
---|---|---|
1 |
# chrono - agendas system |
|
2 |
# Copyright (C) 2016-2017 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import sys |
|
18 |
import logging |
|
19 | ||
20 |
from django.core.management.base import BaseCommand |
|
21 |
from chrono.agendas.models import Desk, ICSError |
|
22 | ||
23 | ||
24 |
class Command(BaseCommand): |
|
25 |
help = 'Synchronize time period exceptions from desks remote ics' |
|
26 | ||
27 |
def handle(self, **options): |
|
28 |
logger = logging.getLogger(__name__) |
|
29 |
for desk in Desk.objects.filter(timeperiod_exceptions_remote_url__isnull=False): |
|
30 |
try: |
|
31 |
desk.create_timeperiod_exceptions_from_remote_ics(desk.timeperiod_exceptions_remote_url) |
|
32 |
except ICSError as e: |
|
33 |
logger.warning(u'unable to create timeperiod exceptions for "%s": %s' % (desk, e)) |
tests/test_agendas.py | ||
---|---|---|
2 | 2 |
import datetime |
3 | 3 |
import mock |
4 | 4 |
import requests |
5 |
import logging |
|
5 | 6 | |
6 | 7 |
from django.utils.timezone import now, make_aware, localtime |
8 |
from django.core.management import call_command |
|
7 | 9 | |
8 | 10 |
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, |
9 | 11 |
Desk, TimePeriodException, ICSError) |
... | ... | |
267 | 269 |
with pytest.raises(ICSError) as e: |
268 | 270 |
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') |
269 | 271 |
assert str(e.value) == "Error 403 is returned while trying to get file." |
272 | ||
273 | ||
274 |
@mock.patch('chrono.agendas.models.requests.get') |
|
275 |
def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, caplog): |
|
276 |
agenda = Agenda(label=u'Test 11 agenda') |
|
277 |
agenda.save() |
|
278 |
desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics') |
|
279 |
desk.save() |
|
280 |
mocked_response = mock.Mock() |
|
281 |
mocked_response.status_code = 403 |
|
282 |
mocked_get.return_value = mocked_response |
|
283 |
def mocked_requests_http_forbidden_error(*args, **kwargs): |
|
284 |
raise requests.exceptions.HTTPError(response=mocked_response) |
|
285 |
mocked_get.side_effect = mocked_requests_http_forbidden_error |
|
286 |
call_command('sync_desks_timeperiod_exceptions') |
|
287 |
records = caplog.records() |
|
288 |
assert len(records) == 1 |
|
289 |
for record in records: |
|
290 |
assert record.name == 'chrono.manager.management.commands.sync_desks_timeperiod_exceptions' |
|
291 |
assert record.levelno == logging.WARNING |
|
292 |
assert record.getMessage() == 'unable to create timeperiod exceptions for "Test 11 desk": Error 403 is returned while trying to get file.' |
tests/test_manager.py | ||
---|---|---|
701 | 701 |
assert 'Desk A' in resp.text |
702 | 702 |
assert 'Desk B' in resp.text |
703 | 703 | |
704 |
@mock.patch('chrono.manager.forms.requests.get') |
|
705 |
def test_meetings_agenda_add_desk_with_non_existing_exceptions_url(mocked_get, app, admin_user): |
|
706 |
app = login(app) |
|
707 |
resp = app.get('/manage/', status=200) |
|
708 |
resp = resp.click('New') |
|
709 |
resp.form['label'] = 'Foo bar' |
|
710 |
resp.form['kind'] = 'meetings' |
|
711 |
resp = resp.form.submit() |
|
712 |
agenda = Agenda.objects.get(slug='foo-bar') |
|
713 |
resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) |
|
714 |
mocked_response = mock.Mock() |
|
715 |
mocked_response.status_code = 403 |
|
716 |
mocked_get.return_value = mocked_response |
|
717 |
def mocked_requests_http_forbidden_error(*args, **kwargs): |
|
718 |
raise requests.exceptions.HTTPError(response=mocked_response) |
|
719 |
mocked_get.side_effect = mocked_requests_http_forbidden_error |
|
720 |
resp = resp.click('New Desk') |
|
721 |
resp.form['label'] = 'Desk A' |
|
722 |
resp.form['timeperiod_exceptions_remote_url'] = 'http://nowhere.com/unknown.ics' |
|
723 |
resp = resp.form.submit(status=200) |
|
724 |
assert 'Error 403 is returned while trying to get file.' in resp.text |
|
725 | ||
726 |
@mock.patch('chrono.manager.forms.requests.get') |
|
727 |
def test_meetings_agenda_add_desk_with_unreachable_exceptions_url(mocked_get, app, admin_user): |
|
728 |
app = login(app) |
|
729 |
resp = app.get('/manage/', status=200) |
|
730 |
resp = resp.click('New') |
|
731 |
resp.form['label'] = 'Foo bar' |
|
732 |
resp.form['kind'] = 'meetings' |
|
733 |
resp = resp.form.submit() |
|
734 |
agenda = Agenda.objects.get(slug='foo-bar') |
|
735 |
resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) |
|
736 |
mocked_response = mock.Mock() |
|
737 |
mocked_get.return_value = mocked_response |
|
738 |
def mocked_requests_connection_error(*args, **kwargs): |
|
739 |
raise requests.exceptions.ConnectionError('unreachable') |
|
740 |
mocked_get.side_effect = mocked_requests_connection_error |
|
741 |
resp = resp.click('New Desk') |
|
742 |
resp.form['label'] = 'Desk A' |
|
743 |
resp.form['timeperiod_exceptions_remote_url'] = 'http://nowhere.com/unknown.ics' |
|
744 |
resp = resp.form.submit(status=200) |
|
745 |
assert 'URL is unreachable.' in resp.text |
|
746 | ||
704 | 747 | |
705 | 748 |
def test_meetings_agenda_delete_desk(app, admin_user): |
706 | 749 |
app = login(app) |
707 |
- |