api: add support for subscriptions in multiple agendas fillslots (#58446)

This commit is contained in:
Valentin Deniaud 2021-12-01 15:08:18 +01:00
parent 97b0b899af
commit 2e7d87ffe7
3 changed files with 162 additions and 12 deletions

View File

@ -95,9 +95,7 @@ class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
extra_agendas = slots_agenda_slugs - set(allowed_agenda_slugs)
if extra_agendas:
extra_agendas = ', '.join(sorted(extra_agendas))
raise ValidationError(
_('Some events belong to agendas that are not present in querystring: %s' % extra_agendas)
)
raise ValidationError(_('Events from the following agendas cannot be booked: %s') % extra_agendas)
return value
@ -214,7 +212,7 @@ class AgendaOrSubscribedSlugsMixin(metaclass=serializers.SerializerMetaclass):
raise ValidationError(_('Either "agendas" or "subscribed" parameter is required.'))
if 'agendas' in attrs and 'subscribed' in attrs:
raise ValidationError(_('"agendas" and "subscribed" parameters are mutually exclusive.'))
user_external_id = attrs.get('user_external_id')
user_external_id = attrs.get('user_external_id', self.context.get('user_external_id'))
if 'subscribed' in attrs and not user_external_id:
raise ValidationError(
{'user_external_id': _('This field is required when using "subscribed" parameter.')}
@ -225,6 +223,7 @@ class AgendaOrSubscribedSlugsMixin(metaclass=serializers.SerializerMetaclass):
if attrs['subscribed'] != ['all']:
agendas = agendas.filter(category__slug__in=attrs['subscribed'])
attrs['agendas'] = agendas
attrs['agenda_slugs'] = [agenda.slug for agenda in agendas]
else:
attrs['agenda_slugs'] = self.agenda_slugs
return attrs
@ -238,6 +237,10 @@ class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, Datetimes
show_past_events = serializers.BooleanField(default=False)
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
pass
class AgendaSlugsSerializer(serializers.Serializer):
agendas = CommaSeparatedStringField(
required=True, child=serializers.SlugField(max_length=160, allow_blank=False)

View File

@ -1720,8 +1720,16 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
serializer_class = serializers.MultipleAgendasEventsSlotsSerializer
def post(self, request):
self.agenda_slugs = get_agendas_from_request(request)
self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
serializer = serializers.AgendaOrSubscribedSlugsSerializer(
data=request.query_params, context={'user_external_id': request.data.get('user_external_id')}
)
if not serializer.is_valid():
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
data = serializer.validated_data
self.agendas = data['agendas']
self.agenda_slugs = data['agenda_slugs']
return self.fillslots(request)
def get_events(self, request, payload):
@ -1738,6 +1746,20 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
for agenda_slug, event_slugs in events_by_agenda.items():
events |= get_events_from_slots(event_slugs, request, agendas_by_slug[agenda_slug], payload)
if 'subscribed' in request.query_params:
events_outside_subscriptions = events.difference(
events.filter(
agenda__subscriptions__user_external_id=payload['user_external_id'],
agenda__subscriptions__date_start__lt=F('start_datetime'),
agenda__subscriptions__date_end__gt=F('start_datetime'),
)
) # workaround exclude method bug https://code.djangoproject.com/ticket/29697
if events_outside_subscriptions.exists():
event_slugs = ', '.join(
'%s@%s' % (event.agenda.slug, event.slug) for event in events_outside_subscriptions
)
raise APIErrorBadRequest(N_('Some events are outside user subscriptions: %s'), event_slugs)
return events
def get_already_booked_events(self, user_external_id):

View File

@ -11,10 +11,12 @@ from chrono.agendas.models import (
Agenda,
Booking,
BookingColor,
Category,
Desk,
Event,
MeetingType,
Resource,
Subscription,
TimePeriod,
VirtualMember,
)
@ -2416,7 +2418,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
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'
'Events from the following agendas cannot be booked: first-agenda'
]
@ -2671,19 +2673,19 @@ def test_api_events_fillslots_multiple_agendas(app, user):
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
)
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: xxx, yyy'
]
assert resp.json['errors']['slots'] == ['Events from the following agendas cannot be booked: xxx, yyy']
# missing agendas parameter
resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
assert resp.json['errors']['agendas'] == ['This field is required.']
assert resp.json['errors']['non_field_errors'] == [
'Either "agendas" or "subscribed" parameter is required.'
]
# valid agendas parameter and event slugs, but mismatch between the two
params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: second-agenda'
'Events from the following agendas cannot be booked: second-agenda'
]
# missing @ in slot
@ -2754,6 +2756,129 @@ def test_api_events_fillslots_multiple_agendas_check_delays(app, user):
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
category = Category.objects.create(label='Category A')
first_agenda = Agenda.objects.create(label='First agenda', kind='events', category=category)
second_agenda = Agenda.objects.create(label='Second agenda', kind='events', category=category)
category = Category.objects.create(label='Category B')
third_agenda = Agenda.objects.create(label='Third agenda', kind='events', category=category)
for agenda in Agenda.objects.all():
Event.objects.create(
slug='event',
start_datetime=now() + datetime.timedelta(days=5),
places=5,
agenda=agenda,
)
Event.objects.create(
slug='event-2',
start_datetime=now() + datetime.timedelta(days=20),
places=5,
agenda=agenda,
)
# add subscriptions to first and second agenda
for agenda in (first_agenda, second_agenda):
Subscription.objects.create(
agenda=agenda,
user_external_id='xxx',
date_start=now(),
date_end=now() + datetime.timedelta(days=10),
)
# book events
app.authorization = ('Basic', ('john.doe', 'password'))
params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
assert resp.json['booking_count'] == 2
assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
assert Booking.objects.count() == 2
# update bookings for category-a
params = {'user_external_id': 'xxx', 'slots': 'second-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
assert Booking.objects.count() == 1
# try to book event from agenda with no subscription TODO messages
params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
for slug in ('all', 'category-a', 'category-b'):
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=%s' % slug, params=params, status=400)
assert (
resp.json['errors']['slots'][0]
== 'Events from the following agendas cannot be booked: third-agenda'
)
# add subscription to third agenda
Subscription.objects.create(
agenda=third_agenda,
user_external_id='xxx',
date_start=now(),
date_end=now() + datetime.timedelta(days=10),
)
params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-b', params=params)
assert resp.json['booking_count'] == 1
assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
assert Event.objects.get(agenda=third_agenda, slug='event').booking_set.count() == 1
assert Booking.objects.count() == 2
# add subscription to first agenda (disjoint) spanning event-2
for agenda in (first_agenda, second_agenda):
Subscription.objects.create(
agenda=agenda,
user_external_id='xxx',
date_start=now() + datetime.timedelta(days=15),
date_end=now() + datetime.timedelta(days=25),
)
# book event-2 while updating all bookings
params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event-2'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
assert resp.json['booking_count'] == 2
assert resp.json['cancelled_booking_count'] == 2
assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 1
assert Booking.objects.count() == 2
# other user
for agenda in (first_agenda, second_agenda):
Subscription.objects.create(
agenda=agenda,
user_external_id='yyy',
date_start=now(),
date_end=now() + datetime.timedelta(days=25),
)
params = {'user_external_id': 'yyy', 'slots': 'first-agenda@event,second-agenda@event-2'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
assert resp.json['booking_count'] == 2
assert resp.json['cancelled_booking_count'] == 0
assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 2
assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 2
assert Booking.objects.count() == 4
# try to book event outside subscription date range
params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event-2'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400)
assert resp.json['err_class'] == 'Some events are outside user subscriptions: third-agenda@event-2'
# mismatch between subscribed parameter and event
params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params, status=400)
assert (
resp.json['errors']['slots'][0] == 'Events from the following agendas cannot be booked: third-agenda'
)
# missing user_external_id
params = {'slots': 'third-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400)
assert 'required' in resp.json['errors']['user_external_id'][0]
def test_url_translation(app, some_data, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda_id = Agenda.objects.filter(label='Foo bar')[0].id