Projet

Général

Profil

0001-api-filter-by-subscriptions-in-multiple-agendas-date.patch

Valentin Deniaud, 25 novembre 2021 17:53

Télécharger (13 ko)

Voir les différences:

Subject: [PATCH] api: filter by subscriptions in multiple agendas datetimes
 (#58444)

 chrono/agendas/models.py    |  12 +++-
 chrono/api/serializers.py   |  17 ++++-
 chrono/api/views.py         |  36 +++++++---
 tests/api/test_datetimes.py | 128 +++++++++++++++++++++++++++++++++++-
 4 files changed, 182 insertions(+), 11 deletions(-)
chrono/agendas/models.py
898 898

  
899 899
    @staticmethod
900 900
    def prefetch_events_and_exceptions(
901
        qs, user_external_id=None, show_past_events=False, min_start=None, max_start=None
901
        qs,
902
        user_external_id=None,
903
        show_past_events=False,
904
        prefetch_subscriptions=False,
905
        min_start=None,
906
        max_start=None,
902 907
    ):
903 908
        event_queryset = Event.objects.filter(
904 909
            Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now()),
......
931 936
            ),
932 937
        )
933 938
        qs = Agenda.prefetch_recurring_events(qs)
939
        if prefetch_subscriptions:
940
            subscriptions = Subscription.objects.filter(user_external_id=user_external_id)
941
            qs = qs.prefetch_related(
942
                Prefetch('subscriptions', queryset=subscriptions, to_attr='user_subscriptions')
943
            )
934 944
        agendas_exceptions = TimePeriodException.objects.filter(
935 945
            Q(desk__slug='_exceptions_holder', desk__agenda__in=qs)
936 946
            | Q(
chrono/api/serializers.py
192 192

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

  
202
    def validate(self, attrs):
203
        super().validate(attrs)
204
        if 'agendas' not in attrs and 'subscribed' not in attrs:
205
            raise ValidationError(_('Either "agendas" or "subscribed" parameter is required.'))
206
        if 'agendas' in attrs and 'subscribed' in attrs:
207
            raise ValidationError(_('"agendas" and "subscribed" parameters are mutually exclusive.'))
208
        if 'subscribed' in attrs and 'user_external_id' not in attrs:
209
            raise ValidationError(
210
                {'user_external_id': _('This field is required when using "subscribed" parameter.')}
211
            )
212
        return attrs
213

  
199 214

  
200 215
class AgendaSlugsSerializer(serializers.Serializer):
201 216
    agendas = CommaSeparatedStringField(
chrono/api/views.py
866 866
            raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
867 867
        payload = serializer.validated_data
868 868

  
869
        agenda_slugs = payload['agendas']
870
        agendas = get_objects_from_slugs(agenda_slugs, qs=Agenda.objects.filter(kind='events'))
871

  
872 869
        user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
870
        if 'agendas' in payload:
871
            agenda_slugs = payload['agendas']
872
            agendas = get_objects_from_slugs(agenda_slugs, qs=Agenda.objects.filter(kind='events'))
873
        else:
874
            subscribed = payload['subscribed']
875
            agendas = Agenda.objects.filter(subscriptions__user_external_id=user_external_id).distinct()
876
            if subscribed != ['all']:
877
                agendas = agendas.filter(category__slug__in=subscribed)
878

  
873 879
        disable_booked = bool(payload.get('exclude_user_external_id'))
874 880
        show_past_events = bool(payload.get('show_past_events'))
875 881
        agendas = Agenda.prefetch_events_and_exceptions(
876 882
            agendas,
877 883
            user_external_id=user_external_id,
878 884
            show_past_events=show_past_events,
885
            prefetch_subscriptions=bool('subscribed' in payload),
879 886
            min_start=payload.get('date_start'),
880 887
            max_start=payload.get('date_end'),
881 888
        )
882 889

  
883 890
        entries = []
884 891
        for agenda in agendas:
892
            agenda_entries = []
885 893
            if show_past_events:
886
                entries.extend(
894
                agenda_entries.extend(
887 895
                    agenda.get_past_events(
888 896
                        prefetched_queryset=True,
889 897
                        min_start=payload.get('date_start'),
890 898
                        max_start=payload.get('date_end'),
891 899
                    )
892 900
                )
893
            entries.extend(
901
            agenda_entries.extend(
894 902
                agenda.get_open_events(
895 903
                    prefetched_queryset=True,
896 904
                    min_start=payload.get('date_start'),
......
899 907
                    show_out_of_minimal_delay=show_past_events,
900 908
                )
901 909
            )
902

  
903
        agenda_querystring_indexes = {agenda_slug: i for i, agenda_slug in enumerate(agenda_slugs)}
904
        entries.sort(key=lambda event: (event.start_datetime, agenda_querystring_indexes[event.agenda.slug]))
910
            if 'subscribed' in payload:
911
                filtered_entries = []
912
                for entry in agenda_entries:
913
                    for subscription in agenda.user_subscriptions:
914
                        if subscription.date_start <= entry.start_datetime.date() <= subscription.date_end:
915
                            filtered_entries.append(entry)
916
                            break
917
                agenda_entries = filtered_entries
918
            entries.extend(agenda_entries)
919

  
920
        if 'agendas' in payload:
921
            agenda_querystring_indexes = {agenda_slug: i for i, agenda_slug in enumerate(agenda_slugs)}
922
            entries.sort(
923
                key=lambda event: (event.start_datetime, agenda_querystring_indexes[event.agenda.slug])
924
            )
905 925

  
906 926
        response = {
907 927
            'data': [
tests/api/test_datetimes.py
7 7
from django.test.utils import CaptureQueriesContext
8 8
from django.utils.timezone import localtime, make_aware, make_naive, now
9 9

  
10
from chrono.agendas.models import Agenda, Booking, Desk, Event, TimePeriodException
10
from chrono.agendas.models import Agenda, Booking, Category, Desk, Event, Subscription, TimePeriodException
11 11

  
12 12
pytestmark = pytest.mark.django_db
13 13

  
......
1589 1589
    resp = app.get('/api/agendas/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=400)
1590 1590
    assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
1591 1591

  
1592
    # missing agendas parameter
1593
    resp = app.get('/api/agendas/datetimes/', params={}, status=400)
1594
    assert resp.json['err_desc'] == 'invalid payload'
1595

  
1592 1596
    # it's possible to show past events
1593 1597
    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
1594 1598
    assert len(resp.json['data']) == 5
......
1753 1757
def test_datetimes_multiple_agendas_queries(app):
1754 1758
    for i in range(10):
1755 1759
        agenda = Agenda.objects.create(label=str(i), kind='events')
1760
        Subscription.objects.create(
1761
            agenda=agenda,
1762
            user_external_id='xxx',
1763
            date_start=now() - datetime.timedelta(days=10),
1764
            date_end=now() + datetime.timedelta(days=10),
1765
        )
1756 1766
        Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
1757 1767
        Event.objects.create(start_datetime=now() - datetime.timedelta(days=5), places=5, agenda=agenda)
1758 1768
        Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
......
1765 1775
        )
1766 1776
        assert len(resp.json['data']) == 30
1767 1777
        assert len(ctx.captured_queries) == 7
1778

  
1779
    with CaptureQueriesContext(connection) as ctx:
1780
        resp = app.get(
1781
            '/api/agendas/datetimes/',
1782
            params={'subscribed': 'all', 'user_external_id': 'xxx', 'show_past_events': True},
1783
        )
1784
        assert len(resp.json['data']) == 30
1785
        assert len(ctx.captured_queries) == 7
1786

  
1787

  
1788
@pytest.mark.freeze_time('2021-05-06 14:00')
1789
def test_datetimes_multiple_agendas_subscribed(app):
1790
    first_agenda = Agenda.objects.create(label='First agenda', kind='events')
1791
    Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
1792
    Event.objects.create(
1793
        slug='event',
1794
        start_datetime=now() + datetime.timedelta(days=5),
1795
        places=5,
1796
        agenda=first_agenda,
1797
    )
1798
    Event.objects.create(
1799
        slug='event-2',
1800
        start_datetime=now() + datetime.timedelta(days=20),
1801
        places=5,
1802
        agenda=first_agenda,
1803
    )
1804
    category = Category.objects.create(label='Category A')
1805
    second_agenda = Agenda.objects.create(
1806
        label='Second agenda', kind='events', category=category, maximal_booking_delay=400
1807
    )
1808
    Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
1809
    Event.objects.create(
1810
        slug='event',
1811
        start_datetime=now() + datetime.timedelta(days=20),
1812
        places=5,
1813
        agenda=second_agenda,
1814
    )
1815
    Event.objects.create(
1816
        slug='next-year-event',
1817
        start_datetime=now() + datetime.timedelta(days=365),
1818
        places=5,
1819
        agenda=second_agenda,
1820
    )
1821

  
1822
    # no subscription
1823
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1824
    assert len(resp.json['data']) == 0
1825

  
1826
    # add subscription to first agenda
1827
    Subscription.objects.create(
1828
        agenda=first_agenda,
1829
        user_external_id='xxx',
1830
        date_start=now(),
1831
        date_end=now() + datetime.timedelta(days=10),
1832
    )
1833

  
1834
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1835
    assert len(resp.json['data']) == 1
1836
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1837

  
1838
    # no subscription to second agenda
1839
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'category-a', 'user_external_id': 'xxx'})
1840
    assert len(resp.json['data']) == 0
1841

  
1842
    # add subscription to second agenda
1843
    Subscription.objects.create(
1844
        agenda=second_agenda,
1845
        user_external_id='xxx',
1846
        date_start=now() + datetime.timedelta(days=15),
1847
        date_end=now() + datetime.timedelta(days=25),
1848
    )
1849

  
1850
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'category-a', 'user_external_id': 'xxx'})
1851
    assert len(resp.json['data']) == 1
1852
    assert resp.json['data'][0]['id'] == 'second-agenda@event'
1853

  
1854
    # add new subscription to second agenda
1855
    Subscription.objects.create(
1856
        agenda=second_agenda,
1857
        user_external_id='xxx',
1858
        date_start=now() + datetime.timedelta(days=355),
1859
        date_end=now() + datetime.timedelta(days=375),
1860
    )
1861

  
1862
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'category-a', 'user_external_id': 'xxx'})
1863
    assert len(resp.json['data']) == 2
1864
    assert resp.json['data'][0]['id'] == 'second-agenda@event'
1865
    assert resp.json['data'][1]['id'] == 'second-agenda@next-year-event'
1866

  
1867
    # view events from all subscriptions
1868
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1869
    assert len(resp.json['data']) == 3
1870
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1871
    assert resp.json['data'][1]['id'] == 'second-agenda@event'
1872
    assert resp.json['data'][2]['id'] == 'second-agenda@next-year-event'
1873

  
1874
    # overlapping subscription changes nothing
1875
    Subscription.objects.create(
1876
        agenda=first_agenda,
1877
        user_external_id='xxx',
1878
        date_start=now() + datetime.timedelta(days=1),
1879
        date_end=now() + datetime.timedelta(days=11),
1880
    )
1881
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1882
    assert len(resp.json['data']) == 3
1883

  
1884
    # check errors
1885
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all'}, status=400)
1886
    assert 'required' in resp.json['errors']['user_external_id'][0]
1887

  
1888
    resp = app.get(
1889
        '/api/agendas/datetimes/',
1890
        params={'subscribed': 'all', 'agendas': 'xxx', 'user_external_id': 'xxx'},
1891
        status=400,
1892
    )
1893
    assert 'mutually exclusive' in resp.json['errors']['non_field_errors'][0]
1768
-