From 2f5ccb4da1dd120ba42b424ace872454f1a8d655 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 12 May 2020 16:57:11 +0200
Subject: [PATCH 2/2] api: check publication_date in datetimes & fillslots api
 (#40728)

---
 chrono/agendas/models.py |   2 +
 chrono/api/views.py      |  10 ++++
 tests/test_agendas.py    |  52 +++++++++++---------
 tests/test_api.py        | 103 +++++++++++++++++++++++++--------------
 4 files changed, 107 insertions(+), 60 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 4bc0798..74240a2 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -551,6 +551,8 @@ class Event(models.Model):
         )
 
     def in_bookable_period(self):
+        if self.publication_date and localtime(now()).date() < self.publication_date:
+            return False
         if (
             localtime(now()).date()
             > localtime(
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 2d98f7e..86d5d03 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -18,6 +18,7 @@ from collections import defaultdict
 import datetime
 
 from django.db import transaction
+from django.db.models import Q
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.urls import reverse
@@ -256,6 +257,9 @@ class Datetimes(APIView):
         # we never want to allow booking for past events.
         entries = entries.filter(start_datetime__gte=localtime(now()))
 
+        # exclude non published events
+        entries = entries.filter(Q(publication_date__isnull=True) | Q(publication_date__lte=now().date()))
+
         if agenda.minimal_booking_delay:
             entries = entries.filter(
                 start_datetime__gte=localtime(
@@ -698,6 +702,12 @@ class Fillslots(APIView):
             except ValueError:
                 events = Event.objects.filter(slug__in=slots).order_by('start_datetime')
 
+            for event in events:
+                if not event.in_bookable_period():
+                    return Response(
+                        {'err': 1, 'err_class': 'event not bookable', 'err_desc': _('event not bookable')}
+                    )
+
             if not events.count():
                 return Response(
                     {
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index 03d28b5..90b195c 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -142,30 +142,34 @@ def test_event_manager():
     assert Event.objects.all()[0].booked_places == 0
 
 
-def test_event_bookable_period():
-    agenda = Agenda(label=u'Foo bar')
-    agenda.save()
-    event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda)
-    event.save()
-    assert event.in_bookable_period() is True
-    agenda.minimal_booking_delay = 20
-    assert event.in_bookable_period() is False
-    agenda.minimal_booking_delay = 1
-    agenda.maximal_booking_delay = 5
-    assert event.in_bookable_period() is False
-    agenda.maximal_booking_delay = 20
-    assert event.in_bookable_period() is True
-
-    # special case for events that happens today
-    agenda.minimal_booking_delay = 0
-    agenda.save()
-    event = Event(start_datetime=now() + datetime.timedelta(minutes=10), places=10, agenda=agenda)
-    event.save()
-    assert event.in_bookable_period() is True
-
-    event = Event(start_datetime=now() - datetime.timedelta(minutes=10), places=10, agenda=agenda)
-    event.save()
-    assert event.in_bookable_period() is False
+@pytest.mark.parametrize(
+    'start_days, start_minutes, min_delay, max_delay, pub_days, expected',
+    [
+        # no delay
+        (10, 0, 0, 0, None, True),
+        # test publication_date
+        (10, 0, 0, 0, 1, False),
+        (10, 0, 0, 0, 0, True),
+        # test min and max delays
+        (10, 0, 20, 0, None, False),
+        (10, 0, 1, 5, None, False),
+        (10, 0, 1, 20, None, True),
+        # special case for events that happens today
+        (0, 10, 0, 20, None, True),
+        (0, -10, 0, 20, None, False),
+    ],
+)
+def test_event_bookable_period(start_days, start_minutes, min_delay, max_delay, pub_days, expected):
+    agenda = Agenda.objects.create(
+        label=u'Foo bar', minimal_booking_delay=min_delay, maximal_booking_delay=max_delay
+    )
+    event = Event.objects.create(
+        start_datetime=now() + datetime.timedelta(days=start_days, minutes=start_minutes),
+        publication_date=(now().date() + datetime.timedelta(days=pub_days)) if pub_days else None,
+        places=10,
+        agenda=agenda,
+    )
+    assert event.in_bookable_period() == expected
 
 
 def test_meeting_type_slugs():
diff --git a/tests/test_api.py b/tests/test_api.py
index f235dca..1c5ddd8 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 import datetime
+import mock
 import urllib.parse as urlparse
 import pytest
 import sys
@@ -269,6 +270,16 @@ def test_datetimes_api(app, some_data):
     check_bookability(resp.json['data'])
     assert resp.json['data'][0]['description'] is None
 
+    agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=1))
+    resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
+    assert len(resp.json['data']) == 0
+    check_bookability(resp.json['data'])
+
+    agenda.event_set.update(publication_date=now().date())
+    resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
+    assert len(resp.json['data']) == 2
+    check_bookability(resp.json['data'])
+
     # add description, URL and pricing to events
     for i, event in enumerate(agenda.event_set.all()):
         event.description = 'Description %s' % i
@@ -745,6 +756,24 @@ def test_booking_api_fillslots(app, some_data, user):
     resp = app.post('/api/agenda/foobar/fillslots/', status=404)
     resp = app.post('/api/agenda/0/fillslots/', status=404)
 
+    # check bookable period
+    with mock.patch('chrono.agendas.models.Event.in_bookable_period') as in_bookable_period:
+        in_bookable_period.return_value = True
+        resp = app.post_json(
+            '/api/agenda/%s/fillslots/' % agenda.id,
+            params={'slots': events_ids, 'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'},
+        )
+        assert resp.json['err'] == 0
+        in_bookable_period.return_value = False
+        resp = app.post_json(
+            '/api/agenda/%s/fillslots/' % agenda.id,
+            params={'slots': events_ids, 'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'},
+        )
+        assert resp.json['err'] == 1
+        assert resp.json['reason'] == 'event not bookable'  # legacy
+        assert resp.json['err_class'] == 'event not bookable'
+        assert resp.json['err_desc'] == 'event not bookable'
+
 
 def test_booking_api_fillslots_slots_string_param(app, some_data, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
@@ -957,24 +986,24 @@ def test_booking_api_meeting_different_durations_book_long(app, meetings_agenda,
 
 
 def test_booking_api_with_data(app, some_data, user):
-    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
-    event = Event.objects.filter(agenda_id=agenda_id)[0]
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
 
     app.authorization = ('Basic', ('john.doe', 'password'))
-    resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), params={'hello': 'world'})
+    app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), params={'hello': 'world'})
     assert Booking.objects.count() == 1
     assert Booking.objects.all()[0].extra_data == {'hello': 'world'}
 
 
 def test_booking_api_available(app, some_data, meetings_agenda, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
-    event = Event.objects.filter(agenda=agenda)[0]
+    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
 
     app.authorization = ('Basic', ('john.doe', 'password'))
     resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 9
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 19
     assert resp.json['places']['reserved'] == 1
     assert resp.json['places']['full'] is False
     assert 'waiting_list_total' not in resp.json['places']
@@ -985,8 +1014,8 @@ def test_booking_api_available(app, some_data, meetings_agenda, user):
 
     resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 9
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 19
     assert resp.json['places']['reserved'] == 1
     assert resp.json['places']['full'] is False
     assert resp.json['places']['waiting_list_total'] == 5
@@ -1015,7 +1044,7 @@ def test_booking_api_available(app, some_data, meetings_agenda, user):
 
 def test_booking_api_force_waiting_list(app, some_data, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
-    event = Event.objects.filter(agenda=agenda)[0]
+    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
 
     app.authorization = ('Basic', ('john.doe', 'password'))
 
@@ -1034,8 +1063,8 @@ def test_booking_api_force_waiting_list(app, some_data, user):
     # add a booking
     resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 9
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 19
     assert resp.json['places']['reserved'] == 1
     assert resp.json['places']['full'] is False
     assert resp.json['places']['waiting_list_total'] == 2
@@ -1048,8 +1077,8 @@ def test_booking_api_force_waiting_list(app, some_data, user):
         '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': False}
     )
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 8
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 18
     assert resp.json['places']['reserved'] == 2
     assert resp.json['places']['full'] is False
     assert resp.json['places']['waiting_list_total'] == 2
@@ -1062,8 +1091,8 @@ def test_booking_api_force_waiting_list(app, some_data, user):
         '/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': True}
     )
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 8
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 18
     assert resp.json['places']['reserved'] == 2
     assert resp.json['places']['full'] is False
     assert resp.json['places']['waiting_list_total'] == 2
