Projet

Général

Profil

0001-api-add-add-event-endpoint-47337.patch

Nicolas Roche, 23 septembre 2021 17:35

Télécharger (15,6 ko)

Voir les différences:

Subject: [PATCH] api: add add-event endpoint (#47337)

 chrono/api/serializers.py |  25 +++++-
 chrono/api/urls.py        |   5 ++
 chrono/api/views.py       |  96 ++++++++++++++-------
 tests/api/test_event.py   | 173 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 269 insertions(+), 30 deletions(-)
chrono/api/serializers.py
1 1
from django.utils.translation import ugettext_lazy as _
2 2
from rest_framework import serializers
3 3
from rest_framework.exceptions import ValidationError
4 4

  
5
from chrono.agendas.models import AbsenceReason, Booking
5
from chrono.agendas.models import AbsenceReason, Booking, Event
6 6

  
7 7

  
8 8
class StringOrListField(serializers.ListField):
9 9
    def to_internal_value(self, data):
10 10
        if isinstance(data, str):
11 11
            data = [s.strip() for s in data.split(',') if s.strip()]
12 12
        return super().to_internal_value(data)
13 13

  
......
128 128
            )
129 129
        return attrs
130 130

  
131 131

  
132 132
class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
133 133
    agendas = CommaSeparatedStringField(
134 134
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
135 135
    )
136

  
137

  
138
class EventSerializer(serializers.ModelSerializer):
139
    recurrence_days = CommaSeparatedStringField(
140
        required=False, child=serializers.IntegerField(min_value=0, max_value=6)
141
    )
142

  
143
    class Meta:
144
        model = Event
145
        fields = [
146
            'start_datetime',
147
            'recurrence_days',
148
            'recurrence_week_interval',
149
            'recurrence_end_date',
150
            'duration',
151
            'publication_date',
152
            'places',
153
            'waiting_list_places',
154
            'label',
155
            'description',
156
            'pricing',
157
            'url',
158
        ]
chrono/api/urls.py
59 59
        views.event_bookings,
60 60
        name='api-event-bookings',
61 61
    ),
62 62
    url(
63 63
        r'^agenda/(?P<agenda_identifier>[\w-]+)/check/(?P<event_identifier>[\w:-]+)/$',
64 64
        views.event_check,
65 65
        name='api-event-check',
66 66
    ),
67
    url(
68
        r'^agenda/(?P<agenda_identifier>[\w-]+)/add-event/$',
69
        views.agenda_add_event,
70
        name='api-agenda-add-event',
71
    ),
67 72
    url(
68 73
        r'^agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
69 74
        views.meeting_datetimes,
70 75
        name='api-agenda-meeting-datetimes-legacy',
71 76
    ),
72 77
    url(r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/$', views.meeting_list, name='api-agenda-meetings'),
73 78
    url(
74 79
        r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\w-]+)/$',
chrono/api/views.py
435 435
            )
436 436
        except (VariableDoesNotExist, TemplateSyntaxError):
437 437
            pass
438 438
    elif event.label and event.primary_event_id is not None:
439 439
        event_text = '%s (%s)' % (
440 440
            event.label,
441 441
            date_format(localtime(event.start_datetime), 'DATETIME_FORMAT'),
442 442
        )
443
    elif event.recurrence_days:
443
    elif day is not None:
444 444
        event_text = _('%(weekday)s: %(event)s') % {
445 445
            'weekday': WEEKDAYS[day].capitalize(),
446 446
            'event': event_text,
447 447
        }
448 448
    return event_text
449 449

  
450 450

  
451 451
def get_event_detail(
......
465 465
        'text': get_event_text(event, agenda),
466 466
        'label': event.label or '',
467 467
        'date': format_response_date(event.start_datetime),
468 468
        'datetime': format_response_datetime(event.start_datetime),
469 469
        'description': event.description,
470 470
        'pricing': event.pricing,
471 471
        'url': event.url,
472 472
        'duration': event.duration,
473
        'disabled': is_event_disabled(event, min_places=min_places, disable_booked=disable_booked),
474
        'api': {
475
            'bookings_url': request.build_absolute_uri(
476
                reverse(
477
                    'api-event-bookings',
478
                    kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
479
                )
480
            ),
481
            'fillslot_url': request.build_absolute_uri(
482
                reverse(
483
                    'api-fillslot',
484
                    kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
485
                )
486
            ),
487
            'status_url': request.build_absolute_uri(
488
                reverse(
489
                    'api-event-status',
490
                    kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
491
                )
492
            ),
493
            'check_url': request.build_absolute_uri(
494
                reverse(
495
                    'api-event-check',
496
                    kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
497
                )
498
            ),
499
        },
500
        'places': get_event_places(event),
501 473
    }
474
    if event.recurrence_days:
475
        details.update(
476
            {
477
                'recurrence_days': event.recurrence_days,
478
                'recurrence_week_interval': event.recurrence_week_interval,
479
                'recurrence_end_date': event.recurrence_end_date,
480
            }
481
        )
482
    else:
