diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 113bf2fa..c3e66c4f 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -33,6 +33,7 @@ from chrono.agendas.models import ( TimePeriod, Desk, TimePeriodException, + TimePeriodExceptionSource, WEEKDAYS_LIST, ) @@ -283,5 +284,17 @@ class ExceptionsImportForm(forms.ModelForm): raise forms.ValidationError(_('Please provide an ICS File or an URL.')) +class TimePeriodExceptionSourceReplaceForm(forms.ModelForm): + ics_file = forms.FileField( + label=_('ICS File'), + required=False, + help_text=_('ICS file containing events which will be considered as exceptions.'), + ) + + class Meta: + model = TimePeriodExceptionSource + fields = [] + + class AgendasImportForm(forms.Form): agendas_json = forms.FileField(label=_('Agendas Export File')) diff --git a/chrono/manager/templates/chrono/manager_import_exceptions.html b/chrono/manager/templates/chrono/manager_import_exceptions.html index a6358279..dda92e38 100644 --- a/chrono/manager/templates/chrono/manager_import_exceptions.html +++ b/chrono/manager/templates/chrono/manager_import_exceptions.html @@ -31,9 +31,11 @@ - - {% if object.ics_filename %}{% trans "replace" %}{% else %}{% trans "refresh" %}{% endif %} - + {% if object.ics_filename %} + {% trans "replace" %} + {% else %} + {% trans "refresh" %} + {% endif %} {% trans "remove" %} diff --git a/chrono/manager/templates/chrono/manager_replace_exceptions.html b/chrono/manager/templates/chrono/manager_replace_exceptions.html new file mode 100644 index 00000000..31fe4e5f --- /dev/null +++ b/chrono/manager/templates/chrono/manager_replace_exceptions.html @@ -0,0 +1,28 @@ +{% extends "chrono/manager_import_exceptions.html" %} +{% load i18n %} + +{% block appbar %} +

{% if form.instance.ics_filename %}{% trans "Replace exceptions" %}{% else %}{% trans "Refresh exceptions" %}{% endif %}

+{% endblock %} + +{% block content %} +
+ {% if form.instance.ics_filename %} +

{% trans "To replace existing exceptions, please upload a new file." %}

+ {% else %} +

+ {% trans 'Press the button "Refresh" to refresh existing exceptions from:' %} +
+ {{ form.instance.ics_url }} +

+ {% endif %} + {% csrf_token %} + {{ form.as_p }} +

+

