diff options
| author | Tom Christie | 2014-12-12 15:37:43 +0000 |
|---|---|---|
| committer | Tom Christie | 2014-12-12 15:37:43 +0000 |
| commit | baaa356489dd51d7c68161db40e99cd59b1124c3 (patch) | |
| tree | 23dc5c4cbe1065580ff88ddd1bfa6dcda956ac68 /rest_framework | |
| parent | 5e6052811716a494e995a84c497579867ee6acaa (diff) | |
| parent | fd473aa905337908b41c9a1087967a19f0558f89 (diff) | |
| download | django-rest-framework-baaa356489dd51d7c68161db40e99cd59b1124c3.tar.bz2 | |
Merge master
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 |
