Projet

Général

Profil

0001-api-return-err_desc-reason-in-case-of-error-24025.patch

Lauréline Guérin, 31 octobre 2019 09:43

Télécharger (21,4 ko)

Voir les différences:

Subject: [PATCH] api: return err_desc & reason in case of error (#24025)

 chrono/api/utils.py     | 27 ++++++++++++++++++
 chrono/api/views.py     | 30 ++++++++++----------
 tests/test_api.py       | 62 ++++++++++++++++++++---------------------
 tests/test_api_utils.py | 15 ++++++++++
 4 files changed, 88 insertions(+), 46 deletions(-)
 create mode 100644 chrono/api/utils.py
 create mode 100644 tests/test_api_utils.py
chrono/api/utils.py
1
# -*- coding: utf-8 -*-
2
# chrono - agendas system
3
# Copyright (C) 2016  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
from rest_framework.response import Response as DRFResponse
19

  
20

  
21
class Response(DRFResponse):
22
    def __init__(self, data=None, *args, **kwargs):
23
        # add reason for compatibility (https://dev.entrouvert.org/issues/24025)
24
        if data is not None and 'err_desc' in data:
25
            data['reason'] = data['err_desc']
26

  
27
        super(Response, self).__init__(data=data, *args, **kwargs)
chrono/api/views.py
26 26
from django.utils.timezone import now, make_aware, localtime
27 27

  
28 28
from rest_framework import permissions, serializers, status
29
from rest_framework.response import Response
30 29
from rest_framework.views import APIView
31 30

  
31
from chrono.api.utils import Response
32 32
from ..agendas.models import (Agenda, Event, Booking, MeetingType,
33 33
                              TimePeriod, Desk)
34 34
from ..interval import Intervals
......
351 351
        if not serializer.is_valid():
352 352
            return Response({
353 353
                'err': 1,
354
                'reason': 'invalid payload',
354
                'err_desc': 'invalid payload',
355 355
                'errors': serializer.errors
356 356
            }, status=status.HTTP_400_BAD_REQUEST)
357 357
        payload = serializer.validated_data
......
361 361
        if not slots:
362 362
            return Response({
363 363
                'err': 1,
364
                'reason': 'slots list cannot be empty',
364
                'err_desc': 'slots list cannot be empty',
365 365
            }, status=status.HTTP_400_BAD_REQUEST)
366 366

  
367 367
        if 'count' in payload:
......
373 373
            except ValueError:
374 374
                return Response({
375 375
                    'err': 1,
376
                    'reason': 'invalid value for count (%s)' % request.query_params['count'],
376
                    'err_desc': 'invalid value for count (%s)' % request.query_params['count'],
377 377
                }, status=status.HTTP_400_BAD_REQUEST)
378 378
        else:
379 379
            places_count = 1
......
381 381
        if places_count <= 0:
382 382
            return Response({
383 383
                'err': 1,
384
                'reason': 'count cannot be less than or equal to zero'
384
                'err_desc': 'count cannot be less than or equal to zero'
385 385
            }, status=status.HTTP_400_BAD_REQUEST)
386 386

  
387 387
        to_cancel_booking = None
......
392 392
            except (ValueError, TypeError):
393 393
                return Response({
394 394
                    'err': 1,
395
                    'reason': 'cancel_booking_id is not an integer'
395
                    'err_desc': 'cancel_booking_id is not an integer'
396 396
                }, status=status.HTTP_400_BAD_REQUEST)
397 397

  
398 398
        if cancel_booking_id is not None:
......
409 409
                cancel_error = 'cancel booking: booking does no exist'
410 410

  
411 411
            if cancel_error:
412
                return Response({'err': 1, 'reason': cancel_error})
412
                return Response({'err': 1, 'err_desc': cancel_error})
413 413

  
414 414
        extra_data = {}
415 415
        for k, v in request.data.items():
......
429 429
                except ValueError:
430 430
                    return Response({
431 431
                        'err': 1,
432
                        'reason': 'invalid slot: %s' % slot,
432
                        'err_desc': 'invalid slot: %s' % slot,
433 433
                    }, status=status.HTTP_400_BAD_REQUEST)
434 434
                if meeting_type_id_ != meeting_type_id:
435 435
                    return Response({
436 436
                        'err': 1,
437
                        'reason': 'all slots must have the same meeting type id (%s)' % meeting_type_id
437
                        'err_desc': 'all slots must have the same meeting type id (%s)' % meeting_type_id
438 438
                    }, status=status.HTTP_400_BAD_REQUEST)
439 439
                datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
440 440

  
......
451 451
                    available_desk = Desk.objects.get(id=available_desk_id)
452 452
                    break
453 453
            else:
454
                return Response({'err': 1, 'reason': 'no more desk available'})
454
                return Response({'err': 1, 'err_desc': 'no more desk available'})
455 455

  
456 456
            # all datetimes are free, book them in order
457 457
            datetimes = list(datetimes)
......
478 478
                    # in the waiting list.
479 479
                    in_waiting_list = True
480 480
                    if (event.waiting_list + places_count) > event.waiting_list_places:
481
                        return Response({'err': 1, 'reason': 'sold out'})
481
                        return Response({'err': 1, 'err_desc': 'sold out'})
482 482
            else:
483 483
                if (event.booked_places + places_count) > event.places:
484
                    return Response({'err': 1, 'reason': 'sold out'})
484
                    return Response({'err': 1, 'err_desc': 'sold out'})
485 485

  
486 486
        with transaction.atomic():
487 487
            if to_cancel_booking:
......
574 574
    def post(self, request, booking_pk=None, format=None):
575 575
        booking = get_object_or_404(Booking, id=booking_pk)
576 576
        if booking.cancellation_datetime:
577
            response = {'err': 1, 'reason': 'already cancelled'}
577
            response = {'err': 1, 'err_desc': 'already cancelled'}
578 578
            return Response(response)
579 579
        booking.cancel()
580 580
        response = {'err': 0, 'booking_id': booking.id}
......
595 595
    def post(self, request, booking_pk=None, format=None):
596 596
        booking = get_object_or_404(Booking, id=booking_pk)
597 597
        if booking.cancellation_datetime:
598
            response = {'err': 1, 'reason': 'booking is cancelled'}
598
            response = {'err': 1, 'err_desc': 'booking is cancelled'}
599 599
            return Response(response)
600 600
        if not booking.in_waiting_list:
601
            response = {'err': 2, 'reason': 'booking is not in waiting list'}
601
            response = {'err': 2, 'err_desc': 'booking is not in waiting list'}
602 602
            return Response(response)
603 603
        booking.accept()
604 604
        response = {'err': 0, 'booking_id': booking.id}
tests/test_api.py
316 316
    app.authorization = ('Basic', ('john.doe', 'password'))
317 317
    resp = app.post(fillslot_url)
318 318
    assert resp.json['err'] == 1
319
    assert resp.json['reason'] == 'no more desk available'
319
    assert resp.json['err_desc'] == 'no more desk available'
320 320
    # booking the two slots fails too
321 321
    fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug
322 322
    resp = app.post(fillslots_url, params={'slots': two_slots})
323 323
    assert resp.json['err'] == 1
324
    assert resp.json['reason'] == 'no more desk available'
324
    assert resp.json['err_desc'] == 'no more desk available'
325 325

  
326 326
def test_booking_api(app, some_data, user):
327 327
    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
......
376 376
    resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
377 377
            params={'user_name': {'foo': 'bar'}}, status=400)
