diff options
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 4 | ||||
| -rw-r--r-- | docs/api-guide/renderers.md | 16 | ||||
| -rw-r--r-- | docs/api-guide/serializers.md | 2 | ||||
| -rw-r--r-- | docs/api-guide/status-codes.md | 2 | ||||
| -rw-r--r-- | docs/topics/credits.md | 4 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 12 | ||||
| -rw-r--r-- | rest_framework/__init__.py | 2 | ||||
| -rw-r--r-- | rest_framework/fields.py | 53 | ||||
| -rw-r--r-- | rest_framework/generics.py | 2 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 6 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 44 | ||||
| -rw-r--r-- | rest_framework/response.py | 4 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 2 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/default.css | 2 | ||||
| -rw-r--r-- | rest_framework/status.py | 2 | ||||
| -rw-r--r-- | rest_framework/tests/htmlrenderer.py | 65 | ||||
| -rw-r--r-- | rest_framework/utils/breadcrumbs.py | 14 | ||||
| -rw-r--r-- | rest_framework/views.py | 10 |
19 files changed, 220 insertions, 36 deletions
@@ -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, |