483
        details.update(
484
            {
485
                'disabled': is_event_disabled(event, min_places=min_places, disable_booked=disable_booked),
486
                'api': {
487
                    'bookings_url': request.build_absolute_uri(
488
                        reverse(
489
                            'api-event-bookings',
490
                            kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
491
                        )
492
                    ),
493
                    'fillslot_url': request.build_absolute_uri(
494
                        reverse(
495
                            'api-fillslot',
496
                            kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
497
                        )
498
                    ),
499
                    'status_url': request.build_absolute_uri(
500
                        reverse(
501
                            'api-event-status',
502
                            kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
503
                        )
504
                    ),
505
                    'check_url': request.build_absolute_uri(
506
                        reverse(
507
                            'api-event-check',
508
                            kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
509
                        )
510
                    ),
511
                },
512
                'places': get_event_places(event),
513
            }
514
        )
502 515
    if show_events is not None:
503 516
        details['api']['fillslot_url'] += '?events=%s' % show_events
504 517
    if booked_user_external_id:
505 518
        if getattr(event, 'user_places_count', 0) > 0:
506 519
            details['booked_for_external_user'] = 'main-list'
507 520
        elif getattr(event, 'user_waiting_places_count', 0) > 0:
508 521
            details['booked_for_external_user'] = 'waiting-list'
509 522

  
......
2402 2415
                    'series': series,
2403 2416
                },
2404 2417
                'err': 0,
2405 2418
            }
2406 2419
        )
2407 2420

  
2408 2421

  
2409 2422
bookings_statistics = BookingsStatistics.as_view()
2423

  
2424

  
2425
class AgendaAddEventView(APIView):
2426
    permission_classes = (permissions.IsAuthenticated,)
2427
    serializer_class = serializers.EventSerializer
2428

  
2429
    def post(self, request, agenda_identifier):
2430
        agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
2431

  
2432
        serializer = self.serializer_class(data=request.data)
2433
        if not serializer.is_valid():
2434
            raise APIError(
2435
                _('invalid payload'),
2436
                err_class='invalid payload',
2437
                errors=serializer.errors,
2438
                http_status=status.HTTP_400_BAD_REQUEST,
2439
            )
2440
        payload = serializer.validated_data
2441
        event = Event.objects.create(agenda=agenda, **payload)
2442
        if event.recurrence_days and event.recurrence_end_date:
2443
            event.create_all_recurrences()
2444
        return Response({'err': 0, 'data': get_event_detail(request, event)})
2445

  
2446

  
2447
agenda_add_event = AgendaAddEventView.as_view()
tests/api/test_event.py
153 153

  
154 154
    # wrong kind
155 155
    agenda.kind = 'meetings'
156 156
    agenda.save()
157 157
    app.post('/api/agenda/%s/check/%s/' % (agenda.slug, event.slug), status=404)
158 158
    agenda.kind = 'virtual'
159 159
    agenda.save()
160 160
    app.post('/api/agenda/%s/check/%s/' % (agenda.slug, event.slug), status=404)
161

  
162

  
163
def test_add_event(app, user):
164
    api_url = '/api/agenda/%s/add-event/' % ('999')
165

  
166
    # no authentication
167
    resp = app.post(api_url, status=401)
168
    assert resp.json['detail'] == 'Authentication credentials were not provided.'
169

  
170
    # wrong password
171
    app.authorization = ('Basic', ('john.doe', 'wrong'))
172
    resp = app.post(api_url, status=401)
173
    assert resp.json['detail'] == 'Invalid username/password.'
174

  
175
    app.authorization = ('Basic', ('john.doe', 'password'))
176

  
177
    # missing agenda
178
    resp = app.post(api_url, status=404)
179
    assert resp.json['detail'] == 'Not found.'
180

  
181
    # using meeting agenda
182
    meeting_agenda = Agenda(label='Foo bar Meeting', kind='meetings')
183
    meeting_agenda.save()
184
    api_url = '/api/agenda/%s/add-event/' % (meeting_agenda.slug)
185
    resp = app.post(api_url, status=404)
186
    assert resp.json['detail'] == 'Not found.'
187

  
188
    agenda = Agenda(label='Foo bar')
189
    agenda.maximal_booking_delay = 0
190
    agenda.save()
191
    api_url = '/api/agenda/%s/add-event/' % (agenda.slug)
192

  
193
    # missing fields
194
    resp = app.post(api_url, status=400)
195
    assert resp.json['err']
196
    assert resp.json['errors'] == {
197
        'start_datetime': ['This field is required.'],
198
        'places': ['This field is required.'],
199
    }
200

  
201
    # add with errors in datetime parts
202
    params = {
203
        'start_datetime': '2021-11-15 minuit',
204
        'places': 10,
205
    }
206
    resp = app.post(api_url, params=params, status=400)
207
    assert resp.json['err']
208
    assert resp.json['err_desc'] == 'invalid payload'
209
    assert 'Datetime has wrong format' in resp.json['errors']['start_datetime'][0]
210

  
211
    # add an event
212
    params = {
213
        'start_datetime': '2021-11-15 15:38',
214
        'places': 10,
215
    }
216
    resp = app.post(api_url, params=params)
