api: user_external_id for datetimes (#55002)

This commit is contained in:
Lauréline Guérin 2021-06-21 16:15:03 +02:00
parent 3999cf7a22
commit dd424bc388
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
4 changed files with 249 additions and 28 deletions

View File

@ -608,7 +608,7 @@ class Agenda(models.Model):
include_full=True,
min_start=None,
max_start=None,
excluded_user_external_id=None,
user_external_id=None,
):
assert self.kind == 'events'
@ -646,8 +646,8 @@ class Agenda(models.Model):
else:
entries = entries.filter(start_datetime__lt=max_start)
if excluded_user_external_id and not prefetched_queryset:
entries = Event.annotate_queryset_for_user(entries, excluded_user_external_id)
if user_external_id and not prefetched_queryset:
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if annotate_queryset and not prefetched_queryset:
entries = Event.annotate_queryset(entries)
@ -668,7 +668,7 @@ class Agenda(models.Model):
annotate_queryset=False,
min_start=None,
max_start=None,
excluded_user_external_id=None,
user_external_id=None,
):
assert self.kind == 'events'
@ -685,8 +685,8 @@ class Agenda(models.Model):
if max_start:
entries = entries.filter(start_datetime__lt=max_start)
if excluded_user_external_id:
entries = Event.annotate_queryset_for_user(entries, excluded_user_external_id)
if user_external_id:
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if annotate_queryset:
entries = Event.annotate_queryset(entries)
@ -1367,7 +1367,7 @@ class Event(models.Model):
)
@staticmethod
def annotate_queryset_for_user(qs, excluded_user_external_id):
def annotate_queryset_for_user(qs, user_external_id):
if django.VERSION < (2, 0):
return qs.annotate(
user_places_count=Count(
@ -1375,7 +1375,17 @@ class Event(models.Model):
When(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=False,
booking__user_external_id=excluded_user_external_id,
booking__user_external_id=user_external_id,
then='booking',
)
)
),
user_waiting_places_count=Count(
Case(
When(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=True,
booking__user_external_id=user_external_id,
then='booking',
)
)
@ -1388,7 +1398,15 @@ class Event(models.Model):
filter=Q(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=False,
booking__user_external_id=excluded_user_external_id,
booking__user_external_id=user_external_id,
),
),
user_waiting_places_count=Count(
'booking',
filter=Q(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=True,
booking__user_external_id=user_external_id,
),
),
)

View File

