aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authtoken/models.py5
-rw-r--r--rest_framework/compat.py105
-rw-r--r--rest_framework/decorators.py4
-rw-r--r--rest_framework/exceptions.py2
-rw-r--r--rest_framework/fields.py60
-rw-r--r--rest_framework/metadata.py7
-rw-r--r--rest_framework/parsers.py16
-rw-r--r--rest_framework/permissions.py2
-rw-r--r--rest_framework/relations.py76
-rw-r--r--rest_framework/renderers.py18
-rw-r--r--rest_framework/request.py2
-rw-r--r--rest_framework/serializers.py304
-rw-r--r--rest_framework/settings.py2
-rw-r--r--rest_framework/static/rest_framework/js/default.js3
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select.html2
-rw-r--r--rest_framework/templates/rest_framework/inline/select.html2
-rw-r--r--rest_framework/templates/rest_framework/login_base.html23
-rw-r--r--rest_framework/templates/rest_framework/raw_data_form.html8
-rw-r--r--rest_framework/templates/rest_framework/vertical/select.html2
-rw-r--r--rest_framework/templatetags/rest_framework.py4
-rw-r--r--rest_framework/test.py5
-rw-r--r--rest_framework/utils/encoders.py2
-rw-r--r--rest_framework/utils/field_mapping.py14
-rw-r--r--rest_framework/utils/mediatypes.py5
-rw-r--r--rest_framework/utils/representation.py2
-rw-r--r--rest_framework/utils/serializer_helpers.py14
-rw-r--r--rest_framework/validators.py2
-rw-r--r--rest_framework/views.py3
-rw-r--r--rest_framework/viewsets.py12
30 files changed, 410 insertions, 298 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 261c9c98..f30f781a 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '3.0.0'
+__version__ = '3.0.1'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index db21d44c..a1a9315f 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,7 +1,9 @@
import binascii
import os
+
from django.conf import settings
from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
@@ -11,6 +13,7 @@ from django.db import models
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+@python_2_unicode_compatible
class Token(models.Model):
"""
The default authorization token model.
@@ -35,5 +38,5 @@ class Token(models.Model):
def generate_key(self):
return binascii.hexlify(os.urandom(20)).decode()
- def __unicode__(self):
+ def __str__(self):
return self.key
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 08dd9df6..43ad6eaa 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -6,24 +6,14 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
+import inspect
+
from django.core.exceptions import ImproperlyConfigured
+from django.utils.encoding import force_text
+from django.utils.six.moves.urllib import parse as urlparse
from django.conf import settings
from django.utils import six
import django
-import inspect
-
-
-# Handle django.utils.encoding rename in 1.5 onwards.
-# smart_unicode -> smart_text
-# force_unicode -> force_text
-try:
- from django.utils.encoding import smart_text
-except ImportError:
- from django.utils.encoding import smart_unicode as smart_text
-try:
- from django.utils.encoding import force_text
-except ImportError:
- from django.utils.encoding import force_unicode as force_text
# OrderedDict only available in Python 2.7.
@@ -32,7 +22,7 @@ except ImportError:
# For Django <= 1.6 and Python 2.6 fall back to OrderedDict.
try:
from collections import OrderedDict
-except:
+except ImportError:
from django.utils.datastructures import SortedDict as OrderedDict
@@ -49,7 +39,6 @@ try:
except ImportError:
django_filters = None
-
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
@@ -72,30 +61,6 @@ if 'guardian' in settings.INSTALLED_APPS:
pass
-# cStringIO only if it's available, otherwise StringIO
-try:
- import cStringIO.StringIO as StringIO
-except ImportError:
- StringIO = six.StringIO
-
-BytesIO = six.BytesIO
-
-
-# urlparse compat import (Required because it changed in python 3.x)
-try:
- from urllib import parse as urlparse
-except ImportError:
- import urlparse
-
-# UserDict moves in Python 3
-try:
- from UserDict import UserDict
- from UserDict import DictMixin
-except ImportError:
- from collections import UserDict
- from collections import MutableMapping as DictMixin
-
-
def get_model_name(model_cls):
try:
return model_cls._meta.model_name
@@ -104,14 +69,6 @@ def get_model_name(model_cls):
return model_cls._meta.module_name
-def get_concrete_model(model_cls):
- try:
- return model_cls._meta.concrete_model
- except AttributeError:
- # 1.3 does not include concrete model
- return model_cls
-
-
# View._allowed_methods only present from 1.5 onwards
if django.VERSION >= (1, 5):
from django.views.generic import View
@@ -123,7 +80,6 @@ else:
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
-
# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
if django.VERSION >= (1, 8):
from django.core.validators import MinValueValidator, MaxValueValidator
@@ -187,6 +143,7 @@ if 'patch' not in View.http_method_names:
# RequestFactory only provides `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
+
try:
# In 1.5 the test client uses force_bytes
from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
@@ -194,26 +151,22 @@ except ImportError:
# In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
+
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
parsed = urlparse.urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_text(parsed[4]),
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': force_text(parsed[4]),
'REQUEST_METHOD': six.text_type(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
- 'CONTENT_TYPE': six.text_type(content_type),
- 'wsgi.input': FakePayload(data),
- })
- elif django.VERSION <= (1, 4):
- # For 1.3 we need an empty WSGI payload
- r.update({
- 'wsgi.input': FakePayload('')
+ 'CONTENT_TYPE': six.text_type(content_type),
+ 'wsgi.input': FakePayload(data),
})
r.update(extra)
return self.request(**r)
@@ -237,7 +190,7 @@ except ImportError:
apply_markdown = None
-# `seperators` argument to `json.dumps()` differs between 2.x and 3.x
+# `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: http://bugs.python.org/issue22767
if six.PY3:
SHORT_SEPARATORS = (',', ':')
@@ -245,37 +198,3 @@ if six.PY3:
else:
SHORT_SEPARATORS = (b',', b':')
LONG_SEPARATORS = (b', ', b': ')
-
-
-# Handle lazy strings across Py2/Py3
-from django.utils.functional import Promise
-
-if six.PY3:
- def is_non_str_iterable(obj):
- if (isinstance(obj, str) or
- (isinstance(obj, Promise) and obj._delegate_text)):
- return False
- return hasattr(obj, '__iter__')
-else:
- def is_non_str_iterable(obj):
- return hasattr(obj, '__iter__')
-
-
-try:
- from django.utils.encoding import python_2_unicode_compatible
-except ImportError:
- def python_2_unicode_compatible(klass):
- """
- A decorator that defines __unicode__ and __str__ methods under Python 2.
- Under Python 3 it does nothing.
-
- To support Python 2 and 3 with a single code base, define a __str__ method
- returning text and apply this decorator to the class.
- """
- if '__str__' not in klass.__dict__:
- raise ValueError("@python_2_unicode_compatible cannot be applied "
- "to %s because it doesn't define __str__()." %
- klass.__name__)
- klass.__unicode__ = klass.__str__
- klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
- return klass
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index d28d6e22..325435b3 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -12,12 +12,14 @@ from rest_framework.views import APIView
import types
-def api_view(http_method_names):
+def api_view(http_method_names=None):
"""
Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument.
"""
+ if http_method_names is None:
+ http_method_names = ['GET']
def decorator(func):
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 906de3b0..be41d08d 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,11 +5,11 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from rest_framework import status
-from rest_framework.compat import force_text
import math
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index ca9c479f..25122e14 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -5,16 +5,17 @@ from django.core.validators import RegexValidator
from django.forms import ImageField as DjangoImageField
from django.utils import six, timezone
from django.utils.dateparse import parse_date, parse_datetime, parse_time
-from django.utils.encoding import is_protected_type
+from django.utils.encoding import is_protected_type, smart_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
from rest_framework.compat import (
- smart_text, EmailValidator, MinValueValidator, MaxValueValidator,
+ EmailValidator, MinValueValidator, MaxValueValidator,
MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict
)
from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, representation, humanize_datetime
+import collections
import copy
import datetime
import decimal
@@ -60,14 +61,12 @@ def get_attribute(instance, attrs):
# Break out early if we get `None` at any point in a nested lookup.
return None
try:
- instance = getattr(instance, attr)
+ if isinstance(instance, collections.Mapping):
+ instance = instance[attr]
+ else:
+ instance = getattr(instance, attr)
except ObjectDoesNotExist:
return None
- except AttributeError as exc:
- try:
- return instance[attr]
- except (KeyError, TypeError, AttributeError):
- raise exc
if is_simple_callable(instance):
instance = instance()
return instance
@@ -294,31 +293,47 @@ class Field(object):
return self.default()
return self.default
- def run_validation(self, data=empty):
+ def validate_empty_values(self, data):
"""
- Validate a simple representation and return the internal value.
-
- The provided data may be `empty` if no representation was included
- in the input.
-
- May raise `SkipField` if the field should not be included in the
- validated data.
+ Validate empty values, and either:
+
+ * Raise `ValidationError`, indicating invalid data.
+ * Raise `SkipField`, indicating that the field should be ignored.
+ * Return (True, data), indicating an empty value that should be
+ returned without any furhter validation being applied.
+ * Return (False, data), indicating a non-empty value, that should
+ have validation applied as normal.
"""
if self.read_only:
- return self.get_default()
+ return (True, self.get_default())
if data is empty:
if getattr(self.root, 'partial', False):
raise SkipField()
if self.required:
self.fail('required')
- return self.get_default()
+ return (True, self.get_default())
if data is None:
if not self.allow_null:
self.fail('null')
- return None
+ return (True, None)
+
+ return (False, data)
+
+ def run_validation(self, data=empty):
+ """
+ Validate a simple representation and return the internal value.
+ The provided data may be `empty` if no representation was included
+ in the input.
+
+ May raise `SkipField` if the field should not be included in the
+ validated data.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
value = self.to_internal_value(data)
self.run_validators(value)
return value
@@ -494,7 +509,7 @@ class CharField(Field):
default_error_messages = {
'blank': _('This field may not be blank.'),
'max_length': _('Ensure this field has no more than {max_length} characters.'),
- 'min_length': _('Ensure this field has no more than {min_length} characters.')
+ 'min_length': _('Ensure this field has at least {min_length} characters.')
}
initial = ''
coerce_blank_to_null = False
@@ -942,9 +957,14 @@ class ChoiceField(Field):
(six.text_type(key), key) for key in self.choices.keys()
])
+ self.allow_blank = kwargs.pop('allow_blank', False)
+
super(ChoiceField, self).__init__(**kwargs)
def to_internal_value(self, data):
+ if data == '' and self.allow_blank:
+ return ''
+
try:
return self.choice_strings_to_values[six.text_type(data)]
except KeyError:
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index de829d00..3b058fab 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -1,17 +1,18 @@
"""
-The metadata API is used to allow cusomization of how `OPTIONS` requests
+The metadata API is used to allow customization of how `OPTIONS` requests
are handled. We currently provide a single default implementation that returns
some fairly ad-hoc information about the view.
-Future implementations might use JSON schema or other definations in order
+Future implementations might use JSON schema or other definitions in order
to return this information in a more standardized way.
"""
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
+from django.utils.encoding import force_text
from rest_framework import exceptions, serializers
-from rest_framework.compat import force_text, OrderedDict
+from rest_framework.compat import OrderedDict
from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 1960e5a8..cb23423d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -12,7 +12,8 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import force_text, urlparse
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import force_text
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
@@ -159,23 +160,24 @@ class FileUploadParser(BaseParser):
chunks = ChunkIter(stream, chunk_size)
counters = [0] * len(upload_handlers)
- for handler in upload_handlers:
+ for index, handler in enumerate(upload_handlers):
try:
handler.new_file(None, filename, content_type,
content_length, encoding)
except StopFutureHandlers:
+ upload_handlers = upload_handlers[:index + 1]
break
for chunk in chunks:
- for i, handler in enumerate(upload_handlers):
+ for index, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
- chunk = handler.receive_data_chunk(chunk, counters[i])
- counters[i] += chunk_length
+ chunk = handler.receive_data_chunk(chunk, counters[index])
+ counters[index] += chunk_length
if chunk is None:
break
- for i, handler in enumerate(upload_handlers):
- file_obj = handler.file_complete(counters[i])
+ for index, handler in enumerate(upload_handlers):
+ file_obj = handler.file_complete(counters[index])
if file_obj:
return DataAndFiles(None, {'file': file_obj})
raise ParseError("FileUpload parse error - "
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 7c498645..9069d315 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -183,7 +183,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):
if not user.has_perms(perms, obj):
# If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see
- # a 404 reponse.
+ # a 404 response.
if request.method in ('GET', 'OPTIONS', 'HEAD'):
# Read permissions already checked and failed, no need
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index d1ea497a..892ce6c1 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,4 +1,4 @@
-from rest_framework.compat import smart_text, urlparse
+from django.utils.encoding import smart_text
from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
from rest_framework.utils import html
@@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet
from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
@@ -83,9 +84,20 @@ class RelatedField(Field):
queryset = queryset.all()
return queryset
- def get_iterable(self, instance, source_attrs):
- relationship = get_attribute(instance, source_attrs)
- return relationship.all() if (hasattr(relationship, 'all')) else relationship
+ def use_pk_only_optimization(self):
+ return False
+
+ def get_attribute(self, instance):
+ if self.use_pk_only_optimization() and self.source_attrs:
+ # Optimized case, return a mock object only containing the pk attribute.
+ try:
+ instance = get_attribute(instance, self.source_attrs[:-1])
+ return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
+ except AttributeError:
+ pass
+
+ # Standard case, return the object instance.
+ return get_attribute(instance, self.source_attrs)
@property
def choices(self):
@@ -114,11 +126,14 @@ class StringRelatedField(RelatedField):
class PrimaryKeyRelatedField(RelatedField):
default_error_messages = {
- 'required': 'This field is required.',
- 'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.",
- 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
+ 'required': _('This field is required.'),
+ 'does_not_exist': _("Invalid pk '{pk_value}' - object does not exist."),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
}
+ def use_pk_only_optimization(self):
+ return True
+
def to_internal_value(self, data):
try:
return self.get_queryset().get(pk=data)
@@ -127,32 +142,6 @@ class PrimaryKeyRelatedField(RelatedField):
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
- def get_attribute(self, instance):
- # We customize `get_attribute` here for performance reasons.
- # For relationships the instance will already have the pk of
- # the related object. We return this directly instead of returning the
- # object itself, which would require a database lookup.
- try:
- instance = get_attribute(instance, self.source_attrs[:-1])
- return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
- except AttributeError:
- return get_attribute(instance, self.source_attrs)
-
- def get_iterable(self, instance, source_attrs):
- # For consistency with `get_attribute` we're using `serializable_value()`
- # here. Typically there won't be any difference, but some custom field
- # types might return a non-primative value for the pk otherwise.
- #
- # We could try to get smart with `values_list('pk', flat=True)`, which
- # would be better in some case, but would actually end up with *more*
- # queries if the developer is using `prefetch_related` across the
- # relationship.
- relationship = super(PrimaryKeyRelatedField, self).get_iterable(instance, source_attrs)
- return [
- PKOnlyObject(pk=item.serializable_value('pk'))
- for item in relationship
- ]
-
def to_representation(self, value):
return value.pk
@@ -161,11 +150,11 @@ class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
default_error_messages = {
- 'required': 'This field is required.',
- 'no_match': 'Invalid hyperlink - No URL match',
- 'incorrect_match': 'Invalid hyperlink - Incorrect URL match.',
- 'does_not_exist': 'Invalid hyperlink - Object does not exist.',
- 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
+ 'required': _('This field is required.'),
+ 'no_match': _('Invalid hyperlink - No URL match'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
+ 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
}
def __init__(self, view_name=None, **kwargs):
@@ -183,6 +172,9 @@ class HyperlinkedRelatedField(RelatedField):
super(HyperlinkedRelatedField, self).__init__(**kwargs)
+ def use_pk_only_optimization(self):
+ return self.lookup_field == 'pk'
+
def get_object(self, view_name, view_args, view_kwargs):
"""
Return the object corresponding to a matched URL.
@@ -284,6 +276,11 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
kwargs['source'] = '*'
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
+ def use_pk_only_optimization(self):
+ # We have the complete object instance already. We don't need
+ # to run the 'only get the pk for this relationship' code.
+ return False
+
class SlugRelatedField(RelatedField):
"""
@@ -348,7 +345,8 @@ class ManyRelatedField(Field):
]
def get_attribute(self, instance):
- return self.child_relation.get_iterable(instance, self.source_attrs)
+ relationship = get_attribute(instance, self.source_attrs)
+ return relationship.all() if (hasattr(relationship, 'all')) else relationship
def to_representation(self, iterable):
return [
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 173e48dc..f8d18fc6 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -12,6 +12,7 @@ import json
import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
+from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
@@ -99,6 +100,11 @@ class JSONRenderer(BaseRenderer):
# and may (or may not) be unicode.
# On python 3.x json.dumps() returns unicode strings.
if isinstance(ret, six.text_type):
+ # We always fully escape \u2028 and \u2029 to ensure we output JSON
+ # that is a strict javascript subset. If bytes were returned
+ # by json.dumps() then we don't have these characters in any case.
+ # See: http://timelessrepo.com/json-isnt-a-javascript-subset
+ ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
return bytes(ret.encode('utf-8'))
return ret
@@ -173,7 +179,9 @@ class TemplateHTMLRenderer(BaseRenderer):
return view.get_template_names()
elif hasattr(view, 'template_name'):
return [view.template_name]
- raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response')
+ raise ImproperlyConfigured(
+ 'Returned a template response with no `template_name` attribute set on either the view or response'
+ )
def get_exception_template(self, response):
template_names = [name % {'status_code': response.status_code}
@@ -265,6 +273,10 @@ class HTMLFormRenderer(BaseRenderer):
'base_template': 'input.html',
'input_type': 'time'
},
+ serializers.FileField: {
+ 'base_template': 'input.html',
+ 'input_type': 'file'
+ },
serializers.BooleanField: {
'base_template': 'checkbox.html'
},
@@ -413,6 +425,8 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = getattr(data, 'serializer', None)
if serializer and not getattr(serializer, 'many', False):
instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
else:
instance = None
@@ -471,6 +485,8 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = getattr(data, 'serializer', None)
if serializer and not getattr(serializer, 'many', False):
instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
else:
instance = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index d7e74674..20e049ed 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -14,9 +14,9 @@ from django.http import QueryDict
from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import MergeDict as DjangoMergeDict
+from django.utils.six import BytesIO
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
-from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings
import warnings
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index f7aa3a7d..9226895e 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,16 +10,13 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from django.core.exceptions import ImproperlyConfigured
+import warnings
+
from django.db import models
from django.db.models.fields import FieldDoesNotExist
-from django.utils import six
from django.utils.translation import ugettext_lazy as _
-from rest_framework.compat import OrderedDict
-from rest_framework.exceptions import ValidationError
-from rest_framework.fields import empty, set_value, Field, SkipField
-from rest_framework.settings import api_settings
-from rest_framework.utils import html, model_meta, representation
+
+from rest_framework.utils import model_meta
from rest_framework.utils.field_mapping import (
get_url_kwargs, get_field_kwargs,
get_relation_kwargs, get_nested_relation_kwargs,
@@ -32,9 +29,7 @@ from rest_framework.validators import (
UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
UniqueTogetherValidator
)
-import copy
-import inspect
-import warnings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -64,6 +59,7 @@ class BaseSerializer(Field):
The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations.
"""
+
def __init__(self, instance=None, data=None, **kwargs):
self.instance = instance
self._initial_data = data
@@ -126,6 +122,14 @@ class BaseSerializer(Field):
(self.__class__.__module__, self.__class__.__name__)
)
+ assert hasattr(self, '_errors'), (
+ 'You must call `.is_valid()` before calling `.save()`.'
+ )
+
+ assert not self.errors, (
+ 'You cannot call `.save()` on a serializer with invalid data.'
+ )
+
validated_data = dict(
list(self.validated_data.items()) +
list(kwargs.items())
@@ -225,6 +229,35 @@ class SerializerMetaclass(type):
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+def get_validation_error_detail(exc):
+ assert isinstance(exc, (ValidationError, DjangoValidationError))
+
+ if isinstance(exc, DjangoValidationError):
+ # Normally you should raise `serializers.ValidationError`
+ # inside your codebase, but we handle Django's validation
+ # exception class as well for simpler compat.
+ # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+ }
+ elif isinstance(exc.detail, dict):
+ # If errors may be a dict we use the standard {key: list of values}.
+ # Here we ensure that all the values are *lists* of errors.
+ return dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ])
+ elif isinstance(exc.detail, list):
+ # Errors raised as a list are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ }
+ # Errors raised as a string are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ }
+
+
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
default_error_messages = {
@@ -236,7 +269,7 @@ class Serializer(BaseSerializer):
"""
A dictionary of {field_name: field_instance}.
"""
- # `fields` is evalutated lazily. We do this to ensure that we don't
+ # `fields` is evaluated lazily. We do this to ensure that we don't
# have issues importing modules that use ModelSerializers as fields,
# even if Django's app-loading stage has not yet run.
if not hasattr(self, '_fields'):
@@ -289,47 +322,17 @@ class Serializer(BaseSerializer):
performed by validators and the `.validate()` method should
be coerced into an error dictionary with a 'non_fields_error' key.
"""
- if data is empty:
- if getattr(self.root, 'partial', False):
- raise SkipField()
- if self.required:
- self.fail('required')
- return self.get_default()
-
- if data is None:
- if not self.allow_null:
- self.fail('null')
- return None
-
- if not isinstance(data, dict):
- message = self.error_messages['invalid'].format(
- datatype=type(data).__name__
- )
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [message]
- })
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
value = self.to_internal_value(data)
try:
self.run_validators(value)
value = self.validate(value)
assert value is not None, '.validate() should return the validated data'
- except ValidationError as exc:
- if isinstance(exc.detail, dict):
- # .validate() errors may be a dict, in which case, use
- # standard {key: list of values} style.
- raise ValidationError(dict([
- (key, value if isinstance(value, list) else [value])
- for key, value in exc.detail.items()
- ]))
- elif isinstance(exc.detail, list):
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: exc.detail
- })
- else:
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
- })
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
return value
@@ -337,6 +340,14 @@ class Serializer(BaseSerializer):
"""
Dict of native values <- Dict of primitive datatypes.
"""
+ if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
ret = OrderedDict()
errors = OrderedDict()
fields = [
@@ -353,6 +364,8 @@ class Serializer(BaseSerializer):
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
+ except DjangoValidationError as exc:
+ errors[field.field_name] = list(exc.messages)
except SkipField:
pass
else:
@@ -448,6 +461,26 @@ class ListSerializer(BaseSerializer):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
+
+ return value
+
def to_internal_value(self, data):
"""
List of dicts of native values <- List of dicts of primitive datatypes.
@@ -484,11 +517,16 @@ class ListSerializer(BaseSerializer):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
- iterable = data.all() if (hasattr(data, 'all')) else data
+ # Dealing with nested relationships, data can be a Manager,
+ # so, first get a queryset from the Manager if needed
+ iterable = data.all() if isinstance(data, models.Manager) else data
return [
self.child.to_representation(item) for item in iterable
]
+ def validate(self, attrs):
+ return attrs
+
def update(self, instance, validated_data):
raise NotImplementedError(
"Serializers with many=True do not support multiple update by "
@@ -547,6 +585,64 @@ class ListSerializer(BaseSerializer):
# ModelSerializer & HyperlinkedModelSerializer
# --------------------------------------------
+def raise_errors_on_nested_writes(method_name, serializer, validated_data):
+ """
+ Give explicit errors when users attempt to pass writable nested data.
+
+ If we don't do this explicitly they'd get a less helpful error when
+ calling `.save()` on the serializer.
+
+ We don't *automatically* support these sorts of nested writes brecause
+ there are too many ambiguities to define a default behavior.
+
+ Eg. Suppose we have a `UserSerializer` with a nested profile. How should
+ we handle the case of an update, where the `profile` realtionship does
+ not exist? Any of the following might be valid:
+
+ * Raise an application error.
+ * Silently ignore the nested part of the update.
+ * Automatically create a profile instance.
+ """
+
+ # Ensure we don't have a writable nested field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # profile = ProfileSerializer()
+ assert not any(
+ isinstance(field, BaseSerializer) and (key in validated_data)
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable nested'
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'nested serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+ # Ensure we don't have a writable dotted-source field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # address = serializer.CharField('profile.address')
+ assert not any(
+ '.' in field.source and (key in validated_data)
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable dotted-source '
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'dotted-source serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+
class ModelSerializer(Serializer):
"""
A `ModelSerializer` is just a regular `Serializer`, except that:
@@ -554,6 +650,14 @@ class ModelSerializer(Serializer):
* A set of default fields are automatically populated.
* A set of default validators are automatically populated.
* Default `.create()` and `.update()` implementations are provided.
+
+ The process of automatically determining a set of serializer fields
+ based on the model fields is reasonably complex, but you almost certainly
+ don't need to dig into the implementation.
+
+ If the `ModelSerializer` class *doesn't* generate the set of fields that
+ you need you should either declare the extra/differing fields explicitly on
+ the serializer class, or simply use a `Serializer` class.
"""
_field_mapping = ClassLookupDict({
models.AutoField: IntegerField,
@@ -581,32 +685,58 @@ class ModelSerializer(Serializer):
})
_related_class = PrimaryKeyRelatedField
- def create(self, validated_attrs):
- # Check that the user isn't trying to handle a writable nested field.
- # If we don't do this explicitly they'd likely get a confusing
- # error at the point of calling `Model.objects.create()`.
- assert not any(
- isinstance(field, BaseSerializer) and not field.read_only
- for field in self.fields.values()
- ), (
- 'The `.create()` method does not suport nested writable fields '
- 'by default. Write an explicit `.create()` method for serializer '
- '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
- (self.__class__.__module__, self.__class__.__name__)
- )
+ def create(self, validated_data):
+ """
+ We have a bit of extra checking around this in order to provide
+ descriptive messages when something goes wrong, but this method is
+ essentially just:
+
+ return ExampleModel.objects.create(**validated_data)
+
+ If there are many to many fields present on the instance then they
+ cannot be set until the model is instantiated, in which case the
+ implementation is like so:
+
+ example_relationship = validated_data.pop('example_relationship')
+ instance = ExampleModel.objects.create(**validated_data)
+ instance.example_relationship = example_relationship
+ return instance
+
+ The default implementation also does not handle nested relationships.
+ If you want to support writable nested relationships you'll need
+ to write an explicit `.create()` method.
+ """
+ raise_errors_on_nested_writes('create', self, validated_data)
ModelClass = self.Meta.model
- # Remove many-to-many relationships from validated_attrs.
+ # Remove many-to-many relationships from validated_data.
# They are not valid arguments to the default `.create()` method,
# as they require that the instance has already been saved.
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
for field_name, relation_info in info.relations.items():
- if relation_info.to_many and (field_name in validated_attrs):
- many_to_many[field_name] = validated_attrs.pop(field_name)
+ if relation_info.to_many and (field_name in validated_data):
+ many_to_many[field_name] = validated_data.pop(field_name)
- instance = ModelClass.objects.create(**validated_attrs)
+ try:
+ instance = ModelClass.objects.create(**validated_data)
+ except TypeError as exc:
+ msg = (
+ 'Got a `TypeError` when calling `%s.objects.create()`. '
+ 'This may be because you have a writable field on the '
+ 'serializer class that is not a valid argument to '
+ '`%s.objects.create()`. You may need to make the field '
+ 'read-only, or override the %s.create() method to handle '
+ 'this correctly.\nOriginal exception text was: %s.' %
+ (
+ ModelClass.__name__,
+ ModelClass.__name__,
+ self.__class__.__name__,
+ exc
+ )
+ )
+ raise TypeError(msg)
# Save many-to-many relationships after the instance is created.
if many_to_many:
@@ -615,31 +745,29 @@ class ModelSerializer(Serializer):
return instance
- def update(self, instance, validated_attrs):
- assert not any(
- isinstance(field, BaseSerializer) and not field.read_only
- for field in self.fields.values()
- ), (
- 'The `.update()` method does not suport nested writable fields '
- 'by default. Write an explicit `.update()` method for serializer '
- '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
- (self.__class__.__module__, self.__class__.__name__)
- )
+ def update(self, instance, validated_data):
+ raise_errors_on_nested_writes('update', self, validated_data)
- for attr, value in validated_attrs.items():
+ for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
+
return instance
def get_validators(self):
+ # If the validators have been declared explicitly then use that.
+ validators = getattr(getattr(self, 'Meta', None), 'validators', None)
+ if validators is not None:
+ return validators
+
+ # Determine the default set of validators.
+ validators = []
+ model_class = self.Meta.model
field_names = set([
field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source)
])
- validators = getattr(getattr(self, 'Meta', None), 'validators', [])
- model_class = self.Meta.model
-
# Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes.
for parent_class in [model_class] + list(model_class._meta.parents.keys()):
@@ -690,6 +818,18 @@ class ModelSerializer(Serializer):
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
+
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
+
assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
extra_kwargs = self._include_additional_options(extra_kwargs)
@@ -755,7 +895,7 @@ class ModelSerializer(Serializer):
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
for unique_constraint_name in unique_constraint_names:
- # Get the model field that is refered too.
+ # Get the model field that is referred too.
unique_constraint_field = model._meta.get_field(unique_constraint_name)
if getattr(unique_constraint_field, 'auto_now_add', None):
@@ -804,7 +944,7 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field
# matched to the model field.
kwargs.pop('model_field', None)
- if not issubclass(field_cls, CharField):
+ if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
# `allow_blank` is only valid for textual fields.
kwargs.pop('allow_blank', None)
@@ -838,7 +978,7 @@ class ModelSerializer(Serializer):
)
# Check that any fields declared on the class are
- # also explicity included in `Meta.fields`.
+ # also explicitly included in `Meta.fields`.
missing_fields = set(declared_fields.keys()) - set(fields)
if missing_fields:
missing_field = list(missing_fields)[0]
@@ -932,6 +1072,7 @@ class ModelSerializer(Serializer):
class Meta:
model = relation_info.related
depth = nested_depth
+
return NestedSerializer
@@ -958,4 +1099,5 @@ class HyperlinkedModelSerializer(ModelSerializer):
class Meta:
model = relation_info.related
depth = nested_depth
+
return NestedSerializer
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 3abc1fe8..0aac6d43 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -47,7 +47,7 @@ DEFAULTS = {
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
- # Genric view behavior
+ # Generic view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
'DEFAULT_FILTER_BACKENDS': (),
diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js
index bcb1964d..c8812132 100644
--- a/rest_framework/static/rest_framework/js/default.js
+++ b/rest_framework/static/rest_framework/js/default.js
@@ -24,7 +24,8 @@ prettyPrint();
// Bootstrap tooltips.
$('.js-tooltip').tooltip({
- delay: 1000
+ delay: 1000,
+ container: 'body'
});
// Deal with rounded tab styling after tab clicks.
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
index 380b38e9..8a7fca37 100644
--- a/rest_framework/templates/rest_framework/horizontal/select.html
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -4,7 +4,7 @@
{% endif %}
<div class="col-sm-10">
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html
index 53af2772..6b30e4d6 100644
--- a/rest_framework/templates/rest_framework/inline/select.html
+++ b/rest_framework/templates/rest_framework/inline/select.html
@@ -3,7 +3,7 @@
<label class="sr-only">{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index e050cbdc..8e6240a6 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -21,11 +21,11 @@
{% csrf_token %}
<div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
- <div class="controls">
- <label class="span4">Username:</label>
- <input style="height: 25px" type="text" name="username" maxlength="100"
+ <div class="form-group">
+ <label for="id_username">Username:</label>
+ <input type="text" name="username" maxlength="100"
autocapitalize="off"
- autocorrect="off" class="span12 textinput textInput"
+ autocorrect="off" class="form-control textinput textInput"
id="id_username" required
{% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
{% if form.username.errors %}
@@ -36,12 +36,11 @@
</div>
</div>
<div id="div_id_password"
- class="clearfix control-group {% if form.password.errors %}error{% endif %}"
- style="margin-top: 10px">
- <div class="controls">
- <label class="span4">Password:</label>
- <input style="height: 25px" type="password" name="password" maxlength="100"
- autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ <div class="form-group">
+ <label for="id_password">Password:</label>
+ <input type="password" name="password" maxlength="100"
+ autocapitalize="off" autocorrect="off" class="form-control textinput textInput"
id="id_password" required>
{% if form.password.errors %}
<p class="text-error">
@@ -56,8 +55,8 @@
<div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %}
{% endif %}
- <div class="form-actions-no-box" style="margin-top: 20px">
- <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
+ <div class="form-actions-no-box">
+ <input type="submit" name="submit" value="Log in" class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div>
diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html
index 075279f7..b4c9f1a1 100644
--- a/rest_framework/templates/rest_framework/raw_data_form.html
+++ b/rest_framework/templates/rest_framework/raw_data_form.html
@@ -2,10 +2,10 @@
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
- <div class="control-group">
- {{ field.label_tag|add_class:"control-label" }}
- <div class="controls">
- {{ field }}
+ <div class="form-group">
+ {{ field.label_tag|add_class:"col-sm-2 control-label" }}
+ <div class="col-sm-10">
+ {{ field|add_class:"form-control" }}
<span class="help-block">{{ field.help_text }}</span>
</div>
</div>
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
index de72e1dd..1d1109f6 100644
--- a/rest_framework/templates/rest_framework/vertical/select.html
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -3,7 +3,7 @@
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
- {% if field.allow_null %}
+ {% if field.allow_null or field.allow_blank %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index f1825a24..69e03af4 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -3,11 +3,11 @@ from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict
from django.utils import six
-from django.utils.encoding import iri_to_uri
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import iri_to_uri, force_text
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from django.utils.html import smart_urlquote
-from rest_framework.compat import urlparse, force_text
from rest_framework.renderers import HTMLFormRenderer
import re
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 74d2c868..4f4b7c20 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -204,6 +204,11 @@ class APIClient(APIRequestFactory, DjangoClient):
def logout(self):
self._credentials = {}
+
+ # Also clear any `force_authenticate`
+ self.handler._force_user = None
+ self.handler._force_token = None
+
return super(APIClient, self).logout()
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 2c97f1d7..0bd24939 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -4,8 +4,8 @@ Helper classes for parsers.
from __future__ import unicode_literals
from django.db.models.query import QuerySet
from django.utils import six, timezone
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import force_text
import datetime
import decimal
import json
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index 9c187176..fca97b4b 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -59,7 +59,7 @@ def get_field_kwargs(field_name, model_field):
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
- validator_kwarg = model_field.validators
+ validator_kwarg = list(model_field.validators)
# The following will only be used by ModelField classes.
# Gets removed for everything else.
@@ -91,18 +91,18 @@ def get_field_kwargs(field_name, model_field):
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False
- if model_field.flatchoices:
- # If this model field contains choices, then return early.
- # Further keyword arguments are not valid.
- kwargs['choices'] = model_field.flatchoices
- return kwargs
-
if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True
if model_field.blank:
kwargs['allow_blank'] = True
+ if model_field.flatchoices:
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['choices'] = model_field.flatchoices
+ return kwargs
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index 87b3cc6a..de2931c2 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -5,6 +5,7 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
"""
from __future__ import unicode_literals
from django.http.multipartparser import parse_header
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import HTTP_HEADER_ENCODING
@@ -43,6 +44,7 @@ def order_by_precedence(media_type_lst):
return [media_types for media_types in ret if media_types]
+@python_2_unicode_compatible
class _MediaType(object):
def __init__(self, media_type_str):
if media_type_str is None:
@@ -79,9 +81,6 @@ class _MediaType(object):
return 3
def __str__(self):
- return self.__unicode__().encode('utf-8')
-
- def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items():
ret += "; %s=%s" % (key, val)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 2a7c4675..3f17a8b9 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -3,8 +3,8 @@ Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
from django.db import models
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import force_text
import re
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
index 92d19857..277cf649 100644
--- a/rest_framework/utils/serializer_helpers.py
+++ b/rest_framework/utils/serializer_helpers.py
@@ -1,3 +1,4 @@
+import collections
from rest_framework.compat import OrderedDict
@@ -70,7 +71,7 @@ class NestedBoundField(BoundField):
return BoundField(field, value, error, prefix=self.name + '.')
-class BindingDict(object):
+class BindingDict(collections.MutableMapping):
"""
This dict-like object is used to store fields on a serializer.
@@ -92,11 +93,8 @@ class BindingDict(object):
def __delitem__(self, key):
del self.fields[key]
- def items(self):
- return self.fields.items()
-
- def keys(self):
- return self.fields.keys()
+ def __iter__(self):
+ return iter(self.fields)
- def values(self):
- return self.fields.values()
+ def __len__(self):
+ return len(self.fields)
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index 7ca4e6a9..63eb7b22 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -4,7 +4,7 @@ the using Django's `.full_clean()`.
This gives us better separation of concerns, allows us to use single-step
object creation, and makes it possible to switch between using the implicit
-`ModelSerializer` class and an equivelent explicit `Serializer` class.
+`ModelSerializer` class and an equivalent explicit `Serializer` class.
"""
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 292431c8..bc870417 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
+from django.utils.encoding import smart_text
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import smart_text, HttpResponseBase, View
+from rest_framework.compat import HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index 84b4bd8d..88c763da 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -44,10 +44,16 @@ class ViewSetMixin(object):
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
- # The suffix initkwarg is reserved for identifing the viewset type
+ # The suffix initkwarg is reserved for identifying the viewset type
# eg. 'List' or 'Instance'.
cls.suffix = None
+ # actions must not be empty
+ if not actions:
+ raise TypeError("The `actions` argument must be provided when "
+ "calling `.as_view()` on a ViewSet. For example "
+ "`.as_view({'get': 'list'})`")
+
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
@@ -92,12 +98,12 @@ class ViewSetMixin(object):
view.suffix = initkwargs.get('suffix', None)
return csrf_exempt(view)
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Set the `.action` attribute on the view,
depending on the request method.
"""
- request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs)
+ request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
self.action = self.action_map.get(request.method.lower())
return request