From 295452a949c144dc2c0bd4f54296db0427ddc01e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 26 Jan 2021 09:58:28 +0100
Subject: [PATCH 04/11] agendas: is an agenda available for desk simple
 management (#48924)

---
 chrono/agendas/models.py |  47 +++++++++++
 tests/test_agendas.py    | 175 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 222 insertions(+)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 2b86133..698a8ec 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -560,6 +560,53 @@ class Agenda(models.Model):
                 if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
             ]
 
+    def is_available_for_simple_management(self):
+        if self.kind != 'meetings':
+            return False
+
+        desks = self.desk_set.all()
+        if len(desks) < 2:
+            # no desk or just one, it's ok
+            return True
+
+        desk = desks[0]
+
+        period_fields = ['weekday', 'start_time', 'end_time']
+        exception_fields = ['label', 'start_datetime', 'end_datetime']
+        source_fields = ['ics_filename', 'ics_url', 'settings_slug', 'enabled']
+        desk_time_periods = set(desk.timeperiod_set.values_list(*period_fields))
+        desk_exceptions = set(
+            desk.timeperiodexception_set.filter(source__isnull=True).values_list(*exception_fields)
+        )
+        desk_sources = set(desk.timeperiodexceptionsource_set.values_list(*source_fields))
+        desk_unavaibility_calendars = set(desk.unavailability_calendars.values_list('pk', flat=True))
+        for other_desk in desks[1:]:
+            # compare time periods
+            other_desk_time_periods = set(other_desk.timeperiod_set.values_list(*period_fields))
+            if desk_time_periods != other_desk_time_periods:
+                return False
+
+            # compare exceptions
+            other_desk_exceptions = set(
+                other_desk.timeperiodexception_set.filter(source__isnull=True).values_list(*exception_fields)
+            )
+            if desk_exceptions != other_desk_exceptions:
+                return False
+
+            # compare sources
+            other_desk_sources = set(other_desk.timeperiodexceptionsource_set.values_list(*source_fields))
+            if desk_sources != other_desk_sources:
+                return False
+
+            # compare unavailability calendars
+            other_desk_unavaibility_calendars = set(
+                other_desk.unavailability_calendars.values_list('pk', flat=True)
+            )
+            if desk_unavaibility_calendars != other_desk_unavaibility_calendars:
+                return False
+
+        return True
+
 
 class VirtualMember(models.Model):
     """Trough model to link virtual agendas to their real agendas.
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index 1d3f979..049d9c5 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -177,6 +177,181 @@ def test_category_duplicate_slugs():
     assert category.slug == 'foo-baz-2'
 
 
+def test_agenda_is_available_for_simple_management(settings):
+    settings.EXCEPTIONS_SOURCES = {
+        'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
+    }
+
+    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
+    # no desks
+    assert agenda.is_available_for_simple_management() is True
+
+    # check kind
+    agenda.kind = 'events'
+    agenda.save()
+    assert agenda.is_available_for_simple_management() is False
+    agenda.kind = 'virtual'
+    agenda.save()
+    assert agenda.is_available_for_simple_management() is False
+
+    # only one desk
+    agenda.kind = 'meetings'
+    agenda.save()
+    desk = Desk.objects.create(label='Desk', agenda=agenda)
+    assert agenda.is_available_for_simple_management() is True
+
+    # create some related data for this desk
+    time_period = TimePeriod.objects.create(
+        weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
+    )
+    desk.import_timeperiod_exceptions_from_settings(enable=True)
+    source1 = desk.timeperiodexceptionsource_set.get(settings_slug='holidays')
+    source2 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
+    source3 = TimePeriodExceptionSource.objects.create(
+        desk=desk,
+        ics_filename='sample.ics',
+        ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
+    )
+    date_now = now()
+    exception = TimePeriodException.objects.create(
+        label='Exception',
+        desk=desk,
+        start_datetime=date_now + datetime.timedelta(days=1),
+        end_datetime=date_now + datetime.timedelta(days=2),
+    )
+    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
+    unavailability_calendar.desks.add(desk)
+    unavailability_calendar2 = UnavailabilityCalendar.objects.create(label='Calendar 2')
+
+    # still ok
+    assert agenda.is_available_for_simple_management() is True
+
+    # duplicate the desk twice
+    desk2 = desk.duplicate()
+    desk.duplicate()
+
+    # still ok
+    assert agenda.is_available_for_simple_management() is True
+
+    # changes on time periods
+    for _desk in [desk, desk2]:
+        time_period2 = TimePeriod.objects.create(
+            weekday=2, desk=_desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
+        )
+        assert agenda.is_available_for_simple_management() is False
+        time_period2.delete()
+        assert agenda.is_available_for_simple_management() is True
+    time_period.weekday = 2
+    time_period.save()
+    assert agenda.is_available_for_simple_management() is False
+    time_period.weekday = 1
+    time_period.start_time = datetime.time(10, 1)
+    time_period.save()
+    assert agenda.is_available_for_simple_management() is False
+    time_period.start_time = datetime.time(10, 0)
+    time_period.end_time = datetime.time(12, 1)
+    time_period.save()
+    assert agenda.is_available_for_simple_management() is False
+    time_period.end_time = datetime.time(12, 0)
+    time_period.save()
+    assert agenda.is_available_for_simple_management() is True
+
+    # changes on exceptions
+    for _desk in [desk, desk2]:
+        exception2 = TimePeriodException.objects.create(
+            label='Exception',
+            desk=_desk,
+            start_datetime=date_now + datetime.timedelta(days=3),
+            end_datetime=date_now + datetime.timedelta(days=4),
+        )
+        assert agenda.is_available_for_simple_management() is False
+        exception2.delete()
+        assert agenda.is_available_for_simple_management() is True
+    exception.label = 'Exception blah'
+    exception.save()
+    assert agenda.is_available_for_simple_management() is False
+    exception.label = 'Exception'
+    exception.start_datetime = date_now + datetime.timedelta(days=3)
+    exception.save()
+    assert agenda.is_available_for_simple_management() is False
+    exception.start_datetime = date_now + datetime.timedelta(days=1)
+    exception.end_datetime = date_now + datetime.timedelta(days=1)
+    exception.save()
+    assert agenda.is_available_for_simple_management() is False
+    exception.end_datetime = date_now + datetime.timedelta(days=2)
+    exception.save()
+    assert agenda.is_available_for_simple_management() is True
+    # exceptions from source are not checked
+    exception3 = TimePeriodException.objects.create(
+        label='Exception',
+        desk=desk,
+        start_datetime=date_now + datetime.timedelta(days=3),
+        end_datetime=date_now + datetime.timedelta(days=4),
+        source=source2,
+    )
+    assert agenda.is_available_for_simple_management() is True
+    exception3.delete()
+
+    # changes on sources - from settings
+    for _desk in [desk, desk2]:
+        source = TimePeriodExceptionSource.objects.create(
+            desk=_desk, settings_slug='holidays-bis', enabled=True
+        )
+        assert agenda.is_available_for_simple_management() is False
+        source.delete()
+        assert agenda.is_available_for_simple_management() is True
+    source1.enabled = False
+    source1.save()
+    assert agenda.is_available_for_simple_management() is False
+    source1.enabled = True
+    source1.settings_slug = 'holidays-bis'
+    source1.save()
+    assert agenda.is_available_for_simple_management() is False
+    source1.settings_slug = 'holidays'
+    source1.save()
+    assert agenda.is_available_for_simple_management() is True
+
+    # changes on sources - from url
+    for _desk in [desk, desk2]:
+        source = TimePeriodExceptionSource.objects.create(
+            desk=_desk, ics_url='http://example.com/sample-bis.ics'
+        )
+        assert agenda.is_available_for_simple_management() is False
+        source.delete()
+        assert agenda.is_available_for_simple_management() is True
+    source2.ics_url = 'http://example.com/sample-bis.ics'
+    source2.save()
+    assert agenda.is_available_for_simple_management() is False
+    source2.ics_url = 'http://example.com/sample.ics'
+    source2.save()
+    assert agenda.is_available_for_simple_management() is True
+
+    # changes on sources - from file
+    for _desk in [desk, desk2]:
+        source = TimePeriodExceptionSource.objects.create(
+            desk=_desk,
+            ics_filename='sample-bis.ics',
+            ics_file=ContentFile(ICS_SAMPLE, name='sample-bis.ics'),
+        )
+        assert agenda.is_available_for_simple_management() is False
+        source.delete()
+        assert agenda.is_available_for_simple_management() is True
+    source3.ics_filename = 'sample-bis.ics'
+    source3.save()
+    assert agenda.is_available_for_simple_management() is False
+    source3.ics_filename = 'sample.ics'
+    source3.save()
+    assert agenda.is_available_for_simple_management() is True
+    # ics_file content is not checked
+
+    # changes on unavailability calendars
+    for _desk in [desk, desk2]:
+        unavailability_calendar2.desks.add(_desk)
+        assert agenda.is_available_for_simple_management() is False
+        unavailability_calendar2.desks.remove(_desk)
+        assert agenda.is_available_for_simple_management() is True
+
+
 def test_event_slug():
     other_agenda = Agenda.objects.create(label='Foo bar')
     Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='foo-bar')
-- 
2.29.2

