0003-api-allow-booking-all-recurrences-of-one-event-at-on.patch
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 |
- |