api: move MultipleAgendasEventsFillslots validation to serializers (#57957)

This commit is contained in:
Valentin Deniaud 2021-10-21 11:15:38 +02:00
parent d69c919ca8
commit 84f5dbc4c8
3 changed files with 81 additions and 28 deletions

View File

@ -65,6 +65,30 @@ class EventsSlotsSerializer(SlotSerializer):
return attrs
class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
def validate_slots(self, value):
allowed_agenda_slugs = self.context['allowed_agenda_slugs']
slots_agenda_slugs = set()
for slot in value:
try:
agenda_slug, event_slug = slot.split('@')
except ValueError:
raise ValidationError(_('Invalid format for slot %s') % slot)
if not agenda_slug:
raise ValidationError(_('Missing agenda slug in slot %s') % slot)
if not event_slug:
raise ValidationError(_('Missing event slug in slot %s') % slot)
slots_agenda_slugs.add(agenda_slug)
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)
)
return value
class BookingSerializer(serializers.ModelSerializer):
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
@ -139,6 +163,12 @@ class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
show_past_events = serializers.BooleanField(default=False)
class AgendaSlugsSerializer(serializers.Serializer):
agendas = CommaSeparatedStringField(
required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
)
class EventSerializer(serializers.ModelSerializer):
recurrence_days = CommaSeparatedStringField(
required=False, child=serializers.IntegerField(min_value=0, max_value=6)

View File

@ -676,6 +676,19 @@ def get_start_and_end_datetime_from_request(request):
return serializer.validated_data.get('date_start'), serializer.validated_data.get('date_end')
def get_agendas_from_request(request):
serializer = serializers.AgendaSlugsSerializer(data=request.query_params)
if not serializer.is_valid():
raise APIError(
_('invalid payload'),
err_class='invalid payload',
errors=serializer.errors,
http_status=status.HTTP_400_BAD_REQUEST,
)
return serializer.validated_data.get('agendas')
def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_list=False, color=None):
return Booking(
event_id=event.pk,
@ -1671,6 +1684,7 @@ recurring_fillslots = RecurringFillslots.as_view()
class EventsFillslots(APIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.EventsSlotsSerializer
serializer_extra_context = None
def post(self, request, agenda_identifier):
self.agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
@ -1679,7 +1693,9 @@ class EventsFillslots(APIView):
def fillslots(self, request):
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
serializer = self.serializer_class(data=request.data, partial=True)
serializer = self.serializer_class(
data=request.data, partial=True, context=self.serializer_extra_context
)
if not serializer.is_valid():
raise APIError(
_('invalid payload'),
@ -1743,15 +1759,10 @@ events_fillslots = EventsFillslots.as_view()
class MultipleAgendasEventsFillslots(EventsFillslots):
serializer_class = serializers.MultipleAgendasEventsSlotsSerializer
def post(self, request):
self.agendas = None
if 'agendas' not in request.GET:
raise APIError(
_('Missing agendas list in querystring'),
err_class='Missing agendas list in querystring',
http_status=status.HTTP_400_BAD_REQUEST,
)
self.agenda_slugs = [s for s in request.GET.get('agendas', '').split(',') if s]
self.agenda_slugs = get_agendas_from_request(request)
self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
return self.fillslots(request)
@ -1761,18 +1772,9 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
agenda, event = slot.split('@')
events_by_agenda[agenda].append(event)
agendas = get_objects_from_slugs(events_by_agenda.keys(), qs=Agenda.objects.filter(kind='events'))
agendas_by_slug = {agenda.slug: agenda for agenda in agendas}
if not set(agendas_by_slug).issubset(self.agenda_slugs):
extra_agendas = set(agendas_by_slug) - set(self.agenda_slugs)
extra_agendas = ','.join(extra_agendas)
raise APIError(
_('Some events belong to agendas that are not present in querystring: %s' % extra_agendas),
err_class='Some events belong to agendas that are not present in querystring: %s'
% extra_agendas,
http_status=status.HTTP_400_BAD_REQUEST,
)
agendas_by_slug = get_objects_from_slugs(
events_by_agenda.keys(), qs=Agenda.objects.filter(kind='events')
).in_bulk(field_name='slug')
events = Event.objects.none()
for agenda_slug, event_slugs in events_by_agenda.items():
@ -1783,6 +1785,10 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
def get_already_booked_events(self, user_external_id):
return Event.objects.filter(agenda__in=self.agendas, booking__user_external_id=user_external_id)
@property
def serializer_extra_context(self):
return {'allowed_agenda_slugs': self.agenda_slugs}
agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()

View File

@ -2566,29 +2566,46 @@ def test_api_events_fillslots_multiple_agendas(app, user):
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'some events are full: Event'
# invalid agenda slugs
# invalid agenda slugs in querystring
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=first-agenda,xxx,yyy', params=params, status=400
)
assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
# invalid agenda slugs in payload
params = {'user_external_id': 'user_id_3', 'slots': 'first-agenda@event,xxx@event,yyy@event'}
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
)
assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: xxx, yyy'
]
# missing agendas parameter
resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
assert resp.json['err_desc'] == 'Missing agendas list in querystring'
assert resp.json['errors']['agendas'] == ['This field 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['err_desc']
== 'Some events belong to agendas that are not present in querystring: second-agenda'
)
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: second-agenda'
]
# missing @ in slot
params['slots'] = 'first-agenda'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Invalid format for slot first-agenda']
# empty event slug
params['slots'] = 'first-agenda@'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Missing event slug in slot first-agenda@']
# empty agenda slug
params['slots'] = '@event'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Missing agenda slug in slot @event']
def test_api_events_fillslots_multiple_agendas_check_delays(app, user):