diff options
| author | Tom Christie | 2014-08-19 10:11:10 +0100 |
|---|---|---|
| committer | Tom Christie | 2014-08-19 10:11:10 +0100 |
| commit | e385a7b8eb6e538698f28128e43fe8bfaefd4e97 (patch) | |
| tree | 3f1cac50c40ec77a0bc5537f1a04628901a4d2ce /rest_framework | |
| parent | 2aad8e4b35c3552a065347d7eccad8bd51938783 (diff) | |
| parent | 48b66ec2a2b744f170034adbdaaa1588e6c14e11 (diff) | |
| download | django-rest-framework-e385a7b8eb6e538698f28128e43fe8bfaefd4e97.tar.bz2 | |
Merge master
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/authentication.py | 14 | ||||
| -rw-r--r-- | rest_framework/compat.py | 15 | ||||
| -rw-r--r-- | rest_framework/filters.py | 4 | ||||
| -rw-r--r-- | rest_framework/generics.py | 17 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 37 | ||||
| -rw-r--r-- | rest_framework/request.py | 11 | ||||
| -rw-r--r-- | rest_framework/response.py | 6 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 2 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 6 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/login_base.html | 15 | ||||
| -rw-r--r-- | rest_framework/test.py | 4 | ||||
| -rw-r--r-- | rest_framework/urls.py | 8 |
12 files changed, 87 insertions, 52 deletions
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cbc83574..82cea70f 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -310,6 +310,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() + if len(auth) == 1: + msg = 'Invalid bearer header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = 'Invalid bearer header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) + if auth and auth[0].lower() == b'bearer': access_token = auth[1] elif 'access_token' in request.POST: @@ -319,13 +326,6 @@ class OAuth2Authentication(BaseAuthentication): else: return None - if len(auth) == 1: - msg = 'Invalid bearer header. No credentials provided.' - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = 'Invalid bearer header. Token string should not contain spaces.' - raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(request, access_token) def authenticate_credentials(self, request, access_token): diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6a5cbbe4..4b16a8ca 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -44,12 +44,15 @@ except ImportError: django_filters = None -# django-guardian is optional -try: - import guardian - import guardian.shortcuts # Fixes #1624 -except ImportError: - guardian = None +# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS +# Fixes (#1712). We keep the try/except for the test suite. +guardian = None +if 'guardian' in settings.INSTALLED_APPS: + try: + import guardian + import guardian.shortcuts # Fixes #1624 + except ImportError: + pass # cStringIO only if it's available, otherwise StringIO diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 96d15eb9..c3b846ae 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -116,6 +116,10 @@ class OrderingFilter(BaseFilterBackend): def get_ordering(self, request): """ Ordering is set by a comma delimited ?ordering=... query parameter. + + The `ordering` query parameter can be overridden by setting + the `ordering_param` value on the OrderingFilter or by + specifying an `ORDERING_PARAM` value in the API settings. """ params = request.QUERY_PARAMS.get(self.ordering_param) if params: diff --git a/rest_framework/generics.py b/rest_framework/generics.py index e38c52b1..cecb548f 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -43,6 +43,10 @@ class GenericAPIView(views.APIView): # You'll need to either set these attributes, # or override `get_queryset()`/`get_serializer_class()`. + # If you are overriding a view method, it is important that you call + # `get_queryset()` instead of accessing the `queryset` property directly, + # as `queryset` will get evaluated only once, and those results are cached + # for all subsequent requests. queryset = None serializer_class = None @@ -185,7 +189,13 @@ class GenericAPIView(views.APIView): """ Returns the list of filter backends that this view requires. """ - filter_backends = self.filter_backends or [] + if self.filter_backends is None: + filter_backends = [] + else: + # Note that we are returning a *copy* of the class attribute, + # so that it is safe for the view to mutate it if needed. + filter_backends = list(self.filter_backends) + if not filter_backends and self.filter_backend: warnings.warn( 'The `filter_backend` attribute and `FILTER_BACKEND` setting ' @@ -195,6 +205,7 @@ class GenericAPIView(views.APIView): DeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] + return filter_backends @@ -258,6 +269,10 @@ class GenericAPIView(views.APIView): This must be an iterable, and may be a queryset. Defaults to using `self.queryset`. + This method should always be used rather than accessing `self.queryset` + directly, as `self.queryset` gets evaluated only once, and those results + are cached for all subsequent requests. + You may want to override this if you need to provide different querysets depending on the incoming request. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 484961ad..7048d87d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -54,32 +54,37 @@ class JSONRenderer(BaseRenderer): format = 'json' encoder_class = encoders.JSONEncoder ensure_ascii = True - charset = None - # JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32. + + # We don't set a charset because JSON is a binary encoding, + # that can be encoded as utf-8, utf-16 or utf-32. # See: http://www.ietf.org/rfc/rfc4627.txt # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/ + charset = None + + def get_indent(self, accepted_media_type, renderer_context): + if accepted_media_type: + # If the media type looks like 'application/json; indent=4', + # then pretty print the result. + base_media_type, params = parse_header(accepted_media_type.encode('ascii')) + try: + return max(min(int(params['indent']), 8), 0) + except (KeyError, ValueError, TypeError): + pass + + # If 'indent' is provided in the context, then pretty print the result. + # E.g. If we're being called by the BrowsableAPIRenderer. + return renderer_context.get('indent', None) + def render(self, data, accepted_media_type=None, renderer_context=None): """ - Render `data` into JSON. + Render `data` into JSON, returning a bytestring. """ if data is None: return bytes() - # If 'indent' is provided in the context, then pretty print the result. - # E.g. If we're being called by the BrowsableAPIRenderer. renderer_context = renderer_context or {} - indent = renderer_context.get('indent', None) - - if accepted_media_type: - # If the media type looks like 'application/json; indent=4', - # then pretty print the result. - base_media_type, params = parse_header(accepted_media_type.encode('ascii')) - indent = params.get('indent', indent) - try: - indent = max(min(int(indent), 8), 0) - except (ValueError, TypeError): - indent = None + indent = self.get_indent(accepted_media_type, renderer_context) ret = json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii) diff --git a/rest_framework/request.py b/rest_framework/request.py index 40467c03..d508f9b4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -42,13 +42,20 @@ class override_method(object): self.view = view self.request = request self.method = method + self.action = getattr(view, 'action', None) def __enter__(self): self.view.request = clone_request(self.request, self.method) + if self.action is not None: + # For viewsets we also set the `.action` attribute. + action_map = getattr(self.view, 'action_map', {}) + self.view.action = action_map.get(self.method.lower()) return self.view.request def __exit__(self, *args, **kwarg): self.view.request = self.request + if self.action is not None: + self.view.action = self.action class Empty(object): @@ -280,8 +287,8 @@ class Request(object): self._method = self._request.method # Allow X-HTTP-METHOD-OVERRIDE header - self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', - self._method) + if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META: + self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper() def _load_stream(self): """ diff --git a/rest_framework/response.py b/rest_framework/response.py index 1dc6abcf..25b78524 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -5,6 +5,7 @@ it is initialized with unrendered data, instead of a pre-rendered string. The appropriate renderer is called during Django's template response rendering. """ from __future__ import unicode_literals +import django from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse from rest_framework.compat import six @@ -15,8 +16,11 @@ class Response(SimpleTemplateResponse): An HttpResponse that allows its data to be rendered into arbitrary media types. """ + # TODO: remove that once Django 1.3 isn't supported + if django.VERSION >= (1, 4): + rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects'] - def __init__(self, data=None, status=200, + def __init__(self, data=None, status=None, template_name=None, headers=None, exception=False, content_type=None): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a15b8f22..2fdc9b9d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -976,7 +976,7 @@ class ModelSerializer(Serializer): try: setattr(instance, key, val) except ValueError: - self._errors[key] = self.error_messages['required'] + self._errors[key] = [self.error_messages['required']] # Any relations that cannot be set until we've # saved the model get hidden away on these diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 210741ed..b6e9ca5c 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -25,6 +25,7 @@ {% endblock %} </head> + {% block body %} <body class="{% block bodyclass %}{% endblock %} container"> <div class="wrapper"> @@ -94,7 +95,7 @@ {% endif %} {% if options_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> {% csrf_token %} <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> @@ -102,7 +103,7 @@ {% endif %} {% if delete_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> {% csrf_token %} <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> @@ -231,4 +232,5 @@ <script src="{% static "rest_framework/js/default.js" %}"></script> {% endblock %} </body> + {% endblock %} </html> diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index be83c2f5..43860e53 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -1,18 +1,9 @@ +{% extends "rest_framework/base.html" %} {% load url from future %} {% load staticfiles %} {% load rest_framework %} -<html> - - <head> - {% block style %} - {% block bootstrap_theme %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> - {% endblock %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> - {% endblock %} - </head> + {% block body %} <body class="container"> <div class="container-fluid" style="margin-top: 30px"> @@ -51,4 +42,4 @@ </div><!-- /.row-fluid --> </div><!-- /.container-fluid --> </body> -</html> + {% endblock %} diff --git a/rest_framework/test.py b/rest_framework/test.py index 284bcee0..d4ec50a0 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -154,6 +154,10 @@ class APIClient(APIRequestFactory, DjangoClient): kwargs.update(self._credentials) return super(APIClient, self).request(**kwargs) + def logout(self): + self._credentials = {} + return super(APIClient, self).logout() + class APITransactionTestCase(testcases.TransactionTestCase): client_class = APIClient diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 87ec0f0a..eed4bd14 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -2,15 +2,15 @@ Login and logout views for the browsable API. Add these to your root URLconf if you're using the browsable API and -your API requires authentication. - -The urls must be namespaced as 'rest_framework', and you should make sure -your authentication settings include `SessionAuthentication`. +your API requires authentication: urlpatterns = patterns('', ... url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) ) + +The urls must be namespaced as 'rest_framework', and you should make sure +your authentication settings include `SessionAuthentication`. """ from __future__ import unicode_literals from django.conf.urls import patterns, url |
