Project

General

Profile

0003-api-allow-booking-all-recurrences-of-one-event-at-on.patch

Valentin Deniaud, 21 Jan 2021 03:41 PM

Download (8.82 KB)

View differences:

Subject: [PATCH 3/3] api: allow booking all recurrences of one event at once
 (#50145)

 chrono/agendas/models.py | 13 +++++---
 chrono/api/views.py      | 68 ++++++++++++++++++++++++++++++++++++++--
 tests/test_api.py        | 33 +++++++++++++++++++
 3 files changed, 107 insertions(+), 7 deletions(-)
chrono/agendas/models.py
500 500
        include_full=True,
501 501
        min_start=None,
502 502
        max_start=None,
503
        show_recurring=False,
503 504
    ):
504 505
        assert self.kind == 'events'
505 506

  
506 507
        if prefetched_queryset:
507 508
            entries = self.prefetched_events
508 509
        else:
509
            # recurring events are never opened
510
            entries = self.event_set.filter(recurrence_rule__isnull=True)
510
            if show_recurring:
511
                entries = self.event_set.filter(
512
                    recurrence_rule__isnull=False, recurrence_end_date__isnull=False
513
                )
514
            else:
515
                entries = self.event_set.filter(recurrence_rule__isnull=True)
511 516
            # exclude canceled events except for event recurrences
512 517
            entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
513 518
            # we never want to allow booking for past events.
......
539 544
            else:
540 545
                entries = entries.filter(start_datetime__lt=max_start)
541 546

  
542
        if annotate_queryset:
547
        if annotate_queryset and not show_recurring:
543 548
            entries = Event.annotate_queryset(entries)
544 549

  
545
        if max_start:
550
        if max_start and not show_recurring:
