Projet

Général

Profil

0002-api-add-support-for-subscriptions-in-multiple-agenda.patch

Valentin Deniaud, 30 novembre 2021 10:51

Télécharger (14 ko)

Voir les différences:

Subject: [PATCH 2/2] api: add support for subscriptions in multiple agendas
 fillslots (#58446)

 chrono/api/serializers.py  |  19 +++--
 chrono/api/views.py        |  32 ++++++++-
 tests/api/test_fillslot.py | 139 +++++++++++++++++++++++++++++++++++--
 3 files changed, 177 insertions(+), 13 deletions(-)
chrono/api/serializers.py
85 85
        extra_agendas = slots_agenda_slugs - set(allowed_agenda_slugs)
86 86
        if extra_agendas:
87 87
            extra_agendas = ', '.join(sorted(extra_agendas))
88
            raise ValidationError(
89
                _('Some events belong to agendas that are not present in querystring: %s' % extra_agendas)
90
            )
88
            raise ValidationError(_('Events from the following agendas cannot be booked: %s') % extra_agendas)
91 89
        return value
92 90

  
93 91

  
......
190 188
        return attrs
191 189

  
192 190

  
193
class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
191
class AgendaOrSubscribedSlugsMixin(metaclass=serializers.SerializerMetaclass):
194 192
    agendas = CommaSeparatedStringField(
195 193
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
196 194
    )
197 195
    subscribed = CommaSeparatedStringField(
198 196
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
199 197
    )
200
    show_past_events = serializers.BooleanField(default=False)
201 198

  
202 199
    def validate(self, attrs):
203 200
        super().validate(attrs)
......
205 202
            raise ValidationError(_('Either "agendas" or "subscribed" parameter is required.'))
206 203
        if 'agendas' in attrs and 'subscribed' in attrs:
207 204
            raise ValidationError(_('"agendas" and "subscribed" parameters are mutually exclusive.'))
205
        return attrs
206

  
207

  
208
class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, DatetimesSerializer):
209
    show_past_events = serializers.BooleanField(default=False)
210

  
211
    def validate(self, attrs):
212
        super().validate(attrs)
208 213
        if 'subscribed' in attrs and 'user_external_id' not in attrs:
