From 1b20f1ea6a3c5ddd8de032c816a56390493e5c75 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Wed, 4 Nov 2020 17:25:24 +0100
Subject: [PATCH 1/2] manager: warn about overlapping of
 unavailability_calendar over booking (#47393)

---
 chrono/agendas/models.py |  18 +++++--
 chrono/manager/views.py  |  16 ++++++
 tests/test_manager.py    | 110 +++++++++++++++++++++++++++++++++------
 3 files changed, 125 insertions(+), 19 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 52f0a7d..1e87c3a 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1650,13 +1650,23 @@ class TimePeriodException(models.Model):
 
         return exc_repr
 
-    def has_booking_within_time_slot(self):
+    def has_booking_within_time_slot(self, target_desk=None):
         if not (self.start_datetime and self.end_datetime):
             # incomplete time period, can't tell
             return False
-        for event in Event.objects.filter(
-            desk=self.desk, booking__isnull=False, booking__cancellation_datetime__isnull=True
-        ):
+
+        query = Event.objects
+        if self.desk:
+            query = query.filter(desk=self.desk)
+        elif self.unavailability_calendar and not target_desk:
+            query = query.filter(desk__in=self.unavailability_calendar.desks.all())
+        elif target_desk:
+            query = query.filter(desk=target_desk)
+        else:
+            # orphan exception
+            return False
+
+        for event in query.filter(booking__isnull=False, booking__cancellation_datetime__isnull=True,):
             if self.start_datetime <= event.start_datetime < self.end_datetime:
                 return True
             if event.meeting_type:
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 74e4dd9..f9a71e3 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1691,6 +1691,7 @@ class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
 
     def get(self, request, *args, **kwargs):
         unavailability_calendar = self.get_object()
+        activate = False
         try:
             self.desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
             self.desk.unavailability_calendars.remove(unavailability_calendar)
@@ -1699,6 +1700,7 @@ class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
             )
 
         except UnavailabilityCalendar.DoesNotExist:
