mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-05 04:50:12 +03:00
Merge branch 'master' into localizedfloatfield
This commit is contained in:
commit
97051799e7
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
BIN
docs/img/books/hwa-cover.png
Normal file
BIN
docs/img/books/hwa-cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/img/books/tsd-cover.png
Normal file
BIN
docs/img/books/tsd-cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -417,3 +417,8 @@ ul.sponsor {
|
||||||
.toclink {
|
.toclink {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.book-cover img {
|
||||||
|
margin: 0 !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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):
|
||||||
return timezone.make_aware(value, field_timezone)
|
try:
|
||||||
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 + '.')
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 == {}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user