0001-api-show_past_events-for-agendas-datetimes-endpoint-.patch
chrono/agendas/models.py | ||
---|---|---|
653 | 653 | |
654 | 654 |
if prefetched_queryset: |
655 | 655 |
entries = self.prefetched_events |
656 |
# we may have past events |
|
657 |
entries = [e for e in entries if e.start_datetime >= localtime(now())] |
|
656 | 658 |
else: |
657 | 659 |
# recurring events are never opened |
658 | 660 |
entries = self.event_set.filter(recurrence_days__isnull=True) |
... | ... | |
701 | 703 | |
702 | 704 |
def get_past_events( |
703 | 705 |
self, |
706 |
prefetched_queryset=False, |
|
704 | 707 |
min_start=None, |
705 | 708 |
max_start=None, |
706 | 709 |
user_external_id=None, |
707 | 710 |
): |
708 | 711 |
assert self.kind == 'events' |
709 | 712 | |
710 |
# recurring events are never opened |
|
711 |
entries = self.event_set.filter(recurrence_days__isnull=True) |
|
712 |
# exclude canceled events except for event recurrences |
|
713 |
entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False)) |
|
714 |
# we want only past events |
|
715 |
entries = entries.filter(start_datetime__lt=localtime(now())) |
|
713 |
if prefetched_queryset: |
|
714 |
entries = self.prefetched_events |
|
715 |
# we may have future events |
|
716 |
entries = [e for e in entries if e.start_datetime < localtime(now())] |
|
717 |
else: |
|
718 |
# recurring events are never opened |
|
719 |
entries = self.event_set.filter(recurrence_days__isnull=True) |
|
720 |
# exclude canceled events except for event recurrences |
|
721 |
entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False)) |
|
722 |
# we want only past events |
|
723 |
entries = entries.filter(start_datetime__lt=localtime(now())) |
|
716 | 724 | |
717 |
if min_start: |
|
725 |
if min_start and not prefetched_queryset:
|
|
718 | 726 |
entries = entries.filter(start_datetime__gte=min_start) |
719 | 727 | |
720 |
if max_start: |
|
728 |
if max_start and not prefetched_queryset:
|
|
721 | 729 |
entries = entries.filter(start_datetime__lt=max_start) |
722 | 730 | |
723 | 731 |
if user_external_id: |
... | ... | |
728 | 736 |
entries, |
729 | 737 |
min_start, |
730 | 738 |
min(max_start or localtime(now()), localtime(now())), |
739 |
prefetched_queryset=prefetched_queryset, |
|
731 | 740 |
) |
732 | 741 | |
733 | 742 |
return entries |
... | ... | |
869 | 878 |
] |
870 | 879 | |
871 | 880 |
@staticmethod |
872 |
def prefetch_events_and_exceptions(qs, annotate_events=False, user_external_id=None): |
|
881 |
def prefetch_events_and_exceptions( |
|
882 |
qs, user_external_id=None, show_past_events=False, min_start=None, max_start=None |
|
883 |
): |
|
873 | 884 |
event_queryset = Event.objects.filter( |
874 | 885 |
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()), |
886 |
recurrence_days__isnull=True, |
|
875 | 887 |
cancelled=False, |
876 |
start_datetime__gte=localtime(now()), |
|
877 | 888 |
).order_by() |
878 | 889 | |
879 | 890 |
if user_external_id: |
880 | 891 |
event_queryset = Event.annotate_queryset_for_user(event_queryset, user_external_id) |
881 |
if annotate_events: |
|
882 |
event_queryset = Event.annotate_queryset(event_queryset) |
|
892 |
if not show_past_events: |
|
893 |
event_queryset = event_queryset.filter(start_datetime__gte=localtime(now())) |
|
894 |
if min_start: |
|
895 |
event_queryset = event_queryset.filter(start_datetime__gte=min_start) |
|
896 |
if max_start: |
|
897 |
event_queryset = event_queryset.filter(start_datetime__lt=max_start) |
|
883 | 898 | |
884 | 899 |
recurring_event_queryset = Event.objects.filter( |
885 | 900 |
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()), |
chrono/api/serializers.py | ||
---|---|---|
133 | 133 |
agendas = CommaSeparatedStringField( |
134 | 134 |
required=True, child=serializers.SlugField(max_length=160, allow_blank=False) |
135 | 135 |
) |
136 |
show_past_events = serializers.BooleanField(default=False) |
chrono/api/views.py | ||
---|---|---|
415 | 415 |
return places |
416 | 416 | |
417 | 417 | |
418 |
def is_event_disabled(event, min_places=1, disable_booked=True): |
|
418 |
def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=None):
|
|
419 | 419 |
if disable_booked and getattr(event, 'user_places_count', 0) > 0: |
420 | 420 |
return True |
421 | 421 |
if event.start_datetime < now(): |
422 |
# event is past => not disabled (always ok to book a past event) |
|
423 |
return False |
|
422 |
# event is past |
|
423 |
if bookable_events in ['all', 'past']: |
|
424 |
# but we want to book past events, and it's always ok |
|
425 |
return False |
|
426 |
# we just want to show past events, but they are not bookable |
|
427 |
return True |
|
424 | 428 |
if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places: |
425 | 429 |
return True |
426 | 430 |
return False |
... | ... | |
454 | 458 |
agenda=None, |
455 | 459 |
min_places=1, |
456 | 460 |
booked_user_external_id=None, |
457 |
show_events=None,
|
|
461 |
bookable_events=None,
|
|
458 | 462 |
multiple_agendas=False, |
459 | 463 |
disable_booked=True, |
460 | 464 |
): |
... | ... | |
470 | 474 |
'pricing': event.pricing, |
471 | 475 |
'url': event.url, |
472 | 476 |
'duration': event.duration, |
473 |
'disabled': is_event_disabled(event, min_places=min_places, disable_booked=disable_booked), |
|
477 |
'disabled': is_event_disabled( |
|
478 |
event, min_places=min_places, disable_booked=disable_booked, bookable_events=bookable_events |
|
479 |
), |
|
474 | 480 |
'api': { |
475 | 481 |
'bookings_url': request.build_absolute_uri( |
476 | 482 |
reverse( |
... | ... | |
499 | 505 |
}, |
500 | 506 |
'places': get_event_places(event), |
501 | 507 |
} |
502 |
if show_events is not None:
|
|
503 |
details['api']['fillslot_url'] += '?events=%s' % show_events
|
|
508 |
if bookable_events is not None:
|
|
509 |
details['api']['fillslot_url'] += '?events=%s' % bookable_events
|
|
504 | 510 |
if booked_user_external_id: |
505 | 511 |
if getattr(event, 'user_places_count', 0) > 0: |
506 | 512 |
details['booked_for_external_user'] = 'main-list' |
... | ... | |
511 | 517 | |
512 | 518 | |
513 | 519 |
def get_events_meta_detail( |
514 |
request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False
|
|
520 |
request, events, agenda=None, min_places=1, bookable_events=None, multiple_agendas=False
|
|
515 | 521 |
): |
516 | 522 |
bookable_datetimes_number_total = 0 |
517 | 523 |
bookable_datetimes_number_available = 0 |
518 | 524 |
first_bookable_slot = None |
519 | 525 |
for event in events: |
520 | 526 |
bookable_datetimes_number_total += 1 |
521 |
if not is_event_disabled(event, min_places=min_places): |
|
527 |
if not is_event_disabled(event, min_places=min_places, bookable_events=bookable_events):
|
|
522 | 528 |
bookable_datetimes_number_available += 1 |
523 | 529 |
if not first_bookable_slot: |
524 | 530 |
first_bookable_slot = get_event_detail( |
... | ... | |
526 | 532 |
event, |
527 | 533 |
agenda=agenda, |
528 | 534 |
min_places=min_places, |
529 |
show_events=show_events,
|
|
535 |
bookable_events=bookable_events,
|
|
530 | 536 |
multiple_agendas=multiple_agendas, |
531 | 537 |
) |
532 | 538 |
return { |
... | ... | |
749 | 755 | |
750 | 756 |
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id') |
751 | 757 |
disable_booked = bool(payload.get('exclude_user_external_id')) |
752 |
show_events_raw = payload.get('events')
|
|
753 |
show_events = show_events_raw or 'future'
|
|
754 |
show_past = show_events in ['all', 'past']
|
|
755 |
show_future = show_events in ['all', 'future']
|
|
758 |
bookable_events_raw = payload.get('events')
|
|
759 |
bookable_events = bookable_events_raw or 'future'
|
|
760 |
book_past = bookable_events in ['all', 'past']
|
|
761 |
book_future = bookable_events in ['all', 'future']
|
|
756 | 762 | |
757 | 763 |
entries = [] |
758 |
if show_past:
|
|
764 |
if book_past:
|
|
759 | 765 |
entries += agenda.get_past_events( |
760 | 766 |
min_start=payload.get('date_start'), |
761 | 767 |
max_start=payload.get('date_end'), |
762 | 768 |
user_external_id=user_external_id, |
763 | 769 |
) |
764 |
if show_future:
|
|
770 |
if book_future:
|
|
765 | 771 |
entries += agenda.get_open_events( |
766 | 772 |
min_start=payload.get('date_start'), |
767 | 773 |
max_start=payload.get('date_end'), |
... | ... | |
772 | 778 |
entries = [ |
773 | 779 |
e |
774 | 780 |
for e in entries |
775 |
if not is_event_disabled(e, payload['min_places'], disable_booked=disable_booked) |
|
781 |
if not is_event_disabled( |
|
782 |
e, payload['min_places'], disable_booked=disable_booked, bookable_events=bookable_events |
|
783 |
) |
|
776 | 784 |
] |
777 | 785 | |
778 | 786 |
response = { |
... | ... | |
783 | 791 |
agenda=agenda, |
784 | 792 |
min_places=payload['min_places'], |
785 | 793 |
booked_user_external_id=payload.get('user_external_id'), |
786 |
show_events=show_events_raw,
|
|
794 |
bookable_events=bookable_events_raw,
|
|
787 | 795 |
disable_booked=disable_booked, |
788 | 796 |
) |
789 | 797 |
for x in entries |
790 | 798 |
], |
791 | 799 |
'meta': get_events_meta_detail( |
792 |
request, entries, agenda=agenda, min_places=payload['min_places'], show_events=show_events_raw |
|
800 |
request, |
|
801 |
entries, |
|
802 |
agenda=agenda, |
|
803 |
min_places=payload['min_places'], |
|
804 |
bookable_events=bookable_events_raw, |
|
793 | 805 |
), |
794 | 806 |
} |
795 | 807 |
return Response(response) |
... | ... | |
825 | 837 | |
826 | 838 |
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id') |
827 | 839 |
disable_booked = bool(payload.get('exclude_user_external_id')) |
828 |
agendas = Agenda.prefetch_events_and_exceptions(agendas, user_external_id=user_external_id) |
|
840 |
show_past_events = bool(payload.get('show_past_events')) |
|
841 |
agendas = Agenda.prefetch_events_and_exceptions( |
|
842 |
agendas, |
|
843 |
user_external_id=user_external_id, |
|
844 |
show_past_events=show_past_events, |
|
845 |
min_start=payload.get('date_start'), |
|
846 |
max_start=payload.get('date_end'), |
|
847 |
) |
|
829 | 848 | |
830 | 849 |
entries = [] |
831 | 850 |
for agenda in agendas: |
851 |
if show_past_events: |
|
852 |
entries.extend( |
|
853 |
agenda.get_past_events( |
|
854 |
prefetched_queryset=True, |
|
855 |
) |
|
856 |
) |
|
832 | 857 |
entries.extend( |
833 | 858 |
agenda.get_open_events( |
834 | 859 |
prefetched_queryset=True, |
835 |
min_start=payload.get('date_start'), |
|
836 |
max_start=payload.get('date_end'), |
|
837 | 860 |
) |
838 | 861 |
) |
839 | 862 |
tests/api/test_datetimes.py | ||
---|---|---|
1376 | 1376 |
places=5, |
1377 | 1377 |
agenda=first_agenda, |
1378 | 1378 |
) |
1379 |
Event.objects.create( # not visible in datetimes api |
|
1380 |
slug='recurring', |
|
1381 |
start_datetime=now() + datetime.timedelta(days=5), |
|
1382 |
recurrence_days=[localtime().weekday()], |
|
1383 |
recurrence_end_date=now() + datetime.timedelta(days=5), |
|
1384 |
places=5, |
|
1385 |
agenda=first_agenda, |
|
1386 |
) |
|
1379 | 1387 |
second_agenda = Agenda.objects.create(label='Second agenda', kind='events') |
1380 | 1388 |
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder') |
1381 | 1389 |
event = Event.objects.create( |
... | ... | |
1454 | 1462 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=400) |
1455 | 1463 |
assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy' |
1456 | 1464 | |
1457 |
# no support for past events |
|
1465 |
# no support for past events booking (they are never bookable)
|
|
1458 | 1466 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400) |
1459 | 1467 | |
1468 |
# but it's possible to show past events |
|
1469 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True}) |
|
1470 |
assert len(resp.json['data']) == 2 |
|
1471 |
assert resp.json['data'][0]['id'] == 'first-agenda@event' |
|
1472 |
assert resp.json['data'][1]['id'] == 'second-agenda@event' |
|
1473 | ||
1474 |
Event.objects.create( |
|
1475 |
slug='event-in-past', |
|
1476 |
start_datetime=now() - datetime.timedelta(days=5), |
|
1477 |
places=5, |
|
1478 |
agenda=first_agenda, |
|
1479 |
) |
|
1480 |
Event.objects.create( # not visible in datetimes api |
|
1481 |
slug='recurring-in-past', |
|
1482 |
start_datetime=now() - datetime.timedelta(days=5), |
|
1483 |
recurrence_days=[localtime().weekday()], |
|
1484 |
recurrence_end_date=now() - datetime.timedelta(days=5), |
|
1485 |
places=5, |
|
1486 |
agenda=first_agenda, |
|
1487 |
) |
|
1488 | ||
1489 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True}) |
|
1490 |
assert len(resp.json['data']) == 3 |
|
1491 |
assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past' |
|
1492 |
assert resp.json['data'][0]['disabled'] is True |
|
1493 |
assert resp.json['data'][1]['id'] == 'first-agenda@event' |
|
1494 |
assert resp.json['data'][1]['disabled'] is False |
|
1495 |
assert resp.json['data'][2]['id'] == 'second-agenda@event' |
|
1496 |
assert resp.json['data'][2]['disabled'] is False |
|
1497 | ||
1498 |
date_start = localtime() - datetime.timedelta(days=4) |
|
1499 |
resp = app.get( |
|
1500 |
'/api/agendas/datetimes/', |
|
1501 |
params={'agendas': agenda_slugs, 'date_start': date_start, 'show_past_events': True}, |
|
1502 |
) |
|
1503 |
assert len(resp.json['data']) == 2 |
|
1504 |
assert resp.json['data'][0]['id'] == 'first-agenda@event' |
|
1505 |
assert resp.json['data'][1]['id'] == 'second-agenda@event' |
|
1506 | ||
1507 |
date_end = localtime() + datetime.timedelta(days=5, hours=1) |
|
1508 |
resp = app.get( |
|
1509 |
'/api/agendas/datetimes/', |
|
1510 |
params={'agendas': agenda_slugs, 'date_end': date_end, 'show_past_events': True}, |
|
1511 |
) |
|
1512 |
assert len(resp.json['data']) == 2 |
|
1513 |
assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past' |
|
1514 |
assert resp.json['data'][1]['id'] == 'first-agenda@event' |
|
1515 | ||
1460 | 1516 | |
1461 | 1517 |
@pytest.mark.freeze_time('2021-05-06 14:00') |
1462 | 1518 |
def test_datetimes_multiple_agendas_sort(app): |
... | ... | |
1466 | 1522 |
second_agenda = Agenda.objects.create(label='Second agenda', kind='events') |
1467 | 1523 |
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder') |
1468 | 1524 |
Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=second_agenda) |
1525 |
Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=second_agenda) |
|
1469 | 1526 |
third_agenda = Agenda.objects.create(label='Third agenda', kind='events') |
1470 | 1527 |
Desk.objects.create(agenda=third_agenda, slug='_exceptions_holder') |
1471 | 1528 |
Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=third_agenda) |
1529 |
Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=third_agenda) |
|
1472 | 1530 | |
1473 | 1531 |
# check events are ordered by start_datetime and then by agenda order in querystring |
1474 | 1532 |
agenda_slugs = ','.join((first_agenda.slug, third_agenda.slug, second_agenda.slug)) |
... | ... | |
1478 | 1536 |
assert resp.json['data'][1]['id'] == 'second-agenda@09-05' |
1479 | 1537 |
assert resp.json['data'][2]['id'] == 'first-agenda@10-05' |
1480 | 1538 | |
1539 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True}) |
|
1540 |
assert len(resp.json['data']) == 5 |
|
1541 |
assert resp.json['data'][0]['id'] == 'third-agenda@04-05' |
|
1542 |
assert resp.json['data'][1]['id'] == 'second-agenda@04-05' |
|
1543 |
assert resp.json['data'][2]['id'] == 'third-agenda@09-05' |
|
1544 |
assert resp.json['data'][3]['id'] == 'second-agenda@09-05' |
|
1545 |
assert resp.json['data'][4]['id'] == 'first-agenda@10-05' |
|
1546 | ||
1481 | 1547 | |
1482 | 1548 |
@pytest.mark.freeze_time('2021-05-06 14:00') |
1483 | 1549 |
def test_datetimes_multiple_agendas_queries(app): |
1484 | 1550 |
for i in range(10): |
1485 | 1551 |
agenda = Agenda.objects.create(label=str(i), kind='events') |
1486 | 1552 |
Desk.objects.create(agenda=agenda, slug='_exceptions_holder') |
1553 |
Event.objects.create(start_datetime=now() - datetime.timedelta(days=5), places=5, agenda=agenda) |
|
1487 | 1554 |
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda) |
1488 | 1555 |
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda) |
1489 | 1556 | |
1490 | 1557 |
with CaptureQueriesContext(connection) as ctx: |
1491 |
resp = app.get('/api/agendas/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))}) |
|
1492 |
assert len(resp.json['data']) == 20 |
|
1558 |
resp = app.get( |
|
1559 |
'/api/agendas/datetimes/', |
|
1560 |
params={'agendas': ','.join(str(i) for i in range(10)), 'show_past_events': True}, |
|
1561 |
) |
|
1562 |
assert len(resp.json['data']) == 30 |
|
1493 | 1563 |
assert len(ctx.captured_queries) == 7 |
1494 |
- |