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

Commit

Permalink
Error are jsonify and should have the same schema
Browse files Browse the repository at this point in the history
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
  • Loading branch information
MoiTux committed Oct 8, 2016
1 parent 1a5ae48 commit 0a5d14a
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 57 deletions.
34 changes: 19 additions & 15 deletions baobab/apirest/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from tastypie import fields
from tastypie.resources import ALL_WITH_RELATIONS
from tastypie.utils import trailing_slash
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.exceptions import BadRequest
from tastypie.http import HttpBadRequest

from baobab.backoffice.models import (Event as BOEvent,
Expand Down Expand Up @@ -71,10 +71,11 @@ def build_filters(self, filters=None):
try:
tmp = self.service_choices[val.upper()]
except KeyError:
raise ImmediateHttpResponse(
HttpBadRequest('Bad value for filter services '
'choises are: %s' %
', '.join(self.service_choices)))
list_ = ', '.join(self.service_choices)
raise BadRequest({
'error': "'{}' not in list: {}".format(val, list_),
'field': 'services',
})
del filters[key]
key = key.split('__', 1)
key.insert(1, 'name')
Expand All @@ -83,10 +84,11 @@ def build_filters(self, filters=None):
try:
filters[key] = self.category_choices[val.upper()]
except KeyError:
raise ImmediateHttpResponse(
HttpBadRequest('Bad value for filter category '
'choises are: %s' %
', '.join(self.category_choices)))
list_ = ', '.join(self.category_choices)
raise BadRequest({
'error': "'{}' not in list: {}".format(val, list_),
'field': 'category',
})
if key == 'date_end' and val.lower() == 'null':
del filters['date_end']
filters['date_end__isnull'] = True
Expand All @@ -102,14 +104,16 @@ def get_object_list(self, request):
if current:
val = current.lower()
if val not in ('true', 'false'):
raise ImmediateHttpResponse(
HttpBadRequest('Bad value for filter current '
'choises are: ["true", "false"]'))
raise BadRequest({
'error': "choises are: ['true', 'false']",
'field': 'current',
})
for key in request.GET:
if 'date' in key:
raise ImmediateHttpResponse(
HttpBadRequest('Incompatible filter: "%s" '
'with filter "current"' % key))
raise BadRequest({
'error': "Can't be used in the same time as 'current'",
'field': 'date',
})
current = ((Q(date_end__isnull=True) | Q(date_end__gt=now)) &
Q(date_start__lte=now))
if val == 'false':
Expand Down
302 changes: 262 additions & 40 deletions baobab/apirest/modelresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@
import textwrap
import pytz
import json
import logging
import warnings

from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.conf import settings
from django.http import HttpResponse
from django.http import HttpResponse, Http404
from django.utils.timezone import is_naive

from tastypie.exceptions import (UnsupportedFormat,
ImmediateHttpResponse, NotFound)
from tastypie.http import HttpNotImplemented, HttpApplicationError
# Django 1.5 has moved this constant up one level.
try:
from django.db.models.constants import LOOKUP_SEP
except ImportError:
from django.db.models.sql.constants import LOOKUP_SEP

import tastypie
from tastypie.exceptions import (UnsupportedFormat, NotFound, BadRequest,
InvalidFilterError, InvalidSortError)
from tastypie.http import HttpApplicationError, HttpNotImplemented
from tastypie import http
from tastypie.resources import ALL, ALL_WITH_RELATIONS

try:
from tastypie.http import HttpNotFound
Expand All @@ -33,6 +44,9 @@ class HttpNotFound(HttpResponse):
from tastypie.serializers import Serializer
from tastypie.bundle import Bundle

LOG = logging.getLogger(__name__)
logging.basicConfig(level=logging.WARNING)


def my_handler(obj):
"""
Expand Down Expand Up @@ -112,15 +126,7 @@ def serialize(self, bundle, format='application/json', options={}):
try:
return super(MySerializer, self).serialize(bundle, format, options)
except (ImproperlyConfigured, UnsupportedFormat):
raise ImmediateHttpResponse(
HttpNotImplemented(settings.HTTP_NOT_IMPLEMENTED_ERROR)
)
except ImmediateHttpResponse:
raise
except Exception:
raise ImmediateHttpResponse(
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
)
raise HttpNotImplemented(settings.HTTP_NOT_IMPLEMENTED_ERROR)


class RawModelResource(ModelResource):
Expand Down Expand Up @@ -181,33 +187,249 @@ def build_schema(self):
del schema['fields'][key]['default']
return schema

def obj_get(self, **kwargs):
try:
return super(RawModelResource, self).obj_get(**kwargs)
except (NotFound, ObjectDoesNotExist):
raise ImmediateHttpResponse(
HttpNotFound(settings.HTTP_NOT_FOUND)
)
except ImmediateHttpResponse:
raise
except Exception:
raise ImmediateHttpResponse(
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
)

def obj_get_list(self, **kwargs):
try:
return super(RawModelResource, self).obj_get_list(**kwargs)
except (NotFound, ObjectDoesNotExist):
raise ImmediateHttpResponse(
HttpNotFound(settings.HTTP_NOT_FOUND)
)
except ImmediateHttpResponse:
raise
except Exception:
raise ImmediateHttpResponse(
HttpApplicationError(settings.HTTP_APPLICATION_ERROR)
)
# overwrite some method to have the same 'json' schema return for all
# kind of error

def _handle_500(self, request, exception):
response_class = HttpApplicationError
response_code = 500
if isinstance(exception, (NotFound, ObjectDoesNotExist, Http404)):
response_class = HttpResponseNotFound
response_code = 404

LOG.error('Internal Server Error: %s' % request.path, exc_info=True,
extra={'status_code': response_code, 'request': request})

if settings.DEBUG:
import traceback
import sys
tb = '\n'.join(traceback.format_exception(*(sys.exc_info())))
data = {
"error": unicode(exception),
"traceback": tb,
}
return self.error_response(request, data,
response_class=response_class)

msg = "Sorry, this request could not be processed. " \
"Please try again later."
data = {
'error': getattr(settings, 'HTTP_APPLICATION_ERROR', msg),
}
return self.error_response(request, data,
response_class=response_class)

def error_response(self, request, errors, response_class=None):
desired_format = self._meta.default_format
if request:
try:
desired_format = self.determine_format(request)
except BadRequest:
pass

if response_class is None:
response_class = http.HttpBadRequest

if isinstance(errors, dict) and 'error' in errors:
errors = errors['error']
if isinstance(errors, basestring):
errors = {'error': errors}

serialized = self.serialize(request, errors, desired_format)
return response_class(content=serialized, content_type=desired_format)

def check_filtering(self, field_name, filter_type='exact',
filter_bits=None):
"""
Given a field name, a optional filter type and an optional list of
additional relations, determine if a field can be filtered on.
If a filter does not meet the needed conditions, it should raise an
``InvalidFilterError``.
If the filter meets the conditions, a list of attribute names (not
field names) will be returned.
"""
if filter_bits is None:
filter_bits = []

if field_name not in self._meta.filtering:
raise InvalidFilterError({
'error': ("Filtering on '{}' is not allowed."
"".format(field_name)),
'field': field_name,
})

# Check to see if it's an allowed lookup type.
if not self._meta.filtering[field_name] in (ALL, ALL_WITH_RELATIONS):
# Must be an explicit whitelist.
if filter_type not in self._meta.filtering[field_name]:
raise InvalidFilterError({
'error': ("Filter '{}' is not allowed on field '{}'"
"".format(filter_type, field_name)),
'field': field_name,
})
if self.fields[field_name].attribute is None:
raise InvalidFilterError({
'error': ("The '{}' field has no 'attribute' for "
"searching with.".format(field_name)),
'field': field_name,
})

# Check to see if it's a relational lookup and if that's allowed.
if len(filter_bits):
if not getattr(self.fields[field_name], 'is_related', False):
raise InvalidFilterError({
'error': ("The '{}' field does not support relations."
"".format(field_name)),
'field': field_name,
})

if not self._meta.filtering[field_name] == ALL_WITH_RELATIONS:
raise InvalidFilterError({
'error': ("Lookups are not allowed more than one level "
"deep on the '{}' field.".format(field_name)),
'field': field_name,
})

# Recursively descend through the remaining lookups in the filter,
# if any. We should ensure that all along the way, we're allowed
# to filter on that field by the related resource.
resource = self.fields[field_name]
related_resource = resource.get_related_resource(None)
return [resource.attribute] + \
related_resource.check_filtering(filter_bits[0], filter_type,
filter_bits[1:])

return [self.fields[field_name].attribute]

def apply_sorting(self, obj_list, options=None):
"""
Given a dictionary of options, apply some ORM-level sorting to the
provided ``QuerySet``.
Looks for the ``order_by`` key and handles either ascending (just the
field name) or descending (the field name with a ``-`` in front).
The field name should be the resource field, **NOT** model field.
"""
if options is None:
options = {}

parameter_name = 'order_by'

if 'order_by' not in options:
if 'sort_by' not in options:
# Nothing to alter the order. Return what we've got.
return obj_list
else:
warnings.warn("'sort_by' is a deprecated parameter. "
"Please use 'order_by' instead.")
parameter_name = 'sort_by'

order_by_args = []

if hasattr(options, 'getlist'):
order_bits = options.getlist(parameter_name)
else:
order_bits = options.get(parameter_name)

if not isinstance(order_bits, (list, tuple)):
order_bits = [order_bits]

for order_by in order_bits:
order_by_bits = order_by.split(LOOKUP_SEP)

field_name = order_by_bits[0]
order = ''

if order_by_bits[0].startswith('-'):
field_name = order_by_bits[0][1:]
order = '-'

if field_name not in self.fields:
# It's not a field we know about. Move along citizen.
raise InvalidSortError({
'error': ("No matching '{}' field for ordering on."
"".format(field_name)),
'field': field_name,
})

if field_name not in self._meta.ordering:
raise InvalidSortError({
'error': ("The '{}' field does not allow ordering."
"".format(field_name)),
'field': field_name,
})

if self.fields[field_name].attribute is None:
raise InvalidSortError({
'error': ("The '{}' field has no 'attribute' for "
"ordering with.".format(field_name)),
'field': field_name,
})

order_by_args.append("%s%s" % (order, LOOKUP_SEP.join([
self.fields[field_name].attribute] + order_by_bits[1:])))

return obj_list.order_by(*order_by_args)

if tastypie.__version__ < (0, 9, 12):

# this in need to hanble the error for thoes versions

def wrap_view(self, view):

try:
from django.views.decorators.csrf import csrf_exempt
except ImportError:
def csrf_exempt(func):
return func

@csrf_exempt
def wrapper(request, *args, **kwargs):
from django.utils.cache import patch_cache_control
from tastypie.fields import ApiFieldError
from django.core.exceptions import ValidationError

try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)

if request.is_ajax():
patch_cache_control(response, no_cache=True)

return response
except (BadRequest, ApiFieldError, InvalidSortError) as e:
data = {"error": e.args[0] if getattr(e, 'args') else ''}
return self.error_response(
request, data, response_class=http.HttpBadRequest)
except ValidationError, e:
data = {"error": e.messages}
return self.error_response(
request, data, response_class=http.HttpBadRequest)
except Exception, e:
if hasattr(e, 'response'):
return e.response

# A real, non-expected exception.
# Handle the case where the full traceback is more helpful
# than the serialized error.
if settings.DEBUG and getattr(
settings, 'TASTYPIE_FULL_DEBUG', False):
raise

# Re-raise the error to get a proper traceback when the
# error happend during a test case
if request.META.get('SERVER_NAME') == 'testserver':
raise

# Rather than re-raising, we're going to things similar to
# what Django does. The difference is returning a
# serialized error message.
return self._handle_500(request, e)

return wrapper

class Meta:
max_limit = None
Expand Down
Loading

0 comments on commit 0a5d14a

Please sign in to comment.