aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py18
-rw-r--r--rest_framework/authentication.py6
-rw-r--r--rest_framework/authtoken/models.py8
-rw-r--r--rest_framework/compat.py29
-rw-r--r--rest_framework/fields.py17
-rw-r--r--rest_framework/filters.py1
-rw-r--r--rest_framework/generics.py12
-rw-r--r--rest_framework/mixins.py10
-rw-r--r--rest_framework/parsers.py4
-rw-r--r--rest_framework/permissions.py22
-rw-r--r--rest_framework/renderers.py9
-rw-r--r--rest_framework/request.py11
-rw-r--r--rest_framework/response.py4
-rw-r--r--rest_framework/routers.py14
-rw-r--r--rest_framework/serializers.py52
-rw-r--r--rest_framework/status.py17
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/templatetags/rest_framework.py5
-rw-r--r--rest_framework/tests/models.py1
-rw-r--r--rest_framework/tests/test_authentication.py26
-rw-r--r--rest_framework/tests/test_fields.py92
-rw-r--r--rest_framework/tests/test_filters.py39
-rw-r--r--rest_framework/tests/test_genericrelations.py29
-rw-r--r--rest_framework/tests/test_generics.py60
-rw-r--r--rest_framework/tests/test_renderers.py72
-rw-r--r--rest_framework/tests/test_serializer.py120
-rw-r--r--rest_framework/tests/test_serializer_nested.py35
-rw-r--r--rest_framework/tests/test_status.py33
-rw-r--r--rest_framework/tests/test_templatetags.py19
-rw-r--r--rest_framework/tests/test_validation.py21
-rw-r--r--rest_framework/urlpatterns.py2
-rw-r--r--rest_framework/utils/encoders.py5
-rw-r--r--rest_framework/viewsets.py2
33 files changed, 685 insertions, 112 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 2bd2991b..f5483b9d 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,6 +1,20 @@
-__version__ = '2.3.8'
+"""
+______ _____ _____ _____ __ _
+| ___ \ ___/ ___|_ _| / _| | |
+| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
+| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
+| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
+\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
+"""
-VERSION = __version__ # synonym
+__title__ = 'Django REST framework'
+__version__ = '2.3.10'
+__author__ = 'Tom Christie'
+__license__ = 'BSD 2-Clause'
+__copyright__ = 'Copyright 2011-2013 Tom Christie'
+
+# Version synonym
+VERSION = __version__
# Header encoding (see RFC5987)
HTTP_HEADER_ENCODING = 'iso-8859-1'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index cf001a24..e491ce5f 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -9,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, provider_now
+from rest_framework.compat import oauth2_provider, provider_now, check_nonce
from rest_framework.authtoken.models import Token
@@ -281,7 +281,9 @@ class OAuthAuthentication(BaseAuthentication):
"""
Checks nonce of request, and return True if valid.
"""
- return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce'])
+ oauth_nonce = oauth_request['oauth_nonce']
+ oauth_timestamp = oauth_request['oauth_timestamp']
+ return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp)
class OAuth2Authentication(BaseAuthentication):
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 7601f5b7..024f62bf 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,11 +1,17 @@
import uuid
import hmac
from hashlib import sha1
-from rest_framework.compat import AUTH_USER_MODEL
from django.conf import settings
from django.db import models
+# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
+# Note that we don't perform this code in the compat module due to
+# bug report #1297
+# See: https://github.com/tomchristie/django-rest-framework/issues/1297
+AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+
+
class Token(models.Model):
"""
The default authorization token model.
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 581e29fc..b69749fe 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -7,6 +7,7 @@ versions of django/python, and compatibility wrappers around optional packages.
from __future__ import unicode_literals
import django
+import inspect
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
@@ -69,6 +70,13 @@ try:
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
# Try to import PIL in either of the two ways it can end up installed.
try:
@@ -96,13 +104,6 @@ def get_concrete_model(model_cls):
return model_cls
-# Django 1.5 add support for custom auth user model
-if django.VERSION >= (1, 5):
- AUTH_USER_MODEL = settings.AUTH_USER_MODEL
-else:
- AUTH_USER_MODEL = 'auth.User'
-
-
if django.VERSION >= (1, 5):
from django.views.generic import View
else:
@@ -529,9 +530,23 @@ except ImportError:
try:
import oauth_provider
from oauth_provider.store import store as oauth_provider_store
+
+ # check_nonce's calling signature in django-oauth-plus changes sometime
+ # between versions 2.0 and 2.2.1
+ def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp):
+ check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args
+ if 'timestamp' in check_nonce_args:
+ return oauth_provider_store.check_nonce(
+ request, oauth_request, oauth_nonce, oauth_timestamp
+ )
+ return oauth_provider_store.check_nonce(
+ request, oauth_request, oauth_nonce
+ )
+
except (ImportError, ImproperlyConfigured):
oauth_provider = None
oauth_provider_store = None
+ check_nonce = None
# OAuth 2 support is optional
try:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e23fc001..f1de447c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -428,7 +428,7 @@ class BooleanField(WritableField):
def field_from_native(self, data, files, field_name, into):
# HTML checkboxes do not explicitly represent unchecked as `False`
# we deal with that here...
- if isinstance(data, QueryDict):
+ if isinstance(data, QueryDict) and self.default is None:
self.default = False
return super(BooleanField, self).field_from_native(
@@ -497,6 +497,7 @@ class ChoiceField(WritableField):
}
def __init__(self, choices=(), *args, **kwargs):
+ self.empty = kwargs.pop('empty', '')
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
if not self.required:
@@ -513,6 +514,11 @@ class ChoiceField(WritableField):
choices = property(_get_choices, _set_choices)
+ def metadata(self):
+ data = super(ChoiceField, self).metadata()
+ data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
+ return data
+
def validate(self, value):
"""
Validates that the input is in self.choices.
@@ -537,9 +543,10 @@ class ChoiceField(WritableField):
return False
def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
- return super(ChoiceField, self).from_native(value)
+ value = super(ChoiceField, self).from_native(value)
+ if value == self.empty or value in validators.EMPTY_VALUES:
+ return self.empty
+ return value
class EmailField(CharField):
@@ -964,7 +971,7 @@ class ImageField(FileField):
return None
from rest_framework.compat import Image
- assert Image is not None, 'PIL must be installed for ImageField support'
+ assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.'
# We need to get a file object for PIL. We might have a path or we might
# have to read the data into memory.
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index e287a168..5c6a187c 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -124,6 +124,7 @@ class OrderingFilter(BaseFilterBackend):
def remove_invalid_fields(self, queryset, ordering):
field_names = [field.name for field in queryset.model._meta.fields]
+ field_names += queryset.query.aggregates.keys()
return [term for term in ordering if term.lstrip('-') in field_names]
def filter_queryset(self, request, queryset, view):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7cb80a84..fd411ad3 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -344,6 +344,18 @@ class GenericAPIView(views.APIView):
"""
pass
+ def pre_delete(self, obj):
+ """
+ Placeholder method for calling before deleting an object.
+ """
+ pass
+
+ def post_delete(self, obj):
+ """
+ Placeholder method for calling after saving an object.
+ """
+ pass
+
def metadata(self, request):
"""
Return a dictionary of metadata about the view.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 4606c78b..43950c4b 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
+from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
@@ -127,7 +128,12 @@ class UpdateModelMixin(object):
files=request.FILES, partial=partial)
if serializer.is_valid():
- self.pre_save(serializer.object)
+ try:
+ self.pre_save(serializer.object)
+ except ValidationError as err:
+ # full_clean on model instance may be called in pre_save, so we
+ # have to handle eventual errors.
+ return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
self.object = serializer.save(**save_kwargs)
self.post_save(self.object, created=created)
return Response(serializer.data, status=success_status_code)
@@ -186,5 +192,7 @@ class DestroyModelMixin(object):
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
+ self.pre_delete(obj)
obj.delete()
+ self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 98fc0341..f1b3e38d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -83,7 +83,7 @@ class YAMLParser(BaseParser):
data = stream.read().decode(encoding)
return yaml.safe_load(data)
except (ValueError, yaml.parser.ParserError) as exc:
- raise ParseError('YAML parse error - %s' % six.u(exc))
+ raise ParseError('YAML parse error - %s' % six.text_type(exc))
class FormParser(BaseParser):
@@ -153,7 +153,7 @@ class XMLParser(BaseParser):
try:
tree = etree.parse(stream, parser=parser, forbid_dtd=True)
except (etree.ParseError, ValueError) as exc:
- raise ParseError('XML parse error - %s' % six.u(exc))
+ raise ParseError('XML parse error - %s' % six.text_type(exc))
data = self._xml_convert(tree.getroot())
return data
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index ab6655e7..f24a5123 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -54,9 +54,7 @@ class IsAuthenticated(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_authenticated():
- return True
- return False
+ return request.user and request.user.is_authenticated()
class IsAdminUser(BasePermission):
@@ -65,9 +63,7 @@ class IsAdminUser(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_staff:
- return True
- return False
+ return request.user and request.user.is_staff
class IsAuthenticatedOrReadOnly(BasePermission):
@@ -76,11 +72,9 @@ class IsAuthenticatedOrReadOnly(BasePermission):
"""
def has_permission(self, request, view):
- if (request.method in SAFE_METHODS or
- request.user and
- request.user.is_authenticated()):
- return True
- return False
+ return (request.method in SAFE_METHODS or
+ request.user and
+ request.user.is_authenticated())
class DjangoModelPermissions(BasePermission):
@@ -138,11 +132,9 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls)
- if (request.user and
+ return (request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and
- request.user.has_perms(perms)):
- return True
- return False
+ request.user.has_perms(perms))
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index fe4f43d4..2fdd3337 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -20,6 +20,7 @@ from rest_framework.compat import StringIO
from rest_framework.compat import six
from rest_framework.compat import smart_text
from rest_framework.compat import yaml
+from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
@@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer):
In the absence of the View having an associated form then return None.
"""
if request.method == method:
- data = request.DATA
- files = request.FILES
+ try:
+ data = request.DATA
+ files = request.FILES
+ except ParseError:
+ data = None
+ files = None
else:
data = None
files = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index adf63785..d1f04ec5 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -356,7 +356,16 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
- parsed = parser.parse(stream, media_type, self.parser_context)
+ try:
+ parsed = parser.parse(stream, media_type, self.parser_context)
+ except:
+ # If we get an exception during parsing, fill in empty data and
+ # re-raise. Ensures we don't simply repeat the error when
+ # attempting to render the browsable renderer response, or when
+ # logging the request or similar.
+ self._data = QueryDict('', self._request._encoding)
+ self._files = MultiValueDict()
+ raise
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 5877c8a3..1dc6abcf 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse):
assert charset, 'renderer returned unicode, and did not specify ' \
'a charset value.'
return bytes(ret.encode(charset))
+
+ if not ret:
+ del self['Content-Type']
+
return ret
@property
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 3fee1e49..97b35c10 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -184,18 +184,24 @@ class SimpleRouter(BaseRouter):
bound_methods[method] = action
return bound_methods
- def get_lookup_regex(self, viewset):
+ def get_lookup_regex(self, viewset, lookup_prefix=''):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
+
+ Note that lookup_prefix is not used directly inside REST rest_framework
+ itself, but is required in order to nicely support nested router
+ implementations, such as drf-nested-routers.
+
+ https://github.com/alanjds/drf-nested-routers
"""
if self.trailing_slash:
- base_regex = '(?P<{lookup_field}>[^/]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)'
else:
# Don't consume `.json` style suffixes
- base_regex = '(?P<{lookup_field}>[^/.]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)'
lookup_field = getattr(viewset, 'lookup_field', 'pk')
- return base_regex.format(lookup_field=lookup_field)
+ return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix)
def get_urls(self):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 4210d058..b22ca578 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -6,8 +6,8 @@ form encoded input.
Serialization in REST framework is a two-phase process:
1. Serializers marshal between complex types like model instances, and
-python primatives.
-2. The process of marshalling between python primatives and request and
+python primitives.
+2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
@@ -42,6 +42,7 @@ def pretty_name(name):
class RelationsList(list):
_deleted = []
+
class NestedValidationError(ValidationError):
"""
The default ValidationError behavior is to stringify each item in the list
@@ -56,9 +57,13 @@ class NestedValidationError(ValidationError):
def __init__(self, message):
if isinstance(message, dict):
- self.messages = [message]
+ self._messages = [message]
else:
- self.messages = message
+ self._messages = message
+
+ @property
+ def messages(self):
+ return self._messages
class DictWithMetadata(dict):
@@ -326,7 +331,7 @@ class BaseSerializer(WritableField):
return ret
- def from_native(self, data, files):
+ def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""
@@ -407,11 +412,19 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
- obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
+
+ # If we have a model manager or similar object then we need
+ # to iterate through each instance.
+ if (self.many and
+ not hasattr(obj, '__iter__') and
+ is_simple_callable(getattr(obj, 'all', None))):
+ obj = obj.all()
if self.source == '*':
if value:
- into.update(value)
+ reverted_data = self.restore_fields(value, {})
+ if not self._errors:
+ into.update(reverted_data)
else:
if value in (None, ''):
into[(self.source or field_name)] = None
@@ -606,6 +619,7 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
+ models.NullBooleanField: BooleanField,
models.FileField: FileField,
models.ImageField: ImageField,
}
@@ -699,7 +713,9 @@ class ModelSerializer(Serializer):
is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField)
- if is_m2m and not relation.field.rel.through._meta.auto_created:
+ if (is_m2m and
+ hasattr(relation.field.rel, 'through') and
+ not relation.field.rel.through._meta.auto_created):
has_through_model = True
if nested:
@@ -794,6 +810,8 @@ class ModelSerializer(Serializer):
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
kwargs['choices'] = model_field.flatchoices
+ if model_field.null:
+ kwargs['empty'] = None
return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
@@ -865,18 +883,18 @@ class ModelSerializer(Serializer):
# Reverse fk or one-to-one relations
for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.field.related_query_name()
+ field_name = obj.get_accessor_name()
if field_name in attrs:
related_data[field_name] = attrs.pop(field_name)
# Reverse m2m relations
for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.field.related_query_name()
+ field_name = obj.get_accessor_name()
if field_name in attrs:
m2m_data[field_name] = attrs.pop(field_name)
# Forward m2m relations
- for field in meta.many_to_many:
+ for field in meta.many_to_many + meta.virtual_fields:
if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name)
@@ -889,7 +907,10 @@ class ModelSerializer(Serializer):
# Update an existing instance...
if instance is not None:
for key, val in attrs.items():
- setattr(instance, key, val)
+ try:
+ setattr(instance, key, val)
+ except ValueError:
+ self._errors[key] = self.error_messages['required']
# ...or create a new instance
else:
@@ -933,11 +954,16 @@ class ModelSerializer(Serializer):
del(obj._m2m_data)
if getattr(obj, '_related_data', None):
+ related_fields = dict([
+ (field.get_accessor_name(), field)
+ for field, model
+ in obj._meta.get_all_related_objects_with_model()
+ ])
for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
for related_item in related:
- fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
+ fk_field = related_fields[accessor_name].field.name
setattr(related_item, fk_field, obj)
self.save_object(related_item)
diff --git a/rest_framework/status.py b/rest_framework/status.py
index b9f249f9..76435371 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
from __future__ import unicode_literals
+
+def is_informational(code):
+ return code >= 100 and code <= 199
+
+def is_success(code):
+ return code >= 200 and code <= 299
+
+def is_redirect(code):
+ return code >= 300 and code <= 399
+
+def is_client_error(code):
+ return code >= 400 and code <= 499
+
+def is_server_error(code):
+ return code >= 500 and code <= 599
+
+
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 495163b6..d19d5a2b 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -33,7 +33,7 @@
<div class="navbar-inner">
<div class="container-fluid">
<span href="/">
- {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
+ {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index e9c1cdd5..83c046f9 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import
from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict
+from django.utils.encoding import iri_to_uri
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
@@ -144,7 +145,9 @@ def add_query_param(request, key, val):
"""
Add a query parameter to the current request url, and return the new url.
"""
- return replace_query_param(request.get_full_path(), key, val)
+ iri = request.get_full_path()
+ uri = iri_to_uri(iri)
+ return replace_query_param(uri, key, val)
@register.filter
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 1598ecd9..32a726c0 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel):
class ActionItem(RESTFrameworkModel):
title = models.CharField(max_length=200)
+ started = models.NullBooleanField(default=False)
done = models.BooleanField(default=False)
info = CustomField(default='---', max_length=12)
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
index a44813b6..f072b81b 100644
--- a/rest_framework/tests/test_authentication.py
+++ b/rest_framework/tests/test_authentication.py
@@ -249,7 +249,7 @@ class OAuthTests(TestCase):
def setUp(self):
# these imports are here because oauth is optional and hiding them in try..except block or compat
# could obscure problems if something breaks
- from oauth_provider.models import Consumer, Resource
+ from oauth_provider.models import Consumer, Scope
from oauth_provider.models import Token as OAuthToken
from oauth_provider import consts
@@ -269,8 +269,8 @@ class OAuthTests(TestCase):
self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
name='example', user=self.user, status=self.consts.ACCEPTED)
- self.resource = Resource.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource,
+ self.scope = Scope.objects.create(name="resource name", url="api/")
+ self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope,
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
)
@@ -362,7 +362,8 @@ class OAuthTests(TestCase):
def test_post_form_with_urlencoded_parameters(self):
"""Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth/', params)
+ auth = self._create_authorization_header()
+ response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
@@ -397,10 +398,10 @@ class OAuthTests(TestCase):
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
@unittest.skipUnless(oauth, 'oauth2 not installed')
def test_get_form_with_readonly_resource_passing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
+ """Ensure POSTing with a readonly scope instead of a write scope fails"""
read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
+ read_only_access_token.scope.is_readonly = True
+ read_only_access_token.scope.save()
params = self._create_authorization_url_parameters()
response = self.csrf_client.get('/oauth-with-scope/', params)
self.assertEqual(response.status_code, 200)
@@ -410,8 +411,8 @@ class OAuthTests(TestCase):
def test_post_form_with_readonly_resource_failing_auth(self):
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
+ read_only_access_token.scope.is_readonly = True
+ read_only_access_token.scope.save()
params = self._create_authorization_url_parameters()
response = self.csrf_client.post('/oauth-with-scope/', params)
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
@@ -421,10 +422,11 @@ class OAuthTests(TestCase):
def test_post_form_with_write_resource_passing_auth(self):
"""Ensure POSTing with a write resource succeed"""
read_write_access_token = self.token
- read_write_access_token.resource.is_readonly = False
- read_write_access_token.resource.save()
+ read_write_access_token.scope.is_readonly = False
+ read_write_access_token.scope.save()
params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
+ auth = self._create_authorization_header()
+ response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
index 34fbab9c..5c96bce9 100644
--- a/rest_framework/tests/test_fields.py
+++ b/rest_framework/tests/test_fields.py
@@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer):
model = TimeFieldModel
+SAMPLE_CHOICES = [
+ ('red', 'Red'),
+ ('green', 'Green'),
+ ('blue', 'Blue'),
+]
+
+
+class ChoiceFieldModel(models.Model):
+ choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
+
+
+class ChoiceFieldModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChoiceFieldModel
+
+
+class ChoiceFieldModelWithNull(models.Model):
+ choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
+
+
+class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChoiceFieldModelWithNull
+
+
class BasicFieldTests(TestCase):
def test_auto_now_fields_read_only(self):
"""
@@ -667,34 +692,71 @@ class ChoiceFieldTests(TestCase):
"""
Tests for the ChoiceField options generator
"""
-
- SAMPLE_CHOICES = [
- ('red', 'Red'),
- ('green', 'Green'),
- ('blue', 'Blue'),
- ]
-
def test_choices_required(self):
"""
Make sure proper choices are rendered if field is required
"""
- f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, self.SAMPLE_CHOICES)
+ f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.choices, SAMPLE_CHOICES)
def test_choices_not_required(self):
"""
Make sure proper choices (plus blank) are rendered if the field isn't required
"""
- f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
+ f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
+
+ def test_invalid_choice_model(self):
+ s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
+ self.assertFalse(s.is_valid())
+ self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
+ self.assertEqual(s.data['choice'], '')
+
+ def test_empty_choice_model(self):
+ """
+ Test that the 'empty' value is correctly passed and used depending on
+ the 'null' property on the model field.
+ """
+ s = ChoiceFieldModelSerializer(data={'choice': ''})
+ self.assertTrue(s.is_valid())
+ self.assertEqual(s.data['choice'], '')
+
+ s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
+ self.assertTrue(s.is_valid())
+ self.assertEqual(s.data['choice'], None)
def test_from_native_empty(self):
"""
- Make sure from_native() returns None on empty param.
+ Make sure from_native() returns an empty string on empty param by default.
"""
- f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES)
- result = f.from_native('')
- self.assertEqual(result, None)
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+ self.assertEqual(f.from_native(''), '')
+ self.assertEqual(f.from_native(None), '')
+
+ def test_from_native_empty_override(self):
+ """
+ Make sure you can override from_native() behavior regarding empty values.
+ """
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
+ self.assertEqual(f.from_native(''), None)
+ self.assertEqual(f.from_native(None), None)
+
+ def test_metadata_choices(self):
+ """
+ Make sure proper choices are included in the field's metadata.
+ """
+ choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+ self.assertEqual(f.metadata()['choices'], choices)
+
+ def test_metadata_choices_not_required(self):
+ """
+ Make sure proper choices are included in the field's metadata.
+ """
+ choices = [{'value': v, 'display_name': n}
+ for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
+ f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.metadata()['choices'], choices)
class EmailFieldTests(TestCase):
diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py
index 379db29d..614f45cc 100644
--- a/rest_framework/tests/test_filters.py
+++ b/rest_framework/tests/test_filters.py
@@ -363,6 +363,12 @@ class OrdringFilterModel(models.Model):
text = models.CharField(max_length=100)
+class OrderingFilterRelatedModel(models.Model):
+ related_object = models.ForeignKey(OrdringFilterModel,
+ related_name="relateds")
+
+
+
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -472,3 +478,36 @@ class OrderingFilterTests(TestCase):
{'id': 1, 'title': 'zyx', 'text': 'abc'},
]
)
+
+ def test_ordering_by_aggregate_field(self):
+ # create some related models to aggregate order by
+ num_objs = [2, 5, 3]
+ for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
+ num_objs):
+ for _ in range(num_relateds):
+ new_related = OrderingFilterRelatedModel(
+ related_object=obj
+ )
+ new_related.save()
+
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ queryset = OrdringFilterModel.objects.all().annotate(
+ models.Count("relateds"))
+
+ view = OrderingListView.as_view()
+ request = factory.get('?ordering=relateds__count')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ ]
+ )
+
+
+
diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py
index c38bfb9f..2d341344 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/rest_framework/tests/test_genericrelations.py
@@ -69,6 +69,35 @@ class TestGenericRelations(TestCase):
}
self.assertEqual(serializer.data, expected)
+ def test_generic_nested_relation(self):
+ """
+ Test saving a GenericRelation field via a nested serializer.
+ """
+
+ class TagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ exclude = ('content_type', 'object_id')
+
+ class BookmarkSerializer(serializers.ModelSerializer):
+ tags = TagSerializer()
+
+ class Meta:
+ model = Bookmark
+ exclude = ('id',)
+
+ data = {
+ 'url': 'https://docs.djangoproject.com/',
+ 'tags': [
+ {'tag': 'contenttypes'},
+ {'tag': 'genericrelations'},
+ ]
+ }
+ serializer = BookmarkSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEqual(serializer.object.tags.count(), 2)
+
def test_generic_fk(self):
"""
Test a relationship that spans a GenericForeignKey field.
diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py
index 79cd99ac..996bd5b0 100644
--- a/rest_framework/tests/test_generics.py
+++ b/rest_framework/tests/test_generics.py
@@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
model = BasicModel
+ def get_queryset(self):
+ queryset = super(InstanceView, self).get_queryset()
+ return queryset.exclude(text='filtered out')
+
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field() # read only
@@ -160,10 +164,10 @@ class TestInstanceView(TestCase):
"""
Create 3 BasicModel intances.
"""
- items = ['foo', 'bar', 'baz']
+ items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
BasicModel(text=item).save()
- self.objects = BasicModel.objects
+ self.objects = BasicModel.objects.exclude(text='filtered out')
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
@@ -352,6 +356,17 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1)
self.assertEqual(updated.text, 'foobar')
+ def test_put_to_filtered_out_instance(self):
+ """
+ PUT requests to an URL of instance which is filtered out should not be
+ able to create new objects.
+ """
+ data = {'text': 'foo'}
+ filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
+ request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
+ response = self.view(request, pk=filtered_out_pk).render()
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
def test_put_as_create_on_id_based_url(self):
"""
PUT requests to RetrieveUpdateDestroyAPIView should create an object
@@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object):
return queryset.filter(text='other')
+class TwoFieldModel(models.Model):
+ field_a = models.CharField(max_length=100)
+ field_b = models.CharField(max_length=100)
+
+
+class DynamicSerializerView(generics.ListCreateAPIView):
+ model = TwoFieldModel
+ renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
+
+ def get_serializer_class(self):
+ if self.request.method == 'POST':
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ fields = ('field_b',)
+ return DynamicSerializer
+ return super(DynamicSerializerView, self).get_serializer_class()
+
+
class TestFilterBackendAppliedToViews(TestCase):
def setUp(self):
@@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
-
-class TwoFieldModel(models.Model):
- field_a = models.CharField(max_length=100)
- field_b = models.CharField(max_length=100)
-
-
-class DynamicSerializerView(generics.ListCreateAPIView):
- model = TwoFieldModel
- renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
-
- def get_serializer_class(self):
- if self.request.method == 'POST':
- class DynamicSerializer(serializers.ModelSerializer):
- class Meta:
- model = TwoFieldModel
- fields = ('field_b',)
- return DynamicSerializer
- return super(DynamicSerializerView, self).get_serializer_class()
-
-
-class TestFilterBackendAppliedToViews(TestCase):
-
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py
index 76299a89..2ae8ae18 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/rest_framework/tests/test_renderers.py
@@ -15,7 +15,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
+from collections import MutableMapping
import datetime
+import json
import pickle
import re
@@ -64,11 +66,23 @@ class MockView(APIView):
class MockGETView(APIView):
-
def get(self, request, **kwargs):
return Response({'foo': ['bar', 'baz']})
+
+class MockPOSTView(APIView):
+ def post(self, request, **kwargs):
+ return Response({'foo': request.DATA})
+
+
+class EmptyGETView(APIView):
+ renderer_classes = (JSONRenderer,)
+
+ def get(self, request, **kwargs):
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -88,8 +102,10 @@ urlpatterns = patterns('',
url(r'^cache$', MockGETView.as_view()),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
+ url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
+ url(r'^empty$', EmptyGETView.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -219,6 +235,22 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
+ def test_parse_error_renderers_browsable_api(self):
+ """Invalid data should still render the browsable API correctly."""
+ resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
+ self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_204_no_content_responses_have_no_content_type_set(self):
+ """
+ Regression test for #1196
+
+ https://github.com/tomchristie/django-rest-framework/issues/1196
+ """
+ resp = self.client.get('/empty')
+ self.assertEqual(resp.get('Content-Type', None), None)
+ self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
+
_flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -244,6 +276,44 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
+ def test_render_dict_abc_obj(self):
+ class Dict(MutableMapping):
+ def __init__(self):
+ self._dict = dict()
+ def __getitem__(self, key):
+ return self._dict.__getitem__(key)
+ def __setitem__(self, key, value):
+ return self._dict.__setitem__(key, value)
+ def __delitem__(self, key):
+ return self._dict.__delitem__(key)
+ def __iter__(self):
+ return self._dict.__iter__()
+ def __len__(self):
+ return self._dict.__len__()
+ def keys(self):
+ return self._dict.keys()
+
+ x = Dict()
+ x['key'] = 'string value'
+ x[2] = 3
+ ret = JSONRenderer().render(x)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, {'key': 'string value', '2': 3})
+
+ def test_render_obj_with_getitem(self):
+ class DictLike(object):
+ def __init__(self):
+ self._dict = {}
+ def set(self, value):
+ self._dict = dict(value)
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ x = DictLike()
+ x.set({'a': 1, 'b': 'string'})
+ with self.assertRaises(TypeError):
+ JSONRenderer().render(x)
+
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
index 1f85a474..75d6e785 100644
--- a/rest_framework/tests/test_serializer.py
+++ b/rest_framework/tests/test_serializer.py
@@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
model = Person
+class NestedSerializerWithRenamedField(serializers.Serializer):
+ renamed_info = serializers.Field(source='info')
+
+
+class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer):
+ nested = NestedSerializerWithRenamedField(source='*')
+
+ class Meta:
+ model = Person
+
+
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
"""
Testing for #652.
@@ -456,6 +467,20 @@ class ValidationTests(TestCase):
)
self.assertEqual(serializer.is_valid(), True)
+ def test_writable_star_source_with_inner_source_fields(self):
+ """
+ Tests that a serializer with source="*" correctly expands the
+ it's fields into the outer serializer even if they have their
+ own 'source' parameters.
+ """
+
+ serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={
+ 'name': 'marko',
+ 'nested': {'renamed_info': 'hi'}},
+ )
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.errors, {})
+
class CustomValidationTests(TestCase):
class CommentSerializerWithFieldValidator(CommentSerializer):
@@ -558,6 +583,29 @@ class ModelValidationTests(TestCase):
self.assertFalse(second_serializer.is_valid())
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
+ def test_foreign_key_is_null_with_partial(self):
+ """
+ Test ModelSerializer validation with partial=True
+
+ Specifically test that a null foreign key does not pass validation
+ """
+ album = Album(title='test')
+ album.save()
+
+ class PhotoSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Photo
+
+ photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
+ self.assertTrue(photo_serializer.is_valid())
+ photo = photo_serializer.save()
+
+ # Updating only the album (foreign key)
+ photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
+ self.assertFalse(photo_serializer.is_valid())
+ self.assertTrue('album' in photo_serializer.errors)
+ self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required'])
+
def test_foreign_key_with_partial(self):
"""
Test ModelSerializer validation with partial=True
@@ -1720,3 +1768,75 @@ class TestSerializerTransformMethods(TestCase):
'b_renamed': None,
}
)
+
+
+class DefaultTrueBooleanModel(models.Model):
+ cat = models.BooleanField(default=True)
+ dog = models.BooleanField(default=False)
+
+
+class SerializerDefaultTrueBoolean(TestCase):
+
+ def setUp(self):
+ super(SerializerDefaultTrueBoolean, self).setUp()
+
+ class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DefaultTrueBooleanModel
+ fields = ('cat', 'dog')
+
+ self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
+
+ def test_enabled_as_false(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False,
+ 'dog': False})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
+
+ def test_enabled_as_true(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': True,
+ 'dog': True})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], True)
+ self.assertEqual(serializer.data['dog'], True)
+
+ def test_enabled_partial(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False},
+ partial=True)
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
+
+
+class BoolenFieldTypeTest(TestCase):
+ '''
+ Ensure the various Boolean based model fields are rendered as the proper
+ field type
+
+ '''
+
+ def setUp(self):
+ '''
+ Setup an ActionItemSerializer for BooleanTesting
+ '''
+ data = {
+ 'title': 'b' * 201,
+ }
+ self.serializer = ActionItemSerializer(data=data)
+
+ def test_booleanfield_type(self):
+ '''
+ Test that BooleanField is infered from models.BooleanField
+ '''
+ bfield = self.serializer.get_fields()['done']
+ self.assertEqual(type(bfield), fields.BooleanField)
+
+ def test_nullbooleanfield_type(self):
+ '''
+ Test that BooleanField is infered from models.NullBooleanField
+
+ https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8
+ '''
+ bfield = self.serializer.get_fields()['started']
+ self.assertEqual(type(bfield), fields.BooleanField)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
index 029f8bff..7114a060 100644
--- a/rest_framework/tests/test_serializer_nested.py
+++ b/rest_framework/tests/test_serializer_nested.py
@@ -6,6 +6,7 @@ Doesn't cover model serializers.
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import serializers
+from . import models
class WritableNestedSerializerBasicTests(TestCase):
@@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
serializer = self.AlbumSerializer(instance=original, data=data)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.object, expected)
+
+
+class NestedModelSerializerUpdateTests(TestCase):
+ def test_second_nested_level(self):
+ john = models.Person.objects.create(name="john")
+
+ post = john.blogpost_set.create(title="Test blog post")
+ post.blogpostcomment_set.create(text="I hate this blog post")
+ post.blogpostcomment_set.create(text="I love this blog post")
+
+ class BlogPostCommentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.BlogPostComment
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
+ class Meta:
+ model = models.BlogPost
+ fields = ('id', 'title', 'comments')
+
+ class PersonSerializer(serializers.ModelSerializer):
+ posts = BlogPostSerializer(many=True, source='blogpost_set')
+ class Meta:
+ model = models.Person
+ fields = ('id', 'name', 'age', 'posts')
+
+ serialize = PersonSerializer(instance=john)
+ deserialize = PersonSerializer(data=serialize.data, instance=john)
+ self.assertTrue(deserialize.is_valid())
+
+ result = deserialize.object
+ result.save()
+ self.assertEqual(result.id, john.id)
+
diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py
new file mode 100644
index 00000000..7b1bdae3
--- /dev/null
+++ b/rest_framework/tests/test_status.py
@@ -0,0 +1,33 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.status import (
+ is_informational, is_success, is_redirect, is_client_error, is_server_error
+)
+
+
+class TestStatus(TestCase):
+ def test_status_categories(self):
+ self.assertFalse(is_informational(99))
+ self.assertTrue(is_informational(100))
+ self.assertTrue(is_informational(199))
+ self.assertFalse(is_informational(200))
+
+ self.assertFalse(is_success(199))
+ self.assertTrue(is_success(200))
+ self.assertTrue(is_success(299))
+ self.assertFalse(is_success(300))
+
+ self.assertFalse(is_redirect(299))
+ self.assertTrue(is_redirect(300))
+ self.assertTrue(is_redirect(399))
+ self.assertFalse(is_redirect(400))
+
+ self.assertFalse(is_client_error(399))
+ self.assertTrue(is_client_error(400))
+ self.assertTrue(is_client_error(499))
+ self.assertFalse(is_client_error(500))
+
+ self.assertFalse(is_server_error(499))
+ self.assertTrue(is_server_error(500))
+ self.assertTrue(is_server_error(599))
+ self.assertFalse(is_server_error(600)) \ No newline at end of file
diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py
new file mode 100644
index 00000000..609a9e08
--- /dev/null
+++ b/rest_framework/tests/test_templatetags.py
@@ -0,0 +1,19 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.test import APIRequestFactory
+from rest_framework.templatetags.rest_framework import add_query_param
+
+factory = APIRequestFactory()
+
+
+class TemplateTagTests(TestCase):
+
+ def test_add_query_param_with_non_latin_charactor(self):
+ # Ensure we don't double-escape non-latin characters
+ # that are present in the querystring.
+ # See #1314.
+ request = factory.get("/", {'q': '查询'})
+ json_url = add_query_param(request, "format", "json")
+ self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
+ self.assertIn("format=json", json_url)
diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py
index ebfdff9c..124c874d 100644
--- a/rest_framework/tests/test_validation.py
+++ b/rest_framework/tests/test_validation.py
@@ -47,12 +47,18 @@ class ShouldValidateModel(models.Model):
class ShouldValidateModelSerializer(serializers.ModelSerializer):
renamed = serializers.CharField(source='should_validate_field', required=False)
+ def validate_renamed(self, attrs, source):
+ value = attrs[source]
+ if len(value) < 3:
+ raise serializers.ValidationError('Minimum 3 characters.')
+ return attrs
+
class Meta:
model = ShouldValidateModel
fields = ('renamed',)
-class TestPreSaveValidationExclusions(TestCase):
+class TestPreSaveValidationExclusionsSerializer(TestCase):
def test_renamed_fields_are_model_validated(self):
"""
Ensure fields with 'source' applied do get still get model validation.
@@ -61,6 +67,19 @@ class TestPreSaveValidationExclusions(TestCase):
# does not have `blank=True`, so this serializer should not validate.
serializer = ShouldValidateModelSerializer(data={'renamed': ''})
self.assertEqual(serializer.is_valid(), False)
+ self.assertIn('renamed', serializer.errors)
+ self.assertNotIn('should_validate_field', serializer.errors)
+
+
+class TestCustomValidationMethods(TestCase):
+ def test_custom_validation_method_is_executed(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'})
+ self.assertFalse(serializer.is_valid())
+ self.assertIn('renamed', serializer.errors)
+
+ def test_custom_validation_method_passing(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'})
+ self.assertTrue(serializer.is_valid())
class ValidationSerializer(serializers.Serializer):
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index d9143bb4..0ff137b0 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
allowed_pattern = '(%s)' % '|'.join(allowed)
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
else:
- suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
+ suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 35ad206b..3ac920c6 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -44,6 +44,11 @@ class JSONEncoder(json.JSONEncoder):
return str(o)
elif hasattr(o, 'tolist'):
return o.tolist()
+ elif hasattr(o, '__getitem__'):
+ try:
+ return dict(o)
+ except:
+ pass
elif hasattr(o, '__iter__'):
return [i for i in o]
return super(JSONEncoder, self).default(o)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index d91323f2..7eb29f99 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views.
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Typically, rather than instantiate views from viewsets directly, you'll
-regsiter the viewset with a router and let the URL conf be determined
+register the viewset with a router and let the URL conf be determined
automatically.
router = DefaultRouter()