Projet

Général

Profil

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

Valentin Deniaud, 30 novembre 2021 10:51

Télécharger (12,7 ko)

Voir les différences:

Subject: [PATCH 1/2] api: filter by subscriptions in multiple agendas
 datetimes (#58446)

 chrono/agendas/models.py    |  15 +++-
 chrono/api/serializers.py   |  17 ++++-
 chrono/api/views.py         |  20 ++++--
 tests/api/test_datetimes.py | 134 +++++++++++++++++++++++++++++++++++-
 4 files changed, 177 insertions(+), 9 deletions(-)
chrono/agendas/models.py
33 33
from django.core.exceptions import FieldDoesNotExist, ValidationError
34 34
from django.core.validators import MaxValueValidator, MinValueValidator
35 35
from django.db import IntegrityError, connection, models, transaction
36
from django.db.models import Count, Max, Prefetch, Q
36
from django.db.models import Count, F, Max, Prefetch, Q
37 37
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
38 38
from django.urls import reverse
39 39
from django.utils import functional
......
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
        show_only_subscribed=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()),
......
914 919
            event_queryset = event_queryset.filter(start_datetime__gte=min_start)
915 920
        if max_start:
916 921
            event_queryset = event_queryset.filter(start_datetime__lt=max_start)
922
        if show_only_subscribed:
923
            event_queryset = event_queryset.filter(
924
                agenda__subscriptions__user_external_id=user_external_id,
925
                agenda__subscriptions__date_start__lt=F('start_datetime'),
926
                agenda__subscriptions__date_end__gt=F('start_datetime'),
927
            )
917 928

  
918 929
        exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
919 930
            'unavailability_calendars'
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
            show_only_subscribed=bool('subscribed' in payload),
879 886
            min_start=payload.get('date_start'),
880 887
            max_start=payload.get('date_end'),
881 888
        )
......
900 907
                )
901 908
            )
902 909

  
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 'agendas' in payload:
911
            agenda_querystring_indexes = {agenda_slug: i for i, agenda_slug in enumerate(agenda_slugs)}
912
            entries.sort(
913
                key=lambda event: (event.start_datetime, agenda_querystring_indexes[event.agenda.slug])
914
            )
905 915

  
906 916
        response = {
907 917
            '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) == 6
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
    Subscription.objects.create(
1822
        agenda=first_agenda,
1823
        user_external_id='yyy',
1824
        date_start=now(),
1825
        date_end=now() + datetime.timedelta(days=10),
1826
    )
1827

  
1828
    # no subscription for user xxx
1829
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1830
    assert len(resp.json['data']) == 0
1831

  
1832
    # add subscription to first agenda
1833
    Subscription.objects.create(
1834
        agenda=first_agenda,
1835
        user_external_id='xxx',
1836
        date_start=now(),
1837
        date_end=now() + datetime.timedelta(days=10),
1838
    )
1839

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

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

  
1848
    # add subscription to second agenda
1849
    Subscription.objects.create(
1850
        agenda=second_agenda,
1851
        user_external_id='xxx',
1852
        date_start=now() + datetime.timedelta(days=15),
1853
        date_end=now() + datetime.timedelta(days=25),
1854
    )
1855

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

  
1860
    # add new subscription to second agenda
1861
    Subscription.objects.create(
1862
        agenda=second_agenda,
1863
        user_external_id='xxx',
1864
        date_start=now() + datetime.timedelta(days=355),
1865
        date_end=now() + datetime.timedelta(days=375),
1866
    )
1867

  
1868
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'category-a', 'user_external_id': 'xxx'})
1869
    assert len(resp.json['data']) == 2
1870
    assert resp.json['data'][0]['id'] == 'second-agenda@event'
1871
    assert resp.json['data'][1]['id'] == 'second-agenda@next-year-event'
1872

  
1873
    # view events from all subscriptions
1874
    resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'xxx'})
1875
    assert len(resp.json['data']) == 3
1876
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1877
    assert resp.json['data'][1]['id'] == 'second-agenda@event'
1878
    assert resp.json['data'][2]['id'] == 'second-agenda@next-year-event'
1879

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

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

  
1894
    resp = app.get(
1895
        '/api/agendas/datetimes/',
1896
        params={'subscribed': 'all', 'agendas': 'xxx', 'user_external_id': 'xxx'},
1897
        status=400,
1898
    )
1899
    assert 'mutually exclusive' in resp.json['errors']['non_field_errors'][0]
1768
-