api: make recurring events fillslots work with multiple agendas (#57957)
This commit is contained in:
parent
bb781f8c83
commit
753c7ad6f1
|
@ -1,3 +1,5 @@
|
|||
import collections
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
@ -89,6 +91,33 @@ class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
|
|||
return value
|
||||
|
||||
|
||||
class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
|
||||
def validate_slots(self, value):
|
||||
super().validate_slots(value)
|
||||
open_event_slugs = collections.defaultdict(set)
|
||||
for agenda in self.context['agendas']:
|
||||
for event in agenda.get_open_recurring_events():
|
||||
open_event_slugs[agenda.slug].add(event.slug)
|
||||
|
||||
slots = collections.defaultdict(lambda: collections.defaultdict(list))
|
||||
for slot in value:
|
||||
try:
|
||||
slugs, day = slot.split(':')
|
||||
day = int(day)
|
||||
except ValueError:
|
||||
raise ValidationError(_('invalid slot: %s') % slot)
|
||||
|
||||
agenda_slug, event_slug = slugs.split('@')
|
||||
if event_slug not in open_event_slugs[agenda_slug]:
|
||||
raise ValidationError(_('event %s of agenda %s is not bookable') % (event_slug, agenda_slug))
|
||||
|
||||
# convert ISO day number to db lookup day number
|
||||
day = (day + 1) % 7 + 1
|
||||
slots[agenda_slug][event_slug].append(day)
|
||||
|
||||
return slots
|
||||
|
||||
|
||||
class BookingSerializer(serializers.ModelSerializer):
|
||||
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ urlpatterns = [
|
|||
url(r'^agenda/$', views.agendas),
|
||||
url(r'^agendas/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'),
|
||||
url(r'^agendas/recurring-events/$', views.recurring_events_list, name='api-agenda-recurring-events'),
|
||||
url(r'^agendas/recurring-events/fillslots/$', views.recurring_fillslots, name='api-recurring-fillslots'),
|
||||
url(
|
||||
r'^agendas/events/fillslots/$',
|
||||
views.agendas_events_fillslots,
|
||||
|
@ -40,11 +41,6 @@ urlpatterns = [
|
|||
views.events_fillslots,
|
||||
name='api-agenda-events-fillslots',
|
||||
),
|
||||
url(
|
||||
r'^agenda/(?P<agenda_identifier>[\w-]+)/recurring-events/fillslots/$',
|
||||
views.recurring_fillslots,
|
||||
name='api-recurring-fillslots',
|
||||
),
|
||||
url(
|
||||
r'^agenda/(?P<agenda_identifier>[\w-]+)/event/$',
|
||||
views.events,
|
||||
|
|
|
@ -1601,18 +1601,21 @@ fillslot = Fillslot.as_view()
|
|||
|
||||
class RecurringFillslots(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.EventsSlotsSerializer
|
||||
serializer_class = serializers.RecurringFillslotsSerializer
|
||||
|
||||
def post(self, request, agenda_identifier):
|
||||
def post(self, request):
|
||||
if not settings.ENABLE_RECURRING_EVENT_BOOKING:
|
||||
raise Http404()
|
||||
|
||||
agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
|
||||
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
|
||||
if not start_datetime or start_datetime < now():
|
||||
start_datetime = now()
|
||||
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
agenda_slugs = get_agendas_from_request(request)
|
||||
agendas = get_objects_from_slugs(agenda_slugs, qs=Agenda.objects.filter(kind='events'))
|
||||
|
||||
context = {'allowed_agenda_slugs': agenda_slugs, 'agendas': agendas}
|
||||
serializer = self.serializer_class(data=request.data, partial=True, context=context)
|
||||
if not serializer.is_valid():
|
||||
raise APIError(
|
||||
_('invalid payload'),
|
||||
|
@ -1623,31 +1626,14 @@ class RecurringFillslots(APIView):
|
|||
payload = serializer.validated_data
|
||||
user_external_id = payload['user_external_id']
|
||||
|
||||
open_event_slugs = set(agenda.get_open_recurring_events().values_list('slug', flat=True))
|
||||
slots = collections.defaultdict(list)
|
||||
for slot in payload['slots']:
|
||||
try:
|
||||
slug, day = slot.split(':')
|
||||
day = int(day)
|
||||
except ValueError:
|
||||
raise APIError(
|
||||
_('invalid slot: %s') % slot,
|
||||
err_class='invalid slot: %s' % slot,
|
||||
http_status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if slug not in open_event_slugs:
|
||||
raise APIError(
|
||||
_('event %s is not bookable') % slug,
|
||||
err_class='event %s is not bookable' % slug,
|
||||
http_status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# convert ISO day number to db lookup day number
|
||||
day = (day + 1) % 7 + 1
|
||||
slots[slug].append(day)
|
||||
|
||||
event_filter = Q()
|
||||
for slug, days in slots.items():
|
||||
event_filter |= Q(agenda=agenda, primary_event__slug=slug, start_datetime__week_day__in=days)
|
||||
for agenda_slug, days_by_event in payload['slots'].items():
|
||||
for event_slug, days in days_by_event.items():
|
||||
event_filter |= Q(
|
||||
agenda__slug=agenda_slug,
|
||||
primary_event__slug=event_slug,
|
||||
start_datetime__week_day__in=days,
|
||||
)
|
||||
|
||||
events_to_book = Event.objects.filter(event_filter) if event_filter else Event.objects.none()
|
||||
events_to_book = events_to_book.filter(start_datetime__gte=start_datetime, cancelled=False)
|
||||
|
@ -1684,7 +1670,7 @@ class RecurringFillslots(APIView):
|
|||
'err': 0,
|
||||
'booking_count': len(bookings),
|
||||
'cancelled_booking_count': deleted_count,
|
||||
'full_events': [get_event_detail(request, x, agenda=agenda) for x in full_events],
|
||||
'full_events': [get_event_detail(request, x, multiple_agendas=True) for x in full_events],
|
||||
}
|
||||
return Response(response)
|
||||
|
||||
|
|
|
@ -2147,6 +2147,7 @@ def test_fillslot_past_events_recurring_event(app, user):
|
|||
def test_recurring_events_api_fillslots(app, user, freezer):
|
||||
freezer.move_to('2021-09-06 12:00')
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
|
@ -2168,14 +2169,14 @@ def test_recurring_events_api_fillslots(app, user, freezer):
|
|||
)
|
||||
sunday_event.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug)
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
|
||||
assert len(resp.json['data']) == 5
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agenda/%s/recurring-events/fillslots/' % agenda.slug
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
|
||||
params = {'user_external_id': 'user_id'}
|
||||
# Book Monday and Thursday of first event and Sunday of second event
|
||||
params['slots'] = 'event:0,event:3,sunday-event:6'
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3,foo-bar@sunday-event:6'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 156
|
||||
|
||||
|
@ -2216,15 +2217,15 @@ def test_recurring_events_api_fillslots(app, user, freezer):
|
|||
assert resp.json['booking_count'] == 0
|
||||
|
||||
# no event in range
|
||||
resp = app.post_json(fillslots_url + '?date_start=2020-10-06&date_end=2020-11-06', params=params)
|
||||
resp = app.post_json(fillslots_url + '&date_start=2020-10-06&date_end=2020-11-06', params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
|
||||
params['slots'] = 'event:1'
|
||||
resp = app.post_json(fillslots_url + '?date_start=2021-10-06&date_end=2021-11-06', params=params)
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url + '&date_start=2021-10-06&date_end=2021-11-06', params=params)
|
||||
assert resp.json['booking_count'] == 4
|
||||
assert Booking.objects.filter(user_external_id='user_id_4').count() == 4
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'slots': 'event:0'}, status=400)
|
||||
resp = app.post_json(fillslots_url, params={'slots': 'foo-bar@event:0'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['user_external_id'] == ['This field is required.']
|
||||
|
@ -2234,18 +2235,23 @@ def test_recurring_events_api_fillslots(app, user, freezer):
|
|||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['slots'] == ['This field is required.']
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'a:a'}, status=400)
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:a'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'invalid slot: a:a'
|
||||
assert resp.json['errors']['slots'] == ['invalid slot: foo-bar@a:a']
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'a:1'}, status=400)
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:1'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'event a is not bookable'
|
||||
assert resp.json['errors']['slots'] == ['event a of agenda foo-bar is not bookable']
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['slots'] == ['Invalid format for slot foo-bar']
|
||||
|
||||
|
||||
def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
|
||||
freezer.move_to('2021-09-06 12:00')
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
|
@ -2265,8 +2271,8 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
|
|||
assert events.filter(booked_waiting_list_places=1).count() == 5
|
||||
|
||||
# check that new bookings are put in waiting list despite free slots on main list
|
||||
params = {'user_external_id': 'user_id', 'slots': 'event:0'}
|
||||
resp = app.post_json('/api/agenda/%s/recurring-events/fillslots/' % agenda.slug, params=params)
|
||||
params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'}
|
||||
resp = app.post_json('/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug, params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert events.filter(booked_waiting_list_places=2).count() == 5
|
||||
|
||||
|
@ -2274,6 +2280,7 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
|
|||
def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
||||
freezer.move_to('2021-09-06 12:00')
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
|
@ -2286,10 +2293,10 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
|||
event.create_all_recurrences()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agenda/%s/recurring-events/fillslots/' % agenda.slug
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
|
||||
params = {'user_external_id': 'user_id'}
|
||||
# Book Monday and Thursday
|
||||
params['slots'] = 'event:0,event:3'
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 104
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -2298,7 +2305,7 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
|||
assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
|
||||
|
||||
# Change booking to Monday and Tuesday
|
||||
params['slots'] = 'event:0,event:1'
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 52
|
||||
assert resp.json['cancelled_booking_count'] == 52
|
||||
|
@ -2313,7 +2320,7 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
|||
assert Booking.objects.count() == 104
|
||||
|
||||
params = {'user_external_id': 'user_id_2'}
|
||||
params['slots'] = 'event:0,event:3'
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 104
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -2324,7 +2331,7 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
|||
assert events.filter(booked_places=1).count() == 156
|
||||
assert events.filter(booked_waiting_list_places=1).count() == 52
|
||||
|
||||
params['slots'] = 'event:1,event:4'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 104
|
||||
assert resp.json['cancelled_booking_count'] == 104
|
||||
|
@ -2346,11 +2353,98 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
|
|||
start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda
|
||||
)
|
||||
Booking.objects.create(event=normal_event, user_external_id='user_id')
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event:0'})
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'})
|
||||
assert resp.json['cancelled_booking_count'] == 52
|
||||
assert Booking.objects.filter(user_external_id='user_id', event=normal_event).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
||||
agenda = Agenda.objects.create(label='First Agenda', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start, end = now(), now() + datetime.timedelta(days=30)
|
||||
event_a = Event.objects.create(
|
||||
label='A',
|
||||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[0, 2, 5],
|
||||
agenda=agenda,
|
||||
)
|
||||
event_a.create_all_recurrences()
|
||||
event_b = Event.objects.create(
|
||||
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
|
||||
)
|
||||
event_b.create_all_recurrences()
|
||||
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
|
||||
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
|
||||
event_c = Event.objects.create(
|
||||
label='C',
|
||||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[2, 3],
|
||||
agenda=agenda2,
|
||||
)
|
||||
event_c.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda')
|
||||
assert len(resp.json['data']) == 6
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s'
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:0,first-agenda@a:5,second-agenda@c:3'}
|
||||
resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
|
||||
assert resp.json['booking_count'] == 13
|
||||
|
||||
assert Booking.objects.count() == 13
|
||||
assert Booking.objects.filter(event__primary_event=event_a).count() == 9
|
||||
assert Booking.objects.filter(event__primary_event=event_b).count() == 0
|
||||
assert Booking.objects.filter(event__primary_event=event_c).count() == 4
|
||||
|
||||
# update bookings
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@b:1'}
|
||||
resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
|
||||
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert resp.json['cancelled_booking_count'] == 13
|
||||
assert Booking.objects.filter(event__primary_event=event_a).count() == 0
|
||||
assert Booking.objects.filter(event__primary_event=event_b).count() == 5
|
||||
assert Booking.objects.filter(event__primary_event=event_c).count() == 0
|
||||
|
||||
# error if slot's agenda is not in querystring
|
||||
resp = app.post_json(fillslots_url % 'second-agenda', params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['slots'] == [
|
||||
'Some events belong to agendas that are not present in querystring: first-agenda'
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
|
||||
for i in range(20):
|
||||
agenda = Agenda.objects.create(slug=f'{i}', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start, end = now(), now() + datetime.timedelta(days=30)
|
||||
event = Event.objects.create(
|
||||
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
agenda_slugs = ','.join(str(i) for i in range(20))
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda_slugs)
|
||||
events_to_book = [x['id'] for x in resp.json['data']]
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.post_json(
|
||||
'/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda_slugs,
|
||||
params={'slots': events_to_book, 'user_external_id': 'user'},
|
||||
)
|
||||
assert resp.json['booking_count'] == 180
|
||||
assert len(ctx.captured_queries) == 36
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_api_events_fillslots(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
|
|
Loading…
Reference in New Issue