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.SlugField(max_length=160, allow_blank=False) |
|
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 |
] |
|
159 | ||
160 |
def validate_recurrence_days(self, values): |
|
161 |
super().validate(values) |
|
162 |
results = [] |
|
163 |
for value in values: |
|
164 |
try: |
|
165 |
if int(value) in [x[0] for x in Event.WEEKDAY_CHOICES]: |
|
166 |
results.append(int(value)) |
|
167 |
continue |
|
168 |
except ValueError: |
|
169 |
pass |
|
170 |
raise serializers.ValidationError(_('[0-6] string numbers was expected.')) |
|
171 |
return results |
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( |
... | ... | |
501 | 501 |
} |
502 | 502 |
if show_events is not None: |
503 | 503 |
details['api']['fillslot_url'] += '?events=%s' % show_events |
504 | 504 |
if booked_user_external_id: |
505 | 505 |
if getattr(event, 'user_places_count', 0) > 0: |
506 | 506 |
details['booked_for_external_user'] = 'main-list' |
507 | 507 |
elif getattr(event, 'user_waiting_places_count', 0) > 0: |
508 | 508 |
details['booked_for_external_user'] = 'waiting-list' |
509 |
if event.recurrence_days is not None: |
|
510 |
details['recurrence_days'] = event.recurrence_days |
|
511 |
details['recurrence_week_interval'] = event.recurrence_week_interval |
|
512 |
details['recurrence_end_date'] = event.recurrence_end_date |
|
509 | 513 | |
510 | 514 |
return details |
511 | 515 | |
512 | 516 | |
513 | 517 |
def get_events_meta_detail( |
514 | 518 |
request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False |
515 | 519 |
): |
516 | 520 |
bookable_datetimes_number_total = 0 |
... | ... | |
2402 | 2406 |
'series': series, |
2403 | 2407 |
}, |
2404 | 2408 |
'err': 0, |
2405 | 2409 |
} |
2406 | 2410 |
) |
2407 | 2411 | |
2408 | 2412 | |
2409 | 2413 |
bookings_statistics = BookingsStatistics.as_view() |
2414 | ||
2415 | ||
2416 |
class AgendaAddEventView(APIView): |
|
2417 |
permission_classes = (permissions.IsAuthenticated,) |
|
2418 |
serializer_class = serializers.EventSerializer |
|
2419 | ||
2420 |
def post(self, request, agenda_identifier): |
|
2421 |
agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events') |
|
2422 | ||
2423 |
serializer = self.serializer_class(data=request.data) |
|
2424 |
if not serializer.is_valid(): |
|
2425 |
raise APIError( |
|
2426 |
_('invalid payload'), |
|
2427 |
err_class='invalid payload', |
|
2428 |
errors=serializer.errors, |
|
2429 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
2430 |
) |
|
2431 |
payload = serializer.validated_data |
|
2432 |
event = Event.objects.create(agenda=agenda, **payload) |
|
2433 |
if event.recurrence_days: |
|
2434 |
if event.recurrence_end_date: |
|
2435 |
event.create_all_recurrences() |
|
2436 | ||
2437 |
return Response({'err': 0, 'data': get_event_detail(request, event)}) |
|
2438 | ||
2439 | ||
2440 |
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 {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.isdisjoint( |
|
220 |
resp.json['data'].keys() |
|
221 |
) |
|
222 |
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event') |
|
223 |
assert str(event.start_datetime) == '2021-11-15 14:38:00+00:00' |
|
224 |
assert str(event.start_datetime.tzinfo) == 'UTC' |
|
225 |
assert event.places == 10 |
|
226 |
assert event.publication_date is None |
|
227 | ||
228 |
# add with almost all optional managed fields |
|
229 |
params = { |
|
230 |
'start_datetime': '2021-11-15 15:38', |
|
231 |
'duration': 42, |
|
232 |
'publication_date': '2021-09-20', |
|
233 |
'places': 11, |
|
234 |
'waiting_list_places': 3, |
|
235 |
'label': 'FOO camp', |
|
236 |
'description': 'An event', |
|
237 |
'pricing': 'free', |
|
238 |
'url': 'http://example.org/foo/bar/?', |
|
239 |
} |
|
240 |
resp = app.post(api_url, params=params) |
|
241 |
assert not resp.json['err'] |
|
242 |
assert resp.json['data']['id'] == 'foo-camp' |
|
243 |
assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.isdisjoint( |
|
244 |
resp.json['data'].keys() |
|
245 |
) |
|
246 |
event = Event.objects.filter(agenda=agenda).get(slug='foo-camp') |
|
247 |
assert event.duration == 42 |
|
248 |
assert event.waiting_list_places == 3 |
|
249 |
assert event.label == 'FOO camp' |
|
250 |
assert event.description == 'An event' |
|
251 |
assert event.pricing == 'free' |
|
252 |
assert event.url == 'http://example.org/foo/bar/?' |
|
253 | ||
254 |
# add with errors in recurrence_days list |
|
255 |
params = { |
|
256 |
'start_datetime': '2021-11-15 15:38', |
|
257 |
'places': 10, |
|
258 |
'recurrence_days': 'oups', |
|
259 |
} |
|
260 |
resp = app.post(api_url, params=params, status=400) |
|
261 |
assert resp.json['err'] |
|
262 |
assert resp.json['err_desc'] == 'invalid payload' |
|
263 |
assert '[0-6] string numbers was expected' in resp.json['errors']['recurrence_days'][0] |
|
264 |
params = { |
|
265 |
'start_datetime': '2021-11-15 15:38', |
|
266 |
'places': 10, |
|
267 |
'recurrence_days': '7', |
|
268 |
} |
|
269 |
resp = app.post(api_url, params=params, status=400) |
|
270 |
assert resp.json['err'] |
|
271 |
assert resp.json['err_desc'] == 'invalid payload' |
|
272 |
assert '[0-6] string numbers was expected' in resp.json['errors']['recurrence_days'][0] |
|
273 | ||
274 |
# add a recurrent event |
|
275 |
params = { |
|
276 |
'start_datetime': '2021-11-15 15:38', |
|
277 |
'places': 12, |
|
278 |
'recurrence_days': '0,3,5', |
|
279 |
'recurrence_week_interval': '2', |
|
280 |
'description': 'A recurrent event', |
|
281 |
} |
|
282 |
assert Event.objects.filter(agenda=agenda).count() == 2 |
|
283 |
resp = app.post(api_url, params=params) |
|
284 |
assert Event.objects.filter(agenda=agenda).count() == 3 |
|
285 |
assert not resp.json['err'] |
|
286 |
assert resp.json['data']['id'] == 'foo-bar-event-1' |
|
287 |
assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.issubset( |
|
288 |
resp.json['data'].keys() |
|
289 |
) |
|
290 |
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-1') |
|
291 |
assert event.description == 'A recurrent event' |
|
292 |
assert event.recurrence_days == [0, 3, 5] |
|
293 |
assert event.recurrence_week_interval == 2 |
|
294 |
assert event.recurrence_end_date is None |
|
295 | ||
296 |
# add a recurrent event with end recurrence date creates 9 recurrences |
|
297 |
params = { |
|
298 |
'start_datetime': '2021-11-15 15:38', |
|
299 |
'places': 13, |
|
300 |
'recurrence_days': '0,3,5', # Monday, Tuesday, Saturday |
|
301 |
'recurrence_week_interval': '2', |
|
302 |
'recurrence_end_date': '2021-12-27', |
|
303 |
'description': 'A recurrent event having recurrences', |
|
304 |
} |
|
305 |
resp = app.post(api_url, params=params) |
|
306 |
assert not resp.json['err'] |
|
307 |
assert resp.json['data']['id'] == 'foo-bar-event-2' |
|
308 |
assert {'recurrence_days', 'recurrence_week_interval', 'recurrence_end_date'}.issubset( |
|
309 |
resp.json['data'].keys() |
|
310 |
) |
|
311 |
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-2') |
|
312 |
assert Event.objects.filter(agenda=agenda).count() == 13 |
|
313 |
assert event.description == 'A recurrent event having recurrences' |
|
314 |
assert event.recurrence_days == [0, 3, 5] |
|
315 |
assert event.recurrence_week_interval == 2 |
|
316 |
assert event.recurrence_end_date == datetime.date(2021, 12, 27) |
|
317 |
assert sorted( |
|
318 |
str(x.start_datetime.date()) for x in Event.objects.all() if 'foo-bar-event-2--' in x.slug |
|
319 |
) == [ |
|
320 |
'2021-11-15', |
|
321 |
'2021-11-18', |
|
322 |
'2021-11-20', |
|
323 |
'2021-11-29', |
|
324 |
'2021-12-02', |
|
325 |
'2021-12-04', |
|
326 |
'2021-12-13', |
|
327 |
'2021-12-16', |
|
328 |
'2021-12-18', |
|
329 |
] |
|
161 |
- |