378 378
    assert resp.json['err'] == 1
379
    assert resp.json['reason'] == 'invalid payload'
379
    assert resp.json['err_desc'] == 'invalid payload'
380 380
    assert len(resp.json['errors']) == 1
381 381
    assert 'user_name' in resp.json['errors']
382 382

  
......
538 538
            params={'slots': events_ids,
539 539
                    'user_name': {'foo': 'bar'}}, status=400)
540 540
    assert resp.json['err'] == 1
541
    assert resp.json['reason'] == 'invalid payload'
541
    assert resp.json['err_desc'] == 'invalid payload'
542 542
    assert len(resp.json['errors']) == 1
543 543
    assert 'user_name' in resp.json['errors']
544 544

  
545 545
    # empty or missing slots
546 546
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400)
547 547
    assert resp.json['err'] == 1
548
    assert resp.json['reason'] == 'slots list cannot be empty'
548
    assert resp.json['err_desc'] == 'slots list cannot be empty'
549 549
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400)
550 550
    assert resp.json['err'] == 1
551
    assert resp.json['reason'] == 'slots list cannot be empty'
551
    assert resp.json['err_desc'] == 'slots list cannot be empty'
552 552
    # invalid slots format
553 553
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': 'foobar'}, status=400)
554 554
    assert resp.json['err'] == 1
555
    assert resp.json['reason'] == 'invalid payload'
555
    assert resp.json['err_desc'] == 'invalid payload'
556 556
    assert len(resp.json['errors']) == 1
557 557
    assert 'slots' in resp.json['errors']
558 558

  
......
589 589
    # try booking the same timeslot
590 590
    resp2 = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
591 591
    assert resp2.json['err'] == 1
592
    assert resp2.json['reason'] == 'no more desk available'
592
    assert resp2.json['err_desc'] == 'no more desk available'
593 593

  
594 594
    # try booking another timeslot
595 595
    event_id = resp.json['data'][3]['id']
......
619 619
    # try booking the same timeslots
