From 9a05b367f310a613482104c4602b3de7b98905c9 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Mon, 26 Oct 2020 17:15:47 +0100
Subject: [PATCH] agendas: handle import/export of unavailability calendars
 (#47394)

---
 chrono/agendas/models.py    | 40 +++++++++++++++++++++++
 chrono/manager/utils.py     | 29 +++++++++++------
 tests/test_import_export.py | 65 +++++++++++++++++++++++++++++++++++++
 3 files changed, 124 insertions(+), 10 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 3c81830..aba5007 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1147,6 +1147,7 @@ class Desk(models.Model):
         timeperiods = data.pop('timeperiods', [])
         exceptions = data.pop('exceptions', [])
         sources = data.pop('exception_sources', [])
+        unavailability_calendars = data.pop('unavailability_calendars', [])
         data = clean_import_data(cls, data)
         desk, created = cls.objects.update_or_create(slug=data['slug'], agenda=data['agenda'], defaults=data)
         for timeperiod in timeperiods:
@@ -1158,6 +1159,14 @@ class Desk(models.Model):
         for source in sources:
             source['desk'] = desk
             TimePeriodExceptionSource.import_json(source)
+        for unavailability_calendar in unavailability_calendars:
+            try:
+                slug = unavailability_calendar['slug']
+                target_calendar = UnavailabilityCalendar.objects.get(slug=slug)
+                desk.unavailability_calendars.add(target_calendar)
+            except UnavailabilityCalendar.DoesNotExist:
+                raise AgendaImportError(_('The unavailability calendar "%s" does not exist.') % slug)
+
         return desk
 
     def export_json(self):
@@ -1169,6 +1178,7 @@ class Desk(models.Model):
             'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.filter()],
             'exceptions': [exception.export_json() for exception in time_period_exceptions],
             'exception_sources': [source.export_json() for source in time_period_exception_sources],
+            'unavailability_calendars': [{'slug': x.slug} for x in self.unavailability_calendars.all()],
         }
 
     def duplicate(self, label=None, agenda_target=None):
@@ -1562,6 +1572,36 @@ class UnavailabilityCalendar(models.Model):
     def get_absolute_url(self):
         return reverse('chrono-manager-unavailability-calendar-view', kwargs={'pk': self.id})
 
+    def export_json(self):
+        unavailability_calendar = {
+            'label': self.label,
+            'slug': self.slug,
+            'permissions': {
+                'view': self.view_role.name if self.view_role else None,
+                'edit': self.edit_role.name if self.edit_role else None,
+            },
+            'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()],
+        }
+        return unavailability_calendar
+
+    @classmethod
+    def import_json(cls, data, overwrite=False):
+        data = data.copy()
+        permissions = data.pop('permissions', {})
+        exceptions = data.pop('exceptions', [])
+        for permission in ('view', 'edit'):
+            if permissions.get(permission):
+                data[permission + '_role'] = Group.objects.get(name=permissions[permission])
+        data = clean_import_data(cls, data)
+        unavailability_calendar, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
+        if overwrite:
+            TimePeriodException.objects.filter(unavailability_calendar=unavailability_calendar).delete()
+        for exception in exceptions:
+            exception['unavailability_calendar'] = unavailability_calendar
+            TimePeriodException.import_json(exception)
+
+        return created
+
 
 class TimePeriodException(models.Model):
     desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True)
diff --git a/chrono/manager/utils.py b/chrono/manager/utils.py
index 36fa663..c69ec64 100644
--- a/chrono/manager/utils.py
+++ b/chrono/manager/utils.py
@@ -14,18 +14,20 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import collections
 import itertools
 
 from django.contrib.auth.models import Group
 from django.db import transaction
 from django.db.models import Q
 
-from chrono.agendas.models import Agenda, AgendaImportError
+from chrono.agendas.models import Agenda, AgendaImportError, UnavailabilityCalendar
 
 
 def export_site():
     '''Dump site objects to JSON-dumpable dictionnary'''
-    data = {}
+    data = collections.OrderedDict()
+    data['unavailability_calendars'] = [x.export_json() for x in UnavailabilityCalendar.objects.all()]
     qs1 = Agenda.objects.filter(~Q(kind='virtual'))
     qs2 = Agenda.objects.filter(kind='virtual')
     data['agendas'] = [x.export_json() for x in itertools.chain(qs1, qs2)]
@@ -33,16 +35,22 @@ def export_site():
 
 
 def import_site(data, if_empty=False, clean=False, overwrite=False):
-    if if_empty and Agenda.objects.count():
+    if if_empty and (Agenda.objects.count() or UnavailabilityCalendar.objects.count()):
         return
 
     if clean:
         Agenda.objects.all().delete()
+        UnavailabilityCalendar.objects.all().count()
 
     results = {'created': 0, 'updated': 0}
     agendas = data.get('agendas', [])
+    unavailability_calendars = data.get('unavailability_calendars', [])
 
-    role_names = {name for data in agendas for _, name in data.get('permissions', {}).items() if name}
+    role_names = set()
+    for objs in (agendas, unavailability_calendars):
+        role_names = role_names.union(
+            {name for data in agendas for _, name in data.get('permissions', {}).items() if name}
+        )
     existing_roles = Group.objects.filter(name__in=role_names)
 
     if existing_roles.count() != len(role_names):
@@ -50,10 +58,11 @@ def import_site(data, if_empty=False, clean=False, overwrite=False):
         raise AgendaImportError('Missing roles: "%s"' % ', '.join(role_names - existing_roles_names))
 
     with transaction.atomic():
-        for data in agendas:
-            created = Agenda.import_json(data, overwrite=overwrite)
-            if created:
-                results['created'] += 1
-            else:
-                results['updated'] += 1
+        for objs, cls in ((unavailability_calendars, UnavailabilityCalendar), (agendas, Agenda)):
+            for data in objs:
+                created = cls.import_json(data, overwrite=overwrite)
+                if created:
+                    results['created'] += 1
+                else:
+                    results['updated'] += 1
     return results
diff --git a/tests/test_import_export.py b/tests/test_import_export.py
index 643753c..0c1239f 100644
--- a/tests/test_import_export.py
+++ b/tests/test_import_export.py
@@ -32,6 +32,7 @@ from chrono.agendas.models import (
     VirtualMember,
     AgendaNotificationsSettings,
     AgendaReminderSettings,
+    UnavailabilityCalendar,
 )
 from chrono.manager.utils import import_site
 
@@ -565,3 +566,67 @@ def test_import_export_time_period_exception_source():
     source = desk.timeperiodexceptionsource_set.first()
     assert not source.enabled
     assert not desk.timeperiodexception_set.exists()
+
+
+def test_import_export_unavailability_calendar(app):
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    assert len(payload['unavailability_calendars']) == 0
+
+    group1 = Group.objects.create(name=u'gé1')
+    group2 = Group.objects.create(name=u'gé2')
+    calendar = UnavailabilityCalendar.objects.create(label='Calendar', view_role=group1, edit_role=group2)
+    tp1_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0))
+    tp1_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30))
+    tp1 = TimePeriodException.objects.create(
+        unavailability_calendar=calendar, start_datetime=tp1_start, end_datetime=tp1_end
+    )
+    tp2_start = make_aware(datetime.datetime(2018, 5, 22, 8, 0))
+    tp2_end = make_aware(datetime.datetime(2018, 5, 22, 12, 30))
+    tp2 = TimePeriodException.objects.create(
+        unavailability_calendar=calendar, start_datetime=tp2_start, end_datetime=tp2_end
+    )
+    meetings_agenda = Agenda.objects.create(label='Foo Bar', kind='meetings')
+    MeetingType.objects.create(agenda=meetings_agenda, label='Meeting Type', duration=30)
+    desk = Desk.objects.create(agenda=meetings_agenda, label='Desk')
+    desk.unavailability_calendars.add(calendar)
+
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    assert len(payload['unavailability_calendars']) == 1
+    assert len(payload['agendas']) == 1
+
+    calendar.delete()
+    tp1.delete()
+    tp2.delete()
+    meetings_agenda.delete()
+    assert not UnavailabilityCalendar.objects.exists()
+    assert not TimePeriodException.objects.exists()
+    assert not Agenda.objects.exists()
+    assert not Desk.objects.exists()
+
+    import_site(copy.deepcopy(payload))
+    assert UnavailabilityCalendar.objects.count() == 1
+    calendar = UnavailabilityCalendar.objects.first()
+    assert calendar.label == 'Calendar'
+    assert calendar.view_role == group1
+    assert calendar.edit_role == group2
+    assert calendar.timeperiodexception_set.count() == 2
+    assert TimePeriodException.objects.get(
+        unavailability_calendar=calendar, start_datetime=tp1_start, end_datetime=tp1_end
+    )
+    assert TimePeriodException.objects.get(
+        unavailability_calendar=calendar, start_datetime=tp2_start, end_datetime=tp2_end
+    )
+
+    agenda = Agenda.objects.get(label='Foo Bar')
+    desk = agenda.desk_set.first()
+    assert desk.unavailability_calendars.count() == 1
+    assert desk.unavailability_calendars.first() == calendar
+
+    # update
+    update_payload = copy.deepcopy(payload)
+    update_payload['unavailability_calendars'][0]['label'] = 'Calendar Updated'
+    import_site(update_payload)
+    calendar.refresh_from_db()
+    assert calendar.label == 'Calendar Updated'
-- 
2.20.1