+            activate = True
             self.desk.unavailability_calendars.add(unavailability_calendar)
             message = _(
                 'Unavailability calendar %(unavailability_calendar)s has been enabled on desk %(desk)s.'
@@ -1706,6 +1708,13 @@ class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
         messages.info(
             self.request, message % {'unavailability_calendar': unavailability_calendar, 'desk': self.desk}
         )
+        if activate:
+            for exception in unavailability_calendar.timeperiodexception_set.all():
+                if exception.has_booking_within_time_slot(target_desk=self.desk):
+                    message = _('One or several bookings overlap with exceptions.')
+                    messages.warning(self.request, message)
+                    break
+
         return HttpResponseRedirect(
             reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
         )
@@ -2460,6 +2469,13 @@ class UnavailabilityCalendarAddUnavailabilityView(ManagedUnavailabilityCalendarM
         )
         return context
 
+    def form_valid(self, form):
+        result = super().form_valid(form)
+        messages.info(self.request, _('Unavailability added.'))
+        if self.object.has_booking_within_time_slot():
+            messages.warning(self.request, _('One or several bookings exists within this time slot.'))
+        return result
+
 
 unavailability_calendar_add_unavailability = UnavailabilityCalendarAddUnavailabilityView.as_view()
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 375df57..5f0901f 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -2231,30 +2231,49 @@ def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists
 def test_meetings_agenda_edit_time_period_exception(app, admin_user):
     agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
     desk = Desk.objects.create(agenda=agenda, label='Desk A')
-    time_period_exception = TimePeriodException.objects.create(
+    calendar = UnavailabilityCalendar.objects.create(label='foo')
+    calendar.desks.add(desk)
+    time_period_exception_desk = TimePeriodException.objects.create(
         label='Exception',
         desk=desk,
         start_datetime=make_aware(datetime.datetime(2018, 12, 16, 5, 0)),
         end_datetime=make_aware(datetime.datetime(2018, 12, 16, 23, 0)),
     )
-    event = Event.objects.create(
-        agenda=agenda, places=1, desk=desk, start_datetime=make_aware(datetime.datetime(2018, 12, 16, 10, 30))
+    time_period_exception_calendar = TimePeriodException.objects.create(
+        label='Exception',
+        unavailability_calendar=calendar,
+        start_datetime=make_aware(datetime.datetime(2018, 12, 16, 5, 0)),
+        end_datetime=make_aware(datetime.datetime(2018, 12, 16, 23, 0)),
     )
+    for time_period_exception in (time_period_exception_desk, time_period_exception_calendar):
+        event = Event.objects.create(
+            agenda=agenda,
+            places=1,
+            desk=desk,
+            start_datetime=make_aware(datetime.datetime(2018, 12, 16, 10, 30)),
+        )
 
-    login(app)
-    resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
-    resp = resp.form.submit().follow()
-    assert 'Exception updated. Note: one or several bookings exists within this time slot.' not in resp.text
+        login(app)
+        resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
+        resp = resp.form.submit().follow()
+        assert (
+            'Exception updated. Note: one or several bookings exists within this time slot.' not in resp.text
+        )
 
-    booking = Booking.objects.create(event=event)
-    resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
-    resp = resp.form.submit().follow()
-    assert 'Exception updated. Note: one or several bookings exists within this time slot.' in resp.text
+        booking = Booking.objects.create(event=event)
+        resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
+        resp = resp.form.submit().follow()
+        assert 'Exception updated. Note: one or several bookings exists within this time slot.' in resp.text
 
-    booking.cancel()
-    resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
-    resp = resp.form.submit().follow()
-    assert 'Exception updated. Note: one or several bookings exists within this time slot.' not in resp.text
+        booking.cancel()
+        resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
+        resp = resp.form.submit().follow()
+        assert (
+            'Exception updated. Note: one or several bookings exists within this time slot.' not in resp.text
+        )
+
+        booking.delete()
+        event.delete()
 
 
 def test_meetings_agenda_add_invalid_time_period_exception():
@@ -4835,12 +4854,41 @@ def test_unavailability_calendar_add_time_period_exeptions(app, admin_user):
     resp.form['end_datetime_1'] = '16:00'
     resp = resp.form.submit().follow()
     assert 'Exception 1' in resp.text
+    assert 'Unavailability added.' in resp.text
+    assert 'One or several bookings exists within this time slot.' not in resp.text
+    assert TimePeriodException.objects.count() == 1
 
     time_period_exception = TimePeriodException.objects.first()
     assert time_period_exception.unavailability_calendar == unavailability_calendar
     assert time_period_exception.desk is None
     assert time_period_exception.label == 'Exception 1'
 
+    # info message if new exception overlaps with a booking
+    agenda = Agenda.objects.create(label='Agenda')
+    desk = Desk.objects.create(label='Desk', agenda=agenda)
+    unavailability_calendar.desks.add(desk)
+    meeting_type = MeetingType.objects.create(label='Meeting Type', agenda=agenda)
+    event = Event.objects.create(
+        agenda=agenda,
+        places=1,
+        desk=desk,
+        meeting_type=meeting_type,
+        start_datetime=tomorrow.replace(hour=10, minute=30, second=0),
+    )
+    Booking.objects.create(event=event)
+    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
+    resp = resp.click('Add Unavailability')
+    resp.form['label'] = 'Exception 2'
+    resp.form['start_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
+    resp.form['start_datetime_1'] = '08:00'
+    resp.form['end_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
+    resp.form['end_datetime_1'] = '16:00'
+    resp = resp.form.submit().follow()
+    assert TimePeriodException.objects.count() == 2
+    assert 'Exception 2' in resp.text
+    assert 'Unavailability added.' in resp.text
+    assert 'One or several bookings exists within this time slot.' in resp.text
+
 
 def test_unavailability_calendar_edit_time_period_exeptions(app, admin_user):
     unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
@@ -4912,11 +4960,43 @@ def test_activate_unavailability_calendar_in_desk(app, admin_user):
     # exception from calendar displayed on the settigns page
     resp = app.get(settings_url)
     assert 'what' in resp.text
+    assert 'One or several bookings overlap with exceptions.' not in resp.text
+
     resp = app.get(exceptions_url)
     assert 'calendar' in resp.text
     assert 'disable' in resp.text
     assert desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
 
+    # reset
+    unavailability_calendar.desks.remove(desk)
+
+    # info message if some exceptions overlaps with a booking
+    today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
+    tomorrow = make_aware(today + datetime.timedelta(days=1))
+    TimePeriodException.objects.create(
+        start_datetime=tomorrow,
+        end_datetime=tomorrow.replace(hour=15, minute=30, second=0),
+        unavailability_calendar=unavailability_calendar,
+    )
+    meeting_type = MeetingType.objects.create(label='Meeting Type', agenda=agenda)
+    event = Event.objects.create(
+        agenda=agenda,
+        places=1,
+        desk=desk,
+        meeting_type=meeting_type,
+        start_datetime=tomorrow.replace(hour=10, minute=30, second=0),
+    )
+    Booking.objects.create(event=event)
+    resp = app.get(exceptions_url)
+    assert 'calendar' in resp.text
+    assert 'enable' in resp.text
+    resp = resp.click(
+        href='/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk)
+    )
+    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
+    resp = resp.follow()
+    assert 'One or several bookings overlap with exceptions.' in resp.text
+
 
 def test_deactivate_unavailability_calendar_in_desk(app, admin_user):
     app = login(app)
-- 
2.20.1

