0001-api-add-add-event-endpoint-47337.patch
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 |
- |