From 95474dee79f401392dce2696679836d440c4f896 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Wed, 26 Feb 2020 11:52:05 +0100
Subject: [PATCH 2/3] manager: add excluded timeperiods management (#40058)

---
 chrono/manager/forms.py                       |  9 +++
 .../chrono/manager_time_period_form.html      |  2 +-
 .../manager_virtual_agenda_settings.html      | 16 +++++
 chrono/manager/urls.py                        |  5 ++
 chrono/manager/views.py                       | 69 ++++++++++++++++---
 tests/test_manager.py                         | 65 ++++++++++++++++-
 6 files changed, 154 insertions(+), 12 deletions(-)

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index a5350a7..a5e6a4c 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -138,9 +138,18 @@ class TimePeriodForm(forms.ModelForm):
             'start_time': widgets.TimeWidget(),
             'end_time': widgets.TimeWidget(),
             'desk': forms.HiddenInput(),
+            'agenda': forms.HiddenInput(),
         }
         exclude = []
 
+    def __init__(self, *args, **kwargs):
+        has_desk = kwargs.pop('has_desk')
+        super(TimePeriodForm, self).__init__(*args, **kwargs)
+        if has_desk:
+            del self.fields['agenda']
+        else:
+            del self.fields['desk']
+
     def clean_end_time(self):
         if self.cleaned_data['end_time'] <= self.cleaned_data['start_time']:
             raise ValidationError(_('End time must come after start time.'))
diff --git a/chrono/manager/templates/chrono/manager_time_period_form.html b/chrono/manager/templates/chrono/manager_time_period_form.html
index 7b80933..da2ba2b 100644
--- a/chrono/manager/templates/chrono/manager_time_period_form.html
+++ b/chrono/manager/templates/chrono/manager_time_period_form.html
@@ -33,7 +33,7 @@
   {{ form.as_p }}
   <div class="buttons">
     <button class="submit-button">{% trans "Save" %}</button>
-    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=desk.agenda.id %}">{% trans 'Cancel' %}</a>
+    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
   </div>
 </form>
 {% endblock %}
diff --git a/chrono/manager/templates/chrono/manager_virtual_agenda_settings.html b/chrono/manager/templates/chrono/manager_virtual_agenda_settings.html
index 71ad303..8f0aae9 100644
--- a/chrono/manager/templates/chrono/manager_virtual_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_virtual_agenda_settings.html
@@ -2,6 +2,7 @@
 {% load i18n %}
 
 {% block agenda-extra-management-actions %}
+  <a rel="popup" href="{% url 'chrono-manager-virtual-agenda-add-time-period' pk=object.id %}">{% trans 'Add Excluded Period' %}</a>
   <a rel="popup" href="{% url 'chrono-manager-agenda-add-virtual-member' pk=object.id %}">{% trans 'Include Agenda' %}</a>
 {% endblock %}
 
@@ -60,5 +61,20 @@
 </div>
 {% endif %}
 
+{% if agenda.excluded_timeperiods.count %}
+<div class="section">
+<h3>{% trans 'Excluded Periods' %}</h3>
+<div>
+  <ul class="objects-list single-links">
+    {% for time_period in agenda.excluded_timeperiods.all %}
+    <li><a rel="popup" href="{% url 'chrono-manager-time-period-edit' pk=time_period.id %}">
+        {{time_period.weekday_str}} / {{time_period.start_time}} → {{time_period.end_time}}</a>
+      <a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-delete' pk=time_period.id %}">{% trans "remove" %}</a>
+    </li>
+    {% endfor %}
+  </ul>
+</div>
+</div>
+{% endif %}
 
 {% endblock %}
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index 382a5c1..56b2b9f 100644
--- a/chrono/manager/urls.py
+++ b/chrono/manager/urls.py
@@ -74,6 +74,11 @@ urlpatterns = [
         views.agenda_add_time_period,
         name='chrono-manager-agenda-add-time-period',
     ),
+    url(
+        r'^agendas/(?P<pk>\d+)/add-time-period$',
+        views.virtual_agenda_add_time_period,
+        name='chrono-manager-virtual-agenda-add-time-period',
+    ),
     url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit, name='chrono-manager-time-period-edit'),
     url(
         r'^timeperiods/(?P<pk>\d+)/delete$',
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 2f9acce..0edef97 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -644,6 +644,30 @@ class ManagedDeskSubobjectMixin(object):
         return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
 
 
+class ManagedTimePeriodMixin(object):
+    agenda = None
+
+    def dispatch(self, request, *args, **kwargs):
+        self.time_period = self.get_object()
+        self.agenda = self.time_period.agenda
+        self.has_desk = False
+        if self.time_period.desk:
+            self.agenda = self.time_period.desk.agenda
+            self.has_desk = True
+
+        if not self.agenda.can_be_managed(request.user):
+            raise PermissionDenied()
+        return super(ManagedTimePeriodMixin, self).dispatch(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        context = super(ManagedTimePeriodMixin, self).get_context_data(**kwargs)
+        context['agenda'] = self.agenda
+        return context
+
+    def get_success_url(self):
+        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
+
+
 class AgendaSettings(ManagedAgendaMixin, DetailView):
     model = Agenda
 
@@ -846,35 +870,60 @@ class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
 meeting_type_delete = MeetingTypeDeleteView.as_view()
 
 
+def process_time_period_add_form(form, desk=None, agenda=None):
+    assert desk or agenda, "a time period requires a desk or a agenda"
+    for weekday in form.cleaned_data.get('weekdays'):
+        period = TimePeriod(
+            weekday=weekday,
+            start_time=form.cleaned_data['start_time'],
+            end_time=form.cleaned_data['end_time'],
+        )
+        if desk:
+            period.desk = desk
+        elif agenda:
+            period.agenda = agenda
+        period.save()
+
+
 class AgendaAddTimePeriodView(ManagedDeskMixin, FormView):
     template_name = 'chrono/manager_time_period_form.html'
     form_class = TimePeriodAddForm
 
     def form_valid(self, form):
-        for weekday in form.cleaned_data.get('weekdays'):
-            period = TimePeriod(
-                weekday=weekday,
-                start_time=form.cleaned_data['start_time'],
-                end_time=form.cleaned_data['end_time'],
-                desk=self.desk,
-            )
-            period.save()
+        process_time_period_add_form(form, desk=self.desk)
         return super(AgendaAddTimePeriodView, self).form_valid(form)
 
 
 agenda_add_time_period = AgendaAddTimePeriodView.as_view()
 
 
-class TimePeriodEditView(ManagedDeskSubobjectMixin, UpdateView):
+class VirtualAgendaAddTimePeriodView(ManagedAgendaMixin, FormView):
+    template_name = 'chrono/manager_time_period_form.html'
+    form_class = TimePeriodAddForm
+
+    def form_valid(self, form):
+        process_time_period_add_form(form, agenda=self.agenda)
+        return super(VirtualAgendaAddTimePeriodView, self).form_valid(form)
+
+
+virtual_agenda_add_time_period = VirtualAgendaAddTimePeriodView.as_view()
+
+
+class TimePeriodEditView(ManagedTimePeriodMixin, UpdateView):
     template_name = 'chrono/manager_time_period_form.html'
     model = TimePeriod
     form_class = TimePeriodForm
 
+    def get_form_kwargs(self):
+        kwargs = super(TimePeriodEditView, self).get_form_kwargs()
+        kwargs['has_desk'] = self.has_desk
+        return kwargs
+
 
 time_period_edit = TimePeriodEditView.as_view()
 
 
-class TimePeriodDeleteView(ManagedDeskSubobjectMixin, DeleteView):
+class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView):
     template_name = 'chrono/manager_confirm_delete.html'
     model = TimePeriod
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 7e0bc85..a8d26d4 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -2195,9 +2195,12 @@ def test_virtual_agenda_settings_empty(app, admin_user):
     assert 'Export' in resp.text
     assert 'Delete' in resp.text
     assert 'Included Agendas' in resp.text
+    assert 'Add Excluded Period' in resp.text
     assert "This virtual agenda doesn't include any agenda yet" in resp.text
     # No meeting types yet
     assert 'Meeting Types' not in resp.text
+    # No absence yet
+    assert 'Excluded Periods' not in resp.text
 
 
 def test_virtual_agenda_settings(app, admin_user):
@@ -2208,7 +2211,9 @@ def test_virtual_agenda_settings(app, admin_user):
     VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meeting_agenda_2)
     MeetingType.objects.create(agenda=meeting_agenda_1, label='MT', slug='mt', duration=10)
     mt2 = MeetingType.objects.create(agenda=meeting_agenda_2, label='MT', slug='mt', duration=10)
