Skip to content
This repository was archived by the owner on Mar 26, 2021. It is now read-only.

Commit e972550

Browse files
author
MoiTux
committed
Error are jsonify and should have the same schema
Error will be a dict: { 'error': 'This is a message explaing the error', 'field': 'field_name' } if the error is on a 'filter' or 'sort' parameters (query string) the key 'field' will be set with the name of the faulty parameters Fix #15
1 parent 731eaab commit e972550

File tree

3 files changed

+283
-57
lines changed

3 files changed

+283
-57
lines changed

baobab/apirest/events.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tastypie import fields
1212
from tastypie.resources import ALL_WITH_RELATIONS
1313
from tastypie.utils import trailing_slash
14-
from tastypie.exceptions import ImmediateHttpResponse
14+
from tastypie.exceptions import BadRequest
1515
from tastypie.http import HttpBadRequest
1616

1717
from baobab.backoffice.models import (Event as BOEvent,
@@ -71,10 +71,11 @@ def build_filters(self, filters=None):
7171
try:
7272
tmp = self.service_choices[val.upper()]
7373
except KeyError:
74-
raise ImmediateHttpResponse(
75-
HttpBadRequest('Bad value for filter services '
76-
'choises are: %s' %
77-
', '.join(self.service_choices)))
74+
list_ = ', '.join(self.service_choices)
75+
raise BadRequest({
76+
'error': "'{}' not in list: {}".format(val, list_),
77+
'field': 'services',
78+
})
7879
del filters[key]
7980
key = key.split('__', 1)
8081
key.insert(1, 'name')
@@ -83,10 +84,11 @@ def build_filters(self, filters=None):
8384
try:
8485
filters[key] = self.category_choices[val.upper()]
8586
except KeyError:
86-
raise ImmediateHttpResponse(
87-
HttpBadRequest('Bad value for filter category '
88-
'choises are: %s' %
89-
', '.join(self.category_choices)))
87+
list_ = ', '.join(self.category_choices)
88+
raise BadRequest({
89+
'error': "'{}' not in list: {}".format(val, list_),
90+
'field': 'category',
91+
})
9092
if key == 'date_end' and val.lower() == 'null':
9193
del filters['date_end']
9294
filters['date_end__isnull'] = True
@@ -102,14 +104,16 @@ def get_object_list(self, request):
102104
if current:
103105
val = current.lower()
104106
if val not in ('true', 'false'):
105-
raise ImmediateHttpResponse(
106-
HttpBadRequest('Bad value for filter current '
107-
'choises are: ["true", "false"]'))
107+
raise BadRequest({
108+
'error': "choises are: ['true', 'false']",
109+
'field': 'current',
110+
})
108111
for key in request.GET:
109112
if 'date' in key:
110-
raise ImmediateHttpResponse(
111-
HttpBadRequest('Incompatible filter: "%s" '
112-
'with filter "current"' % key))
113+
raise BadRequest({
114+
'error': "Can't be used in the same time as 'current'",
115+
'field': 'date',
116+
})
113117
current = ((Q(date_end__isnull=True) | Q(date_end__gt=now)) &
114118
Q(date_start__lte=now))
115119
if val == 'false':

baobab/apirest/modelresource.py

Lines changed: 262 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,26 @@
1212
import textwrap
1313
import pytz
1414
import json
15+
import logging
16+
import warnings
1517

1618
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
1719
from django.conf import settings
18-
from django.http import HttpResponse
20+
from django.http import HttpResponse, Http404
1921
from django.utils.timezone import is_naive
2022

21-
from tastypie.exceptions import (UnsupportedFormat,
22-
ImmediateHttpResponse, NotFound)
23-
from tastypie.http import HttpNotImplemented, HttpApplicationError
23+
# Django 1.5 has moved this constant up one level.
24+
try:
25+
from django.db.models.constants import LOOKUP_SEP
26+
except ImportError:
27+
from django.db.models.sql.constants import LOOKUP_SEP
28+
29+
import tastypie
30+
from tastypie.exceptions import (UnsupportedFormat, NotFound, BadRequest,
31+
InvalidFilterError, InvalidSortError)
32+
from tastypie.http import HttpApplicationError, HttpNotImplemented
33+
from tastypie import http
34+
from tastypie.resources import ALL, ALL_WITH_RELATIONS
2435

2536
try:
2637
from tastypie.http import HttpNotFound
@@ -33,6 +44,9 @@ class HttpNotFound(HttpResponse):
3344
from tastypie.serializers import Serializer
3445
from tastypie.bundle import Bundle
3546

47+
LOG = logging.getLogger(__name__)
48+
logging.basicConfig(level=logging.WARNING)
49+
3650

3751
def my_handler(obj):
3852
"""
@@ -112,15 +126,7 @@ def serialize(self, bundle, format='application/json', options={}):
112126
try:
113127
return super(MySerializer, self).serialize(bundle, format, options)
114128
except (ImproperlyConfigured, UnsupportedFormat):
115-
raise ImmediateHttpResponse(
116-
HttpNotImplemented(settings.HTTP_NOT_IMPLEMENTED_ERROR)
117-
)
118-
except ImmediateHttpResponse:
119-
raise
120-
except Exception:
121-
raise ImmediateHttpResponse(
122-
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
123-
)
129+
raise HttpNotImplemented(settings.HTTP_NOT_IMPLEMENTED_ERROR)
124130

125131

126132
class RawModelResource(ModelResource):
@@ -181,33 +187,249 @@ def build_schema(self):
181187
del schema['fields'][key]['default']
182188
return schema
183189

184-
def obj_get(self, **kwargs):
185-
try:
186-
return super(RawModelResource, self).obj_get(**kwargs)
187-
except (NotFound, ObjectDoesNotExist):
188-
raise ImmediateHttpResponse(
189-
HttpNotFound(settings.HTTP_NOT_FOUND)
190-
)
191-
except ImmediateHttpResponse:
192-
raise
193-
except Exception:
194-
raise ImmediateHttpResponse(
195-
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
196-
)
197-
198-
def obj_get_list(self, **kwargs):
199-
try:
200-
return super(RawModelResource, self).obj_get_list(**kwargs)
201-
except (NotFound, ObjectDoesNotExist):
202-
raise ImmediateHttpResponse(
203-
HttpNotFound(settings.HTTP_NOT_FOUND)
204-
)
205-
except ImmediateHttpResponse:
206-
raise
207-
except Exception:
208-
raise ImmediateHttpResponse(
209-
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
210-
)
190+
# overwrite some method to have the same 'json' schema return for all
191+
# kind of error
192+
193+
def _handle_500(self, request, exception):
194+
response_class = HttpApplicationError
195+
response_code = 500
196+
if isinstance(exception, (NotFound, ObjectDoesNotExist, Http404)):
197+
response_class = HttpResponseNotFound
198+
response_code = 404
199+
200+
LOG.error('Internal Server Error: %s' % request.path, exc_info=True,
201+
extra={'status_code': response_code, 'request': request})
202+
203+
if settings.DEBUG:
204+
import traceback
205+
import sys
206+
tb = '\n'.join(traceback.format_exception(*(sys.exc_info())))
207+
data = {
208+
"error": unicode(exception),
209+
"traceback": tb,
210+
}
211+
return self.error_response(request, data,
212+
response_class=response_class)
213+
214+
msg = "Sorry, this request could not be processed. " \
215+
"Please try again later."
216+
data = {
217+
'error': getattr(settings, 'HTTP_APPLICATION_ERROR', msg),
218+
}
219+
return self.error_response(request, data,
220+
response_class=response_class)
221+
222+
def error_response(self, request, errors, response_class=None):
223+
desired_format = self._meta.default_format
224+
if request:
225+
try:
226+
desired_format = self.determine_format(request)
227+
except BadRequest:
228+
pass
229+
230+
if response_class is None:
231+
response_class = http.HttpBadRequest
232+
233+
if isinstance(errors, dict) and 'error' in errors:
234+
errors = errors['error']
235+
if isinstance(errors, basestring):
236+
errors = {'error': errors}
237+
238+
serialized = self.serialize(request, errors, desired_format)
239+
return response_class(content=serialized, content_type=desired_format)
240+
241+
def check_filtering(self, field_name, filter_type='exact',
242+
filter_bits=None):
243+
"""
244+
Given a field name, a optional filter type and an optional list of
245+
additional relations, determine if a field can be filtered on.
246+
247+
If a filter does not meet the needed conditions, it should raise an
248+
``InvalidFilterError``.
249+
250+
If the filter meets the conditions, a list of attribute names (not
251+
field names) will be returned.
252+
"""
253+
if filter_bits is None:
254+
filter_bits = []
255+
256+
if field_name not in self._meta.filtering:
257+
raise InvalidFilterError({
258+
'error': ("Filtering on '{}' is not allowed."
259+
"".format(field_name)),
260+
'field': field_name,
261+
})
262+
263+
# Check to see if it's an allowed lookup type.
264+
if not self._meta.filtering[field_name] in (ALL, ALL_WITH_RELATIONS):
265+
# Must be an explicit whitelist.
266+
if filter_type not in self._meta.filtering[field_name]:
267+
raise InvalidFilterError({
268+
'error': ("Filter '{}' is not allowed on field '{}'"
269+
"".format(filter_type, field_name)),
270+
'field': field_name,
271+
})
272+
if self.fields[field_name].attribute is None:
273+
raise InvalidFilterError({
274+
'error': ("The '{}' field has no 'attribute' for "
275+
"searching with.".format(field_name)),
276+
'field': field_name,
277+
})
278+
279+
# Check to see if it's a relational lookup and if that's allowed.
280+
if len(filter_bits):
281+
if not getattr(self.fields[field_name], 'is_related', False):
282+
raise InvalidFilterError({
283+
'error': ("The '{}' field does not support relations."
284+
"".format(field_name)),
285+
'field': field_name,
286+
})
287+
288+
if not self._meta.filtering[field_name] == ALL_WITH_RELATIONS:
289+
raise InvalidFilterError({
290+
'error': ("Lookups are not allowed more than one level "
291+
"deep on the '{}' field.".format(field_name)),
292+
'field': field_name,
293+
})
294+
295+
# Recursively descend through the remaining lookups in the filter,
296+
# if any. We should ensure that all along the way, we're allowed
297+
# to filter on that field by the related resource.
298+
resource = self.fields[field_name]
299+
related_resource = resource.get_related_resource(None)
300+
return [resource.attribute] + \
301+
related_resource.check_filtering(filter_bits[0], filter_type,
302+
filter_bits[1:])
303+
304+
return [self.fields[field_name].attribute]
305+
306+
def apply_sorting(self, obj_list, options=None):
307+
"""
308+
Given a dictionary of options, apply some ORM-level sorting to the
309+
provided ``QuerySet``.
310+
311+
Looks for the ``order_by`` key and handles either ascending (just the
312+
field name) or descending (the field name with a ``-`` in front).
313+
314+
The field name should be the resource field, **NOT** model field.
315+
"""
316+
if options is None:
317+
options = {}
318+
319+
parameter_name = 'order_by'
320+
321+
if 'order_by' not in options:
322+
if 'sort_by' not in options:
323+
# Nothing to alter the order. Return what we've got.
324+
return obj_list
325+
else:
326+
warnings.warn("'sort_by' is a deprecated parameter. "
327+
"Please use 'order_by' instead.")
328+
parameter_name = 'sort_by'
329+
330+
order_by_args = []
331+
332+
if hasattr(options, 'getlist'):
333+
order_bits = options.getlist(parameter_name)
334+
else:
335+
order_bits = options.get(parameter_name)
336+
337+
if not isinstance(order_bits, (list, tuple)):
338+
order_bits = [order_bits]
339+
340+
for order_by in order_bits:
341+
order_by_bits = order_by.split(LOOKUP_SEP)
342+
343+
field_name = order_by_bits[0]
344+
order = ''
345+
346+
if order_by_bits[0].startswith('-'):
347+
field_name = order_by_bits[0][1:]
348+
order = '-'
349+
350+
if field_name not in self.fields:
351+
# It's not a field we know about. Move along citizen.
352+
raise InvalidSortError({
353+
'error': ("No matching '{}' field for ordering on."
354+
"".format(field_name)),
355+
'field': field_name,
356+
})
357+
358+
if field_name not in self._meta.ordering:
359+
raise InvalidSortError({
360+
'error': ("The '{}' field does not allow ordering."
361+
"".format(field_name)),
362+
'field': field_name,
363+
})
364+
365+
if self.fields[field_name].attribute is None:
366+
raise InvalidSortError({
367+
'error': ("The '{}' field has no 'attribute' for "
368+
"ordering with.".format(field_name)),
369+
'field': field_name,
370+
})
371+
372+
order_by_args.append("%s%s" % (order, LOOKUP_SEP.join([
373+
self.fields[field_name].attribute] + order_by_bits[1:])))
374+
375+
return obj_list.order_by(*order_by_args)
376+
377+
if tastypie.__version__ < (0, 9, 12):
378+
379+
# this in need to hanble the error for thoes versions
380+
381+
def wrap_view(self, view):
382+
383+
try:
384+
from django.views.decorators.csrf import csrf_exempt
385+
except ImportError:
386+
def csrf_exempt(func):
387+
return func
388+
389+
@csrf_exempt
390+
def wrapper(request, *args, **kwargs):
391+
from django.utils.cache import patch_cache_control
392+
from tastypie.fields import ApiFieldError
393+
from django.core.exceptions import ValidationError
394+
395+
try:
396+
callback = getattr(self, view)
397+
response = callback(request, *args, **kwargs)
398+
399+
if request.is_ajax():
400+
patch_cache_control(response, no_cache=True)
401+
402+
return response
403+
except (BadRequest, ApiFieldError, InvalidSortError) as e:
404+
data = {"error": e.args[0] if getattr(e, 'args') else ''}
405+
return self.error_response(
406+
request, data, response_class=http.HttpBadRequest)
407+
except ValidationError, e:
408+
data = {"error": e.messages}
409+
return self.error_response(
410+
request, data, response_class=http.HttpBadRequest)
411+
except Exception, e:
412+
if hasattr(e, 'response'):
413+
return e.response
414+
415+
# A real, non-expected exception.
416+
# Handle the case where the full traceback is more helpful
417+
# than the serialized error.
418+
if settings.DEBUG and getattr(
419+
settings, 'TASTYPIE_FULL_DEBUG', False):
420+
raise
421+
422+
# Re-raise the error to get a proper traceback when the
423+
# error happend during a test case
424+
if request.META.get('SERVER_NAME') == 'testserver':
425+
raise
426+
427+
# Rather than re-raising, we're going to things similar to
428+
# what Django does. The difference is returning a
429+
# serialized error message.
430+
return self._handle_500(request, e)
431+
432+
return wrapper
211433

212434
class Meta:
213435
max_limit = None

0 commit comments

Comments
 (0)