Merge branch 'master' into version-3.6

This commit is contained in:
Tom Christie 2017-03-09 13:47:27 +00:00
commit 903f7fd5b5
6 changed files with 116 additions and 89 deletions

View File

@ -55,6 +55,8 @@ The `default` is not applied during partial update operations. In the partial up
May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context).
When serializing the instance, default will be used if the the object attribute or dictionary key is not present in the instance.
Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error.
### `source`

View File

@ -69,7 +69,7 @@ The following attributes control the basic view behavior.
The following attributes are used to control pagination when used with list views.
* `pagination_class` - The pagination class that should be used when paginating list results. Defaults to the same value as the `DEFAULT_PAGINATION_CLASS` setting, which is `'rest_framework.pagination.PageNumberPagination'`.
* `pagination_class` - The pagination class that should be used when paginating list results. Defaults to the same value as the `DEFAULT_PAGINATION_CLASS` setting, which is `'rest_framework.pagination.PageNumberPagination'`. Setting `pagination_class=None` will disable pagination on this view.
**Filtering**:

View File

@ -443,7 +443,9 @@ class Field(object):
try:
return get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc:
if not self.required and self.default is empty:
if self.default is not empty:
return self.get_default()
if not self.required:
raise SkipField()
msg = (
'Got {exc_type} when attempting to get a value for field '

View File

@ -22,12 +22,11 @@ from collections import OrderedDict, namedtuple
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from rest_framework import exceptions, renderers, views
from rest_framework import views
from rest_framework.compat import NoReverseMatch
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.schemas import SchemaGenerator
from rest_framework.schemas import SchemaGenerator, SchemaView
from rest_framework.settings import api_settings
from rest_framework.urlpatterns import format_suffix_patterns
@ -276,6 +275,36 @@ class SimpleRouter(BaseRouter):
return ret
class APIRootView(views.APIView):
"""
The default basic root view for DefaultRouter
"""
_ignore_model_permissions = True
exclude_from_schema = True
api_root_dict = None
def get(self, request, *args, **kwargs):
# Return a plain {"name": "hyperlink"} response.
ret = OrderedDict()
namespace = request.resolver_match.namespace
for key, url_name in self.api_root_dict.items():
if namespace:
url_name = namespace + ':' + url_name
try:
ret[key] = reverse(
url_name,
args=args,
kwargs=kwargs,
request=request,
format=kwargs.get('format', None)
)
except NoReverseMatch:
# Don't bail out if eg. no list routes exist, only detail routes.
continue
return Response(ret)
class DefaultRouter(SimpleRouter):
"""
The default router extends the SimpleRouter, but also adds in a default
@ -284,7 +313,9 @@ class DefaultRouter(SimpleRouter):
include_root_view = True
include_format_suffixes = True
root_view_name = 'api-root'
default_schema_renderers = [renderers.CoreJSONRenderer, BrowsableAPIRenderer]
default_schema_renderers = None
APIRootView = APIRootView
APISchemaView = SchemaView
def __init__(self, *args, **kwargs):
if 'schema_title' in kwargs:
@ -300,6 +331,14 @@ class DefaultRouter(SimpleRouter):
self.schema_title = kwargs.pop('schema_title', None)
self.schema_url = kwargs.pop('schema_url', None)
self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers)
if self.default_schema_renderers:
warnings.warn(
"The 'DefaultRouter.default_schema_renderers' is pending "
"deprecation. You should override "
"'DefaultRouter.APISchemaView' instead.",
PendingDeprecationWarning
)
if 'root_renderers' in kwargs:
self.root_renderers = kwargs.pop('root_renderers')
else:
@ -310,25 +349,16 @@ class DefaultRouter(SimpleRouter):
"""
Return a schema root view.
"""
schema_renderers = self.schema_renderers
schema_generator = SchemaGenerator(
title=self.schema_title,
url=self.schema_url,
patterns=api_urls
)
class APISchemaView(views.APIView):
_ignore_model_permissions = True
exclude_from_schema = True
renderer_classes = schema_renderers
def get(self, request, *args, **kwargs):
schema = schema_generator.get_schema(request)
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)
return APISchemaView.as_view()
return self.APISchemaView.as_view(
renderer_classes=self.schema_renderers,
schema_generator=schema_generator,
)
def get_api_root_view(self, api_urls=None):
"""
@ -339,32 +369,7 @@ class DefaultRouter(SimpleRouter):
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
class APIRootView(views.APIView):
_ignore_model_permissions = True
exclude_from_schema = True
def get(self, request, *args, **kwargs):
# Return a plain {"name": "hyperlink"} response.
ret = OrderedDict()
namespace = request.resolver_match.namespace
for key, url_name in api_root_dict.items():
if namespace:
url_name = namespace + ':' + url_name
try:
ret[key] = reverse(
url_name,
args=args,
kwargs=kwargs,
request=request,
format=kwargs.get('format', None)
)
except NoReverseMatch:
# Don't bail out if eg. no list routes exist, only detail routes.
continue
return Response(ret)
return APIRootView.as_view()
return self.APIRootView.as_view(api_root_dict=api_root_dict)
def get_urls(self):
"""

View File

@ -665,28 +665,38 @@ class SchemaGenerator(object):
return named_path_components + [action]
class SchemaView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
renderer_classes = None
schema_generator = None
public = False
def __init__(self, *args, **kwargs):
super(SchemaView, self).__init__(*args, **kwargs)
if self.renderer_classes is None:
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
self.renderer_classes = [
renderers.CoreJSONRenderer,
renderers.BrowsableAPIRenderer,
]
else:
self.renderer_classes = [renderers.CoreJSONRenderer]
def get(self, request, *args, **kwargs):
schema = self.schema_generator.get_schema(request, self.public)
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)
def get_schema_view(title=None, url=None, description=None, urlconf=None, renderer_classes=None, public=False):
"""
Return a schema view.
"""
generator = SchemaGenerator(title=title, url=url, description=description, urlconf=urlconf)
if renderer_classes is None:
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer]
else:
rclasses = [renderers.CoreJSONRenderer]
else:
rclasses = renderer_classes
class SchemaView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
renderer_classes = rclasses
def get(self, request, *args, **kwargs):
schema = generator.get_schema(request, public)
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)
return SchemaView.as_view()
return SchemaView.as_view(
renderer_classes=renderer_classes,
schema_generator=generator,
public=public,
)

View File

@ -372,36 +372,44 @@ class TestNotRequiredOutput:
serializer.save()
assert serializer.data == {'included': 'abc'}
def test_default_required_output_for_dict(self):
"""
'default="something"' should require dictionary key.
We need to handle this as the field will have an implicit
'required=False', but it should still have a value.
"""
class TestDefaultOutput:
def setup(self):
class ExampleSerializer(serializers.Serializer):
omitted = serializers.CharField(default='abc')
included = serializers.CharField()
has_default = serializers.CharField(default='x')
has_default_callable = serializers.CharField(default=lambda: 'y')
no_default = serializers.CharField()
self.Serializer = ExampleSerializer
serializer = ExampleSerializer({'included': 'abc'})
with pytest.raises(KeyError):
serializer.data
def test_default_required_output_for_object(self):
def test_default_used_for_dict(self):
"""
'default="something"' should require object attribute.
We need to handle this as the field will have an implicit
'required=False', but it should still have a value.
'default="something"' should be used if dictionary key is missing from input.
"""
class ExampleSerializer(serializers.Serializer):
omitted = serializers.CharField(default='abc')
included = serializers.CharField()
serializer = self.Serializer({'no_default': 'abc'})
assert serializer.data == {'has_default': 'x', 'has_default_callable': 'y', 'no_default': 'abc'}
instance = MockObject(included='abc')
serializer = ExampleSerializer(instance)
with pytest.raises(AttributeError):
serializer.data
def test_default_used_for_object(self):
"""
'default="something"' should be used if object attribute is missing from input.
"""
instance = MockObject(no_default='abc')
serializer = self.Serializer(instance)
assert serializer.data == {'has_default': 'x', 'has_default_callable': 'y', 'no_default': 'abc'}
def test_default_not_used_when_in_dict(self):
"""
'default="something"' should not be used if dictionary key is present in input.
"""
serializer = self.Serializer({'has_default': 'def', 'has_default_callable': 'ghi', 'no_default': 'abc'})
assert serializer.data == {'has_default': 'def', 'has_default_callable': 'ghi', 'no_default': 'abc'}
def test_default_not_used_when_in_object(self):
"""
'default="something"' should not be used if object attribute is present in input.
"""
instance = MockObject(has_default='def', has_default_callable='ghi', no_default='abc')
serializer = self.Serializer(instance)
assert serializer.data == {'has_default': 'def', 'has_default_callable': 'ghi', 'no_default': 'abc'}
class TestCacheSerializerData: