From af17dc072fb23adb3415542a65343306bbb4a896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Tue, 28 Jan 2020 15:08:24 +0100 Subject: [PATCH] agendas: keep the file of an exception source (#39259) --- .../sync_desks_timeperiod_exceptions.py | 10 ++++- ...0037_timeperiodexceptionsource_ics_file.py | 20 +++++++++ chrono/agendas/models.py | 10 ++++- chrono/manager/forms.py | 12 +++++- .../chrono/manager_replace_exceptions.html | 14 ++----- chrono/manager/views.py | 10 ++++- tests/test_agendas.py | 41 ++++++++++++++++++- tests/test_manager.py | 9 +++- 8 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 chrono/agendas/migrations/0037_timeperiodexceptionsource_ics_file.py diff --git a/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py b/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py index a9ec0ff7..735181d7 100644 --- a/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py +++ b/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py @@ -27,10 +27,18 @@ class Command(BaseCommand): help = 'Synchronize time period exceptions from desks remote ics' def handle(self, **options): - for source in TimePeriodExceptionSource.objects.filter(ics_url__isnull=False): + for source in TimePeriodExceptionSource.objects.filter(ics_url__isnull=False, ics_file=''): try: source.desk.import_timeperiod_exceptions_from_remote_ics(source.ics_url, source=source) except ICSError as e: print( u'unable to create timeperiod exceptions for "%s": %s' % (source.desk, e), file=sys.stderr ) + + for source in TimePeriodExceptionSource.objects.filter(ics_url__isnull=True).exclude(ics_file=''): + try: + source.desk.import_timeperiod_exceptions_from_ics_file(source.ics_file, source=source) + except ICSError as e: + print( + u'unable to create timeperiod exceptions for "%s": %s' % (source.desk, e), file=sys.stderr + ) diff --git a/chrono/agendas/migrations/0037_timeperiodexceptionsource_ics_file.py b/chrono/agendas/migrations/0037_timeperiodexceptionsource_ics_file.py new file mode 100644 index 00000000..b6ca2e71 --- /dev/null +++ b/chrono/agendas/migrations/0037_timeperiodexceptionsource_ics_file.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import chrono.agendas.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0036_auto_20191223_1758'), + ] + + operations = [ + migrations.AddField( + model_name='timeperiodexceptionsource', + name='ics_file', + field=models.FileField(blank=True, null=True, upload_to=chrono.agendas.models.ics_directory_path), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 04f73411..79ee1a87 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -19,6 +19,7 @@ import datetime import math import requests import vobject +import uuid import django from django.conf import settings @@ -583,13 +584,13 @@ class Desk(models.Model): # often be missing and defaults to iso-8859-15. response.content.decode('utf-8') response.encoding = 'utf-8' - except UnicodeDecodeError as e: + except UnicodeDecodeError: pass return self._import_timeperiod_exceptions_from_ics(source=source, data=response.text) def import_timeperiod_exceptions_from_ics_file(self, ics_file, source=None): if source is None: - source = TimePeriodExceptionSource(desk=self, ics_filename=ics_file.name) + source = TimePeriodExceptionSource(desk=self, ics_filename=ics_file.name, ics_file=ics_file) return self._import_timeperiod_exceptions_from_ics(source=source, data=force_text(ics_file.read())) def _import_timeperiod_exceptions_from_ics(self, source, data, recurring_days=600): @@ -691,9 +692,14 @@ class Desk(models.Model): return openslots.search(aware_date, aware_next_date) +def ics_directory_path(instance, filename): + return 'ics/{0}/{1}'.format(str(uuid.uuid4()), filename) + + class TimePeriodExceptionSource(models.Model): desk = models.ForeignKey(Desk, on_delete=models.CASCADE) ics_filename = models.CharField(null=True, max_length=256) + ics_file = models.FileField(upload_to=ics_directory_path, blank=True, null=True) ics_url = models.URLField(null=True, max_length=500) def __str__(self): diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index cad034dc..03d2bf64 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -263,7 +263,9 @@ class ExceptionsImportForm(forms.ModelForm): ics_file = forms.FileField( label=_('ICS File'), required=False, - help_text=_('ICS file containing events which will be considered as exceptions.'), + help_text=_( + 'ICS file containing events which will be considered as exceptions. Will be synchronised hourly' + ), ) ics_url = forms.URLField( label=_('URL'), @@ -282,7 +284,7 @@ class ExceptionsImportForm(forms.ModelForm): class TimePeriodExceptionSourceReplaceForm(forms.ModelForm): - ics_file = forms.FileField( + ics_newfile = forms.FileField( label=_('ICS File'), required=False, help_text=_('ICS file containing events which will be considered as exceptions.'), @@ -292,6 +294,12 @@ class TimePeriodExceptionSourceReplaceForm(forms.ModelForm): model = TimePeriodExceptionSource fields = [] + def save(self, *args, **kwargs): + if bool(self.instance.ics_file): + self.instance.ics_file.delete() + self.instance.ics_file = self.cleaned_data['ics_newfile'] + self.instance.save() + class AgendasImportForm(forms.Form): agendas_json = forms.FileField(label=_('Agendas Export File')) diff --git a/chrono/manager/templates/chrono/manager_replace_exceptions.html b/chrono/manager/templates/chrono/manager_replace_exceptions.html index 31fe4e5f..6f9f8309 100644 --- a/chrono/manager/templates/chrono/manager_replace_exceptions.html +++ b/chrono/manager/templates/chrono/manager_replace_exceptions.html @@ -2,26 +2,18 @@ {% load i18n %} {% block appbar %} -

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

+

{% trans "Replace exceptions" %}

{% 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 %} +

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

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

- + {% trans 'Cancel' %}
diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 4580c8cd..e0c0b559 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -988,11 +988,15 @@ class TimePeriodExceptionSourceReplaceView(ManagedDeskSubobjectMixin, UpdateView form_class = TimePeriodExceptionSourceReplaceForm template_name = 'chrono/manager_replace_exceptions.html' + def get_queryset(self): + queryset = super(TimePeriodExceptionSourceReplaceView, self).get_queryset() + return queryset.filter(ics_filename__isnull=False) + 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 + form.cleaned_data['ics_newfile'], source=form.instance ) except ICSError as e: form.add_error(None, force_text(e)) @@ -1013,6 +1017,10 @@ time_period_exception_source_replace = TimePeriodExceptionSourceReplaceView.as_v class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, DetailView): model = TimePeriodExceptionSource + def get_queryset(self): + queryset = super(TimePeriodExceptionSourceRefreshView, self).get_queryset() + return queryset.filter(ics_url__isnull=False) + def get(self, request, *args, **kwargs): try: source = self.get_object() diff --git a/tests/test_agendas.py b/tests/test_agendas.py index abd3a809..5e99c240 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -370,7 +370,7 @@ def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys): agenda.save() desk = Desk(label='Test 11 desk', agenda=agenda) desk.save() - TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') + source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') mocked_response = mock.Mock() mocked_response.status_code = 403 mocked_get.return_value = mocked_response @@ -386,6 +386,45 @@ def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys): == 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n' ) + assert source.ics_url is not None + assert source.ics_filename is None + assert source.ics_file.name is None + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_remote_ics' + ) as import_remote_ics: + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_ics_file' + ) as import_file_ics: + call_command('sync_desks_timeperiod_exceptions') + assert import_remote_ics.call_args_list == [mock.call('http://example.com/sample.ics', source=source)] + assert import_file_ics.call_args_list == [] + + source.ics_url = None + source.ics_filename = 'sample.ics' + source.ics_file = ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics') + source.save() + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_remote_ics' + ) as import_remote_ics: + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_ics_file' + ) as import_file_ics: + call_command('sync_desks_timeperiod_exceptions') + assert import_remote_ics.call_args_list == [] + assert import_file_ics.call_args_list == [mock.call(mock.ANY, source=source)] + + source.ics_file.delete() + source.save() + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_remote_ics' + ) as import_remote_ics: + with mock.patch( + 'chrono.agendas.models.Desk.import_timeperiod_exceptions_from_ics_file' + ) as import_file_ics: + call_command('sync_desks_timeperiod_exceptions') + assert import_remote_ics.call_args_list == [] + assert import_file_ics.call_args_list == [] + def test_base_meeting_duration(): agenda = Agenda(label='Meeting', kind='meetings') diff --git a/tests/test_manager.py b/tests/test_manager.py index 9f67b3f7..50e3748d 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import copy import json +import os from django.contrib.auth.models import User, Group from django.utils.encoding import force_text @@ -1266,6 +1267,7 @@ END:VCALENDAR""" exception = TimePeriodException.objects.latest('pk') assert exception.source == source assert source.ics_filename == 'exceptions.ics' + assert 'exceptions.ics' in source.ics_file.name assert source.ics_url is None resp = resp.follow() assert 'An exception has been imported.' in resp.text @@ -1338,6 +1340,7 @@ END:VCALENDAR""" exception = TimePeriodException.objects.latest('pk') assert exception.source == source assert source.ics_filename is None + assert source.ics_file.name == '' assert source.ics_url == 'http://example.com/foo.ics' @@ -1555,14 +1558,18 @@ END:VCALENDAR""" source = TimePeriodExceptionSource.objects.latest('pk') assert source.timeperiodexception_set.count() == 2 exceptions = list(source.timeperiodexception_set.order_by('pk')) + old_ics_file_path = source.ics_file.path # 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.form['ics_newfile'] = Upload('exceptions.ics', ics_file_content, 'text/calendar') resp = resp.form.submit().follow() + source.refresh_from_db() + assert source.ics_file.path != old_ics_file_path + assert os.path.exists(old_ics_file_path) is False assert TimePeriodException.objects.count() == 2 assert source.timeperiodexception_set.count() == 2 new_exceptions = list(source.timeperiodexception_set.order_by('pk'))