From 4beae1428cad997064efaae3a888d2808143bfb0 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                       | 17 +++-
 .../chrono/manager_import_exceptions.html     |  2 +-
 .../chrono/manager_refresh_exceptions.html    | 28 +++++++
 chrono/manager/urls.py                        |  2 +
 chrono/manager/views.py                       | 33 +++++++-
 tests/test_manager.py                         | 83 +++++++++++++++++++
 6 files changed, 162 insertions(+), 3 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_refresh_exceptions.html

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 5865d50..d9a30ff 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -27,7 +27,7 @@ from django.utils.timezone import make_aware
 from django.utils.translation import ugettext_lazy as _
 
 from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, Desk,
-                                   TimePeriodException, WEEKDAYS_LIST)
+                                   TimePeriodException, TimePeriodExceptionSource, WEEKDAYS_LIST)
 
 from . import widgets
 
@@ -263,5 +263,20 @@ class ExceptionsImportForm(forms.ModelForm):
             raise forms.ValidationError(_('Please provide an ICS File or an URL.'))
 
 
+class TimePeriodExceptionSourceRefreshForm(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 = []
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        if self.instance.ics_filename is None:
+            del self.fields['ics_file']
+
+
 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..4b72e21 100644
--- a/chrono/manager/templates/chrono/manager_import_exceptions.html
+++ b/chrono/manager/templates/chrono/manager_import_exceptions.html
@@ -31,7 +31,7 @@
           </span>
         </td>
         <td>
-          <a rel="popup" href="">
+          <a rel="popup" href="{% url 'chrono-manager-time-period-exception-source-refresh' object.pk %}">
             {% if object.ics_filename %}{% trans "replace" %}{% else %}{% trans "refresh" %}{% endif %}
           </a>
         </td>
diff --git a/chrono/manager/templates/chrono/manager_refresh_exceptions.html b/chrono/manager/templates/chrono/manager_refresh_exceptions.html
new file mode 100644
index 0000000..31fe4e5
--- /dev/null
+++ b/chrono/manager/templates/chrono/manager_refresh_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 831ff2f..0b35414 100644
--- a/chrono/manager/urls.py
+++ b/chrono/manager/urls.py
@@ -82,6 +82,8 @@ urlpatterns = [
             name='chrono-manager-time-period-exception-list'),
         url(r'^time-period-exceptions-source/(?P<pk>\d+)/delete$', 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'^agendas/events.csv$', views.agenda_import_events_sample_csv,
             name='chrono-manager-sample-events-csv'),
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 8cce482..46cf031 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -38,7 +38,7 @@ from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
 
 from .forms import (AgendaAddForm, AgendaEditForm, NewEventForm, EventForm, NewMeetingTypeForm, MeetingTypeForm,
                     TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm,
-                    ExceptionsImportForm, AgendasImportForm, TimePeriodAddForm)
+                    ExceptionsImportForm, TimePeriodExceptionSourceRefreshForm, AgendasImportForm, TimePeriodAddForm)
 from .utils import import_site
 
 
@@ -840,6 +840,37 @@ class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView)
 time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view()
 
 
+class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, UpdateView):
+    model = TimePeriodExceptionSource
+    form_class = TimePeriodExceptionSourceRefreshForm
+    template_name = 'chrono/manager_refresh_exceptions.html'
+
+    def form_valid(self, form):
+        exceptions = None
+        try:
+            if form.instance.ics_filename is not None:
+                exceptions = form.instance.desk.import_timeperiod_exceptions_from_ics_file(
+                    form.cleaned_data['ics_file'],
+                    source=form.instance)
+            else:
+                exceptions = form.instance.desk.import_timeperiod_exceptions_from_remote_ics(
+                    form.instance.ics_url,
+                    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(TimePeriodExceptionSourceRefreshView, self).form_valid(form)
+
+
+time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
+
+
 def menu_json(request):
     label = _('Agendas')
     json_str = json.dumps([{'label': force_text(label),
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 32474a9..da73803 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1470,6 +1470,89 @@ END:VCALENDAR"""
     assert TimePeriodExceptionSource.objects.filter(pk=source1.pk).exists() is False
 
 
+@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_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
+    source1 = TimePeriodExceptionSource.objects.latest('pk')
+    assert source1.timeperiodexception_set.count() == 2
+    exceptions1 = list(source1.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')
+    resp = resp.click(href='/manage/time-period-exceptions-source/%d/refresh' % source1.pk)
+    resp.form['ics_file'] = Upload('exceptions.ics', ics_file_content, 'text/calendar')
+    resp = resp.form.submit().follow()
+    assert TimePeriodException.objects.count() == 2
+    assert source1.timeperiodexception_set.count() == 2
+    new_exceptions1 = list(source1.timeperiodexception_set.order_by('pk'))
+    assert exceptions1[0].pk != new_exceptions1[0].pk
+    assert exceptions1[1].pk != new_exceptions1[1].pk
+
+    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"""
+
+    # 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() == 3
+    source2 = TimePeriodExceptionSource.objects.latest('pk')
+    assert source2.timeperiodexception_set.count() == 1
+    exceptions2 = list(source2.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')
+    resp = resp.click(href='/manage/time-period-exceptions-source/%d/refresh' % source2.pk)
+    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.count() == 3
+    assert source2.timeperiodexception_set.count() == 1
+    new_exceptions2 = list(source2.timeperiodexception_set.order_by('pk'))
+    assert exceptions2[0].pk != new_exceptions2[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

