aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2014-08-19 10:11:10 +0100
committerTom Christie2014-08-19 10:11:10 +0100
commite385a7b8eb6e538698f28128e43fe8bfaefd4e97 (patch)
tree3f1cac50c40ec77a0bc5537f1a04628901a4d2ce /rest_framework
parent2aad8e4b35c3552a065347d7eccad8bd51938783 (diff)
parent48b66ec2a2b744f170034adbdaaa1588e6c14e11 (diff)
downloaddjango-rest-framework-e385a7b8eb6e538698f28128e43fe8bfaefd4e97.tar.bz2
Merge master
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/authentication.py14
-rw-r--r--rest_framework/compat.py15
-rw-r--r--rest_framework/filters.py4
-rw-r--r--rest_framework/generics.py17
-rw-r--r--rest_framework/renderers.py37
-rw-r--r--rest_framework/request.py11
-rw-r--r--rest_framework/response.py6
-rw-r--r--rest_framework/serializers.py2
-rw-r--r--rest_framework/templates/rest_framework/base.html6
-rw-r--r--rest_framework/templates/rest_framework/login_base.html15
-rw-r--r--rest_framework/test.py4
-rw-r--r--rest_framework/urls.py8
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