From 94d1c510fa36dffc263ec60b7bcf22fb07733e49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 12 Dec 2019 10:56:14 +0100
Subject: [PATCH 4/4] agendas: refresh an exception source (#29209)

---
 chrono/manager/forms.py                       | 13 +++
 .../chrono/manager_import_exceptions.html     |  8 +-
 .../chrono/manager_replace_exceptions.html    | 28 ++++++
 chrono/manager/urls.py                        | 10 ++
 chrono/manager/views.py                       | 52 +++++++++++
 tests/test_manager.py                         | 91 +++++++++++++++++++
 6 files changed, 199 insertions(+), 3 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_replace_exceptions.html

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 82abc8e..9eaa2e8 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -34,6 +34,7 @@ from chrono.agendas.models import (
     MeetingType,
     TimePeriod,
     TimePeriodException,
+    TimePeriodExceptionSource,
 )
 
 from . import widgets
@@ -282,5 +283,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 a635827..dda92e3 100644
--- a/chrono/manager/templates/chrono/manager_import_exceptions.html
+++ b/chrono/manager/templates/chrono/manager_import_exceptions.html
@@ -31,9 +31,11 @@
           </span>
         </td>
         <td>
-          <a rel="popup" href="">
-            {% if object.ics_filename %}{% trans "replace" %}{% else %}{% trans "refresh" %}{% endif %}
-          </a>
+          {% if object.ics_filename %}
+            <a rel="popup" href="{% url 'chrono-manager-time-period-exception-source-replace' object.pk %}">{% trans "replace" %}</a>
+          {% else %}
+            <a href="{% url 'chrono-manager-time-period-exception-source-refresh' object.pk %}">{% trans "refresh" %}</a>
+          {% endif %}
         </td>
         <td><a rel="popup" href="{% url 'chrono-manager-time-period-exception-source-delete' object.pk %}">{% trans "remove" %}</a></td>
       </tr>
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 0000000..31fe4e5
--- /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 %}
+<h2>{% if form.instance.ics_filename %}{% trans "Replace exceptions" %}{% else %}{% trans "Refresh exceptions" %}{% endif %}</h2>
+{% endblock %}
+
+{% block content %}
+<form method="post" enctype="multipart/form-data">
+  {% if form.instance.ics_filename %}
+    <p class="notice">{% trans "To replace existing exceptions, please upload a new file." %}</p>
+  {% else %}
+  <p class="notice">
+    {% trans 'Press the button "Refresh" to refresh existing exceptions from:' %}
+    <br />
+    <a href="{{ form.instance.ics_url }}">{{ form.instance.ics_url }}</a>
+  </p>
+  {% endif %}
+  {% csrf_token %}
+  {{ form.as_p }}
+  <p>
+  </p>
+  <div class="buttons">
+    <button>{% if form.instance.ics_filename %}{% trans "Replace" %}{% else %}{% trans "Refresh" %}{% endif %}</button>
+    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
+  </div>
+</form>
+{% endblock %}
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index 5ccd361..a163f0a 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<pk>\d+)/refresh$',
+        views.time_period_exception_source_refresh,
+        name='chrono-manager-time-period-exception-source-refresh',
+    ),
+    url(
+        r'^time-period-exceptions-source/(?P<pk>\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 c0ac321..56295fa 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -68,6 +68,7 @@ from .forms import (
     NewMeetingTypeForm,
     TimePeriodAddForm,
     TimePeriodExceptionForm,
+    TimePeriodExceptionSourceReplaceForm,
     TimePeriodForm,
 )
 from .utils import import_site
@@ -926,6 +927,57 @@ 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 ba82b32..83b017c 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1471,6 +1471,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')
-- 
2.20.1

