From 4a62c085cdc0776a8b4d0a674f82638fe9ececaf Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sat, 30 May 2020 14:49:58 +0200
Subject: [PATCH 2/2] api: return end_datetime on booking events (#37352)

---
 chrono/agendas/models.py |  6 +--
 chrono/api/views.py      |  5 +++
 tests/test_api.py        | 84 +++++++++++++++++++++++++++++++++++++++-
 3 files changed, 89 insertions(+), 6 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 1304a33..a8b6c6e 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -841,20 +841,18 @@ class Booking(models.Model):
             self.event.agenda.pk,
             self.pk,
         )
 
         vevent.add('summary').value = self.user_display_label or self.label
         vevent.add('dtstart').value = self.event.start_datetime
         if self.user_name:
             vevent.add('attendee').value = self.user_name
-        if self.event.meeting_type:
-            vevent.add('dtend').value = self.event.start_datetime + datetime.timedelta(
-                minutes=self.event.meeting_type.duration
-            )
+        if self.event.end_datetime:
+            vevent.add('dtend').value = self.event.end_datetime
 
         for field in ('description', 'location', 'comment', 'url'):
             field_value = request and request.GET.get(field) or self.extra_data.get(field)
             if field_value:
                 vevent.add(field).value = field_value
         ics.add(vevent)
         return ics.serialize()
 
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 0d6c7e5..a91a046 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -907,22 +907,27 @@ class Fillslots(APIView):
             response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
         if to_cancel_booking:
             response['cancelled_booking_id'] = cancelled_booking_id
         if agenda.kind == 'events' and not multiple_booking:
             event = events[0]
             # event.full is not up to date, it might have been changed by previous new_booking.save().
             event.refresh_from_db()
             response['places'] = get_event_places(event)
+            if event.end_datetime:
+                response['end_datetime'] = format_response_datetime(event.end_datetime)
+            else:
+                response['end_datetime'] = None
         if agenda.kind == 'events' and multiple_booking:
             response['events'] = [
                 {
                     'slug': x.slug,
                     'text': str(x),
                     'datetime': format_response_datetime(x.start_datetime),
+                    'end_datetime': format_response_datetime(x.end_datetime) if x.end_datetime else None,
                     'description': x.description,
                 }
                 for x in events
             ]
 
         return Response(response)
 
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 69d5f99..2c9cf4e 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -67,18 +67,20 @@ def mock_now(request, monkeypatch):
 
 
 @pytest.fixture
 def some_data(time_zone, mock_now):
     agenda = Agenda(label=u'Foo bar')
     agenda.save()
     first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0)
     first_date += datetime.timedelta(days=1)
+    durations = [None, 0, 45]
     for i in range(3):
-        event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda)
+        event = Event(start_datetime=first_date + datetime.timedelta(days=i), duration=durations[i],
+                      places=20, agenda=agenda)
         event.save()
 
     agenda2 = Agenda(label=u'Foo bar 2')
     agenda2.save()
     first_date = localtime(now()).replace(hour=20, minute=0, second=0, microsecond=0)
     first_date += datetime.timedelta(days=1)
     for i in range(2):
         event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda2)
@@ -536,17 +538,17 @@ def test_booking_ics(app, some_data, meetings_agenda, user):
     formatted_start_date = event.start_datetime.strftime('%Y%m%dT%H%M%S')
     booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics()
     assert (
         'UID:%s-%s-%s\r\n' % (event.start_datetime.isoformat(), agenda.pk, resp.json['booking_id'])
         in booking_ics
     )
     assert 'SUMMARY:\r\n' in booking_ics
     assert 'DTSTART:%sZ\r\n' % formatted_start_date in booking_ics
