0001-api-add-an-enpoint-to-patch-an-event-57305.patch
chrono/api/urls.py | ||
---|---|---|
44 | 44 |
views.recurring_events_list, |
45 | 45 |
name='api-agenda-recurring-events', |
46 | 46 |
), |
47 | 47 |
url( |
48 | 48 |
r'^agenda/(?P<agenda_identifier>[\w-]+)/recurring-events/fillslots/$', |
49 | 49 |
views.recurring_fillslots, |
50 | 50 |
name='api-recurring-fillslots', |
51 | 51 |
), |
52 |
url( |
|
53 |
r'^agenda/(?P<agenda_identifier>[\w-]+)/event/(?P<event_identifier>[\w:-]+)/$', |
|
54 |
views.events, |
|
55 |
name='api-event', |
|
56 |
), |
|
52 | 57 |
url( |
53 | 58 |
r'^agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w:-]+)/$', |
54 | 59 |
views.event_status, |
55 | 60 |
name='api-event-status', |
56 | 61 |
), |
57 | 62 |
url( |
58 | 63 |
r'^agenda/(?P<agenda_identifier>[\w-]+)/bookings/(?P<event_identifier>[\w:-]+)/$', |
59 | 64 |
views.event_bookings, |
chrono/api/views.py | ||
---|---|---|
2159 | 2159 |
def success(self, booking): |
2160 | 2160 |
response = {'err': 0, 'booking_id': booking.pk} |
2161 | 2161 |
return Response(response) |
2162 | 2162 | |
2163 | 2163 | |
2164 | 2164 |
resize_booking = ResizeBooking.as_view() |
2165 | 2165 | |
2166 | 2166 | |
2167 |
class Events(APIView): |
|
2168 |
permission_classes = (permissions.IsAuthenticated,) |
|
2169 |
serializer_class = serializers.EventSerializer |
|
2170 | ||
2171 |
def get_object(self, agenda_identifier, event_identifier): |
|
2172 |
try: |
|
2173 |
agenda = Agenda.objects.get(slug=agenda_identifier, kind='events') |
|
2174 |
except Agenda.DoesNotExist: |
|
2175 |
try: |
|
2176 |
# legacy access by agenda id |
|
2177 |
agenda = Agenda.objects.get(pk=agenda_identifier, kind='events') |
|
2178 |
except (ValueError, Agenda.DoesNotExist): |
|
2179 |
raise Http404() |
|
2180 |
if ':' in event_identifier: |
|
2181 |
return get_event_recurrence(agenda, event_identifier) |
|
2182 |
try: |
|
2183 |
return agenda.event_set.get(slug=event_identifier) |
|
2184 |
except Event.DoesNotExist: |
|
2185 |
try: |
|
2186 |
# legacy access by event id |
|
2187 |
return agenda.event_set.get(pk=event_identifier) |
|
2188 |
except (ValueError, Event.DoesNotExist): |
|
2189 |
raise Http404() |
|
2190 | ||
2191 |
def patch(self, request, agenda_identifier=None, event_identifier=None, format=None): |
|
2192 |
event = self.get_object(agenda_identifier, event_identifier) |
|
2193 |
serializer = self.serializer_class(event, data=request.data, partial=True) |
|
2194 |
if not serializer.is_valid(): |
|
2195 |
raise APIError( |
|
2196 |
_('invalid payload'), |
|
2197 |
err_class='invalid payload', |
|
2198 |
errors=serializer.errors, |
|
2199 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
2200 |
) |
|
2201 | ||
2202 |
payload = serializer.validated_data |
|
2203 |
protected_fields = ['start_datetime', 'recurrence_days', 'recurrence_week_interval'] |
|
2204 |
changed_data = [] |
|
2205 |
for field in serializer.fields.keys(): |
|
2206 |
if payload.get(field) and payload.get(field) != getattr(event, field): |
|
2207 |
changed_data.append(field) |
|
2208 | ||
2209 |
if event.recurrence_days and event.has_recurrences_booked(): |
|
2210 |
errors = {} |
|
2211 |
for field in changed_data: |
|
2212 |
if field in protected_fields: |
|
2213 |
errors[field] = [ |
|
2214 |
_( |
|
2215 |
'This field cannot be modified because some recurrences have bookings attached to them.' |
|
2216 |
) |
|
2217 |
] |
|
2218 |
if 'recurrence_end_date' in changed_data and event.has_recurrences_booked( |
|
2219 |
after=payload['recurrence_end_date'] |
|
2220 |
): |
|
2221 |
errors['recurrence_end_date'] = [_('Bookings exist after this date.')] |
|
2222 |
if errors: |
|
2223 |
raise APIError( |
|
2224 |
_('invalid payload'), |
|
2225 |
err_class='invalid payload', |
|
2226 |
errors=errors, |
|
2227 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
2228 |
) |
|
2229 | ||
2230 |
with transaction.atomic(): |
|
2231 |
if any(field for field in changed_data if field in protected_fields): |
|
2232 |
event.recurrences.all().delete() |
|
2233 |
elif event.recurrence_days: |
|
2234 |
protected_fields.append('recurrence_end_date') |
|
2235 |
update_fields = { |
|
2236 |
field: value for field, value in payload.items() if field not in protected_fields |
|
2237 |
} |
|
2238 |
event.recurrences.update(**update_fields) |
|
2239 | ||
2240 |
event = serializer.save() |
|
2241 |
if event.recurrence_end_date: |
|
2242 |
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete() |
|
2243 |
excluded_datetimes = [evt.datetime_slug for evt in event.recurrences.all()] |
|
2244 |
event.create_all_recurrences(excluded_datetimes) |
|
2245 | ||
2246 |
return Response({'err': 0, 'data': get_event_detail(request, event)}) |
|
2247 | ||
2248 | ||
2249 |
events = Events.as_view() |
|
2250 | ||
2251 | ||
2167 | 2252 |
class EventStatus(APIView): |
2168 | 2253 |
permission_classes = (permissions.IsAuthenticated,) |
2169 | 2254 | |
2170 | 2255 |
def get_object(self, agenda_identifier, event_identifier): |
2171 | 2256 |
try: |
2172 | 2257 |
agenda = Agenda.objects.get(slug=agenda_identifier, kind='events') |
2173 | 2258 |
except Agenda.DoesNotExist: |
2174 | 2259 |
try: |
tests/api/test_event.py | ||
---|---|---|
326 | 326 |
'2021-11-20', |
327 | 327 |
'2021-11-29', |
328 | 328 |
'2021-12-02', |
329 | 329 |
'2021-12-04', |
330 | 330 |
'2021-12-13', |
331 | 331 |
'2021-12-16', |
332 | 332 |
'2021-12-18', |
333 | 333 |
] |
334 | ||
335 | ||
336 |
def test_update_event(app, user): |
|
337 |
api_url = '/api/agenda/%s/event/%s/' % ('nop', 'nop') |
|
338 | ||
339 |
# no authentication |
|
340 |
resp = app.patch(api_url, status=401) |
|
341 |
assert resp.json['detail'] == 'Authentication credentials were not provided.' |
|
342 | ||
343 |
# wrong password |
|
344 |
app.authorization = ('Basic', ('john.doe', 'wrong')) |
|
345 |
resp = app.patch(api_url, status=401) |
|
346 |
assert resp.json['detail'] == 'Invalid username/password.' |
|
347 | ||
348 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
349 | ||
350 |
# missing agenda |
|
351 |
resp = app.patch(api_url, status=404) |
|
352 |
assert resp.json['detail'] == 'Not found.' |
|
353 | ||
354 |
meeting_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='meetings') |
|
355 | ||
356 |
# using meeting agenda |
|
357 |
api_url = '/api/agenda/%s/event/%s/' % (meeting_agenda.slug, 'nop') |
|
358 |
resp = app.patch(api_url, status=404) |
|
359 |
assert resp.json['detail'] == 'Not found.' |
|
360 | ||
361 |
agenda = Agenda.objects.create(label='Foo bar') |
|
362 | ||
363 |
# missing event |
|
364 |
api_url = '/api/agenda/%s/event/%s/' % (agenda.slug, 'nop') |
|
365 |
resp = app.patch(api_url, status=404) |
|
366 |
assert resp.json['detail'] == 'Not found.' |
|
367 | ||
368 |
# missing recurring event |
|
369 |
api_url = '/api/agenda/%s/event/%s/' % (agenda.slug, 'nop:2021-11-15-1538') |
|
370 |
resp = app.patch(api_url, status=400) |
|
371 |
assert resp.json['err'] |
|
372 |
assert resp.json['err_desc'] == 'unknown recurring event slug: nop' |
|
373 | ||
374 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, waiting_list_places=5) |
|
375 |
api_url = '/api/agenda/%s/event/%s/' % (agenda.slug, event.slug) |
|
376 | ||
377 |
# update with errors in datetime parts |
|
378 |
params = { |
|
379 |
'start_datetime': '2021-11-15 minuit', |
|
380 |
'recurrence_days': '7, 8', |
|
381 |
} |
|
382 |
resp = app.patch(api_url, params=params, status=400) |
|
383 |
assert resp.json['err'] |
|
384 |
assert resp.json['err_desc'] == 'invalid payload' |
|
385 |
assert 'Datetime has wrong format' in resp.json['errors']['start_datetime'][0] |
|
386 |
assert resp.json['errors']['recurrence_days']['0'][0] == 'Ensure this value is less than or equal to 6.' |
|
387 | ||
388 |
# update with almost all optional managed fields |
|
389 |
params = { |
|
390 |
'start_datetime': '2021-11-15 15:38', |
|
391 |
'duration': 42, |
|
392 |
'publication_date': '2021-09-20', |
|
393 |
'places': 8, |
|
394 |
'waiting_list_places': 3, |
|
395 |
'label': 'FOO camp', |
|
396 |
'description': 'An event', |
|
397 |
'pricing': 'free', |
|
398 |
'url': 'http://example.org/foo/bar/?', |
|
399 |
} |
|
400 |
resp = app.patch(api_url, params=params) |
|
401 |
assert not resp.json['err'] |
|
402 |
event = Event.objects.filter(agenda=agenda).get(slug=event.slug) |
|
403 |
assert event.duration == 42 |
|
404 |
assert event.places == 8 |
|
405 |
assert event.waiting_list_places == 3 |
|
406 |
assert event.label == 'FOO camp' |
|
407 |
assert event.description == 'An event' |
|
408 |
assert event.pricing == 'free' |
|
409 |
assert event.url == 'http://example.org/foo/bar/?' |
|
410 | ||
411 |
# update event as a recurring event |
|
412 |
params = { |
|
413 |
'recurrence_days': '0,3,5', |
|
414 |
'recurrence_week_interval': 2, |
|
415 |
'recurrence_end_date': '2021-12-27', |
|
416 |
} |
|
417 |
resp = app.patch(api_url, params=params) |
|
418 |
assert not resp.json['err'] |
|
419 |
event = Event.objects.filter(agenda=agenda).get(slug=event.slug) |
|
420 |
assert event.recurrences.count() == 9 |
|
421 |
assert [x.places for x in event.recurrences.all()] == [8] * 9 |
|
422 | ||
423 |
booking = Booking.objects.create(event=event.recurrences.all()[2]) |
|
424 | ||
425 |
# update unprotected fields |
|
426 |
params = { |
|
427 |
'places': 7, |
|
428 |
} |
|
429 |
resp = app.patch(api_url, params=params) |
|
430 |
assert not resp.json['err'] |
|
431 |
event = Event.objects.filter(agenda=agenda).get(slug=event.slug) |
|
432 |
assert [x.places for x in event.recurrences.all()] == [7] * 9 |
|
433 | ||
434 |
# try to update recurring event protected fields |
|
435 |
params = { |
|
436 |
'recurrence_days': '1,2', |
|
437 |
'recurrence_week_interval': 1, |
|
438 |
} |
|
439 |
resp = app.patch(api_url, params=params, status=400) |
|
440 |
assert resp.json['err'] |
|
441 |
assert 'cannot be modified' in resp.json['errors']['recurrence_days'][0] |
|
442 |
assert 'cannot be modified' in resp.json['errors']['recurrence_week_interval'][0] |
|
443 | ||
444 |
assert booking.event.start_datetime.date() == datetime.date(2021, 11, 20) |
|
445 | ||
446 |
# try to reduce recurrence end date before booked event |
|
447 |
params = { |
|
448 |
'recurrence_end_date': '2021-11-20', |
|
449 |
} |
|
450 |
resp = app.patch(api_url, params=params, status=400) |
|
451 |
assert resp.json['err'] |
|
452 |
assert resp.json['errors']['recurrence_end_date'][0] == 'Bookings exist after this date.' |
|
453 | ||
454 |
# reduce recurrence end date after booked event |
|
455 |
params = { |
|
456 |
'recurrence_end_date': '2021-11-21', |
|
457 |
} |
|
458 |
resp = app.patch(api_url, params=params) |
|
459 |
assert not resp.json['err'] |
|
460 |
event = Event.objects.filter(agenda=agenda).get(slug=event.slug) |
|
461 |
assert event.recurrences.count() == 3 |
|
462 | ||
463 |
booking.cancel() |
|
464 | ||
465 |
# update no more protected fields |
|
466 |
params = { |
|
467 |
'recurrence_days': '1,2,3,4,5', |
|
468 |
'recurrence_week_interval': 1, |
|
469 |
} |
|
470 |
resp = app.patch(api_url, params=params) |
|
471 |
assert not resp.json['err'] |
|
472 |
event = Event.objects.filter(agenda=agenda).get(slug=event.slug) |
|
473 |
assert event.recurrences.count() == 5 |
|
334 |
- |