diff --git a/README.md b/README.md index 02a20aff2..d710f3d4a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # 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) # Installation diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index f43ff56bd..afa058d94 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -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 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 Allow: GET, HEAD, OPTIONS 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] ) +#### `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 If you need a little more control than the `get_schema_view()` shortcut gives you, diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 13c746017..753c77e2f 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -162,7 +162,7 @@ The `credentials` method is appropriate for testing APIs that require authentica #### .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. diff --git a/docs/img/books/hwa-cover.png b/docs/img/books/hwa-cover.png new file mode 100644 index 000000000..93d7a3088 Binary files /dev/null and b/docs/img/books/hwa-cover.png differ diff --git a/docs/img/books/tsd-cover.png b/docs/img/books/tsd-cover.png new file mode 100644 index 000000000..5034edfad Binary files /dev/null and b/docs/img/books/tsd-cover.png differ diff --git a/docs/index.md b/docs/index.md index 9bf36cce7..23433a6b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**. 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) The following packages are optional: diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index bc1431cfc..a0ca6626b 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -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. -Better support for autocomplete inputs is planned in future versions. - --- [cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead diff --git a/docs/topics/tutorials-and-resources.md b/docs/topics/tutorials-and-resources.md index 46cbefea6..48719a618 100644 --- a/docs/topics/tutorials-and-resources.md +++ b/docs/topics/tutorials-and-resources.md @@ -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. +## Books + +
+ ## Tutorials * [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] * [Blog posts about Django REST Framework][medium-django-rest-framework] -## Books - -* [Hello Web App: Intermediate Concepts, Chapter 10][hello-web-app-intermediate] - ### Documentations * [Classy Django REST Framework][cdrf.co] * [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/ [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/ -[hello-web-app-intermediate]: https://hellowebapp.com/order/ [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 [drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index fa54ca6a3..558797816 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -310,7 +310,7 @@ Quit out of the shell... Validating models... 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/ Quit the server with CONTROL-C. diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index e7bcfbafb..4d7193986 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -36,6 +36,7 @@ API schema. We can now include a schema for our API, by including an autogenerated schema view in our URL configuration. +``` from rest_framework.schemas import get_schema_view schema_view = get_schema_view(title='Pastebin API') @@ -44,6 +45,7 @@ view in our URL configuration. url(r'^schema/$', schema_view), ... ] +``` If you visit the API root endpoint in a browser you should now see `corejson` representation become available as an option. diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 192e86860..a0a286b22 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -417,3 +417,8 @@ ul.sponsor { .toclink { color: #333; } + +.book-cover img { + margin: 0 !important; + display: inline-block !important; +} diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 45ac49841..168bccf83 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -275,6 +275,14 @@ except ImportError: def pygments_css(style): 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 # See: http://bugs.python.org/issue22767 if six.PY3: @@ -339,6 +347,7 @@ def set_many(instance, field, value): field = getattr(instance, field) field.set(value) + def include(module, namespace=None, app_name=None): from django.conf.urls import include if django.VERSION < (1,9): diff --git a/rest_framework/documentation.py b/rest_framework/documentation.py index a95259ddc..48458e188 100644 --- a/rest_framework/documentation.py +++ b/rest_framework/documentation.py @@ -6,7 +6,9 @@ from rest_framework.renderers import ( 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] return get_schema_view( @@ -15,11 +17,14 @@ def get_docs_view(title=None, description=None, schema_url=None, public=True, ge description=description, renderer_classes=renderer_classes, public=public, + patterns=patterns, 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] return get_schema_view( @@ -28,16 +33,20 @@ def get_schemajs_view(title=None, description=None, schema_url=None, public=True description=description, renderer_classes=renderer_classes, public=public, + patterns=patterns, 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( title=title, description=description, schema_url=schema_url, public=public, + patterns=patterns, generator_class=generator_class, ) 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, schema_url=schema_url, public=public, + patterns=patterns, generator_class=generator_class, ) urls = [ diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b78f40e7e..045c22dea 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -35,7 +35,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 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.settings import api_settings @@ -1102,6 +1103,7 @@ class DateTimeField(Field): default_error_messages = { 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), 'date': _('Expected a datetime but got a date.'), + 'make_aware': _('Invalid datetime for the timezone "{timezone}".') } datetime_parser = datetime.datetime.strptime @@ -1122,7 +1124,10 @@ class DateTimeField(Field): field_timezone = getattr(self, 'timezone', self.default_timezone()) 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): return timezone.make_naive(value, utc) return value diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 100b31b71..0255cfc7f 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -332,12 +332,12 @@ class LimitOffsetPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' def paginate_queryset(self, queryset, request, view=None): + self.count = _get_count(queryset) self.limit = self.get_limit(request) if self.limit is None: return None self.offset = self.get_offset(request) - self.count = _get_count(queryset) self.request = request if self.count > self.limit and self.template is not None: self.display_page_controls = True diff --git a/rest_framework/relations.py b/rest_framework/relations.py index eac9647b0..54e67cd16 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -7,7 +7,9 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db.models import Manager from django.db.models.query import QuerySet 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.translation import ugettext_lazy as _ @@ -324,6 +326,8 @@ class HyperlinkedRelatedField(RelatedField): if data.startswith(prefix): data = '/' + data[len(prefix):] + data = uri_to_iri(data) + try: match = resolve(data) except Resolver404: diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 859a6c9bd..8d226043f 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -604,7 +604,7 @@ class SchemaGenerator(object): return [] 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 [] paginator = view.pagination_class() @@ -695,18 +695,15 @@ class SchemaView(APIView): def get_schema_view( - title=None, - url=None, - description=None, - urlconf=None, - renderer_classes=None, - public=False, - generator_class=SchemaGenerator, -): + title=None, url=None, description=None, urlconf=None, renderer_classes=None, + public=False, patterns=None, generator_class=SchemaGenerator): """ 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( renderer_classes=renderer_classes, schema_generator=generator, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ba01c3434..5bd9b6473 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -38,7 +38,8 @@ from rest_framework.utils.field_mapping import ( get_relation_kwargs, get_url_kwargs ) from rest_framework.utils.serializer_helpers import ( - BindingDict, BoundField, NestedBoundField, ReturnDict, ReturnList + BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict, + ReturnList ) from rest_framework.validators import ( UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator, @@ -521,6 +522,8 @@ class Serializer(BaseSerializer): error = self.errors.get(key) if hasattr(self, '_errors') else None if isinstance(field, Serializer): return NestedBoundField(field, value, error) + if isinstance(field, JSONField): + return JSONBoundField(field, value, error) return BoundField(field, value, error) # Include a backlink to the serializer class on return objects. @@ -562,6 +565,10 @@ class ListSerializer(BaseSerializer): super(ListSerializer, self).__init__(*args, **kwargs) 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): if hasattr(self, 'initial_data'): return self.to_representation(self.initial_data) @@ -613,6 +620,9 @@ class ListSerializer(BaseSerializer): }, code='not_a_list') if not self.allow_empty and len(data) == 0: + if self.parent and self.partial: + raise SkipField() + message = self.error_messages['empty'] raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b8817d976..cc82492dc 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -8,7 +8,7 @@ from django.core import validators from django.db import models 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 NUMERIC_FIELD_TYPES = ( @@ -88,7 +88,7 @@ def get_field_kwargs(field_name, model_field): if decimal_places is not None: 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'} if isinstance(model_field, models.AutoField) or not model_field.editable: diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 4734332af..d1bba9666 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import collections +import json from collections import OrderedDict from django.utils.encoding import force_text @@ -82,6 +83,16 @@ class BoundField(object): 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): """ This `BoundField` additionally implements __iter__ and __getitem__ @@ -101,7 +112,7 @@ class NestedBoundField(BoundField): def __getitem__(self, key): field = self.fields[key] 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'): return NestedBoundField(field, value, error, prefix=self.name + '.') return BoundField(field, value, error, prefix=self.name + '.') diff --git a/tests/test_fields.py b/tests/test_fields.py index 9fdb46b62..50c48ef41 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -12,7 +12,7 @@ from django.utils import six from django.utils.timezone import utc import rest_framework -from rest_framework import serializers +from rest_framework import compat, serializers from rest_framework.fields import is_simple_callable try: @@ -1235,6 +1235,30 @@ class TestNaiveDateTimeField(FieldValues): 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): """ Valid and invalid values for `TimeField`. diff --git a/tests/test_relations.py b/tests/test_relations.py index a070ad6de..c903ee557 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,7 +1,9 @@ import uuid import pytest +from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured +from django.test import override_settings from django.utils.datastructures import MultiValueDict from rest_framework import serializers @@ -87,10 +89,21 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk.int +@override_settings(ROOT_URLCONF=[ + url(r'^example/(?P