Projet

Général

Profil

0001-api-APIError-handling-51181.patch

Lauréline Guérin, 18 février 2021 17:13

Télécharger (10,2 ko)

Voir les différences:

Subject: [PATCH] api: APIError handling (#51181)

 chrono/api/utils.py | 30 ++++++++++++++++++
 chrono/api/views.py | 75 ++++++++++++++++++---------------------------
 chrono/settings.py  |  2 ++
 tests/settings.py   |  1 +
 tests/test_api.py   | 24 +++++++++++++++
 5 files changed, 87 insertions(+), 45 deletions(-)
chrono/api/utils.py
15 15
# You should have received a copy of the GNU Affero General Public License
16 16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 17

  
18

  
19
from django.utils.encoding import force_text
20

  
18 21
from rest_framework.response import Response as DRFResponse
22
from rest_framework.views import exception_handler as DRF_exception_handler
19 23

  
20 24

  
21 25
class Response(DRFResponse):
......
24 28
        if data is not None and 'err_class' in data:
25 29
            data['reason'] = data['err_class']
26 30
        super(Response, self).__init__(data=data, *args, **kwargs)
31

  
32

  
33
class APIError(Exception):
34
    err = 1
35
    http_status = 200
36

  
37
    def __init__(self, *args, **kwargs):
38
        self.__dict__.update(kwargs)
39
        super(APIError, self).__init__(*args)
40

  
41
    def to_response(self):
42
        data = {
43
            'err': self.err,
44
            'err_class': self.err_class,
45
            'err_desc': force_text(self),
46
        }
47
        if hasattr(self, 'errors'):
48
            data['errors'] = self.errors
49
        return Response(data, status=self.http_status)
50

  
51

  
52
def exception_handler(exc, context):
53
    if isinstance(exc, APIError):
54
        return exc.to_response()
55

  
56
    return DRF_exception_handler(exc, context)
chrono/api/views.py
36 36

  
37 37
from rest_framework.views import APIView
38 38

  
39
from chrono.api.utils import Response
39
from chrono.api.utils import Response, APIError
40 40
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriodException, Desk, BookingColor
41 41
from ..interval import IntervalSet
42 42

  
43 43

  
44
class APIError(Exception):
45
    err = 1
46
    http_status = 200
47

  
48
    def __init__(self, *args, **kwargs):
49
        self.__dict__.update(kwargs)
50
        super(APIError, self).__init__(*args)
51

  
52
    def to_response(self):
53
        data = {
54
            'err': self.err,
55
            'err_class': self.err_class,
56
            'err_desc': force_text(self),
57
        }
58
        if hasattr(self, 'errors'):
59
            data['errors'] = self.errors
60
        return Response(data, status=self.http_status)
61

  
62

  
63 44
def format_response_datetime(dt):
64 45
    return localtime(dt).strftime('%Y-%m-%d %H:%M:%S')
65 46

  
......
552 533

  
553 534
        date_start, date_end = request.GET.get('date_start'), request.GET.get('date_end')
554 535
        if date_start:
555
            date_start = make_aware(datetime.datetime.combine(parse_date(date_start), datetime.time(0, 0)))
536
            try:
537
                date_start = make_aware(
538
                    datetime.datetime.combine(parse_date(date_start), datetime.time(0, 0))
539
                )
540
            except TypeError:
541
                raise APIError(
542
                    _('date_start format must be YYYY-MM-DD'),
543
                    err_class='date_start format must be YYYY-MM-DD',
544
                    http_status=status.HTTP_400_BAD_REQUEST,
545
                )
556 546
        if date_end:
557
            date_end = make_aware(datetime.datetime.combine(parse_date(date_end), datetime.time(0, 0)))
547
            try:
548
                date_end = make_aware(datetime.datetime.combine(parse_date(date_end), datetime.time(0, 0)))
549
            except TypeError:
550
                raise APIError(
551
                    _('date_end format must be YYYY-MM-DD'),
552
                    err_class='date_end format must be YYYY-MM-DD',
553
                    http_status=status.HTTP_400_BAD_REQUEST,
554
                )
558 555

  
559 556
        entries = agenda.get_open_events(annotate_queryset=True, min_start=date_start, max_start=date_end)
560 557

  
......
586 583

  
587 584
        now_datetime = now()
588 585

  
589
        try:
590
            resources = get_resources_from_request(request, agenda)
591
        except APIError as e:
592
            return e.to_response()
586
        resources = get_resources_from_request(request, agenda)
593 587

  
594 588
        start_datetime = None
595 589
        if 'date_start' in request.GET:
......
597 591
                start_datetime = make_aware(
598 592
                    datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
599 593
                )
600
            except TypeError as e:
594
            except TypeError:
601 595
                raise APIError(
602 596
                    _('date_start format must be YYYY-MM-DD'),
603 597
                    err_class='date_start format must be YYYY-MM-DD',
......
610 604
                end_datetime = make_aware(
611 605
                    datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
612 606
                )
613
            except TypeError as e:
607
            except TypeError:
614 608
                raise APIError(
615 609
                    _('date_end format must be YYYY-MM-DD'),
616 610
                    err_class='date_end format must be YYYY-MM-DD',
......
836 830
    serializer_class = SlotsSerializer
837 831

  
838 832
    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
839
        try:
840
            return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
841
        except APIError as e:
842
            return e.to_response()
833
        return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
843 834

  
844 835
    def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
845 836
        multiple_booking = bool(not slots)
......
961 952
                        http_status=status.HTTP_400_BAD_REQUEST,
962 953
                    )
963 954

  
964
            try:
965
                resources = get_resources_from_request(request, agenda)
966
            except APIError as e:
967
                return e.to_response()
955
            resources = get_resources_from_request(request, agenda)
968 956

  
969 957
            # get all free slots and separate them by desk
970 958
            try:
......
1224 1212
    serializer_class = SlotSerializer
1225 1213

  
1226 1214
    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
1227
        try:
1228
            return self.fillslot(
1229
                request=request,
1230
                agenda_identifier=agenda_identifier,
1231
                slots=[event_identifier],  # fill a "list on one slot"
1232
                format=format,
1233
            )
1234
        except APIError as e:
1235
            return e.to_response()
1215
        return self.fillslot(
1216
            request=request,
1217
            agenda_identifier=agenda_identifier,
1218
            slots=[event_identifier],  # fill a "list on one slot"
1219
            format=format,
1220
        )
1236 1221

  
1237 1222

  
1238 1223
fillslot = Fillslot.as_view()
chrono/settings.py
184 184
SMS_URL = ''
185 185
SMS_SENDER = ''
186 186

  
187
REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
188

  
187 189
local_settings_file = os.environ.get(
188 190
    'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
189 191
)
tests/settings.py
5 5

  
6 6
REST_FRAMEWORK = {
7 7
    'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.BasicAuthentication'],
8
    'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler',
8 9
}
9 10

  
10 11
DATABASES = {
tests/test_api.py
3176 3176
            event_dt = datetime.datetime.combine(day, datetime.datetime.strptime(event, '%H:%M').time())
3177 3177
            Event.objects.create(agenda=agenda2, start_datetime=make_aware(event_dt), places=2)
3178 3178

  
3179
    params = {'date_start': 'foo'}
3180
    resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params, status=400)
3181
    assert resp.json['err'] == 1
3182
    assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD'
3183
    assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD'
3184

  
3185
    params = {'date_end': 'foo'}
3186
    resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params, status=400)
3187
    assert resp.json['err'] == 1
3188
    assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD'
3189
    assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD'
3190

  
3179 3191
    params = {'date_start': base_date.isoformat()}
3180 3192
    resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params)
3181 3193
    assert len(resp.json['data']) == 6
......
4977 4989
    resp = app.get(virtual_api_url)
4978 4990
    assert len(resp.json['data']) == 24
4979 4991

  
4992
    params = {'date_start': 'foo'}
4993
    resp = app.get(foo_api_url, params=params, status=400)
4994
    assert resp.json['err'] == 1
4995
    assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD'
4996
    assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD'
4997

  
4998
    params = {'date_end': 'foo'}
4999
    resp = app.get(foo_api_url, params=params, status=400)
5000
    assert resp.json['err'] == 1
5001
    assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD'
5002
    assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD'
5003

  
4980 5004
    # exclude weekday1 through date_start, 4 slots each day * 5 days
4981 5005
    params = {'date_start': (localtime(now()) + datetime.timedelta(days=2)).date().isoformat()}
4982 5006
    resp = app.get(foo_api_url, params=params)
4983
-