620 620
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
621 621
    assert resp2.json['err'] == 1
622
    assert resp2.json['reason'] == 'no more desk available'
622
    assert resp2.json['err_desc'] == 'no more desk available'
623 623

  
624 624
    # try booking partially free timeslots (one free, one busy)
625 625
    nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
626 626
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
627 627
    assert resp2.json['err'] == 1
628
    assert resp2.json['reason'] == 'no more desk available'
628
    assert resp2.json['err_desc'] == 'no more desk available'
629 629

  
630 630
    # booking other free timeslots
631 631
    free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
......
647 647
                    params={'slots': impossible_slots},
648 648
                    status=400)
649 649
    assert resp.json['err'] == 1
650
    assert resp.json['reason'] == 'all slots must have the same meeting type id (1)'
650
    assert resp.json['err_desc'] == 'all slots must have the same meeting type id (1)'
651 651

  
652 652
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
653 653
    meetings_agenda.maximal_booking_delay = 365
......
762 762
        params={'cancel_booking_id': first_booking.pk}
763 763
    )
764 764
    assert resp.json['err'] == 1
765
    assert resp.json['reason'] == 'cancel booking: booking already cancelled'
765
    assert resp.json['err_desc'] == 'cancel booking: booking already cancelled'
766 766
    assert Booking.objects.count() == 2
767 767

  
768 768
    # Cancelling a non existent booking returns an error
......
771 771
        params={'cancel_booking_id': '-1'}
772 772
    )
773 773
    assert resp.json['err'] == 1
774
    assert resp.json['reason'] == 'cancel booking: booking does no exist'
774
    assert resp.json['err_desc'] == 'cancel booking: booking does no exist'
775 775
    assert Booking.objects.count() == 2
776 776

  
777 777
    # Cancelling booking with different count than new booking
......
788 788
        params={'cancel_booking_id': booking_id, 'count': 1}
789 789
    )
790 790
    assert resp.json['err'] == 1
791
    assert resp.json['reason'] == 'cancel booking: count is different'
791
    assert resp.json['err_desc'] == 'cancel booking: count is different'
792 792
    assert Booking.objects.count() == 4
793 793

  
794 794
    # cancel_booking_id must be an integer
......
887 887
    app.authorization = ('Basic', ('john.doe', 'password'))
888 888
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
889 889
    assert resp.json['err'] == 1
890
    assert resp.json['reason'] == 'sold out'
890
    assert resp.json['err_desc'] == 'sold out'
891 891

  
892 892
def test_status(app, some_data, user):
893 893
    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
......
980 980
    app.authorization = ('Basic', ('john.doe', 'password'))
981 981
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
982 982
    assert resp.json['err'] == 1
983
    assert resp.json['reason'] == 'sold out'
983
    assert resp.json['err_desc'] == 'sold out'
984 984

  
985 985
def test_accept_booking(app, some_data, user):
986 986
    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
......
1025 1025
    app.authorization = ('Basic', ('john.doe', 'password'))
1026 1026
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=NaN' % (agenda.slug, event.id), status=400)
1027 1027
    assert resp.json['err'] == 1
1028
    assert resp.json['reason'] == "invalid value for count (NaN)"
1028
    assert resp.json['err_desc'] == "invalid value for count (NaN)"
1029 1029

  
1030 1030
    app.authorization = ('Basic', ('john.doe', 'password'))
1031 1031
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=0' % (agenda.slug, event.id), status=400)
1032 1032
    assert resp.json['err'] == 1
1033
    assert resp.json['reason'] == "count cannot be less than or equal to zero"
1033
    assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
1034 1034

  
1035 1035
    app.authorization = ('Basic', ('john.doe', 'password'))
1036 1036
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=-3' % (agenda.slug, event.id), status=400)
1037 1037
    assert resp.json['err'] == 1
1038
    assert resp.json['reason'] == "count cannot be less than or equal to zero"
1038
    assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
1039 1039

  
1040 1040
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
1041 1041
    Booking.objects.get(id=resp.json['booking_id'])
......
1062 1062
    # check waiting list overflow
1063 1063
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
1064 1064
    assert resp.json['err'] == 1
1065
    assert resp.json['reason'] == 'sold out'
1065
    assert resp.json['err_desc'] == 'sold out'
1066 1066
    assert Event.objects.get(id=event.id).booked_places == 2
1067 1067
    assert Event.objects.get(id=event.id).waiting_list == 5
1068 1068

  
......
1079 1079

  
1080 1080
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
1081 1081
    assert resp.json['err'] == 1
1082
    assert resp.json['reason'] == 'sold out'
1082
    assert resp.json['err_desc'] == 'sold out'
1083 1083

  
1084 1084
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
1085 1085
    assert resp.json['err'] == 0
