Merge branch 'master' into localizedfloatfield

This commit is contained in:
kgeorgy 2017-05-02 15:25:23 +02:00 committed by GitHub
commit 97051799e7
24 changed files with 386 additions and 36 deletions

View File

@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.7, 3.2, 3.3, 3.4, 3.5) * Python (2.7, 3.2, 3.3, 3.4, 3.5, 3.6)
* Django (1.8, 1.9, 1.10, 1.11) * Django (1.8, 1.9, 1.10, 1.11)
# Installation # Installation

View File

@ -117,7 +117,7 @@ The simplest way to include a schema in your project is to use the
Once the view has been added, you'll be able to make API requests to retrieve Once the view has been added, you'll be able to make API requests to retrieve
the auto-generated schema definition. the auto-generated schema definition.
$ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json $ http http://127.0.0.1:8000/ Accept:application/coreapi+json
HTTP/1.0 200 OK HTTP/1.0 200 OK
Allow: GET, HEAD, OPTIONS Allow: GET, HEAD, OPTIONS
Content-Type: application/vnd.coreapi+json Content-Type: application/vnd.coreapi+json
@ -170,6 +170,22 @@ May be used to pass the set of renderer classes that can be used to render the A
renderer_classes=[CoreJSONRenderer, APIBlueprintRenderer] renderer_classes=[CoreJSONRenderer, APIBlueprintRenderer]
) )
#### `patterns`
List of url patterns to limit the schema introspection to. If you only want the `myproject.api` urls
to be exposed in the schema:
schema_url_patterns = [
url(r'^api/', include('myproject.api.urls')),
]
schema_view = get_schema_view(
title='Server Monitoring API',
url='https://www.example.org/api/',
patterns=schema_url_patterns,
)
## Using an explicit schema view ## Using an explicit schema view
If you need a little more control than the `get_schema_view()` shortcut gives you, If you need a little more control than the `get_schema_view()` shortcut gives you,

View File

@ -162,7 +162,7 @@ The `credentials` method is appropriate for testing APIs that require authentica
#### .force_authenticate(user=None, token=None) #### .force_authenticate(user=None, token=None)
Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated. Sometimes you may want to bypass authentication entirely and force all requests by the test client to be automatically treated as authenticated.
This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests. This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Python (2.7, 3.2, 3.3, 3.4, 3.5) * Python (2.7, 3.2, 3.3, 3.4, 3.5, 3.6)
* Django (1.8, 1.9, 1.10, 1.11) * Django (1.8, 1.9, 1.10, 1.11)
The following packages are optional: The following packages are optional:

View File

