api: user_external_id for datetimes (#55002)
This commit is contained in:
parent
3999cf7a22
commit
dd424bc388
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue