Projet

Général

Profil

0001-api-move-MultipleAgendasEventsFillslots-validation-t.patch

Valentin Deniaud, 21 octobre 2021 18:07

Télécharger (9,25 ko)

Voir les différences:

Subject: [PATCH 1/5] api: move MultipleAgendasEventsFillslots validation to
 serializers (#57957)

 chrono/api/serializers.py  | 30 ++++++++++++++++++++++++
 chrono/api/views.py        | 48 +++++++++++++++++++++-----------------
 tests/api/test_fillslot.py | 31 ++++++++++++++++++------
 3 files changed, 81 insertions(+), 28 deletions(-)
chrono/api/serializers.py
64 64
        return attrs
65 65

  
66 66

  
67
class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
68
    def validate_slots(self, value):
69
        allowed_agenda_slugs = self.context['allowed_agenda_slugs']
70
        slots_agenda_slugs = set()
71
        for slot in value:
72
            try:
73
                agenda_slug, event_slug = slot.split('@')
74
            except ValueError:
75
                raise ValidationError(_('Invalid format for slot %s') % slot)
76
            if not agenda_slug:
77
                raise ValidationError(_('Missing agenda slug in slot %s') % slot)
78
            if not event_slug:
79
                raise ValidationError(_('Missing event slug in slot %s') % slot)
80
            slots_agenda_slugs.add(agenda_slug)
81

  
82
        extra_agendas = slots_agenda_slugs - set(allowed_agenda_slugs)
83
        if extra_agendas:
84
            extra_agendas = ', '.join(sorted(extra_agendas))
85
            raise ValidationError(
86
                _('Some events belong to agendas that are not present in querystring: %s' % extra_agendas)
87
            )
88
        return value
89

  
90

  
67 91
class BookingSerializer(serializers.ModelSerializer):
68 92
    user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
69 93

  
......
137 161
    show_past_events = serializers.BooleanField(default=False)
138 162

  
139 163

  
164
class AgendaSlugsSerializer(serializers.Serializer):
165
    agendas = CommaSeparatedStringField(
166
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
167
    )
168

  
169

  
140 170
class EventSerializer(serializers.ModelSerializer):
141 171
    recurrence_days = CommaSeparatedStringField(
142 172
        required=False, child=serializers.IntegerField(min_value=0, max_value=6)
chrono/api/views.py
675 675
    return serializer.validated_data.get('date_start'), serializer.validated_data.get('date_end')
676 676

  
677 677

  
678
def get_agendas_from_request(request):
679
    serializer = serializers.AgendaSlugsSerializer(data=request.query_params)
680
    if not serializer.is_valid():
681
        raise APIError(
682
            _('invalid payload'),
683
            err_class='invalid payload',
684
            errors=serializer.errors,
685
            http_status=status.HTTP_400_BAD_REQUEST,
686
        )
687

  
688
    return serializer.validated_data.get('agendas')
689

  
690

  
678 691
def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_list=False, color=None):
679 692
    return Booking(
680 693
        event_id=event.pk,
......
1664 1677
class EventsFillslots(APIView):
1665 1678
    permission_classes = (permissions.IsAuthenticated,)
1666 1679
    serializer_class = serializers.EventsSlotsSerializer
1680
    serializer_extra_context = None
1667 1681

  
1668 1682
    def post(self, request, agenda_identifier):
1669 1683
        self.agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
......
1672 1686
    def fillslots(self, request):
1673 1687
        start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
1674 1688

  
1675
        serializer = self.serializer_class(data=request.data, partial=True)
1689
        serializer = self.serializer_class(
1690
            data=request.data, partial=True, context=self.serializer_extra_context
1691
        )
1676 1692
        if not serializer.is_valid():
1677 1693
            raise APIError(
1678 1694
                _('invalid payload'),
......
1736 1752

  
1737 1753

  
1738 1754
class MultipleAgendasEventsFillslots(EventsFillslots):
1755
    serializer_class = serializers.MultipleAgendasEventsSlotsSerializer
1756

  
1739 1757
    def post(self, request):
1740
        self.agendas = None
1741
        if 'agendas' not in request.GET:
1742
            raise APIError(
1743
                _('Missing agendas list in querystring'),
1744
                err_class='Missing agendas list in querystring',
1745
                http_status=status.HTTP_400_BAD_REQUEST,
1746
            )
1747
        self.agenda_slugs = [s for s in request.GET.get('agendas', '').split(',') if s]
1758
        self.agenda_slugs = get_agendas_from_request(request)
1748 1759
        self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
1749 1760
        return self.fillslots(request)
1750 1761

  
......
1754 1765
            agenda, event = slot.split('@')
1755 1766
            events_by_agenda[agenda].append(event)
1756 1767

  
1757
        agendas = get_objects_from_slugs(events_by_agenda.keys(), qs=Agenda.objects.filter(kind='events'))
1758
        agendas_by_slug = {agenda.slug: agenda for agenda in agendas}
1759

  
1760
        if not set(agendas_by_slug).issubset(self.agenda_slugs):
1761
            extra_agendas = set(agendas_by_slug) - set(self.agenda_slugs)
1762
            extra_agendas = ','.join(extra_agendas)
1763
            raise APIError(
1764
                _('Some events belong to agendas that are not present in querystring: %s' % extra_agendas),
1765
                err_class='Some events belong to agendas that are not present in querystring: %s'
1766
                % extra_agendas,
1767
                http_status=status.HTTP_400_BAD_REQUEST,
1768
            )
1768
        agendas_by_slug = get_objects_from_slugs(
1769
            events_by_agenda.keys(), qs=Agenda.objects.filter(kind='events')
1770
        ).in_bulk(field_name='slug')
1769 1771

  
1770 1772
        events = Event.objects.none()
1771 1773
        for agenda_slug, event_slugs in events_by_agenda.items():
......
1776 1778
    def get_already_booked_events(self, user_external_id):
1777 1779
        return Event.objects.filter(agenda__in=self.agendas, booking__user_external_id=user_external_id)
1778 1780

  
1781
    @property
1782
    def serializer_extra_context(self):
1783
        return {'allowed_agenda_slugs': self.agenda_slugs}
1784

  
1779 1785

  
1780 1786
agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()
1781 1787

  
tests/api/test_fillslot.py
2487 2487
    assert resp.json['err'] == 1
2488 2488
    assert resp.json['err_desc'] == 'some events are full: Event'
2489 2489

  
2490
    # invalid agenda slugs
2490
    # invalid agenda slugs in querystring
2491 2491
    resp = app.post_json(
2492 2492
        '/api/agendas/events/fillslots/?agendas=first-agenda,xxx,yyy', params=params, status=400
2493 2493
    )
2494 2494
    assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
2495 2495

  
2496
    # invalid agenda slugs in payload
2496 2497
    params = {'user_external_id': 'user_id_3', 'slots': 'first-agenda@event,xxx@event,yyy@event'}
2497 2498
    resp = app.post_json(
2498 2499
        '/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
2499 2500
    )
2500
    assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
2501
    assert resp.json['errors']['slots'] == [
2502
        'Some events belong to agendas that are not present in querystring: xxx, yyy'
2503
    ]
2501 2504

  
2502 2505
    # missing agendas parameter
2503 2506
    resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
2504
    assert resp.json['err_desc'] == 'Missing agendas list in querystring'
2507
    assert resp.json['errors']['agendas'] == ['This field is required.']
2505 2508

  
2506 2509
    # valid agendas parameter and event slugs, but mismatch between the two
2507 2510
    params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
2508 2511
    resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
2509
    assert (
2510
        resp.json['err_desc']
2511
        == 'Some events belong to agendas that are not present in querystring: second-agenda'
2512
    )
2512
    assert resp.json['errors']['slots'] == [
2513
        'Some events belong to agendas that are not present in querystring: second-agenda'
2514
    ]
2515

  
2516
    # missing @ in slot
2517
    params['slots'] = 'first-agenda'
2518
    resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
2519
    assert resp.json['errors']['slots'] == ['Invalid format for slot first-agenda']
2520

  
2521
    # empty event slug
2522
    params['slots'] = 'first-agenda@'
2523
    resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
2524
    assert resp.json['errors']['slots'] == ['Missing event slug in slot first-agenda@']
2525

  
2526
    # empty agenda slug
2527
    params['slots'] = '@event'
2528
    resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
2529
    assert resp.json['errors']['slots'] == ['Missing agenda slug in slot @event']
2513 2530

  
2514 2531

  
2515 2532
def test_url_translation(app, some_data, user):
2516
-