-    assert 'DTEDND:' not in booking_ics
+    assert 'DTEND:' not in booking_ics
 
     # test with additional data
     resp = app.post_json(
         '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
         params={
             'label': 'foo',
             'user_name': 'bar',
             'backoffice_url': 'http://example.net/',
@@ -3264,8 +3266,86 @@ def test_virtual_agendas_meetings_booking_default_policy(app, mock_now, user):
         )
         if next_agenda:
             assert booking.event.agenda == next_agenda
 
     foo_num_bookings = Booking.objects.filter(event__desk__agenda=foo_agenda).count()
     bar_num_bookings = Booking.objects.filter(event__desk__agenda=bar_agenda).count()
     assert foo_num_bookings == 2
     assert bar_num_bookings == 4
+
+
+def test_duration_on_booking_api_fillslot_response(app, some_data, user):
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    evts = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
+    assert evts[0].duration is None
+    assert evts[0].end_datetime is None
+    assert evts[1].duration == 0
+    assert evts[1].end_datetime == evts[1].start_datetime
+    assert evts[2].duration == 45
+    assert evts[2].end_datetime == evts[2].start_datetime + datetime.timedelta(minutes=45)
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evts[0].id))
+    formatted_start_date = localtime(evts[0].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert resp.json['datetime'] == formatted_start_date
+    assert resp.json['end_datetime'] is None
+    assert 'ics_url' in resp.json['api']
+    ics = app.get(resp.json['api']['ics_url']).text
+    assert 'DTSTART:%sZ\r\n' % evts[0].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+    assert 'DTEND:' not in ics
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evts[1].id))
+    assert resp.json['datetime'] == localtime(evts[1].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert resp.json['end_datetime'] == resp.json['datetime']
+    assert 'ics_url' in resp.json['api']
+    ics = app.get(resp.json['api']['ics_url']).text
+    assert 'DTSTART:%sZ\r\n' % evts[1].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+    assert 'DTEND:%sZ\r\n' % evts[1].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evts[2].id))
+    assert resp.json['datetime'] == localtime(evts[2].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert resp.json['end_datetime'] == localtime(evts[2].end_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert 'ics_url' in resp.json['api']
+    ics = app.get(resp.json['api']['ics_url']).text
+    assert 'DTSTART:%sZ\r\n' % evts[2].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+    assert 'DTEND:%sZ\r\n' % evts[2].end_datetime.strftime('%Y%m%dT%H%M%S') in ics
+
+
+def test_duration_on_booking_api_fillslots_response(app, some_data, user):
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    evts = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
+    assert evts[0].duration is None
+    assert evts[0].end_datetime is None
+    assert evts[1].duration == 0
+    assert evts[1].end_datetime == evts[1].start_datetime
+    assert evts[2].duration == 45
+    assert evts[2].end_datetime == evts[2].start_datetime + datetime.timedelta(minutes=45)
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    # first event having null duration
+    string_param = ','.join([str(e.id) for e in evts[::-1]])  # unordered parameters
+    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
+    r_evts = resp.json['events']
+
+    assert r_evts[0]['datetime'] == localtime(evts[0].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert r_evts[0]['end_datetime'] is None
+    assert r_evts[1]['datetime'] == localtime(evts[1].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert r_evts[1]['end_datetime'] == r_evts[1]['datetime']
+    assert r_evts[2]['datetime'] == localtime(evts[2].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert r_evts[2]['end_datetime'] == localtime(evts[2].end_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert 'ics_url' in resp.json['api']
+    ics = app.get(resp.json['api']['ics_url']).text
+    assert 'DTSTART:%sZ\r\n' % evts[0].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+    assert 'DTEND:' not in ics
+
+    # first event having duration
+    evts[0].duration = 90
+    evts[0].save()
+    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
+    r_evts = resp.json['events']
+
+    assert r_evts[0]['datetime'] == localtime(evts[0].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert r_evts[0]['end_datetime'] == localtime(evts[0].end_datetime).strftime('%Y-%m-%d %H:%M:%S')
+    assert 'ics_url' in resp.json['api']
+    ics = app.get(resp.json['api']['ics_url']).text
+    assert 'DTSTART:%sZ\r\n' % evts[0].start_datetime.strftime('%Y%m%dT%H%M%S') in ics
+    assert 'DTEND:%sZ\r\n' % evts[0].end_datetime.strftime('%Y%m%dT%H%M%S') in ics
-- 
2.26.2

