api: APIError handling (#51181)

This commit is contained in:
Lauréline Guérin 2021-02-18 17:13:17 +01:00
parent 64a8a4b7f1
commit 5e94ddeff3
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
5 changed files with 87 additions and 45 deletions

View File

@ -15,7 +15,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.encoding import force_text
from rest_framework.response import Response as DRFResponse
from rest_framework.views import exception_handler as DRF_exception_handler
class Response(DRFResponse):
@ -24,3 +28,29 @@ class Response(DRFResponse):
if data is not None and 'err_class' in data:
data['reason'] = data['err_class']
super(Response, self).__init__(data=data, *args, **kwargs)
class APIError(Exception):
err = 1
http_status = 200
def __init__(self, *args, **kwargs):
self.__dict__.update(kwargs)
super(APIError, self).__init__(*args)
def to_response(self):
data = {
'err': self.err,
'err_class': self.err_class,
'err_desc': force_text(self),
}
if hasattr(self, 'errors'):
data['errors'] = self.errors
return Response(data, status=self.http_status)
def exception_handler(exc, context):
if isinstance(exc, APIError):
return exc.to_response()
return DRF_exception_handler(exc, context)

View File

@ -36,30 +36,11 @@ from rest_framework import permissions, serializers, status
from rest_framework.views import APIView
from chrono.api.utils import Response
from chrono.api.utils import Response, APIError
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriodException, Desk, BookingColor
from ..interval import IntervalSet
class APIError(Exception):
err = 1
http_status = 200
def __init__(self, *args, **kwargs):
self.__dict__.update(kwargs)
super(APIError, self).__init__(*args)
def to_response(self):
data = {
'err': self.err,
'err_class': self.err_class,
'err_desc': force_text(self),
}
if hasattr(self, 'errors'):
data['errors'] = self.errors
return Response(data, status=self.http_status)
def format_response_datetime(dt):
return localtime(dt).strftime('%Y-%m-%d %H:%M:%S')
@ -552,9 +533,25 @@ class Datetimes(APIView):
date_start, date_end = request.GET.get('date_start'), request.GET.get('date_end')
if date_start:
date_start = make_aware(datetime.datetime.combine(parse_date(date_start), datetime.time(0, 0)))
try:
date_start = make_aware(
datetime.datetime.combine(parse_date(date_start), datetime.time(0, 0))
)
except TypeError:
raise APIError(
_('date_start format must be YYYY-MM-DD'),
err_class='date_start format must be YYYY-MM-DD',
http_status=status.HTTP_400_BAD_REQUEST,
)
if date_end:
date_end = make_aware(datetime.datetime.combine(parse_date(date_end), datetime.time(0, 0)))
try:
date_end = make_aware(datetime.datetime.combine(parse_date(date_end), datetime.time(0, 0)))
except TypeError:
raise APIError(
_('date_end format must be YYYY-MM-DD'),
err_class='date_end format must be YYYY-MM-DD',
http_status=status.HTTP_400_BAD_REQUEST,
)
entries = agenda.get_open_events(annotate_queryset=True, min_start=date_start, max_start=date_end)
@ -586,10 +583,7 @@ class MeetingDatetimes(APIView):
now_datetime = now()
try:
resources = get_resources_from_request(request, agenda)
except APIError as e:
return e.to_response()
resources = get_resources_from_request(request, agenda)
start_datetime = None
if 'date_start' in request.GET:
@ -597,7 +591,7 @@ class MeetingDatetimes(APIView):
start_datetime = make_aware(
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
)
except TypeError as e:
except TypeError:
raise APIError(
_('date_start format must be YYYY-MM-DD'),
err_class='date_start format must be YYYY-MM-DD',
@ -610,7 +604,7 @@ class MeetingDatetimes(APIView):
end_datetime = make_aware(
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
)
except TypeError as e:
except TypeError:
raise APIError(
_('date_end format must be YYYY-MM-DD'),
err_class='date_end format must be YYYY-MM-DD',
@ -836,10 +830,7 @@ class Fillslots(APIView):
serializer_class = SlotsSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
try:
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
except APIError as e:
return e.to_response()
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
multiple_booking = bool(not slots)
@ -961,10 +952,7 @@ class Fillslots(APIView):
http_status=status.HTTP_400_BAD_REQUEST,
)
try:
resources = get_resources_from_request(request, agenda)
except APIError as e:
return e.to_response()
resources = get_resources_from_request(request, agenda)
# get all free slots and separate them by desk
try:
@ -1224,15 +1212,12 @@ class Fillslot(Fillslots):
serializer_class = SlotSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
try:
return self.fillslot(
request=request,
agenda_identifier=agenda_identifier,
slots=[event_identifier], # fill a "list on one slot"
format=format,
)
except APIError as e:
return e.to_response()
return self.fillslot(
request=request,
agenda_identifier=agenda_identifier,
slots=[event_identifier], # fill a "list on one slot"
format=format,
)
fillslot = Fillslot.as_view()

View File

@ -184,6 +184,8 @@ TEMPLATE_VARS = {}
SMS_URL = ''
SMS_SENDER = ''
REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
local_settings_file = os.environ.get(
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)

View File

@ -5,6 +5,7 @@ LANGUAGE_CODE = 'en-us'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.BasicAuthentication'],
'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler',
}
DATABASES = {

View File

@ -3234,6 +3234,18 @@ def test_agenda_api_date_range(app, some_data):
event_dt = datetime.datetime.combine(day, datetime.datetime.strptime(event, '%H:%M').time())
Event.objects.create(agenda=agenda2, start_datetime=make_aware(event_dt), places=2)
params = {'date_start': 'foo'}
resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD'
assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD'
params = {'date_end': 'foo'}
resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD'
assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD'
params = {'date_start': base_date.isoformat()}
resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params)
assert len(resp.json['data']) == 6
@ -5035,6 +5047,18 @@ def test_meetings_and_virtual_datetimes_date_filter(app):
resp = app.get(virtual_api_url)
assert len(resp.json['data']) == 24
params = {'date_start': 'foo'}
resp = app.get(foo_api_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD'
assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD'
params = {'date_end': 'foo'}
resp = app.get(foo_api_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD'
assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD'
# exclude weekday1 through date_start, 4 slots each day * 5 days
params = {'date_start': (localtime(now()) + datetime.timedelta(days=2)).date().isoformat()}
resp = app.get(foo_api_url, params=params)