-
+    TimePeriod.objects.create(
+        agenda=agenda, weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
+    )
     app = login(app)
     resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
     assert "This virtual agenda doesn't include any agenda yet" not in resp.text
@@ -2221,6 +2226,9 @@ def test_virtual_agenda_settings(app, admin_user):
     assert 'mt' in resp.text
     assert '10' in resp.text
 
+    assert 'Excluded Periods' in resp.text
+    assert 'Monday' in resp.text
+
     # Error message when incompatible meeting types
     mt2.delete()
     resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
@@ -2250,6 +2258,61 @@ def test_virtual_agenda_settings_include(app, admin_user):
     assert len(resp.form['real_agenda'].options) == 2
 
 
+def test_virtual_agenda_settings_add_excluded_period(app, admin_user):
+    agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
+
+    app = login(app)
+    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
+    resp = resp.click('Add Excluded Period')
+
+    resp.form['weekdays-0'].checked = True
+    resp.form['start_time'] = '10:00'
+    resp.form['end_time'] = '17:00'
+    resp = resp.form.submit()
+    tp = TimePeriod.objects.get(agenda=agenda)
+    assert tp.weekday == 0
+    assert tp.start_time.hour == 10
+    assert tp.start_time.minute == 0
+    assert tp.end_time.hour == 17
+    assert tp.end_time.minute == 0
+
+    resp = resp.follow()
+    assert u'Monday / 10 a.m. → 5 p.m.' in resp.text
+
+
+def test_virtual_agenda_settings_edit_excluded_period(app, admin_user):
+    agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
+    tp = TimePeriod.objects.create(
+        agenda=agenda, weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
+    )
+    app = login(app)
+    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
+    url = '/manage/timeperiods/%s/edit' % tp.pk
+    resp = resp.click(href=url)
+    resp.form['start_time'] = '11:00'
+    resp = resp.form.submit()
+    tp = TimePeriod.objects.get(agenda=agenda)
+    assert tp.weekday == 0
+    assert tp.start_time.hour == 11
+    assert tp.start_time.minute == 0
+    assert tp.end_time.hour == 18
+    assert tp.end_time.minute == 0
+
+
+def test_virtual_agenda_settings_delete_excluded_period(app, admin_user):
+    agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
+    tp = TimePeriod.objects.create(
+        agenda=agenda, weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
+    )
+    app = login(app)
+    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
+    url = '/manage/timeperiods/%s/delete' % tp.pk
+    resp = resp.click(href=url)
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
+    assert TimePeriod.objects.count() == 0
+
+
 def test_virtual_agenda_settings_include_incompatible_agenda(app, admin_user):
     agenda = Agenda.objects.create(label='My Virtual agenda', kind='virtual')
     meeting_agenda_1 = Agenda.objects.create(label='Meeting agenda 1', kind='meetings')
-- 
2.20.1