@ -146,8 +146,6 @@ An alternative, but more complex option would be to replace the input with an au
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation. There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation.
Better support for autocomplete inputs is planned in future versions.
--- ---
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead [cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead

View File

@ -2,6 +2,17 @@
There are a wide range of resources available for learning and using Django REST framework. We try to keep a comprehensive list available here. There are a wide range of resources available for learning and using Django REST framework. We try to keep a comprehensive list available here.
## Books
<div class="book-covers">
<a class="book-cover" href="https://hellowebapp.com/order/">
<img src="../../img/books/hwa-cover.png"/>
</a>
<a class="book-cover" href="https://www.twoscoopspress.com/products/two-scoops-of-django-1-11">
<img src="../../img/books/tsd-cover.png"/>
</a>
</div>
## Tutorials ## Tutorials
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework] * [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
@ -56,10 +67,6 @@ There are a wide range of resources available for learning and using Django REST
* [New Django Admin with DRF and EmberJS... What are the News?][new-django-admin-with-drf-and-emberjs] * [New Django Admin with DRF and EmberJS... What are the News?][new-django-admin-with-drf-and-emberjs]
* [Blog posts about Django REST Framework][medium-django-rest-framework] * [Blog posts about Django REST Framework][medium-django-rest-framework]
## Books
* [Hello Web App: Intermediate Concepts, Chapter 10][hello-web-app-intermediate]
### Documentations ### Documentations
* [Classy Django REST Framework][cdrf.co] * [Classy Django REST Framework][cdrf.co]
* [DRF-schema-adapter][drf-schema] * [DRF-schema-adapter][drf-schema]
@ -95,7 +102,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[drf-schema]: http://drf-schema-adapter.readthedocs.io/en/latest/ [drf-schema]: http://drf-schema-adapter.readthedocs.io/en/latest/
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/ [creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/ [creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
[hello-web-app-intermediate]: https://hellowebapp.com/order/
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ [django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk [full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
[drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww [drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww

View File

@ -310,7 +310,7 @@ Quit out of the shell...
Validating models... Validating models...
0 errors found 0 errors found
Django version 1.8.3, using settings 'tutorial.settings' Django version 1.11, using settings 'tutorial.settings'
Development server is running at http://127.0.0.1:8000/ Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C. Quit the server with CONTROL-C.

View File

@ -36,6 +36,7 @@ API schema.
We can now include a schema for our API, by including an autogenerated schema We can now include a schema for our API, by including an autogenerated schema
view in our URL configuration. view in our URL configuration.
```
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
schema_view = get_schema_view(title='Pastebin API') schema_view = get_schema_view(title='Pastebin API')
@ -44,6 +45,7 @@ view in our URL configuration.
   url(r'^schema/$', schema_view),    url(r'^schema/$', schema_view),
... ...
] ]
```
If you visit the API root endpoint in a browser you should now see `corejson` If you visit the API root endpoint in a browser you should now see `corejson`
representation become available as an option. representation become available as an option.

View File

@ -417,3 +417,8 @@ ul.sponsor {
.toclink { .toclink {
color: #333; color: #333;
} }
.book-cover img {
margin: 0 !important;
display: inline-block !important;
}

View File

@ -275,6 +275,14 @@ except ImportError:
def pygments_css(style): def pygments_css(style):
return None return None
try:
import pytz
from pytz.exceptions import InvalidTimeError
except ImportError:
InvalidTimeError = Exception
# `separators` 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 # See: http://bugs.python.org/issue22767
if six.PY3: if six.PY3:
@ -339,6 +347,7 @@ def set_many(instance, field, value):
field = getattr(instance, field) field = getattr(instance, field)
field.set(value) field.set(value)
def include(module, namespace=None, app_name=None): def include(module, namespace=None, app_name=None):
from django.conf.urls import include from django.conf.urls import include
if django.VERSION < (1,9): if django.VERSION < (1,9):

View File

@ -6,7 +6,9 @@ from rest_framework.renderers import (
from rest_framework.schemas import SchemaGenerator, get_schema_view from rest_framework.schemas import SchemaGenerator, get_schema_view
def get_docs_view(title=None, description=None, schema_url=None, public=True, generator_class=SchemaGenerator): def get_docs_view(
title=None, description=None, schema_url=None, public=True,
patterns=None, generator_class=SchemaGenerator):
renderer_classes = [DocumentationRenderer, CoreJSONRenderer] renderer_classes = [DocumentationRenderer, CoreJSONRenderer]
return get_schema_view( return get_schema_view(
@ -15,11 +17,14 @@ def get_docs_view(title=None, description=None, schema_url=None, public=True, ge
description=description, description=description,
renderer_classes=renderer_classes, renderer_classes=renderer_classes,
public=public, public=public,
patterns=patterns,
generator_class=generator_class, generator_class=generator_class,
) )
def get_schemajs_view(title=None, description=None, schema_url=None, public=True, generator_class=SchemaGenerator): def get_schemajs_view(
title=None, description=None, schema_url=None, public=True,
patterns=None, generator_class=SchemaGenerator):
renderer_classes = [SchemaJSRenderer] renderer_classes = [SchemaJSRenderer]
return get_schema_view( return get_schema_view(
@ -28,16 +33,20 @@ def get_schemajs_view(title=None, description=None, schema_url=None, public=True
description=description, description=description,
renderer_classes=renderer_classes, renderer_classes=renderer_classes,
public=public, public=public,
patterns=patterns,
generator_class=generator_class, generator_class=generator_class,
) )
def include_docs_urls(title=None, description=None, schema_url=None, public=True, generator_class=SchemaGenerator): def include_docs_urls(
title=None, description=None, schema_url=None, public=True,
patterns=None, generator_class=SchemaGenerator):
docs_view = get_docs_view( docs_view = get_docs_view(
title=title, title=title,
description=description, description=description,
schema_url=schema_url, schema_url=schema_url,
public=public, public=public,
patterns=patterns,
generator_class=generator_class, generator_class=generator_class,
) )
schema_js_view = get_schemajs_view( schema_js_view = get_schemajs_view(
@ -45,6 +54,7 @@ def include_docs_urls(title=None, description=None, schema_url=None, public=True
description=description, description=description,
schema_url=schema_url, schema_url=schema_url,
public=public, public=public,
patterns=patterns,
generator_class=generator_class, generator_class=generator_class,
) )
urls = [ urls = [

View File

@ -35,7 +35,8 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import ( from rest_framework.compat import (
get_remote_field, unicode_repr, unicode_to_repr, value_from_object InvalidTimeError, get_remote_field, unicode_repr, unicode_to_repr,
value_from_object
) )
from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -1102,6 +1103,7 @@ class DateTimeField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
'date': _('Expected a datetime but got a date.'), 'date': _('Expected a datetime but got a date.'),
'make_aware': _('Invalid datetime for the timezone "{timezone}".')
} }
datetime_parser = datetime.datetime.strptime datetime_parser = datetime.datetime.strptime
@ -1122,7 +1124,10 @@ class DateTimeField(Field):
field_timezone = getattr(self, 'timezone', self.default_timezone()) field_timezone = getattr(self, 'timezone', self.default_timezone())
if (field_timezone is not None) and not timezone.is_aware(value): if (field_timezone is not None) and not timezone.is_aware(value):
try:
return timezone.make_aware(value, field_timezone) return timezone.make_aware(value, field_timezone)
except InvalidTimeError:
self.fail('make_aware', timezone=field_timezone)
elif (field_timezone is None) and timezone.is_aware(value): elif (field_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, utc) return timezone.make_naive(value, utc)
return value return value

View File

@ -332,12 +332,12 @@ class LimitOffsetPagination(BasePagination):
template = 'rest_framework/pagination/numbers.html' template = 'rest_framework/pagination/numbers.html'
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
self.count = _get_count(queryset)
self.limit = self.get_limit(request) self.limit = self.get_limit(request)
if self.limit is None: if self.limit is None:
return None return None
self.offset = self.get_offset(request) self.offset = self.get_offset(request)
self.count = _get_count(queryset)
self.request = request self.request = request
if self.count > self.limit and self.template is not None: if self.count > self.limit and self.template is not None:
self.display_page_controls = True self.display_page_controls = True

View File

@ -7,7 +7,9 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db.models import Manager from django.db.models import Manager
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six from django.utils import six
from django.utils.encoding import python_2_unicode_compatible, smart_text from django.utils.encoding import (
python_2_unicode_compatible, smart_text, uri_to_iri
)
from django.utils.six.moves.urllib import parse as urlparse from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -324,6 +326,8 @@ class HyperlinkedRelatedField(RelatedField):
if data.startswith(prefix): if data.startswith(prefix):
data = '/' + data[len(prefix):] data = '/' + data[len(prefix):]
data = uri_to_iri(data)
try: try:
match = resolve(data) match = resolve(data)
except Resolver404: except Resolver404:

View File

@ -604,7 +604,7 @@ class SchemaGenerator(object):
return [] return []
pagination = getattr(view, 'pagination_class', None) pagination = getattr(view, 'pagination_class', None)
if not pagination or not pagination.page_size: if not pagination or not getattr(pagination, 'page_size', None):
return [] return []
paginator = view.pagination_class() paginator = view.pagination_class()
@ -695,18 +695,15 @@ class SchemaView(APIView):
def get_schema_view( def get_schema_view(
title=None, title=None, url=None, description=None, urlconf=None, renderer_classes=None,
url=None, public=False, patterns=None, generator_class=SchemaGenerator):
description=None,
urlconf=None,
renderer_classes=None,
public=False,
generator_class=SchemaGenerator,
):
""" """
Return a schema view. Return a schema view.
""" """
generator = generator_class(title=title, url=url, description=description, urlconf=urlconf) generator = generator_class(
title=title, url=url, description=description,
urlconf=urlconf, patterns=patterns,
)
return SchemaView.as_view( return SchemaView.as_view(
renderer_classes=renderer_classes, renderer_classes=renderer_classes,
schema_generator=generator, schema_generator=generator,

View File

@ -38,7 +38,8 @@ from rest_framework.utils.field_mapping import (
get_relation_kwargs, get_url_kwargs get_relation_kwargs, get_url_kwargs
) )
from rest_framework.utils.serializer_helpers import ( from rest_framework.utils.serializer_helpers import (
BindingDict, BoundField, NestedBoundField, ReturnDict, ReturnList BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict,
ReturnList
) )
from rest_framework.validators import ( from rest_framework.validators import (
UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
@ -521,6 +522,8 @@ class Serializer(BaseSerializer):
error = self.errors.get(key) if hasattr(self, '_errors') else None error = self.errors.get(key) if hasattr(self, '_errors') else None
if isinstance(field, Serializer): if isinstance(field, Serializer):
return NestedBoundField(field, value, error) return NestedBoundField(field, value, error)
if isinstance(field, JSONField):
return JSONBoundField(field, value, error)
return BoundField(field, value, error) return BoundField(field, value, error)
# Include a backlink to the serializer class on return objects. # Include a backlink to the serializer class on return objects.
@ -562,6 +565,10 @@ class ListSerializer(BaseSerializer):
super(ListSerializer, self).__init__(*args, **kwargs) super(ListSerializer, self).__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
def bind(self, field_name, parent):
super(ListSerializer, self).bind(field_name, parent)
self.partial = self.parent.partial
def get_initial(self): def get_initial(self):
if hasattr(self, 'initial_data'): if hasattr(self, 'initial_data'):
return self.to_representation(self.initial_data) return self.to_representation(self.initial_data)
@ -613,6 +620,9 @@ class ListSerializer(BaseSerializer):
}, code='not_a_list') }, code='not_a_list')
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
if self.parent and self.partial:
raise SkipField()
message = self.error_messages['empty'] message = self.error_messages['empty']
raise ValidationError({ raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message] api_settings.NON_FIELD_ERRORS_KEY: [message]

View File

@ -8,7 +8,7 @@ from django.core import validators
from django.db import models from django.db import models
from django.utils.text import capfirst from django.utils.text import capfirst
from rest_framework.compat import DecimalValidator from rest_framework.compat import DecimalValidator, JSONField
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = ( NUMERIC_FIELD_TYPES = (
@ -88,7 +88,7 @@ def get_field_kwargs(field_name, model_field):
if decimal_places is not None: if decimal_places is not None:
kwargs['decimal_places'] = decimal_places kwargs['decimal_places'] = decimal_places
if isinstance(model_field, models.TextField): if isinstance(model_field, models.TextField) or (JSONField and isinstance(model_field, JSONField)):
kwargs['style'] = {'base_template': 'textarea.html'} kwargs['style'] = {'base_template': 'textarea.html'}
if isinstance(model_field, models.AutoField) or not model_field.editable: if isinstance(model_field, models.AutoField) or not model_field.editable:

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import collections import collections
import json
from collections import OrderedDict from collections import OrderedDict
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -82,6 +83,16 @@ class BoundField(object):
return self.__class__(self._field, value, self.errors, self._prefix) return self.__class__(self._field, value, self.errors, self._prefix)
class JSONBoundField(BoundField):
def as_form_field(self):
value = self.value
try:
value = json.dumps(self.value, sort_keys=True, indent=4)
except TypeError:
pass
return self.__class__(self._field, value, self.errors, self._prefix)
class NestedBoundField(BoundField): class NestedBoundField(BoundField):
""" """
This `BoundField` additionally implements __iter__ and __getitem__ This `BoundField` additionally implements __iter__ and __getitem__
@ -101,7 +112,7 @@ class NestedBoundField(BoundField):
def __getitem__(self, key): def __getitem__(self, key):
field = self.fields[key] field = self.fields[key]
value = self.value.get(key) if self.value else None value = self.value.get(key) if self.value else None
error = self.errors.get(key) if self.errors else None error = self.errors.get(key) if isinstance(self.errors, dict) else None
if hasattr(field, 'fields'): if hasattr(field, 'fields'):
return NestedBoundField(field, value, error, prefix=self.name + '.') return NestedBoundField(field, value, error, prefix=self.name + '.')
return BoundField(field, value, error, prefix=self.name + '.') return BoundField(field, value, error, prefix=self.name + '.')

View File

@ -12,7 +12,7 @@ from django.utils import six
from django.utils.timezone import utc from django.utils.timezone import utc
import rest_framework import rest_framework
from rest_framework import serializers from rest_framework import compat, serializers
from rest_framework.fields import is_simple_callable from rest_framework.fields import is_simple_callable
try: try:
@ -1235,6 +1235,30 @@ class TestNaiveDateTimeField(FieldValues):
field = serializers.DateTimeField(default_timezone=None) field = serializers.DateTimeField(default_timezone=None)
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
"""
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017.
"""
valid_inputs = {}
invalid_inputs = {
'2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'],
'2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".']
}
outputs = {}
class MockTimezone:
@staticmethod
def localize(value, is_dst):
raise compat.InvalidTimeError()
def __str__(self):
return 'America/New_York'
field = serializers.DateTimeField(default_timezone=MockTimezone())
class TestTimeField(FieldValues): class TestTimeField(FieldValues):
""" """
Valid and invalid values for `TimeField`. Valid and invalid values for `TimeField`.

View File

@ -1,7 +1,9 @@
import uuid import uuid
import pytest import pytest
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from rest_framework import serializers from rest_framework import serializers
@ -87,10 +89,21 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase):
assert representation == self.instance.pk.int assert representation == self.instance.pk.int
@override_settings(ROOT_URLCONF=[
url(r'^example/(?P<name>.+)/$', lambda: None, name='example'),
])
class TestHyperlinkedRelatedField(APISimpleTestCase): class TestHyperlinkedRelatedField(APISimpleTestCase):
def setUp(self): def setUp(self):
self.queryset = MockQueryset([
MockObject(pk=1, name='foobar'),
MockObject(pk=2, name='baz qux'),
])
self.field = serializers.HyperlinkedRelatedField( self.field = serializers.HyperlinkedRelatedField(
view_name='example', read_only=True) view_name='example',
lookup_field='name',
lookup_url_kwarg='name',
queryset=self.queryset,
)
self.field.reverse = mock_reverse self.field.reverse = mock_reverse
self.field._context = {'request': True} self.field._context = {'request': True}
@ -98,6 +111,20 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
representation = self.field.to_representation(MockObject(pk='')) representation = self.field.to_representation(MockObject(pk=''))
assert representation is None assert representation is None
def test_hyperlinked_related_lookup_exists(self):
instance = self.field.to_internal_value('http://example.org/example/foobar/')
assert instance is self.queryset.items[0]
def test_hyperlinked_related_lookup_url_encoded_exists(self):
instance = self.field.to_internal_value('http://example.org/example/baz%20qux/')
assert instance is self.queryset.items[1]
def test_hyperlinked_related_lookup_does_not_exist(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.field.to_internal_value('http://example.org/example/doesnotexist/')
msg = excinfo.value.detail[0]
assert msg == 'Invalid hyperlink - Object does not exist.'
class TestHyperlinkedIdentityField(APISimpleTestCase): class TestHyperlinkedIdentityField(APISimpleTestCase):
def setUp(self): def setUp(self):

View File

@ -156,6 +156,7 @@ class TestCustomLookupFields(TestCase):
""" """
def setUp(self): def setUp(self):
RouterTestModel.objects.create(uuid='123', text='foo bar') RouterTestModel.objects.create(uuid='123', text='foo bar')
RouterTestModel.objects.create(uuid='a b', text='baz qux')
def test_custom_lookup_field_route(self): def test_custom_lookup_field_route(self):
detail_route = notes_router.urls[-1] detail_route = notes_router.urls[-1]
@ -164,12 +165,19 @@ class TestCustomLookupFields(TestCase):
def test_retrieve_lookup_field_list_view(self): def test_retrieve_lookup_field_list_view(self):
response = self.client.get('/example/notes/') response = self.client.get('/example/notes/')
assert response.data == [{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}] assert response.data == [
{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"},
{"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"},
]
def test_retrieve_lookup_field_detail_view(self): def test_retrieve_lookup_field_detail_view(self):
response = self.client.get('/example/notes/123/') response = self.client.get('/example/notes/123/')
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"} assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
def test_retrieve_lookup_field_url_encoded_detail_view_(self):
response = self.client.get('/example/notes/a%20b/')
assert response.data == {"url": "http://testserver/example/notes/a%20b/", "uuid": "a b", "text": "baz qux"}
class TestLookupValueRegex(TestCase): class TestLookupValueRegex(TestCase):
""" """
@ -211,6 +219,10 @@ class TestLookupUrlKwargs(TestCase):
response = self.client.get('/example2/notes/fo/') response = self.client.get('/example2/notes/fo/')
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"} assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
def test_retrieve_lookup_url_encoded_kwarg_detail_view(self):
response = self.client.get('/example2/notes/foo%20bar/')
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
class TestTrailingSlashIncluded(TestCase): class TestTrailingSlashIncluded(TestCase):
def setUp(self): def setUp(self):

View File

@ -318,3 +318,217 @@ class TestSerializerPartialUsage:
assert serializer.is_valid() assert serializer.is_valid()
assert serializer.validated_data == {} assert serializer.validated_data == {}
assert serializer.errors == {} assert serializer.errors == {}
def test_allow_empty_true(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
instance = [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
serializer = ListSerializer(instance, data=[], partial=True, many=True)
assert serializer.is_valid()
assert serializer.validated_data == []
assert serializer.errors == []
def test_update_allow_empty_true(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
instance = [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
input_data = [{'update_field': 31}, {'update_field': 41}]
updated_data_list = [
{'update_field': 31, 'store_field': 12},
{'update_field': 41, 'store_field': 22},
]
serializer = ListSerializer(
instance, data=input_data, partial=True, many=True)
assert serializer.is_valid()
for index, data in enumerate(serializer.validated_data):
for key, value in data.items():
assert value == updated_data_list[index][key]
assert serializer.errors == []
def test_allow_empty_false(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
instance = [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
serializer = ListSerializer(
instance, data=[], allow_empty=False, partial=True, many=True)
assert not serializer.is_valid()
assert serializer.validated_data == []
assert len(serializer.errors) == 1
assert serializer.errors['non_field_errors'][0] == 'This list may not be empty.'
def test_update_allow_empty_false(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
instance = [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
input_data = [{'update_field': 31}, {'update_field': 41}]
updated_data_list = [
{'update_field': 31, 'store_field': 12},
{'update_field': 41, 'store_field': 22},
]
serializer = ListSerializer(
instance, data=input_data, allow_empty=False, partial=True, many=True)
assert serializer.is_valid()
for index, data in enumerate(serializer.validated_data):
for key, value in data.items():
assert value == updated_data_list[index][key]
assert serializer.errors == []
def test_as_field_allow_empty_true(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
class Serializer(serializers.Serializer):
extra_field = serializers.IntegerField()
list_field = ListSerializer(many=True)
instance = {
'extra_field': 1,
'list_field': [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
}
serializer = Serializer(instance, data={}, partial=True)
assert serializer.is_valid()
assert serializer.validated_data == {}
assert serializer.errors == {}
def test_udate_as_field_allow_empty_true(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
class Serializer(serializers.Serializer):
extra_field = serializers.IntegerField()
list_field = ListSerializer(many=True)
instance = {
'extra_field': 1,
'list_field': [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
}
input_data_1 = {'extra_field': 2}
input_data_2 = {
'list_field': [
{'update_field': 31},
{'update_field': 41},
]
}
# data_1
serializer = Serializer(instance, data=input_data_1, partial=True)
assert serializer.is_valid()
assert len(serializer.validated_data) == 1
assert serializer.validated_data['extra_field'] == 2
assert serializer.errors == {}
# data_2
serializer = Serializer(instance, data=input_data_2, partial=True)
assert serializer.is_valid()
updated_data_list = [
{'update_field': 31, 'store_field': 12},
{'update_field': 41, 'store_field': 22},
]
for index, data in enumerate(serializer.validated_data['list_field']):
for key, value in data.items():
assert value == updated_data_list[index][key]
assert serializer.errors == {}
def test_as_field_allow_empty_false(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
class Serializer(serializers.Serializer):
extra_field = serializers.IntegerField()
list_field = ListSerializer(many=True, allow_empty=False)
instance = {
'extra_field': 1,
'list_field': [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
}
serializer = Serializer(instance, data={}, partial=True)
assert serializer.is_valid()
assert serializer.validated_data == {}
assert serializer.errors == {}
def test_update_as_field_allow_empty_false(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
store_field = serializers.IntegerField()
class Serializer(serializers.Serializer):
extra_field = serializers.IntegerField()
list_field = ListSerializer(many=True, allow_empty=False)
instance = {
'extra_field': 1,
'list_field': [
{'update_field': 11, 'store_field': 12},
{'update_field': 21, 'store_field': 22},
]
}
input_data_1 = {'extra_field': 2}
input_data_2 = {
'list_field': [
{'update_field': 31},
{'update_field': 41},
]
}
updated_data_list = [
{'update_field': 31, 'store_field': 12},
{'update_field': 41, 'store_field': 22},
]
# data_1
serializer = Serializer(instance, data=input_data_1, partial=True)
assert serializer.is_valid()
assert serializer.errors == {}
# data_2
serializer = Serializer(instance, data=input_data_2, partial=True)
assert serializer.is_valid()
for index, data in enumerate(serializer.validated_data['list_field']):
for key, value in data.items():
assert value == updated_data_list[index][key]
assert serializer.errors == {}