chrono/tests/api/fillslot/test_events.py

559 lines
24 KiB
Python

import datetime
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import Agenda, Booking, Event, EventsType
from chrono.utils.timezone import localtime, now
pytestmark = pytest.mark.django_db
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots(app, user):
events_type = EventsType.objects.create(label='Foo')
agenda = Agenda.objects.create(label='Foo bar', kind='events', events_type=events_type)
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=1),
places=2,
waiting_list_places=1,
agenda=agenda,
)
second_event = Event.objects.create(
label='Event 2', start_datetime=now() + datetime.timedelta(days=2), places=2, agenda=agenda
)
third_event = Event.objects.create(
label='Event 3', start_datetime=now() + datetime.timedelta(days=3), places=2, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'check_overlaps': True, 'slots': 'event,event-2'}
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(fillslots_url, params=params)
assert len(ctx.captured_queries) == 12
assert resp.json['booking_count'] == 2
assert len(resp.json['booked_events']) == 2
assert resp.json['booked_events'][0]['id'] == 'event'
assert (
resp.json['booked_events'][0]['booking']['id'] == Booking.objects.filter(event=event).latest('pk').pk
)
assert resp.json['booked_events'][1]['id'] == 'event-2'
assert (
resp.json['booked_events'][1]['booking']['id']
== Booking.objects.filter(event=second_event).latest('pk').pk
)
assert len(resp.json['waiting_list_events']) == 0
assert resp.json['cancelled_booking_count'] == 0
assert len(resp.json['cancelled_events']) == 0
events = Event.objects.all()
assert events.filter(booked_places=1).count() == 2
params['user_external_id'] = 'user_id_2'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert len(resp.json['booked_events']) == 2
assert len(resp.json['waiting_list_events']) == 0
assert resp.json['booked_events'][0]['id'] == 'event'
assert (
resp.json['booked_events'][0]['booking']['id'] == Booking.objects.filter(event=event).latest('pk').pk
)
assert resp.json['booked_events'][1]['id'] == 'event-2'
assert (
resp.json['booked_events'][1]['booking']['id']
== Booking.objects.filter(event=second_event).latest('pk').pk
)
assert resp.json['cancelled_booking_count'] == 0
assert len(resp.json['cancelled_events']) == 0
params['user_external_id'] = 'user_id_3'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'some events are full: Event 2'
params['slots'] = 'event'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 1
assert len(resp.json['booked_events']) == 0
assert len(resp.json['waiting_list_events']) == 1
assert resp.json['waiting_list_events'][0]['slug'] == event.slug
assert (
resp.json['waiting_list_events'][0]['booking']['id']
== Booking.objects.filter(event=event).latest('pk').pk
)
assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 1
assert resp.json['cancelled_booking_count'] == 0
assert len(resp.json['cancelled_events']) == 0
# change bookings
params = {'user_external_id': 'user_id', 'slots': 'event-2,event-3'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 1
assert len(resp.json['cancelled_events']) == 1
assert [x['date'] for x in resp.json['cancelled_events']] == ['2021-09-07']
user_bookings = Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True)
assert {b.event.slug for b in user_bookings} == {'event-2', 'event-3'}
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 2
assert second_event.booking_set.count() == 2
assert third_event.booking_set.count() == 1
# increase waiting_list_places to make "Event" bookable again
event.waiting_list_places = 2
event.save()
# specify time range so that "Event 3" is not cancelled
params['slots'] = 'event,event-2'
resp = app.post_json(fillslots_url + '?date_start=2021-09-06&date_end=2021-09-09', params=params)
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 0
user_bookings = Booking.objects.filter(user_external_id='user_id')
assert {b.event.slug for b in user_bookings} == {'event', 'event-2', 'event-3'}
assert event.booking_set.count() == 3
assert second_event.booking_set.count() == 2
assert third_event.booking_set.count() == 1
# new event booking went in waiting list despite free slots on main list
assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 2
# passing empty slots cancels all bookings
params['slots'] = ''
resp = app.post_json(fillslots_url, params=params)
assert resp.json['cancelled_booking_count'] == 3
assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 0
resp = app.post('/api/agenda/foobar/events/fillslots/', status=404)
resp = app.post('/api/agenda/0/events/fillslots/', status=404)
params = {'user_external_id': 'user_id', 'slots': 'event,event-2', 'foo': 'bar'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert [b.extra_data for b in Booking.objects.order_by('-pk')[:2]] == [{'foo': 'bar'}, {'foo': 'bar'}]
params.update({'foo': ['bar', 'baz']})
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
params.update({'foo': {'bar': 'baz'}})
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_with_cancelled(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event_1 = Event.objects.create(
label='Event 1',
start_datetime=now() + datetime.timedelta(days=1),
places=2,
agenda=agenda,
)
event_2 = Event.objects.create(
label='Event 2',
start_datetime=now() + datetime.timedelta(days=2),
places=2,
agenda=agenda,
)
Event.objects.create(
label='Event 3',
start_datetime=now() + datetime.timedelta(days=3),
places=2,
agenda=agenda,
)
# create cancelled booking for the user
booking_1 = Booking.objects.create(event=event_1, user_external_id='user_id')
booking_1.cancel()
assert booking_1.cancellation_datetime is not None
# and non cancelled booking for the user
booking_2 = Booking.objects.create(event=event_2, user_external_id='user_id')
assert booking_2.cancellation_datetime is None
# secondary booking for this one
booking_2_secondary = Booking.objects.create(event=event_2, primary_booking=booking_2)
# and bookings for another user
Booking.objects.create(event=event_1, user_external_id='user_id_foobar')
other_booking = Booking.objects.create(event=event_2, user_external_id='user_id_foobar')
other_booking.cancel()
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'event-1,event-2,event-3'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert len(resp.json['booked_events']) == 2
assert resp.json['booked_events'][0]['id'] == 'event-1'
assert (
resp.json['booked_events'][0]['booking']['id']
== Booking.objects.filter(event__slug='event-1').latest('pk').pk
)
assert resp.json['booked_events'][1]['id'] == 'event-3'
assert (
resp.json['booked_events'][1]['booking']['id']
== Booking.objects.filter(event__slug='event-3').latest('pk').pk
)
assert Booking.objects.filter(user_external_id='user_id').count() == 3
assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 3
assert resp.json['cancelled_booking_count'] == 0
assert len(resp.json['cancelled_events']) == 0
assert Booking.objects.filter(pk=booking_1.pk).exists() is False # cancelled booking deleted
booking_2.refresh_from_db()
booking_2_secondary.refresh_from_db()
assert booking_2.cancellation_datetime is None
assert booking_2_secondary.cancellation_datetime is None
params = {'user_external_id': 'user_id', 'slots': 'event-3'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 2
assert Booking.objects.filter(user_external_id='user_id').count() == 3
assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 1
assert Booking.objects.filter(pk=booking_1.pk).exists() is False # cancelled booking deleted
booking_2.refresh_from_db()
booking_2_secondary.refresh_from_db()
assert booking_2.cancellation_datetime is not None
assert booking_2_secondary.cancellation_datetime is not None
def test_api_events_fillslots_check_delays(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
Event.objects.create(
slug='event-slug',
start_datetime=localtime() + datetime.timedelta(days=5),
places=5,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 0
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is False
booking.delete()
# test minimal_booking_delay
agenda.minimal_booking_delay = 6
agenda.save()
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
)
assert resp.json['err'] == 0
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is True
booking.delete()
# test maximal_booking_delay
agenda.minimal_booking_delay = 0
agenda.maximal_booking_delay = 3
agenda.save()
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
)
assert resp.json['err'] == 0
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is False
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_past_event(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event1 = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
event2 = Event.objects.create(
label='Today after now',
start_datetime=localtime(now() + datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
params = {'user_external_id': 'user_id', 'slots': ','.join((event1.slug, event2.slug))}
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'future'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'past'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
params['events'] = 'all'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_preserve_past_bookings(app, user, freezer):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=2, agenda=agenda
)
second_event = Event.objects.create(
label='Event 2', start_datetime=now() + datetime.timedelta(days=10), places=2, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'event,event-2'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert resp.json['cancelled_booking_count'] == 0
# book only second event while first event is in the past
freezer.move_to('2021-09-12')
params = {'user_external_id': 'user_id', 'slots': 'event-2'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 0
assert event.booking_set.count() == 1
assert second_event.booking_set.count() == 1
# cancel all future bookings
params = {'user_external_id': 'user_id', 'slots': ''}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert event.booking_set.count() == 1
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=2, maximal_booking_delay=10
)
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=2, agenda=agenda
)
second_event = Event.objects.create(
label='Event 2', start_datetime=now() + datetime.timedelta(days=9), places=2, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'event,event-2'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert resp.json['cancelled_booking_count'] == 0
assert event.booking_set.get().out_of_min_delay is False
assert second_event.booking_set.get().out_of_min_delay is False
# book only second event while first event is out of delay
freezer.move_to('2021-09-10')
params = {'user_external_id': 'user_id', 'slots': 'event-2'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 0
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
assert event.booking_set.get().out_of_min_delay is False
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
assert second_event.booking_set.get().out_of_min_delay is False
booking = event.booking_set.get()
# except if we want to bypass delays
params = {'user_external_id': 'user_id', 'slots': 'event-2', 'bypass_delays': True}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
assert event.booking_set.get().out_of_min_delay is True
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
assert second_event.booking_set.get().out_of_min_delay is False
booking.save() # reset
# cancel all bookings in delays
params = {'user_external_id': 'user_id', 'slots': ''}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert event.booking_set.count() == 1
assert event.booking_set.get().out_of_min_delay is False
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
assert second_event.booking_set.get().out_of_min_delay is False
# bypass delays
params = {'user_external_id': 'user_id', 'slots': '', 'bypass_delays': True}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
assert event.booking_set.get().out_of_min_delay is True
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
assert second_event.booking_set.get().out_of_min_delay is False
booking.save() # reset
# book only first event while second event is out of delay
freezer.move_to('2021-09-04')
params = {'user_external_id': 'user_id', 'slots': 'event'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 0
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
# bypass_delays has no effect on maximal_booking_delay
params = {'user_external_id': 'user_id', 'slots': 'event', 'bypass_delays': True}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 0
assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_overlapping_events(app, user, freezer):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
first_event = Event.objects.create(
label='Event 12-14',
start_datetime=now() + datetime.timedelta(days=5),
duration=120,
places=5,
agenda=agenda,
)
second_event = Event.objects.create(
label='Event 13-15',
start_datetime=now() + datetime.timedelta(days=5, hours=1),
duration=120,
places=5,
agenda=agenda,
)
Event.objects.create(
label='Event 14-16',
start_datetime=now() + datetime.timedelta(days=5, hours=2),
duration=120,
places=5,
agenda=agenda,
)
Event.objects.create(
label='Event no duration',
start_datetime=now() + datetime.timedelta(days=5, hours=1),
places=5,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/foo-bar/events/fillslots/'
params = {'user_external_id': 'user_id', 'check_overlaps': True}
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14'})
assert resp.json['booking_count'] == 1
# booking the same event is still allowed
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14'})
assert resp.json['err'] == 0
assert resp.json['booking_count'] == 0
# changing booking to second event is allowed
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-13-15'})
assert resp.json['err'] == 0
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 1
# events are not overlapping if one ends when the other starts
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-14-16'})
assert resp.json['booking_count'] == 2
assert resp.json['cancelled_booking_count'] == 1
# booking overlapping events is allowed if one has no duration
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-no-duration'})
assert resp.json['err'] == 0
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 1
# default behavior does not check for overlaps
resp = app.post_json(
fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-12-14,event-13-15'}
)
assert resp.json['err'] == 0
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 1
# clearing overlapping bookings is allowed
resp = app.post_json(fillslots_url, params={**params, 'slots': ''})
assert resp.json['err'] == 0
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 2
# booking overlapping events with durations is forbidden
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-13-15'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Some events occur at the same time: Event 12-14, Event 13-15'
# still overlaps but start before
second_event.start_datetime -= datetime.timedelta(hours=2)
second_event.save()
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-13-15'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Some events occur at the same time: Event 12-14, Event 13-15'
# still overlaps but contains first event
second_event.start_datetime = first_event.start_datetime - datetime.timedelta(minutes=10)
second_event.save()
second_event.duration = first_event.duration + 10
second_event.save()
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-13-15'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Some events occur at the same time: Event 12-14, Event 13-15'
# still overlaps but contained by first event
second_event.start_datetime = first_event.start_datetime + datetime.timedelta(minutes=10)
second_event.save()
second_event.duration = first_event.duration - 10
second_event.save()
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-13-15'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Some events occur at the same time: Event 12-14, Event 13-15'
# no more overlap
second_event.start_datetime -= datetime.timedelta(hours=5)
second_event.save()
resp = app.post_json(fillslots_url, params={**params, 'slots': 'event-12-14,event-13-15'})
assert resp.json['booking_count'] == 2
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_exclude_user_forbidden(app, user):
events_type = EventsType.objects.create(label='Foo')
agenda = Agenda.objects.create(label='Foo bar', kind='events', events_type=events_type)
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'xxx', 'exclude_user': True}
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['exclude_user'][0] == 'This parameter is not supported.'