546 551
            entries = self.add_event_recurrences(
547 552
                entries,
548 553
                min_start,
chrono/api/views.py
20 20
import uuid
21 21

  
22 22

  
23
import django
23 24
from django.db import transaction
24
from django.db.models import Prefetch, Q
25
from django.db.models import Prefetch, Q, F, Subquery, OuterRef, Count, IntegerField, Value
26
from django.db.models.functions import Coalesce
25 27
from django.http import Http404, HttpResponse
26 28
from django.shortcuts import get_object_or_404
27 29
from django.urls import reverse
......
531 533
        if date_end:
532 534
            date_end = make_aware(datetime.datetime.combine(parse_date(date_end), datetime.time(0, 0)))
533 535

  
534
        entries = agenda.get_open_events(annotate_queryset=True, min_start=date_start, max_start=date_end)
536
        entries = agenda.get_open_events(
537
            annotate_queryset=True,
538
            show_recurring=bool('recurring' in request.GET),
539
            min_start=date_start,
540
            max_start=date_end,
541
        )
535 542

  
536 543
        response = {'data': [get_event_detail(request, x, agenda=agenda) for x in entries]}
537 544
        return Response(response)
......
1048 1055
            except ValueError:
1049 1056
                events = agenda.event_set.filter(slug__in=slots).order_by('start_datetime')
1050 1057

  
1058
            if events.filter(recurrence_rule__isnull=False).exists():
1059
                events = Event.objects.filter(primary_event__in=events)
1060

  
1051 1061
            for event in events:
1052 1062
                if not event.in_bookable_period():
1053 1063
                    raise APIError(_('event not bookable'), err_class='event not bookable')
......
1098 1108

  
1099 1109
            # now we have a list of events, book them.
1100 1110
            primary_booking = None
1111
            create_bookings = []
1101 1112
            for event in events:
1102 1113
                for i in range(places_count):
1103 1114
                    new_booking = Booking(
......
1117 1128
                    )
1118 1129
                    if primary_booking is not None:
1119 1130
                        new_booking.primary_booking = primary_booking
1120
                    new_booking.save()
1131
                        if agenda.kind == 'events':
1132
                            # "events" list is a queryset, saving can be delayed
1133
                            create_bookings.append(new_booking)
1134
                        else:
1135
                            new_booking.save()
1121 1136
                    if primary_booking is None:
1137
                        new_booking.save()
1122 1138
                        primary_booking = new_booking
1139
            if create_bookings:
1140
                not_cancelled_bookings = Booking.objects.filter(
1141
                    cancellation_datetime__isnull=True, event=OuterRef('pk')
1142
                )
1143
                bookings = not_cancelled_bookings.filter(in_waiting_list=False).order_by().values('event')
1144
                count_bookings = bookings.annotate(count=Count('event')).values('count')
1145
                waiting_list_bookings = (
1146
                    not_cancelled_bookings.filter(in_waiting_list=True).order_by().values('event')
1147
                )
1148
                count_waiting_list = waiting_list_bookings.annotate(count=Count('event')).values('count')
1149
                events = events.annotate(
1150
                    booked_places_count=Coalesce(
1151
                        Subquery(count_bookings, output_field=IntegerField()), Value(0)
1152
                    ),
1153
                    waiting_list_count=Coalesce(
1154
                        Subquery(count_waiting_list, output_field=IntegerField()), Value(0)
1155
                    ),
1156
                )
1157
                with transaction.atomic():
1158
                    Booking.objects.bulk_create(create_bookings)
1159
                    if django.VERSION < (2, 0):
1160
                        from django.db.models import Case, When
1161

  
1162
                        events.update(
1163
                            full=Case(
1164
                                When(
1165
                                    Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1166
                                    | Q(
1167
                                        waiting_list_places__gt=0,
1168
                                        waiting_list_count__gte=F('waiting_list_places'),
1169
                                    ),
1170
                                    then=Value(True),
1171
                                ),
1172
                                default=Value(False),
1173
                            ),
1174
                            almost_full=Case(
1175
                                When(Q(booked_places_count__gte=0.9 * F('places')), then=Value(True)),
1176
                                default=Value(False),
1177
                            ),
1178
                        )
1179
                    else:
1180
                        events.update(
1181
                            full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1182
                            | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
1183
                            almost_full=Q(booked_places_count__gte=0.9 * F('places')),
1184
                        )
1123 1185

  
1124 1186
        response = {
1125 1187
            'err': 0,
tests/test_api.py
4910 4910

  
4911 4911
    new_event = Booking.objects.get(pk=resp.json['booking_id']).event
4912 4912
    assert event.start_datetime == new_event.start_datetime
4913

  
4914

  
4915
def test_recurring_events_api_book_all(app, user, freezer):
4916
    freezer.move_to('2021-09-06 12:00')
4917
    agenda = Agenda.objects.create(
4918
        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
4919
    )
4920
    event = Event.objects.create(
4921
        label='School canteen (Monday)', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
4922
    )
4923
    event.refresh_from_db()
4924

  
4925
    resp = app.get('/api/agenda/%s/datetimes/?recurring' % agenda.slug)
4926
    assert len(resp.json['data']) == 0
4927

  
4928
    event.recurrence_end_date = now() + datetime.timedelta(days=30)
4929
    event.save()
4930
    recurrences = event.get_recurrences(localtime(event.start_datetime), localtime(event.recurrence_end_date))
4931
    assert len(recurrences) == 5
4932
    for recurrence in recurrences:
4933
        event.get_or_create_event_recurrence(recurrence.start_datetime)
4934

  
4935
    resp = app.get('/api/agenda/%s/datetimes/?recurring' % agenda.slug)
4936
    assert len(resp.json['data']) == 1
4937
    fillslot_url = resp.json['data'][0]['api']['fillslot_url']
4938

  
4939
    app.authorization = ('Basic', ('john.doe', 'password'))
4940
    resp = app.post(fillslot_url)
4941
    assert resp.json['err'] == 0
4942

  
4943
    assert Booking.objects.count() == 5
4944
    events = Event.annotate_queryset(Event.objects.filter(primary_event=event))
4945
    assert events.filter(booked_places_count=1).count() == 5
4913
-