+
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index 5ccd361e..a163f0a0 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -105,6 +105,16 @@ urlpatterns = [ views.time_period_exception_source_delete, name='chrono-manager-time-period-exception-source-delete', ), + url( + r'^time-period-exceptions-source/(?P\d+)/refresh$', + views.time_period_exception_source_refresh, + name='chrono-manager-time-period-exception-source-refresh', + ), + url( + r'^time-period-exceptions-source/(?P\d+)/replace$', + views.time_period_exception_source_replace, + name='chrono-manager-time-period-exception-source-replace', + ), url( r'^agendas/events.csv$', views.agenda_import_events_sample_csv, diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 3e90ae0f..e3888f0a 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -69,6 +69,7 @@ from .forms import ( ExceptionsImportForm, AgendasImportForm, TimePeriodAddForm, + TimePeriodExceptionSourceReplaceForm, ) from .utils import import_site @@ -926,6 +927,59 @@ class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView) time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view() +class TimePeriodExceptionSourceReplaceView(ManagedDeskSubobjectMixin, UpdateView): + model = TimePeriodExceptionSource + form_class = TimePeriodExceptionSourceReplaceForm + template_name = 'chrono/manager_replace_exceptions.html' + + def form_valid(self, form): + exceptions = None + try: + exceptions = form.instance.desk.import_timeperiod_exceptions_from_ics_file( + form.cleaned_data['ics_file'], source=form.instance + ) + except ICSError as e: + form.add_error(None, force_text(e)) + return self.form_invalid(form) + + if exceptions is not None: + message = ungettext( + 'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions + ) + message = message % {'count': exceptions} + messages.info(self.request, message) + return super(TimePeriodExceptionSourceReplaceView, self).form_valid(form) + + +time_period_exception_source_replace = TimePeriodExceptionSourceReplaceView.as_view() + + +class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, DetailView): + model = TimePeriodExceptionSource + + def get(self, request, *args, **kwargs): + try: + source = self.get_object() + exceptions = source.desk.import_timeperiod_exceptions_from_remote_ics( + source.ics_url, source=source + ) + except ICSError as e: + messages.error(self.request, force_text(e)) + else: + message = ungettext( + 'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions + ) + message = message % {'count': exceptions} + messages.info(self.request, message) + # redirect to settings + return HttpResponseRedirect( + reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id}) + ) + + +time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view() + + def menu_json(request): label = _('Agendas') json_str = json.dumps( diff --git a/tests/test_manager.py b/tests/test_manager.py index eb219893..4a326709 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1470,6 +1470,97 @@ END:VCALENDAR""" assert TimePeriodExceptionSource.objects.filter(pk=source1.pk).exists() is False +def test_meetings_agenda_replace_time_period_exception_source(app, admin_user): + agenda = Agenda.objects.create(label='Foo bar', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='Desk A') + MeetingType(agenda=agenda, label='Blah').save() + TimePeriod.objects.create( + weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) + ) + ics_file_content = b"""BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//foo.bar//EN +BEGIN:VEVENT +DTSTART:20180101 +DTEND:20180101 +SUMMARY:New Year's Eve +RRULE:FREQ=YEARLY +END:VEVENT +END:VCALENDAR""" + + login(app) + # import a source from a file + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp.form['ics_file'] = Upload('exceptions.ics', ics_file_content, 'text/calendar') + resp = resp.form.submit(status=302).follow() + assert TimePeriodException.objects.filter(desk=desk).count() == 2 + source = TimePeriodExceptionSource.objects.latest('pk') + assert source.timeperiodexception_set.count() == 2 + exceptions = list(source.timeperiodexception_set.order_by('pk')) + + # replace the source + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp = resp.click(href='/manage/time-period-exceptions-source/%d/replace' % source.pk) + resp.form['ics_file'] = Upload('exceptions.ics', ics_file_content, 'text/calendar') + resp = resp.form.submit().follow() + assert TimePeriodException.objects.count() == 2 + assert source.timeperiodexception_set.count() == 2 + new_exceptions = list(source.timeperiodexception_set.order_by('pk')) + assert exceptions[0].pk != new_exceptions[0].pk + assert exceptions[1].pk != new_exceptions[1].pk + + +@mock.patch('chrono.agendas.models.requests.get') +def test_meetings_agenda_refresh_time_period_exception_source(mocked_get, app, admin_user): + agenda = Agenda.objects.create(label='Foo bar', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='Desk A') + MeetingType(agenda=agenda, label='Blah').save() + TimePeriod.objects.create( + weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) + ) + ics_url_content = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//foo.bar//EN +BEGIN:VEVENT +DTSTART:20180101 +DTEND:20180101 +SUMMARY:New Year's Eve +END:VEVENT +END:VCALENDAR""" + + login(app) + # import a source from an url + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + resp.form['ics_url'] = 'http://example.com/foo.ics' + mocked_response = mock.Mock() + mocked_response.text = ics_url_content + mocked_get.return_value = mocked_response + resp = resp.form.submit(status=302).follow() + assert TimePeriodException.objects.filter(desk=desk).count() == 1 + source = TimePeriodExceptionSource.objects.latest('pk') + assert source.timeperiodexception_set.count() == 1 + exceptions = list(source.timeperiodexception_set.order_by('pk')) + + # refresh the source + resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() + resp = resp.click('Settings') + resp = resp.click('upload') + mocked_response = mock.Mock() + mocked_response.text = ics_url_content + mocked_get.return_value = mocked_response + resp = resp.click(href='/manage/time-period-exceptions-source/%d/refresh' % source.pk) + assert TimePeriodException.objects.count() == 1 + assert source.timeperiodexception_set.count() == 1 + new_exceptions = list(source.timeperiodexception_set.order_by('pk')) + assert exceptions[0].pk != new_exceptions[0].pk + + def test_agenda_day_view(app, admin_user, manager_user, api_user): agenda = Agenda.objects.create(label='New Example', kind='meetings') desk = Desk.objects.create(agenda=agenda, label='New Desk')