209 214
            raise ValidationError(
210 215
                {'user_external_id': _('This field is required when using "subscribed" parameter.')}
......
212 217
        return attrs
213 218

  
214 219

  
220
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
221
    pass
222

  
223

  
215 224
class AgendaSlugsSerializer(serializers.Serializer):
216 225
    agendas = CommaSeparatedStringField(
217 226
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
chrono/api/views.py
1702 1702
    serializer_class = serializers.MultipleAgendasEventsSlotsSerializer
1703 1703

  
1704 1704
    def post(self, request):
1705
        self.agenda_slugs = get_agendas_from_request(request)
1706
        self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
1705
        serializer = serializers.AgendaOrSubscribedSlugsSerializer(data=request.query_params)
1706
        if not serializer.is_valid():
1707
            raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
1708
        params = serializer.validated_data
1709

  
1710
        if 'agendas' in params:
1711
            self.agenda_slugs = params['agendas']
1712
            self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
1713
        elif 'subscribed' in params:
1714
            user_external_id = request.data.get('user_external_id')
1715

  
1716
            self.agendas = Agenda.objects.filter(subscriptions__user_external_id=user_external_id).distinct()
1717
            if params['subscribed'] != ['all']:
1718
                self.agendas = self.agendas.filter(category__slug__in=params['subscribed'])
1719

  
1720
            self.agenda_slugs = [agenda.slug for agenda in self.agendas]
1707 1721
        return self.fillslots(request)
1708 1722

  
1709 1723
    def get_events(self, request, payload):
......
1720 1734
        for agenda_slug, event_slugs in events_by_agenda.items():
1721 1735
            events |= get_events_from_slots(event_slugs, request, agendas_by_slug[agenda_slug], payload)
1722 1736

  
1737
        if 'subscribed' in request.query_params:
1738
            events_outside_subscriptions = events.difference(
1739
                events.filter(
1740
                    agenda__subscriptions__user_external_id=payload['user_external_id'],
1741
                    agenda__subscriptions__date_start__lt=F('start_datetime'),
1742
                    agenda__subscriptions__date_end__gt=F('start_datetime'),
1743
                )
1744
            )  # workaround exclude method bug https://code.djangoproject.com/ticket/29697
1745
            if events_outside_subscriptions.exists():
1746
                event_slugs = ', '.join(
1747
                    '%s@%s' % (event.agenda.slug, event.slug) for event in events_outside_subscriptions
1748
                )
1749
                raise APIErrorBadRequest(N_('Some events are outside user subscriptions: %s'), event_slugs)
1750

  
1723 1751
        return events
1724 1752

  
1725 1753
    def get_already_booked_events(self, user_external_id):
tests/api/test_fillslot.py
11 11
    Agenda,
12 12
    Booking,
13 13
    BookingColor,
14
    Category,
14 15
    Desk,
15 16
    Event,
16 17
    MeetingType,
17 18
    Resource,
19
    Subscription,
18 20
    TimePeriod,
19 21
    VirtualMember,
20 22
)
......
2416 2418
    resp = app.post_json(fillslots_url % 'second-agenda', params=params, status=400)
2417 2419
    assert resp.json['err'] == 1
2418 2420
    assert resp.json['errors']['slots'] == [
2419
        'Some events belong to agendas that are not present in querystring: first-agenda'
2421
        'Events from the following agendas cannot be booked: first-agenda'
2420 2422
    ]
2421 2423

  
2422 2424

  
......
2671 2673
    resp = app.post_json(
2672 2674
        '/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
2673 2675
    )
2674
    assert resp.json['errors']['slots'] == [
2675
        'Some events belong to agendas that are not present in querystring: xxx, yyy'
2676
    ]
2676
    assert resp.json['errors']['slots'] == ['Events from the following agendas cannot be booked: xxx, yyy']
2677 2677

  
2678 2678
    # missing agendas parameter
2679 2679
    resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
2680
    assert resp.json['errors']['agendas'] == ['This field is required.']
2680
    assert resp.json['errors']['non_field_errors'] == [
2681
        'Either "agendas" or "subscribed" parameter is required.'
2682
    ]
2681 2683

  
2682 2684
    # valid agendas parameter and event slugs, but mismatch between the two
2683 2685
    params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
2684 2686
    resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
2685 2687
    assert resp.json['errors']['slots'] == [
2686
        'Some events belong to agendas that are not present in querystring: second-agenda'
2688
        'Events from the following agendas cannot be booked: second-agenda'
2687 2689
    ]
2688 2690

  
2689 2691
    # missing @ in slot
......
2754 2756
    assert resp.json['err'] == 0
2755 2757

  
2756 2758

  
2759
@pytest.mark.freeze_time('2021-09-06 12:00')
2760
def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
2761
    category = Category.objects.create(label='Category A')
2762
    first_agenda = Agenda.objects.create(label='First agenda', kind='events', category=category)
2763
    second_agenda = Agenda.objects.create(label='Second agenda', kind='events', category=category)
2764
    category = Category.objects.create(label='Category B')
2765
    third_agenda = Agenda.objects.create(label='Third agenda', kind='events', category=category)
2766
    for agenda in Agenda.objects.all():
2767
        Event.objects.create(
2768
            slug='event',
2769
            start_datetime=now() + datetime.timedelta(days=5),
2770
            places=5,
2771
            agenda=agenda,
2772
        )
2773
        Event.objects.create(
2774
            slug='event-2',
2775
            start_datetime=now() + datetime.timedelta(days=20),
2776
            places=5,
2777
            agenda=agenda,
2778
        )
2779

  
2780
    # add subscriptions to first and second agenda
2781
    for agenda in (first_agenda, second_agenda):
2782
        Subscription.objects.create(
2783
            agenda=agenda,
2784
            user_external_id='xxx',
2785
            date_start=now(),
2786
            date_end=now() + datetime.timedelta(days=10),
2787
        )
2788

  
2789
    # book events
2790
    app.authorization = ('Basic', ('john.doe', 'password'))
2791
    params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event'}
2792
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
2793
    assert resp.json['booking_count'] == 2
2794
    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
2795
    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
2796
    assert Booking.objects.count() == 2
2797

  
2798
    # update bookings for category-a
2799
    params = {'user_external_id': 'xxx', 'slots': 'second-agenda@event'}
2800
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
2801
    assert resp.json['booking_count'] == 0
2802
    assert resp.json['cancelled_booking_count'] == 1
2803
    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
2804
    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
2805
    assert Booking.objects.count() == 1
2806

  
2807
    # try to book event from agenda with no subscription TODO messages
2808
    params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
2809
    for slug in ('all', 'category-a', 'category-b'):
2810
        resp = app.post_json('/api/agendas/events/fillslots/?subscribed=%s' % slug, params=params, status=400)
2811
        assert (
2812
            resp.json['errors']['slots'][0]
2813
            == 'Events from the following agendas cannot be booked: third-agenda'
2814
        )
2815

  
2816
    # add subscription to third agenda
2817
    Subscription.objects.create(
2818
        agenda=third_agenda,
2819
        user_external_id='xxx',
2820
        date_start=now(),
2821
        date_end=now() + datetime.timedelta(days=10),
2822
    )
2823
    params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
2824
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-b', params=params)
2825
    assert resp.json['booking_count'] == 1
2826
    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
2827
    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
2828
    assert Event.objects.get(agenda=third_agenda, slug='event').booking_set.count() == 1
2829
    assert Booking.objects.count() == 2
2830

  
2831
    # add subscription to first agenda spanning event-2
2832
    for agenda in (first_agenda, second_agenda):
2833
        Subscription.objects.create(
2834
            agenda=agenda,
2835
            user_external_id='xxx',
2836
            date_start=now() + datetime.timedelta(days=15),
2837
            date_end=now() + datetime.timedelta(days=25),
2838
        )
2839
    # book event-2 while updating all bookings
2840
    params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event-2'}
2841
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
2842
    assert resp.json['booking_count'] == 2
2843
    assert resp.json['cancelled_booking_count'] == 2
2844
    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
2845
    assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 1
2846
    assert Booking.objects.count() == 2
2847

  
2848
    # other user
2849
    for agenda in (first_agenda, second_agenda):
2850
        Subscription.objects.create(
2851
            agenda=agenda,
2852
            user_external_id='yyy',
2853
            date_start=now(),
2854
            date_end=now() + datetime.timedelta(days=25),
2855
        )
2856
    params = {'user_external_id': 'yyy', 'slots': 'first-agenda@event,second-agenda@event-2'}
2857
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
2858
    assert resp.json['booking_count'] == 2
2859
    assert resp.json['cancelled_booking_count'] == 0
2860
    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 2
2861
    assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 2
2862
    assert Booking.objects.count() == 4
2863

  
2864
    # try to book event outside subscription date range
2865
    params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event-2'}
2866
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400)
2867
    assert resp.json['err_class'] == 'Some events are outside user subscriptions: third-agenda@event-2'
2868

  
2869
    # mismatch between subscribed parameter and event
2870
    params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
2871
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params, status=400)
2872
    assert (
2873
        resp.json['errors']['slots'][0] == 'Events from the following agendas cannot be booked: third-agenda'
2874
    )
2875

  
2876
    # missing user_external_id
2877
    params = {'slots': 'third-agenda@event'}
2878
    resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400)
2879
    assert (
2880
        resp.json['errors']['slots'][0] == 'Events from the following agendas cannot be booked: third-agenda'
2881
    )
2882

  
2883

  
2757 2884
def test_url_translation(app, some_data, user):
2758 2885
    app.authorization = ('Basic', ('john.doe', 'password'))
2759 2886
    agenda_id = Agenda.objects.filter(label='Foo bar')[0].id
2760
-