@@ -1074,8 +1103,8 @@ def test_booking_api_force_waiting_list(app, some_data, user):
     # add a booking => booked in waiting list
     resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
     assert resp.json['err'] == 0
-    assert resp.json['places']['total'] == 10
-    assert resp.json['places']['available'] == 8
+    assert resp.json['places']['total'] == 20
+    assert resp.json['places']['available'] == 18
     assert resp.json['places']['reserved'] == 2
     assert resp.json['places']['full'] is True
     assert resp.json['places']['waiting_list_total'] == 2
@@ -1094,17 +1123,19 @@ def test_booking_api_force_waiting_list(app, some_data, user):
 
 
 def test_booking_api_with_cancel_booking(app, some_data, user):
-    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
-    event_0, event_1, event_2, event_3 = Event.objects.filter(agenda_id=agenda_id)[0:4]
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    event_0, event_1, event_2 = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][
+        0:4
+    ]
 
     app.authorization = ('Basic', ('john.doe', 'password'))
-    app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id))
+    app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id))
     assert Booking.objects.count() == 1
     first_booking = Booking.objects.first()
 
     # Book a new event and cancel previous booking
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id),
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
         params={'cancel_booking_id': first_booking.pk},
     )
     assert resp.json['err'] == 0
@@ -1115,7 +1146,7 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
 
     # Cancelling an already cancelled booking returns an error
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id),
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
         params={'cancel_booking_id': first_booking.pk},
     )
     assert resp.json['err'] == 1
