From db6c9943fd15353b9cb29a086aeba0a10df1fcbf Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Tue, 17 Mar 2020 13:11:49 +0100
Subject: [PATCH] virtual agendas: handle import/export (#40057)

---
 chrono/agendas/models.py    | 23 +++++++++
 chrono/manager/utils.py     |  7 ++-
 tests/test_import_export.py | 99 ++++++++++++++++++++++++++++++++++++-
 3 files changed, 127 insertions(+), 2 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index a8e6177..6615781 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -216,6 +216,9 @@ class Agenda(models.Model):
         elif self.kind == 'meetings':
             agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()]
             agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
+        elif self.kind == 'virtual':
+            agenda['excluded_timeperiods'] = [x.export_json() for x in self.excluded_timeperiods.all()]
+            agenda['real_agendas'] = [{'slug': x.slug, 'kind': x.kind} for x in self.real_agendas.all()]
         return agenda
 
     @classmethod
@@ -227,6 +230,9 @@ class Agenda(models.Model):
         elif data['kind'] == 'meetings':
             meetingtypes = data.pop('meetingtypes')
             desks = data.pop('desks')
+        elif data['kind'] == 'virtual':
+            excluded_timeperiods = data.pop('excluded_timeperiods')
+            real_agendas = data.pop('real_agendas')
         for permission in ('view', 'edit'):
             if permissions.get(permission):
                 try:
@@ -253,6 +259,23 @@ class Agenda(models.Model):
             for desk in desks:
                 desk['agenda'] = agenda
                 Desk.import_json(desk).save()
+        elif data['kind'] == 'virtual':
+            if overwrite:
+                TimePeriod.objects.filter(agenda=agenda).delete()
+                VirtualMember.objects.filter(virtual_agenda=agenda).delete()
+            for excluded_timeperiod in excluded_timeperiods:
+                excluded_timeperiod['agenda'] = agenda
+                TimePeriod.import_json(excluded_timeperiod).save()
+            for real_agenda in real_agendas:
+                real_agenda = Agenda.objects.get(slug=real_agenda['slug'], kind=real_agenda['kind'])
+                try:
+                    vm, created = VirtualMember.objects.get_or_create(
+                        virtual_agenda=agenda, real_agenda=real_agenda
+                    )
+                    vm.clean()
+                except ValidationError as e:
+                    raise AgendaImportError(e.message)
+
         return created
 
 
diff --git a/chrono/manager/utils.py b/chrono/manager/utils.py
index 20ad2e0..82bc981 100644
--- a/chrono/manager/utils.py
+++ b/chrono/manager/utils.py
@@ -14,7 +14,10 @@
 # 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 itertools
+
 from django.db import transaction
+from django.db.models import Q
 
 from chrono.agendas.models import Agenda, AgendaImportError
 
@@ -22,7 +25,9 @@ from chrono.agendas.models import Agenda, AgendaImportError
 def export_site():
     '''Dump site objects to JSON-dumpable dictionnary'''
     data = {}
-    data['agendas'] = [x.export_json() for x in Agenda.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)]
     return data
 
 
diff --git a/tests/test_import_export.py b/tests/test_import_export.py
index 8190577..3328188 100644
--- a/tests/test_import_export.py
+++ b/tests/test_import_export.py
@@ -16,7 +16,16 @@ from django.core.management import call_command, CommandError
 from django.utils.encoding import force_bytes
 from django.utils.timezone import make_aware
 
-from chrono.agendas.models import Agenda, Event, TimePeriod, Desk, TimePeriodException, AgendaImportError
+from chrono.agendas.models import (
+    Agenda,
+    Event,
+    TimePeriod,
+    Desk,
+    TimePeriodException,
+    AgendaImportError,
+    MeetingType,
+    VirtualMember,
+)
 from chrono.manager.utils import import_site
 
 from test_api import some_data, meetings_agenda, time_zone, mock_now
