aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--docs/api-guide/fields.md4
-rw-r--r--docs/api-guide/renderers.md16
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/api-guide/status-codes.md2
-rw-r--r--docs/topics/credits.md4
-rw-r--r--docs/topics/release-notes.md12
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/fields.py53
-rw-r--r--rest_framework/generics.py2
-rw-r--r--rest_framework/mixins.py6
-rw-r--r--rest_framework/renderers.py44
-rw-r--r--rest_framework/response.py4
-rw-r--r--rest_framework/serializers.py2
-rw-r--r--rest_framework/static/rest_framework/css/default.css2
-rw-r--r--rest_framework/status.py2
-rw-r--r--rest_framework/tests/htmlrenderer.py65
-rw-r--r--rest_framework/utils/breadcrumbs.py14
-rw-r--r--rest_framework/views.py10
19 files changed, 220 insertions, 36 deletions
diff --git a/README.md b/README.md
index 7275761c..5d3e884d 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,16 @@ To run the tests.
# Changelog
+## 2.1.1
+
+**Date**: 7th Nov 2012
+
+* Support use of HTML exception templates. Eg. `403.html`
+* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
+* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs.
+* Bugfix: Make textareas same width as other fields in browsable API.
+* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
+
## 2.1.0
**Date**: 5th Nov 2012
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 411f7944..0485b158 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -268,6 +268,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
+* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
## HyperLinkedIdentityField
@@ -280,5 +281,8 @@ This field is always read-only.
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
+* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
+* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
+* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
[cite]: http://www.python.org/dev/peps/pep-0020/
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index c3d12ddb..374ff0ab 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -257,6 +257,21 @@ In [the words of Roy Fielding][quote], "A REST API should spend almost all of it
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
+## HTML error views
+
+Typically a renderer will behave the same regardless of if it's dealing with a regular response, or with a response caused by an exception being raised, such as an `Http404` or `PermissionDenied` exception, or a subclass of `APIException`.
+
+If you're using either the `TemplateHTMLRenderer` or the `StaticHTMLRenderer` and an exception is raised, the behavior is slightly different, and mirrors [Django's default handling of error views][django-error-views].
+
+Exceptions raised and handled by an HTML renderer will attempt to render using one of the following methods, by order of precedence.
+
+* Load and render a template named `{status_code}.html`.
+* Load and render a template named `api_exception.html`.
+* Render the HTTP status code and text, for example "404 Not Found".
+
+Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
+
+
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
@@ -265,3 +280,4 @@ For good examples of custom media types, see GitHub's use of a custom [applicati
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[application/vnd.github+json]: http://developer.github.com/v3/media/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
+[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views \ No newline at end of file
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index ee7f72dd..0cdae1ce 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -190,7 +190,7 @@ As an example, let's create a field that can be used represent the class name of
# ModelSerializers
Often you'll want serializer classes that map closely to model definitions.
-The `ModelSerializer` class lets you automatically create a Serializer class with fields that corrospond to the Model fields.
+The `ModelSerializer` class lets you automatically create a Serializer class with fields that correspond to the Model fields.
class AccountSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md
index 401f45ce..b50c96ae 100644
--- a/docs/api-guide/status-codes.md
+++ b/docs/api-guide/status-codes.md
@@ -87,7 +87,7 @@ Response status codes beginning with the digit "5" indicate cases in which the s
HTTP_503_SERVICE_UNAVAILABLE
HTTP_504_GATEWAY_TIMEOUT
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
- HTTP_511_NETWORD_AUTHENTICATION_REQUIRED
+ HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 3fbcabb9..d2549997 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -56,6 +56,7 @@ The following people have helped make REST framework great.
* Jacob Magnusson - [jmagnusson]
* Osiloke Harold Emoekpere - [osiloke]
* Michael Shepanski - [mjs7231]
+* Toni Michel - [tonimichel]
Many thanks to everyone who's contributed to the project.
@@ -146,4 +147,5 @@ To contact the author directly:
[ottoyiu]: https://github.com/OttoYiu
[jmagnusson]: https://github.com/jmagnusson
[osiloke]: https://github.com/osiloke
-[mjs7231]: https://github.com/mjs7231 \ No newline at end of file
+[mjs7231]: https://github.com/mjs7231
+[tonimichel]: https://github.com/tonimichel \ No newline at end of file
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index b5c81c2b..ecb6c91a 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -4,6 +4,16 @@
>
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
+## 2.1.1
+
+**Date**: 7th Nov 2012
+
+* Support use of HTML exception templates. Eg. `403.html`
+* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
+* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs.
+* Bugfix: Make textareas same width as other fields in browsable API.
+* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
+
## 2.1.0
**Date**: 5th Nov 2012
@@ -12,7 +22,7 @@
* **Serializer `instance` and `data` keyword args have their position swapped.**
* `queryset` argument is now optional on writable model fields.
-* Hyperlinked related fields optionally take `slug_field` and `slug_field_kwarg` arguments.
+* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
* Support Django's cache framework.
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
* Bugfix: Support choice field in Browseable API.
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 5aa2b889..fc99b879 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.0'
+__version__ = '2.1.1'
VERSION = __version__ # synonym
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 45c0cc8e..a4e29a30 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -135,7 +135,7 @@ class WritableField(Field):
self.error_messages = messages
self.validators = self.default_validators + validators
- self.default = default or self.default
+ self.default = default if default is not None else self.default
self.blank = blank
# Widgets are ony used for HTML forms.
@@ -212,10 +212,10 @@ class ModelField(WritableField):
super(ModelField, self).__init__(*args, **kwargs)
def from_native(self, value):
- try:
- rel = self.model_field.rel
+ rel = getattr(self.model_field, "rel", None)
+ if rel is not None:
return rel.to._meta.get_field(rel.field_name).to_python(value)
- except:
+ else:
return self.model_field.to_python(value)
def field_to_native(self, obj, field_name):
@@ -237,7 +237,7 @@ class RelatedField(WritableField):
"""
Base class for related model fields.
- If not overridden, this represents a to-one relatinship, using the unicode
+ If not overridden, this represents a to-one relationship, using the unicode
representation of the target.
"""
widget = widgets.Select
@@ -495,7 +495,7 @@ class HyperlinkedRelatedField(RelatedField):
"""
pk_url_kwarg = 'pk'
slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field`
+ slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False
def __init__(self, *args, **kwargs):
@@ -503,8 +503,12 @@ class HyperlinkedRelatedField(RelatedField):
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+
self.slug_field = kwargs.pop('slug_field', self.slug_field)
- self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', self.slug_field)
+ default_slug_kwarg = self.slug_url_kwarg or self.slug_field
+ self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
+ self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
@@ -596,20 +600,51 @@ class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
+ pk_url_kwarg = 'pk'
+ slug_field = 'slug'
+ slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the
# HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
self.format = kwargs.pop('format', None)
+
+ self.slug_field = kwargs.pop('slug_field', self.slug_field)
+ default_slug_kwarg = self.slug_url_kwarg or self.slug_field
+ self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
+ self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
- view_kwargs = {'pk': obj.pk}
- return reverse(view_name, kwargs=view_kwargs, request=request, format=format)
+ kwargs = {self.pk_url_kwarg: obj.pk}
+ try:
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ slug = getattr(obj, self.slug_field, None)
+
+ if not slug:
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
+
+ kwargs = {self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
+ try:
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
+ except:
+ pass
+
+ raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
##### Typed Fields #####
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index a0721883..ac02d3da 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -43,7 +43,7 @@ class GenericAPIView(views.APIView):
return serializer_class
- def get_serializer(self, data=None, files=None, instance=None):
+ def get_serializer(self, instance=None, data=None, files=None):
# TODO: add support for files
# TODO: add support for seperate serializer/deserializer
serializer_class = self.get_serializer_class()
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 735090f3..c3625a88 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -51,7 +51,7 @@ class ListModelMixin(object):
paginator, page, queryset, is_paginated = packed
serializer = self.get_pagination_serializer(page)
else:
- serializer = self.get_serializer(instance=self.object_list)
+ serializer = self.get_serializer(self.object_list)
return Response(serializer.data)
@@ -63,7 +63,7 @@ class RetrieveModelMixin(object):
"""
def retrieve(self, request, *args, **kwargs):
self.object = self.get_object()
- serializer = self.get_serializer(instance=self.object)
+ serializer = self.get_serializer(self.object)
return Response(serializer.data)
@@ -80,7 +80,7 @@ class UpdateModelMixin(object):
self.object = None
success_status = status.HTTP_201_CREATED
- serializer = self.get_serializer(data=request.DATA, instance=self.object)
+ serializer = self.get_serializer(self.object, data=request.DATA)
if serializer.is_valid():
self.pre_save(serializer.object)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 0a659bd1..22fd6e74 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -10,7 +10,7 @@ import copy
import string
from django import forms
from django.http.multipartparser import parse_header
-from django.template import RequestContext, loader
+from django.template import RequestContext, loader, Template
from django.utils import simplejson as json
from rest_framework.compat import yaml
from rest_framework.exceptions import ConfigurationError
@@ -162,6 +162,10 @@ class TemplateHTMLRenderer(BaseRenderer):
media_type = 'text/html'
format = 'html'
template_name = None
+ exception_template_names = [
+ '%(status_code)s.html',
+ 'api_exception.html'
+ ]
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -178,15 +182,21 @@ class TemplateHTMLRenderer(BaseRenderer):
request = renderer_context['request']
response = renderer_context['response']
- template_names = self.get_template_names(response, view)
- template = self.resolve_template(template_names)
- context = self.resolve_context(data, request)
+ if response.exception:
+ template = self.get_exception_template(response)
+ else:
+ template_names = self.get_template_names(response, view)
+ template = self.resolve_template(template_names)
+
+ context = self.resolve_context(data, request, response)
return template.render(context)
def resolve_template(self, template_names):
return loader.select_template(template_names)
- def resolve_context(self, data, request):
+ def resolve_context(self, data, request, response):
+ if response.exception:
+ data['status_code'] = response.status_code
return RequestContext(request, data)
def get_template_names(self, response, view):
@@ -198,8 +208,21 @@ class TemplateHTMLRenderer(BaseRenderer):
return view.get_template_names()
raise ConfigurationError('Returned a template response with no template_name')
+ def get_exception_template(self, response):
+ template_names = [name % {'status_code': response.status_code}
+ for name in self.exception_template_names]
+
+ try:
+ # Try to find an appropriate error template
+ return self.resolve_template(template_names)
+ except:
+ # Fall back to using eg '404 Not Found'
+ return Template('%d %s' % (response.status_code,
+ response.status_text.title()))
+
-class StaticHTMLRenderer(BaseRenderer):
+# Note, subclass TemplateHTMLRenderer simply for the exception behavior
+class StaticHTMLRenderer(TemplateHTMLRenderer):
"""
An HTML renderer class that simply returns pre-rendered HTML.
@@ -216,6 +239,15 @@ class StaticHTMLRenderer(BaseRenderer):
format = 'html'
def render(self, data, accepted_media_type=None, renderer_context=None):
+ renderer_context = renderer_context or {}
+ response = renderer_context['response']
+
+ if response and response.exception:
+ request = renderer_context['request']
+ template = self.get_exception_template(response)
+ context = self.resolve_context(data, request, response)
+ return template.render(context)
+
return data
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 006d7eeb..0de01204 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -9,7 +9,8 @@ class Response(SimpleTemplateResponse):
"""
def __init__(self, data=None, status=200,
- template_name=None, headers=None):
+ template_name=None, headers=None,
+ exception=False):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
@@ -21,6 +22,7 @@ class Response(SimpleTemplateResponse):
self.data = data
self.headers = headers and headers[:] or []
self.template_name = template_name
+ self.exception = exception
@property
def rendered_content(self):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 28767b16..4f68ada6 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -458,7 +458,7 @@ class ModelSerializer(Serializer):
"""
self.object.save()
- if self.m2m_data and save_m2m:
+ if getattr(self, 'm2m_data', None) and save_m2m:
for accessor_name, object_list in self.m2m_data.items():
setattr(self.object, accessor_name, object_list)
self.m2m_data = {}
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index fdf45659..b2e41b99 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -36,7 +36,7 @@ ul.breadcrumb {
margin: 58px 0 0 0;
}
-form select, form input {
+form select, form input, form textarea {
width: 90%;
}
diff --git a/rest_framework/status.py b/rest_framework/status.py
index f3a5e481..a1eb48da 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -49,4 +49,4 @@ HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
-HTTP_511_NETWORD_AUTHENTICATION_REQUIRED = 511
+HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py
index 10d7e31d..4caed59e 100644
--- a/rest_framework/tests/htmlrenderer.py
+++ b/rest_framework/tests/htmlrenderer.py
@@ -1,4 +1,6 @@
+from django.core.exceptions import PermissionDenied
from django.conf.urls.defaults import patterns, url
+from django.http import Http404
from django.test import TestCase
from django.template import TemplateDoesNotExist, Template
import django.template.loader
@@ -17,8 +19,22 @@ def example(request):
return Response(data, template_name='example.html')
+@api_view(('GET',))
+@renderer_classes((TemplateHTMLRenderer,))
+def permission_denied(request):
+ raise PermissionDenied()
+
+
+@api_view(('GET',))
+@renderer_classes((TemplateHTMLRenderer,))
+def not_found(request):
+ raise Http404()
+
+
urlpatterns = patterns('',
url(r'^$', example),
+ url(r'^permission_denied$', permission_denied),
+ url(r'^not_found$', not_found),
)
@@ -48,3 +64,52 @@ class TemplateHTMLRendererTests(TestCase):
response = self.client.get('/')
self.assertContains(response, "example: foobar")
self.assertEquals(response['Content-Type'], 'text/html')
+
+ def test_not_found_html_view(self):
+ response = self.client.get('/not_found')
+ self.assertEquals(response.status_code, 404)
+ self.assertEquals(response.content, "404 Not Found")
+ self.assertEquals(response['Content-Type'], 'text/html')
+
+ def test_permission_denied_html_view(self):
+ response = self.client.get('/permission_denied')
+ self.assertEquals(response.status_code, 403)
+ self.assertEquals(response.content, "403 Forbidden")
+ self.assertEquals(response['Content-Type'], 'text/html')
+
+
+class TemplateHTMLRendererExceptionTests(TestCase):
+ urls = 'rest_framework.tests.htmlrenderer'
+
+ def setUp(self):
+ """
+ Monkeypatch get_template
+ """
+ self.get_template = django.template.loader.get_template
+
+ def get_template(template_name):
+ if template_name == '404.html':
+ return Template("404: {{ detail }}")
+ if template_name == '403.html':
+ return Template("403: {{ detail }}")
+ raise TemplateDoesNotExist(template_name)
+
+ django.template.loader.get_template = get_template
+
+ def tearDown(self):
+ """
+ Revert monkeypatching
+ """
+ django.template.loader.get_template = self.get_template
+
+ def test_not_found_html_view_with_template(self):
+ response = self.client.get('/not_found')
+ self.assertEquals(response.status_code, 404)
+ self.assertEquals(response.content, "404: Not found")
+ self.assertEquals(response['Content-Type'], 'text/html')
+
+ def test_permission_denied_html_view_with_template(self):
+ response = self.client.get('/permission_denied')
+ self.assertEquals(response.status_code, 403)
+ self.assertEquals(response.content, "403: Permission denied")
+ self.assertEquals(response['Content-Type'], 'text/html')
diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py
index 672d32a3..80e39d46 100644
--- a/rest_framework/utils/breadcrumbs.py
+++ b/rest_framework/utils/breadcrumbs.py
@@ -6,7 +6,7 @@ def get_breadcrumbs(url):
from rest_framework.views import APIView
- def breadcrumbs_recursive(url, breadcrumbs_list, prefix):
+ def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
try:
@@ -16,7 +16,11 @@ def get_breadcrumbs(url):
else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs
if isinstance(getattr(view, 'cls_instance', None), APIView):
- breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
+ # Don't list the same view twice in a row.
+ # Probably an optional trailing slash.
+ if not seen or seen[-1] != view:
+ breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
+ seen.append(view)
if url == '':
# All done
@@ -24,11 +28,11 @@ def get_breadcrumbs(url):
elif url.endswith('/'):
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list, prefix)
+ return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list, prefix, seen)
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list, prefix)
+ return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list, prefix, seen)
prefix = get_script_prefix().rstrip('/')
url = url[len(prefix):]
- return breadcrumbs_recursive(url, [], prefix)
+ return breadcrumbs_recursive(url, [], prefix, [])
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 71e1fe6c..1afbd697 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -320,13 +320,17 @@ class APIView(View):
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
if isinstance(exc, exceptions.APIException):
- return Response({'detail': exc.detail}, status=exc.status_code)
+ return Response({'detail': exc.detail},
+ status=exc.status_code,
+ exception=True)
elif isinstance(exc, Http404):
return Response({'detail': 'Not found'},
- status=status.HTTP_404_NOT_FOUND)
+ status=status.HTTP_404_NOT_FOUND,
+ exception=True)
elif isinstance(exc, PermissionDenied):
return Response({'detail': 'Permission denied'},
- status=status.HTTP_403_FORBIDDEN)
+ status=status.HTTP_403_FORBIDDEN,
+ exception=True)
raise
# Note: session based authentication is explicitly CSRF validated,