api: allow different hours per day in partial bookings recurring fillslots (#78086) #153
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.'
|
||||
|
|
Loading…
Reference in New Issue