@@ -179,3 +188,91 @@ def test_import_export_permissions(app, some_data, meetings_agenda):
     agenda = Agenda.objects.get(slug=meetings_agenda.slug)
     assert agenda.view_role == group1
     assert agenda.edit_role == group2
+
+
+def test_import_export_virtual_agenda(app):
+    virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    assert len(payload['agendas']) == 1
+    virtual_agenda.delete()
+    import_site(json.loads(output), overwrite=True)
+
+    virtual_agenda = Agenda.objects.get(label='Virtual Agenda', slug='virtual-agenda', kind='virtual')
+    assert virtual_agenda.minimal_booking_delay is None
+    assert virtual_agenda.maximal_booking_delay is None
+    assert virtual_agenda.real_agendas.count() == 0
+    assert virtual_agenda.excluded_timeperiods.count() == 0
+
+    # add booking delay
+    virtual_agenda.minimal_booking_delay = 2
+    virtual_agenda.maximal_booking_delay = 10
+    virtual_agenda.save()
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    virtual_agenda.delete()
+    import_site(json.loads(output), overwrite=True)
+    virtual_agenda = Agenda.objects.get(label='Virtual Agenda', slug='virtual-agenda', kind='virtual')
+    assert virtual_agenda.minimal_booking_delay == 2
+    assert virtual_agenda.maximal_booking_delay == 10
+
+    # add excluded timeperiods
+    tp1 = TimePeriod.objects.create(
+        agenda=virtual_agenda, weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
+    )
+    tp2 = TimePeriod.objects.create(
+        agenda=virtual_agenda, weekday=2, start_time=datetime.time(12, 0), end_time=datetime.time(13, 0)
+    )
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    virtual_agenda.delete()
+    tp1.delete()
+    tp2.delete()
+    import_site(json.loads(output), overwrite=True)
+    virtual_agenda = Agenda.objects.get(label='Virtual Agenda', slug='virtual-agenda', kind='virtual')
+    assert virtual_agenda.excluded_timeperiods.count() == 2
+    tp1 = TimePeriod.objects.get(
+        agenda=virtual_agenda, weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
+    )
+    tp2 = TimePeriod.objects.get(
+        agenda=virtual_agenda, weekday=2, start_time=datetime.time(12, 0), end_time=datetime.time(13, 0)
+    )
+
+
+def test_import_export_virtual_agenda_with_included_agenda(app):
+    virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
+    foo_agenda = Agenda.objects.create(label='Foo', kind='meetings')
+    bar_agenda = Agenda.objects.create(label='Bar', kind='meetings')
+    mt1 = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
+    mt2 = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
+    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=foo_agenda)
+    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=bar_agenda)
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    assert len(payload['agendas']) == 3
+    virtual_agenda.delete()
+    foo_agenda.delete()
+    bar_agenda.delete()
+    mt1.delete()
+    mt2.delete()
+    import_site(payload, overwrite=True)
+
+    virtual_agenda = Agenda.objects.get(label='Virtual Agenda', slug='virtual-agenda', kind='virtual')
+    assert virtual_agenda.real_agendas.count() == 2
+    assert virtual_agenda.real_agendas.filter(label='Foo').count() == 1
+    assert virtual_agenda.real_agendas.filter(label='Bar').count() == 1
+
+    # add incompatible meetingtype
+    bar_agenda = Agenda.objects.get(label='Bar', kind='meetings')
+    mt2 = MeetingType.objects.get(agenda=bar_agenda, label='Meeting Type')
+    mt2.duration = 10
+    mt2.save()
+    output = get_output_of_command('export_site')
+    payload = json.loads(output)
+    virtual_agenda.delete()
+    with pytest.raises(AgendaImportError) as excinfo:
+        import_site(json.loads(output), overwrite=False)
+    assert (
+        '%s' % excinfo.value
+        == 'This agenda does not have the same meetingtypes provided by the virtual agenda.'
+    )
-- 
2.20.1

