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). 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. 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` ### `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. 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**: **Filtering**:

View File

@ -443,7 +443,9 @@ class Field(object):
try: try:
return get_attribute(instance, self.source_attrs) return get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc: 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() raise SkipField()
msg = ( msg = (
'Got {exc_type} when attempting to get a value for field ' '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.conf.urls import url
from django.core.exceptions import ImproperlyConfigured 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.compat import NoReverseMatch
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse 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.settings import api_settings
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
@ -276,6 +275,36 @@ class SimpleRouter(BaseRouter):
return ret 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): class DefaultRouter(SimpleRouter):
""" """
The default router extends the SimpleRouter, but also adds in a default The default router extends the SimpleRouter, but also adds in a default
@ -284,7 +313,9 @@ class DefaultRouter(SimpleRouter):
include_root_view = True include_root_view = True
include_format_suffixes = True include_format_suffixes = True
root_view_name = 'api-root' root_view_name = 'api-root'
default_schema_renderers = [renderers.CoreJSONRenderer, BrowsableAPIRenderer] default_schema_renderers = None
APIRootView = APIRootView
APISchemaView = SchemaView
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'schema_title' in kwargs: if 'schema_title' in kwargs:
@ -300,6 +331,14 @@ class DefaultRouter(SimpleRouter):
self.schema_title = kwargs.pop('schema_title', None) self.schema_title = kwargs.pop('schema_title', None)
self.schema_url = kwargs.pop('schema_url', None) self.schema_url = kwargs.pop('schema_url', None)
self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers) 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: if 'root_renderers' in kwargs:
self.root_renderers = kwargs.pop('root_renderers') self.root_renderers = kwargs.pop('root_renderers')
else: else:
@ -310,25 +349,16 @@ class DefaultRouter(SimpleRouter):
""" """
Return a schema root view. Return a schema root view.
""" """
schema_renderers = self.schema_renderers
schema_generator = SchemaGenerator( schema_generator = SchemaGenerator(
title=self.schema_title, title=self.schema_title,
url=self.schema_url, url=self.schema_url,
patterns=api_urls patterns=api_urls
) )
class APISchemaView(views.APIView): return self.APISchemaView.as_view(
_ignore_model_permissions = True renderer_classes=self.schema_renderers,
exclude_from_schema = True schema_generator=schema_generator,
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()
def get_api_root_view(self, api_urls=None): def get_api_root_view(self, api_urls=None):
""" """
@ -339,32 +369,7 @@ class DefaultRouter(SimpleRouter):
for prefix, viewset, basename in self.registry: for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename) api_root_dict[prefix] = list_name.format(basename=basename)
class APIRootView(views.APIView): return self.APIRootView.as_view(api_root_dict=api_root_dict)
_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()
def get_urls(self): def get_urls(self):
""" """

View File

@ -665,28 +665,38 @@ class SchemaGenerator(object):
return named_path_components + [action] 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): def get_schema_view(title=None, url=None, description=None, urlconf=None, renderer_classes=None, public=False):
""" """
Return a schema view. Return a schema view.
""" """
generator = SchemaGenerator(title=title, url=url, description=description, urlconf=urlconf) generator = SchemaGenerator(title=title, url=url, description=description, urlconf=urlconf)
if renderer_classes is None: return SchemaView.as_view(
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES: renderer_classes=renderer_classes,
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer] schema_generator=generator,
else: public=public,
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()

View File

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