0001-api-filter-by-subscriptions-in-multiple-agendas-date.patch
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 |
- |