api: prefetch events in multiple agendas datetimes (#55370)

This commit is contained in:
Valentin Deniaud 2021-08-05 17:17:34 +02:00
parent e86d0cb11f
commit fa34281ade
3 changed files with 70 additions and 53 deletions

View File

@ -32,7 +32,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import IntegrityError, connection, models, transaction
from django.db.models import Count, IntegerField, Max, OuterRef, Q, Subquery, Value
from django.db.models import Count, IntegerField, Max, OuterRef, Prefetch, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
from django.urls import reverse
@ -877,6 +877,62 @@ class Agenda(models.Model):
if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
]
@staticmethod
def prefetch_events_and_exceptions(qs, annotate_events=False, user_external_id=None):
event_queryset = Event.objects.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
cancelled=False,
start_datetime__gte=localtime(now()),
).order_by()
if user_external_id:
event_queryset = Event.annotate_queryset_for_user(event_queryset, user_external_id)
if annotate_events:
event_queryset = Event.annotate_queryset(event_queryset)
recurring_event_queryset = Event.objects.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
recurrence_days__isnull=False,
)
exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
'unavailability_calendars'
)
qs = qs.filter(kind='events').prefetch_related(
Prefetch(
'event_set',
queryset=event_queryset,
to_attr='prefetched_events',
),
Prefetch(
'event_set',
queryset=recurring_event_queryset,
to_attr='prefetched_recurring_events',
),
Prefetch(
'desk_set',
queryset=exceptions_desk,
to_attr='prefetched_desks',
),
)
agendas_exceptions = TimePeriodException.objects.filter(
Q(desk__slug='_exceptions_holder', desk__agenda__in=qs)
| Q(
unavailability_calendar__desks__slug='_exceptions_holder',
unavailability_calendar__desks__agenda__in=qs,
),
start_datetime__gte=localtime(now()),
)
agendas = list(qs)
for agenda in agendas:
desk = agenda.prefetched_desks[0]
uc_ids = [uc.pk for uc in desk.unavailability_calendars.all()]
agenda.prefetched_exceptions = [
e
for e in agendas_exceptions
if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
]
return agendas
def is_available_for_simple_management(self):
if self.kind != 'meetings':
return False

View File

@ -681,57 +681,15 @@ class Agendas(APIView):
with_open_events = request.GET.get('with_open_events') in ['1', 'true']
if with_open_events:
# return only events agenda
event_queryset = Event.objects.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
cancelled=False,
start_datetime__gte=localtime(now()),
).order_by()
recurring_event_queryset = Event.objects.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
recurrence_days__isnull=False,
)
exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
'unavailability_calendars'
)
agendas_queryset = agendas_queryset.filter(kind='events').prefetch_related(
Prefetch(
'event_set',
queryset=event_queryset,
to_attr='prefetched_events',
),
Prefetch(
'event_set',
queryset=recurring_event_queryset,
to_attr='prefetched_recurring_events',
),
Prefetch(
'desk_set',
queryset=exceptions_desk,
to_attr='prefetched_desks',
),
)
agendas_exceptions = TimePeriodException.objects.filter(
Q(desk__slug='_exceptions_holder', desk__agenda__in=agendas_queryset)
| Q(
unavailability_calendar__desks__slug='_exceptions_holder',
unavailability_calendar__desks__agenda__in=agendas_queryset,
),
start_datetime__gte=localtime(now()),
)
agendas_queryset = Agenda.prefetch_events_and_exceptions(agendas_queryset)
agendas = []
for agenda in agendas_queryset:
if with_open_events:
desk = agenda.prefetched_desks[0]
uc_ids = [uc.pk for uc in desk.unavailability_calendars.all()]
agenda.prefetched_exceptions = [
e
for e in agendas_exceptions
if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
]
if not any(not e.full for e in agenda.get_open_events(prefetched_queryset=True)):
# exclude agendas without open events
continue
if with_open_events and not any(
not e.full for e in agenda.get_open_events(prefetched_queryset=True)
):
# exclude agendas without open events
continue
agendas.append(get_agenda_detail(request, agenda))
return Response({'data': agendas})
@ -852,7 +810,7 @@ class MultipleAgendasDatetimes(APIView):
agenda_slugs = payload['agendas']
agendas = Agenda.objects.filter(slug__in=agenda_slugs, kind='events')
if not len(agendas) == len(agenda_slugs):
if len(agendas) != len(set(agenda_slugs)):
not_found_slugs = sorted(set(agenda_slugs) - {agenda.slug for agenda in agendas})
raise APIError(
_('events agendas do not exist: %s') % ', '.join(not_found_slugs),
@ -861,14 +819,17 @@ class MultipleAgendasDatetimes(APIView):
)
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
agendas = Agenda.prefetch_events_and_exceptions(
agendas, annotate_events=True, user_external_id=user_external_id
)
entries = []
for agenda in agendas:
entries.extend(
agenda.get_open_events(
annotate_queryset=True,
prefetched_queryset=True,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
)
)

View File

@ -1489,4 +1489,4 @@ def test_datetimes_multiple_agendas_queries(app):
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))})
assert len(resp.json['data']) == 20
assert len(ctx.captured_queries) == 21
assert len(ctx.captured_queries) == 7