@@ -1126,7 +1157,7 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
 
     # Cancelling a non existent booking returns an error
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), params={'cancel_booking_id': '-1'}
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id), params={'cancel_booking_id': '-1'}
     )
     assert resp.json['err'] == 1
     assert resp.json['reason'] == 'cancel booking: booking does no exist'  # legacy
@@ -1135,13 +1166,13 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
     assert Booking.objects.count() == 2
 
     # Cancelling booking with different count than new booking
-    resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_2.id), params={'count': 2})
+    resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event_2.id), params={'count': 2})
     assert resp.json['err'] == 0
     assert Booking.objects.count() == 4
     booking_id = resp.json['booking_id']
 
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_3.id),
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
         params={'cancel_booking_id': booking_id, 'count': 1},
     )
     assert resp.json['err'] == 1
@@ -1152,21 +1183,21 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
 
     # cancel_booking_id must be an integer
     app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id),
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id),
         params={'cancel_booking_id': 'no an integer'},
         status=400,
     )
 
     # cancel_booking_id can be empty or null
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), params={'cancel_booking_id': ''}
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id), params={'cancel_booking_id': ''}
     )
     assert resp.json['err'] == 0
     assert 'cancelled_booking_id' not in resp.json
     assert Booking.objects.count() == 5
 
     resp = app.post_json(
-        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), params={'cancel_booking_id': None}
+        '/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id), params={'cancel_booking_id': None}
     )
     assert resp.json['err'] == 0
     assert 'cancelled_booking_id' not in resp.json
@@ -1174,12 +1205,12 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
 
 
 def test_booking_cancellation_api(app, some_data, user):
-    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
-    event = Event.objects.filter(agenda_id=agenda_id)[0]
-    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=401)
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), status=401)
 
     app.authorization = ('Basic', ('john.doe', 'password'))
-    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id))
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id))
     booking_id = resp.json['booking_id']
     assert Booking.objects.count() == 1
     resp = app.delete('/api/booking/%s/' % booking_id)
@@ -1187,12 +1218,12 @@ def test_booking_cancellation_api(app, some_data, user):
 
 
 def test_booking_cancellation_post_api(app, some_data, user):
-    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
-    event = Event.objects.filter(agenda_id=agenda_id)[0]
-    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=401)
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), status=401)
 
     app.authorization = ('Basic', ('john.doe', 'password'))
-    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id))
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id))
     booking_id = resp.json['booking_id']
     assert Booking.objects.count() == 1
     assert urlparse.urlparse(resp.json['api']['cancel_url']).path == '/api/booking/%s/cancel/' % booking_id
-- 
2.20.1

