diff options
Diffstat (limited to 'rest_framework')
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 | 
