api: allow different hours per day in partial bookings recurring fillslots (#78086) #153

Merged
vdeniaud merged 1 commits from wip/78086-plages-libres-permettre-de-reser into main 2023-10-03 09:43:44 +02:00
5 changed files with 207 additions and 3 deletions

View File

@ -95,8 +95,8 @@ class FillSlotSerializer(serializers.Serializer):
required=False, child=serializers.CharField(max_length=16, allow_blank=False)
)
check_overlaps = serializers.BooleanField(default=False)
start_time = serializers.TimeField(required=False)
end_time = serializers.TimeField(required=False)
start_time = serializers.TimeField(required=False, allow_null=True)
end_time = serializers.TimeField(required=False, allow_null=True)
def validate(self, attrs):
super().validate(attrs)
@ -229,6 +229,58 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer):
return slots
class RecurringFillslotsByDaySerializer(FillSlotSerializer):
weekdays = {
'monday': 1,
'tuesday': 2,
'wednesday': 3,
'thursday': 4,
'friday': 5,
'saturday': 6,
'sunday': 7,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for weekday in self.weekdays:
self.fields[weekday] = CommaSeparatedStringField(
child=serializers.TimeField(), required=False, min_length=2, max_length=2, allow_null=True
)
setattr(self, 'validate_%s' % weekday, self.validate_hour_range)
def validate_hour_range(self, value):
if not value:
return None
start_time, end_time = value
if start_time >= end_time:
raise ValidationError(_('Start hour must be before end hour.'))
return value
def validate(self, attrs):
agendas = self.context['agendas']
if len(agendas) > 1:
raise ValidationError('Multiple agendas are not supported.')
agenda = agendas[0]
if not agenda.partial_bookings:
raise ValidationError('Agenda kind must be partial bookings.')
attrs['hours_by_days'] = hours_by_days = {}
for weekday, weekday_index in self.weekdays.items():
if attrs.get(weekday):
hours_by_days[weekday_index] = attrs[weekday]
days_by_event = collections.defaultdict(list)
for event in agenda.get_open_recurring_events():
for day in event.recurrence_days:
if day in hours_by_days:
days_by_event[event.slug].append(day)
attrs['slots'] = {agenda.slug: days_by_event}
return attrs
class BookingSerializer(serializers.ModelSerializer):
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
user_presence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)

View File

@ -23,6 +23,11 @@ urlpatterns = [
path('agendas/datetimes/', views.agendas_datetimes, name='api-agendas-datetimes'),
path('agendas/recurring-events/', views.recurring_events_list, name='api-agenda-recurring-events'),
path('agendas/recurring-events/fillslots/', views.recurring_fillslots, name='api-recurring-fillslots'),
path(
'agendas/recurring-events/fillslots-by-day/',
views.recurring_fillslots_by_day,
name='api-recurring-fillslots-by-day',
),
path(
'agendas/events/',
views.agendas_events,

View File

@ -1703,7 +1703,7 @@ class RecurringFillslots(APIView):
# don't reload agendas and events types
for event in events_to_book:
event.agenda = agendas_by_id[event.agenda_id]
bookings = [make_booking(event, payload, extra_data) for event in events_to_book]
bookings = self.make_bookings(events_to_book, payload, extra_data)
bookings_to_cancel = Booking.objects.filter(
user_external_id=user_external_id, event__in=events_to_unbook, cancellation_datetime__isnull=True
@ -1837,10 +1837,29 @@ class RecurringFillslots(APIView):
% ', '.join(sorted('%s / %s' % (x, y) for x, y in overlaps))
)
def make_bookings(self, events, payload, extra_data):
return [make_booking(event, payload, extra_data) for event in events]
recurring_fillslots = RecurringFillslots.as_view()
class RecurringFillslotsByDay(RecurringFillslots):
serializer_class = serializers.RecurringFillslotsByDaySerializer
def make_bookings(self, events, payload, extra_data):
bookings = []
for event in events:
payload['start_time'], payload['end_time'] = payload['hours_by_days'][
event.start_datetime.isoweekday()
]
bookings.append(make_booking(event, payload, extra_data))
return bookings
recurring_fillslots_by_day = RecurringFillslotsByDay.as_view()
class EventsFillslots(APIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.EventsFillSlotsSerializer

View File

@ -2067,6 +2067,17 @@ def test_booking_api_partial_booking(app, user):
== 'must include start_time and end_time for partial bookings agenda'
)
# null end_time
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id),
params={'start_time': '10:00', 'end_time': None},
status=400,
)
assert (
resp.json['errors']['non_field_errors'][0]
== 'must include start_time and end_time for partial bookings agenda'
)
# end before start
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id),