217
    assert not resp.json['err']
218
    assert resp.json['data']['id'] == 'foo-bar-event'
219
    assert {'api', 'disabled', 'places'}.issubset(resp.json['data'].keys())
220
    assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.isdisjoint(
221
        resp.json['data'].keys()
222
    )
223
    event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event')
224
    assert str(event.start_datetime) == '2021-11-15 14:38:00+00:00'
225
    assert str(event.start_datetime.tzinfo) == 'UTC'
226
    assert event.places == 10
227
    assert event.publication_date is None
228

  
229
    # add with almost all optional managed fields
230
    params = {
231
        'start_datetime': '2021-11-15 15:38',
232
        'duration': 42,
233
        'publication_date': '2021-09-20',
234
        'places': 11,
235
        'waiting_list_places': 3,
236
        'label': 'FOO camp',
237
        'description': 'An event',
238
        'pricing': 'free',
239
        'url': 'http://example.org/foo/bar/?',
240
    }
241
    resp = app.post(api_url, params=params)
242
    assert not resp.json['err']
243
    assert resp.json['data']['id'] == 'foo-camp'
244
    assert {'api', 'disabled', 'places'}.issubset(resp.json['data'].keys())
245
    assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.isdisjoint(
246
        resp.json['data'].keys()
247
    )
248
    event = Event.objects.filter(agenda=agenda).get(slug='foo-camp')
249
    assert event.duration == 42
250
    assert event.waiting_list_places == 3
251
    assert event.label == 'FOO camp'
252
    assert event.description == 'An event'
253
    assert event.pricing == 'free'
254
    assert event.url == 'http://example.org/foo/bar/?'
255

  
256
    # add with errors in recurrence_days list
257
    params = {
258
        'start_datetime': '2021-11-15 15:38',
259
        'places': 10,
260
        'recurrence_days': 'oups',
261
    }
262
    resp = app.post(api_url, params=params, status=400)
263
    assert resp.json['err']
264
    assert resp.json['err_desc'] == 'invalid payload'
265
    assert resp.json['errors']['recurrence_days']['0'][0] == 'A valid integer is required.'
266
    params = {
267
        'start_datetime': '2021-11-15 15:38',
268
        'places': 10,
269
        'recurrence_days': '7',
270
    }
271
    resp = app.post(api_url, params=params, status=400)
272
    assert resp.json['err']
273
    assert resp.json['err_desc'] == 'invalid payload'
274
    assert resp.json['errors']['recurrence_days']['0'][0] == 'Ensure this value is less than or equal to 6.'
275

  
276
    # add a recurrent event
277
    params = {
278
        'start_datetime': '2021-11-15 15:38',
279
        'places': 12,
280
        'recurrence_days': '0,3,5',
281
        'recurrence_week_interval': '2',
282
        'description': 'A recurrent event',
283
    }
284
    assert Event.objects.filter(agenda=agenda).count() == 2
285
    resp = app.post(api_url, params=params)
286
    assert Event.objects.filter(agenda=agenda).count() == 3
287
    assert not resp.json['err']
288
    assert resp.json['data']['id'] == 'foo-bar-event-1'
289
    assert {'api', 'disabled', 'places'}.isdisjoint(resp.json['data'].keys())
290
    assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.issubset(
291
        resp.json['data'].keys()
292
    )
293
    event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-1')
294
    assert event.description == 'A recurrent event'
295
    assert event.recurrence_days == [0, 3, 5]
296
    assert event.recurrence_week_interval == 2
297
    assert event.recurrence_end_date is None
298

  
299
    # add a recurrent event with end recurrence date creates 9 recurrences
300
    params = {
301
        'start_datetime': '2021-11-15 15:38',
302
        'places': 13,
303
        'recurrence_days': '0,3,5',  # Monday, Tuesday, Saturday
304
        'recurrence_week_interval': '2',
305
        'recurrence_end_date': '2021-12-27',
306
        'description': 'A recurrent event having recurrences',
307
    }
308
    resp = app.post(api_url, params=params)
309
    assert not resp.json['err']
310
    assert resp.json['data']['id'] == 'foo-bar-event-2'
311
    assert {'api', 'disabled', 'places'}.isdisjoint(resp.json['data'].keys())
312
    assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.issubset(
313
        resp.json['data'].keys()
314
    )
315
    event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-2')
316
    assert Event.objects.filter(agenda=agenda).count() == 13
317
    assert event.description == 'A recurrent event having recurrences'
318
    assert event.recurrence_days == [0, 3, 5]
319
    assert event.recurrence_week_interval == 2
320
    assert event.recurrence_end_date == datetime.date(2021, 12, 27)
321
    assert sorted(
322
        str(x.start_datetime.date()) for x in Event.objects.all() if 'foo-bar-event-2--' in x.slug
323
    ) == [
324
        '2021-11-15',
325
        '2021-11-18',
326
        '2021-11-20',
327
        '2021-11-29',
328
        '2021-12-02',
329
        '2021-12-04',
330
        '2021-12-13',
331
        '2021-12-16',
332
        '2021-12-18',
333
    ]
161
-