......
1088 1088

  
1089 1089
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
1090 1090
    assert resp.json['err'] == 1
1091
    assert resp.json['reason'] == 'sold out'
1091
    assert resp.json['err_desc'] == 'sold out'
1092 1092

  
1093 1093
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=2' % (agenda.slug, event.id))
1094 1094
    assert resp.json['err'] == 0
......
1106 1106
    app.authorization = ('Basic', ('john.doe', 'password'))
1107 1107
    resp = app.post('/api/agenda/%s/fillslots/?count=NaN' % agenda.slug, params={'slots': slots}, status=400)
1108 1108
    assert resp.json['err'] == 1
1109
    assert resp.json['reason'] == "invalid value for count (NaN)"
1109
    assert resp.json['err_desc'] == "invalid value for count (NaN)"
1110 1110

  
1111 1111
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1112 1112
                    params={'slots': slots, 'count': 'NaN'}, status=400)
1113 1113
    assert resp.json['err'] == 1
1114
    assert resp.json['reason'] == "invalid payload"
1114
    assert resp.json['err_desc'] == "invalid payload"
1115 1115
    assert 'count' in resp.json['errors']
1116 1116

  
1117 1117
    # get 3 places on 2 slots
......
1155 1155
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1156 1156
                    params={'slots': slots, 'count': 5})
1157 1157
    assert resp.json['err'] == 1
1158
    assert resp.json['reason'] == 'sold out'
1158
    assert resp.json['err_desc'] == 'sold out'
1159 1159
    for event in events:
1160 1160
        assert Event.objects.get(id=event.id).booked_places == 2
1161 1161
        assert Event.objects.get(id=event.id).waiting_list == 5
......
1176 1176
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1177 1177
                    params={'slots': slots, 'count': 5})
1178 1178
    assert resp.json['err'] == 1
1179
    assert resp.json['reason'] == 'sold out'
1179
    assert resp.json['err_desc'] == 'sold out'
1180 1180

  
1181 1181
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1182 1182
                    params={'slots': slots, 'count': 3})
......
1188 1188
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1189 1189
                    params={'slots': slots, 'count': 3})
1190 1190
    assert resp.json['err'] == 1
1191
    assert resp.json['reason'] == 'sold out'
1191
    assert resp.json['err_desc'] == 'sold out'
1192 1192

  
1193 1193
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1194 1194
                    params={'slots': slots, 'count': '2'})
......
1299 1299
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
1300 1300
    assert Booking.objects.count() == 2
1301 1301
    assert resp.json['err'] == 1
1302
    assert resp.json['reason'] == 'no more desk available'
1302
    assert resp.json['err_desc'] == 'no more desk available'
1303 1303

  
1304 1304
    # cancel first booking and retry
1305 1305
    resp = app.post(cancel_url)
......
1336 1336
    # try booking the same timeslot again and fail
1337 1337
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
1338 1338
    assert resp.json['err'] == 1
1339
    assert resp.json['reason'] == 'no more desk available'
1339
    assert resp.json['err_desc'] == 'no more desk available'
1340 1340

  
1341 1341
    # fill the agenda and make sure big O is O(1)
1342 1342
    for idx, event_data in enumerate(resp2.json['data'][2:10]):
......
1387 1387
    # try booking again: no desk available
1388 1388
    resp = app.post(fillslots_url, params={'slots': slots})
1389 1389
    assert resp.json['err'] == 1
1390
    assert resp.json['reason'] == 'no more desk available'
1390
    assert resp.json['err_desc'] == 'no more desk available'
1391 1391
    assert get_free_places() == start_free_places - len(slots)
1392 1392

  
1393 1393
    # cancel desk 1 booking
......
1406 1406
    # try booking the 3 slots again: no desk available, one slot is not fully available
1407 1407
    resp = app.post(fillslots_url, params={'slots': slots})
1408 1408
    assert resp.json['err'] == 1
1409
    assert resp.json['reason'] == 'no more desk available'
1409
    assert resp.json['err_desc'] == 'no more desk available'
1410 1410

  
1411 1411
    # cancel last signel slot booking, desk1 will be free
1412 1412
    resp = app.post(cancel_url)
tests/test_api_utils.py
1
import pytest
2

  
3
from chrono.api.utils import Response
4

  
5

  
6
@pytest.mark.parametrize('data, expected', [
7
    (None, None),
8
    ({}, {}),
9
    ({'reason': 'foo'}, {'reason': 'foo'}),
10
    ({'err_desc': 'foo'}, {'err_desc': 'foo', 'reason': 'foo'}),
11
    ({'bar': 'foo'}, {'bar': 'foo'}),
12
])
13
def test_response_data(data, expected):
14
    resp = Response(data=data)
15
    assert resp.data == expected
0
-