@ -75,7 +75,9 @@ def get_max_datetime(agenda, end_datetime=None):
return min(agenda.max_booking_datetime, end_datetime)
TimeSlot = collections.namedtuple('TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk'])
TimeSlot = collections.namedtuple(
'TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk', 'booked_for_external_user']
)
def get_all_slots(
@ -85,7 +87,7 @@ def get_all_slots(
unique=False,
start_datetime=None,
end_datetime=None,
excluded_user_external_id=None,
user_external_id=None,
):
"""Get all occupation state of all possible slots for the given agenda (of
its real agendas for a virtual agenda) and the given meeting_type.
@ -245,7 +247,7 @@ def get_all_slots(
# aggregate already booked time intervals by excluded_user_external_id
user_bookings = IntervalSet()
if excluded_user_external_id:
if user_external_id:
used_min_datetime, used_max_datetime = (
min([v[0] for v in agenda_id_min_max_datetime.values()]),
max([v[1] for v in agenda_id_min_max_datetime.values()]),
@ -255,7 +257,7 @@ def get_all_slots(
agenda__in=agenda_ids,
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
start_datetime__lte=used_max_datetime,
booking__user_external_id=excluded_user_external_id,
booking__user_external_id=user_external_id,
)
.exclude(booking__cancellation_datetime__isnull=False)
# ordering is important for the later groupby, it works like sort | uniq
@ -312,13 +314,12 @@ def get_all_slots(
)
if excluded:
continue
# slot is full if an already booked event overlaps it
# check resources first
booked = resources_bookings.overlaps(start_datetime, end_datetime)
# then check user boookings
if not booked:
booked = user_bookings.overlaps(start_datetime, end_datetime)
booked_for_external_user = user_bookings.overlaps(start_datetime, end_datetime)
booked = booked or booked_for_external_user
# then bookings if resources are free
if not booked:
booked = desk.id in bookings and bookings[desk.id].overlaps(
@ -328,7 +329,11 @@ def get_all_slots(
continue
unique_booked[timestamp] = booked
yield TimeSlot(
start_datetime=start_datetime, end_datetime=end_datetime, desk=desk, full=booked
start_datetime=start_datetime,
end_datetime=end_datetime,
desk=desk,
full=booked,
booked_for_external_user=booked_for_external_user,
)
if unique and not booked:
break
@ -416,7 +421,7 @@ def is_event_disabled(event, min_places=1):
return False
def get_event_detail(request, event, agenda=None, min_places=1):
def get_event_detail(request, event, agenda=None, min_places=1, booked_user_external_id=None):
agenda = agenda or event.agenda
event_text = force_text(event)
if agenda.event_display_template:
@ -426,7 +431,7 @@ def get_event_detail(request, event, agenda=None, min_places=1):
event.label,
date_format(localtime(event.start_datetime), 'DATETIME_FORMAT'),
)
return {
details = {
'id': event.slug,
'slug': event.slug, # kept for compatibility
'text': event_text,
@ -458,6 +463,13 @@ def get_event_detail(request, event, agenda=None, min_places=1):
},
'places': get_event_places(event),
}
if booked_user_external_id:
if getattr(event, 'user_places_count', 0) > 0:
details['booked_for_external_user'] = 'main-list'
elif getattr(event, 'user_waiting_places_count', 0) > 0:
details['booked_for_external_user'] = 'waiting-list'
return details
def get_events_meta_detail(request, events, agenda=None, min_places=1):
@ -685,7 +697,19 @@ class Datetimes(APIView):
)
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
user_external_id = request.GET.get('exclude_user_external_id') or None
booked_user_external_id = request.GET.get('user_external_id') or None
excluded_user_external_id = request.GET.get('exclude_user_external_id') or None
if (
booked_user_external_id
and excluded_user_external_id
and booked_user_external_id != excluded_user_external_id
):
raise APIError(
_('user_external_id and exclude_user_external_id have different values'),
err_class='user_external_id and exclude_user_external_id have different values',
http_status=status.HTTP_400_BAD_REQUEST,
)
show_events = request.GET.get('events') or 'future'
show_past = show_events in ['all', 'past']
show_future = show_events in ['all', 'future']
@ -696,21 +720,30 @@ class Datetimes(APIView):
annotate_queryset=True,
min_start=start_datetime,
max_start=end_datetime,
excluded_user_external_id=user_external_id,
user_external_id=booked_user_external_id or excluded_user_external_id,
)
if show_future:
entries += agenda.get_open_events(
annotate_queryset=True,
min_start=start_datetime,
max_start=end_datetime,
excluded_user_external_id=user_external_id,
user_external_id=booked_user_external_id or excluded_user_external_id,
)
if request.GET.get('hide_disabled'):
entries = [e for e in entries if not is_event_disabled(e, min_places)]
response = {
'data': [get_event_detail(request, x, agenda=agenda, min_places=min_places) for x in entries],
'data': [
get_event_detail(
request,
x,
agenda=agenda,
min_places=min_places,
booked_user_external_id=booked_user_external_id,
)
for x in entries
],
'meta': get_events_meta_detail(request, entries, agenda=agenda, min_places=min_places),
}
return Response(response)
@ -739,7 +772,18 @@ class MeetingDatetimes(APIView):
resources = get_resources_from_request(request, agenda)
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
user_external_id = request.GET.get('exclude_user_external_id') or None
booked_user_external_id = request.GET.get('user_external_id') or None
excluded_user_external_id = request.GET.get('exclude_user_external_id') or None
if (
booked_user_external_id
and excluded_user_external_id
and booked_user_external_id != excluded_user_external_id
):
raise APIError(
_('user_external_id and exclude_user_external_id have different values'),
err_class='user_external_id and exclude_user_external_id have different values',
http_status=status.HTTP_400_BAD_REQUEST,
)
# Generate an unique slot for each possible meeting [start_datetime,
# end_datetime] range.
@ -762,7 +806,7 @@ class MeetingDatetimes(APIView):
unique=True,
start_datetime=start_datetime,
end_datetime=end_datetime,
excluded_user_external_id=user_external_id,
user_external_id=booked_user_external_id or excluded_user_external_id,
)
)
for slot in sorted(all_slots, key=lambda slot: slot[:3]):
@ -807,6 +851,8 @@ class MeetingDatetimes(APIView):
'disabled': bool(slot.full),
'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id)},
}
if booked_user_external_id and slot.booked_for_external_user:
slot_data['booked_for_external_user'] = True
data.append(slot_data)
bookable_datetimes_number_total += 1
@ -1141,7 +1187,7 @@ class Fillslots(APIView):
agenda,
meeting_type,
resources=resources,
excluded_user_external_id=user_external_id if exclude_user else None,
user_external_id=user_external_id if exclude_user else None,
),
key=lambda slot: slot.start_datetime,
)

View File

@ -191,6 +191,7 @@ def test_datetimes_api_exclude_slots(app):
assert resp.json['data'][0]['disabled'] is True
assert resp.json['meta']['first_bookable_slot'] is None
assert resp.json['meta']['no_bookable_datetimes'] is True
assert 'booked_for_external_user' not in resp.json['data'][0]
event.delete()
@ -228,12 +229,83 @@ def test_datetimes_api_exclude_slots(app):
assert resp.json['data'][0]['id'] == 'recurrent--2021-02-23-1200'
assert resp.json['data'][0]['places']['full'] is True
assert resp.json['data'][0]['disabled'] is True
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'exclude_user_external_id': '42'})
assert resp.json['data'][0]['id'] == 'recurrent--2021-02-23-1200'
assert resp.json['data'][0]['places']['full'] is True
assert resp.json['data'][0]['disabled'] is True
@pytest.mark.freeze_time('2021-02-23')
def test_datetimes_api_user_external_id(app):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
event = Event.objects.create(
slug='event-slug',
start_datetime=localtime().replace(hour=10, minute=0),
places=5,
agenda=agenda,
)
booking = Booking.objects.create(event=event, user_external_id='42')
cancelled = Booking.objects.create(event=event, user_external_id='35')
cancelled.cancel()
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '35'})
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert resp.json['data'][0]['booked_for_external_user'] == 'main-list'
booking.in_waiting_list = True
booking.save()
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert resp.json['data'][0]['booked_for_external_user'] == 'waiting-list'
booking.cancel()
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert 'booked_for_external_user' not in resp.json['data'][0]
event.delete()
# recurrent event
start_datetime = localtime().replace(hour=12, minute=0)
event = Event.objects.create(
slug='recurrent',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=2,
agenda=agenda,
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert 'booked_for_external_user' not in resp.json['data'][0]
first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
booking = Booking.objects.create(event=first_recurrence, user_external_id='42')
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert resp.json['data'][0]['booked_for_external_user'] == 'main-list'
booking.in_waiting_list = True
booking.save()
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'user_external_id': '42'})
assert resp.json['data'][0]['booked_for_external_user'] == 'waiting-list'
# mix with exclude_user_external_id
resp = app.get(
'/api/agenda/%s/datetimes/' % agenda.slug,
params={'user_external_id': '42', 'exclude_user_external_id': '35'},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'user_external_id and exclude_user_external_id have different values'
def test_datetimes_api_hide_disabled(app):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
@ -1133,9 +1205,33 @@ def test_past_datetimes_recurring_event(app, user):
assert data[6]['disabled'] is False
assert data[7]['disabled'] is False
# check exclude_user_external_id
# check user_external_id
first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
Booking.objects.create(event=first_recurrence, user_external_id='42')
booking = Booking.objects.create(event=first_recurrence, user_external_id='42', in_waiting_list=True)
resp = app.get(
'/api/agenda/%s/datetimes/' % agenda.slug,
params={
'events': 'past',
'user_external_id': '42',
'date_start': localtime(now() - datetime.timedelta(days=6 * 7)),
},
)
data = resp.json['data']
assert data[0]['booked_for_external_user'] == 'waiting-list'
booking.in_waiting_list = False
booking.save()
resp = app.get(
'/api/agenda/%s/datetimes/' % agenda.slug,
params={
'events': 'past',
'user_external_id': '42',
'date_start': localtime(now() - datetime.timedelta(days=6 * 7)),
},
)
data = resp.json['data']
assert data[0]['booked_for_external_user'] == 'main-list'
# check exclude_user_external_id
resp = app.get(
'/api/agenda/%s/datetimes/' % agenda.slug,
params={

View File

@ -504,11 +504,72 @@ def test_datetimes_api_meetings_agenda_exclude_slots(app):
assert len(ctx.captured_queries) == 9
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is True
assert 'booked_for_external_user' not in resp.json['data'][0]
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
assert resp.json['meta']['first_bookable_slot']['id'] == 'foo-bar:2021-02-26-0930'
@pytest.mark.freeze_time('2021-02-25')
def test_datetimes_api_meetings_agenda_user_external_id(app):
tomorrow = now() + datetime.timedelta(days=1)
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
desk.duplicate()
event = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
desk=desk,
)
Booking.objects.create(event=event, user_external_id='42')
event2 = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=10, minute=0),
desk=desk,
)
cancelled = Booking.objects.create(event=event2, user_external_id='35')
cancelled.cancel()
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
assert 'booked_for_external_user' not in resp.json['data'][0]
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '35'},
)
assert 'booked_for_external_user' not in resp.json['data'][0]
with CaptureQueriesContext(connection) as ctx:
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
params={'user_external_id': '42'},
)
assert len(ctx.captured_queries) == 9
assert resp.json['data'][0]['booked_for_external_user'] is True
# mix with exclude_user_external_id
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
params={'user_external_id': '42', 'exclude_user_external_id': '35'},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'user_external_id and exclude_user_external_id have different values'
@pytest.mark.freeze_time('2021-03-15')
def test_datetimes_api_meetings_agenda_hide_disabled(app):
agenda = Agenda.objects.create(