View File

@ -1859,3 +1859,120 @@ def test_recurring_events_api_fillslots_partial_bookings_update(app, user):
).count()
== 5
)
@pytest.mark.freeze_time('2023-05-01 10:00')
def test_recurring_events_api_fillslots_by_days_partial_bookings(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
event = Event.objects.create(
label='Event Mon-Wed',
start_datetime=start_datetime,
end_time=datetime.time(18, 00),
places=2,
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
recurrence_days=[1, 2, 3],
agenda=agenda,
)
event.create_all_recurrences()
event = Event.objects.create(
label='Event Thu-Sat',
start_datetime=start_datetime,
end_time=datetime.time(18, 00),
places=2,
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
recurrence_days=[4, 5, 6],
agenda=agenda,
)
event.create_all_recurrences()
app.authorization = ('Basic', ('john.doe', 'password'))
params = {
'user_external_id': 'user_id',
'tuesday': '09:00,12:00',
'friday': '08:00,18:00',
}
fillslots_url = '/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s' % agenda.slug
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 9
assert Booking.objects.count() == 9
assert (
Booking.objects.filter(
event__slug__startswith='event-mon-wed--2023-05-',
start_time=datetime.time(9, 00),
end_time=datetime.time(12, 00),
event__start_datetime__iso_week_day=2,
).count()
== 5
)
assert (
Booking.objects.filter(
event__slug__startswith='event-thu-sat--2023-05-',
start_time=datetime.time(8, 00),
end_time=datetime.time(18, 00),
event__start_datetime__iso_week_day=5,
).count()
== 4
)
# change bookings
params = {
'user_external_id': 'user_id',
'wednesday': '10:00,14:00',
'thursday': None, # null values are allowed and ignored
'sunday': '12:00,16:00', # unbookable day will be ignored
'slots': 'xxx', # parameter of normal API, ignored
'start_time': None, # parameter of normal API, ignored
}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 5
assert resp.json['cancelled_booking_count'] == 9
assert Booking.objects.count() == 5
assert (
Booking.objects.filter(
event__slug__startswith='event-mon-wed--2023-05-',
start_time=datetime.time(10, 00),
end_time=datetime.time(14, 00),
event__start_datetime__iso_week_day=3,
).count()
== 5
)
agenda2 = Agenda.objects.create(label='Foo Bar 2', kind='events', partial_bookings=True)
resp = app.post_json(
'/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s,%s'
% (agenda.slug, agenda2.slug),
params=params,
status=400,
)
assert resp.json['err'] == 1
assert resp.json['errors']['non_field_errors'][0] == 'Multiple agendas are not supported.'
agenda_events = Agenda.objects.create(label='Not partial bookings', kind='events')
resp = app.post_json(
'/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s' % agenda_events.slug,
params=params,
status=400,
)
assert resp.json['err'] == 1
assert resp.json['errors']['non_field_errors'][0] == 'Agenda kind must be partial bookings.'
params = {'user_external_id': 'user_id', 'wednesday': '11:00,10:00'}
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['wednesday'][0] == 'Start hour must be before end hour.'
params = {'user_external_id': 'user_id', 'wednesday': '11:00,xxx'}
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['wednesday']['1'][0].startswith('Time has wrong format')
params = {'user_external_id': 'user_id', 'wednesday': '11:00'}
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['errors']['wednesday'][0] == 'Ensure this field has at least 2 elements.'
params = {'user_external_id': 'user_id', 'wednesday': '11:00,13:00,15:00'}
resp = app.post_json(fillslots_url, params=params, status=400)
assert resp.json['errors']['wednesday'][0] == 'Ensure this field has no more than 2 elements.'