diff --git a/.travis.yml b/.travis.yml index f89e77531..7266df2d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,13 +14,16 @@ matrix: - { python: "3.6", env: DJANGO=2.0 } - { python: "3.6", env: DJANGO=2.1 } - { python: "3.6", env: DJANGO=2.2 } + - { python: "3.6", env: DJANGO=3.0 } - { python: "3.6", env: DJANGO=master } - { python: "3.7", env: DJANGO=2.0 } - { python: "3.7", env: DJANGO=2.1 } - { python: "3.7", env: DJANGO=2.2 } + - { python: "3.7", env: DJANGO=3.0 } - { python: "3.7", env: DJANGO=master } + - { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } diff --git a/MANIFEST.in b/MANIFEST.in index 6f7cb8f13..262e3dc91 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.md include LICENSE.md +recursive-include tests/* * recursive-include rest_framework/static *.js *.css *.png *.ico *.eot *.svg *.ttf *.woff *.woff2 recursive-include rest_framework/templates *.html schema.js recursive-include rest_framework/locale *.mo diff --git a/README.md b/README.md index 8774bc854..9591bdc17 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements -* Python (3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1, 2.2) +* Python (3.5, 3.6, 3.7, 3.8) +* Django (1.11, 2.0, 2.1, 2.2, 3.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 29cb5aec9..65c83b78e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -50,7 +50,19 @@ If set, this gives the default value that will be used for the field if no input The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned. -May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context). +May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `requires_context = True` attribute, then the serializer field will be passed as an argument. + +For example: + + class CurrentUserDefault: + """ + May be applied as a `default=...` value on a serializer field. + Returns the current user. + """ + requires_context = True + + def __call__(self, serializer_field): + return serializer_field.context['request'].user When serializing the instance, default will be used if the object attribute or dictionary key is not present in the instance. @@ -585,8 +597,6 @@ The `.to_representation()` method is called to convert the initial datatype into The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializers.ValidationError` if the data is invalid. -Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input. - ## Examples ### A Basic Custom Field diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1bdb6c52b..bad57b441 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -212,7 +212,7 @@ The search behavior may be restricted by prepending various characters to the `s * '^' Starts-with search. * '=' Exact matches. -* '@' Full-text search. (Currently only supported Django's MySQL backend.) +* '@' Full-text search. (Currently only supported Django's [PostgreSQL backend](https://docs.djangoproject.com/en/dev/ref/contrib/postgres/search/).) * '$' Regex search. For example: diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index a2f19ff2e..a256eb2d9 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -175,8 +175,6 @@ You can also use these hooks to provide additional validation, by raising a `Val raise ValidationError('You have already signed up') serializer.save(user=self.request.user) -**Note**: These methods replace the old-style version 2.x `pre_save`, `post_save`, `pre_delete` and `post_delete` methods, which are no longer available. - **Other methods**: You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`. diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 14f197b21..ef6efec5e 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -245,7 +245,9 @@ This field is always read-only. # Nested relationships -Nested relationships can be expressed by using serializers as fields. +As opposed to previously discussed _references_ to another entity, the referred entity can instead also be embedded or _nested_ +in the representation of the object that refers to it. +Such nested relationships can be expressed by using serializers as fields. If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 49685838a..009cd2468 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -291,13 +291,17 @@ To write a class-based validator, use the `__call__` method. Class-based validat message = 'This field must be a multiple of %d.' % self.base raise serializers.ValidationError(message) -#### Using `set_context()` +#### Accessing the context -In some advanced cases you might want a validator to be passed the serializer field it is being used with as additional context. You can do so by declaring a `set_context` method on a class-based validator. +In some advanced cases you might want a validator to be passed the serializer +field it is being used with as additional context. You can do so by setting +a `requires_context = True` attribute on the validator. The `__call__` method +will then be called with the `serializer_field` +or `serializer` as an additional argument. - def set_context(self, serializer_field): - # Determine if this is an update or a create operation. - # In `__call__` we can then use that information to modify the validation behavior. - self.is_update = serializer_field.parent.instance is not None + requires_context = True + + def __call__(self, value, serializer_field): + ... [cite]: https://docs.djangoproject.com/en/stable/ref/validators/ diff --git a/docs/community/3.10-announcement.md b/docs/community/3.10-announcement.md index 578e900dc..19748aa40 100644 --- a/docs/community/3.10-announcement.md +++ b/docs/community/3.10-announcement.md @@ -144,4 +144,4 @@ continued development by **[signing up for a paid plan][funding]**. [legacy-core-api-docs]:https://github.com/encode/django-rest-framework/blob/master/docs/coreapi/index.md [sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors -[funding]: community/funding.md +[funding]: funding.md diff --git a/docs/community/3.11-announcement.md b/docs/community/3.11-announcement.md new file mode 100644 index 000000000..83dd636d1 --- /dev/null +++ b/docs/community/3.11-announcement.md @@ -0,0 +1,117 @@ + + +# Django REST framework 3.11 + +The 3.11 release adds support for Django 3.0. + +* Our supported Python versions are now: 3.5, 3.6, 3.7, and 3.8. +* Our supported Django versions are now: 1.11, 2.0, 2.1, 2.2, and 3.0. + +This release will be the last to support Python 3.5 or Django 1.11. + +## OpenAPI Schema Generation Improvements + +The OpenAPI schema generation continues to mature. Some highlights in 3.11 +include: + +* Automatic mapping of Django REST Framework renderers and parsers into OpenAPI + request and response media-types. +* Improved mapping JSON schema mapping types, for example in HStoreFields, and + with large integer values. +* Porting of the old CoreAPI parsing of docstrings to form OpenAPI operation + descriptions. + +In this example view operation descriptions for the `get` and `post` methods will +be extracted from the class docstring: + +```python +class DocStringExampleListView(APIView): +""" +get: A description of my GET operation. +post: A description of my POST operation. +""" + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, request, *args, **kwargs): + ... + + def post(self, request, *args, **kwargs): + ... +``` + +## Validator / Default Context + +In some circumstances a Validator class or a Default class may need to access the serializer field with which it is called, or the `.context` with which the serializer was instantiated. In particular: + +* Uniqueness validators need to be able to determine the name of the field to which they are applied, in order to run an appropriate database query. +* The `CurrentUserDefault` needs to be able to determine the context with which the serializer was instantiated, in order to return the current user instance. + +Previous our approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13. + +Instead, validators or defaults which require the serializer context, should include a `requires_context = True` attribute on the class. + +The `__call__` method should then include an additional `serializer_field` argument. + +Validator implementations will look like this: + +```python +class CustomValidator: + requires_context = True + + def __call__(self, value, serializer_field): + ... +``` + +Default implementations will look like this: + +```python +class CustomDefault: + requires_context = True + + def __call__(self, serializer_field): + ... +``` + +--- + +## Funding + +REST framework is a *collaboratively funded project*. If you use +REST framework commercially we strongly encourage you to invest in its +continued development by **[signing up for a paid plan][funding]**. + +*Every single sign-up helps us make REST framework long-term financially sustainable.* + + +
+ +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), and [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship).* + +[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors +[funding]: funding.md diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 283dbae67..4be05d56b 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -1,9 +1,5 @@ # Release Notes -> Release Early, Release Often -> -> — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. - ## Versioning Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 4d0043252..baa30fd0c 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -270,6 +270,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified). * [django-rest-witchcraft][django-rest-witchcraft] - Provides DRF integration with SQLAlchemy with SQLAlchemy model serializers/viewsets and a bunch of other goodies * [djangorestframework-mvt][djangorestframework-mvt] - An extension for creating views that serve Postgres data as Map Box Vector Tiles. +* [drf-viewset-profiler][drf-viewset-profiler] - Lib to profile all methods from a viewset line by line. * [djangorestframework-features][djangorestframework-features] - Advanced schema generation and more based on named features. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html @@ -351,4 +352,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-restql]: https://github.com/yezyilomo/django-restql [djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt [django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian +[drf-viewset-profiler]: https://github.com/fvlima/drf-viewset-profiler [djangorestframework-features]: https://github.com/cloudcode-hungary/django-rest-framework-features/ diff --git a/docs/index.md b/docs/index.md index e06b21dff..bccc1fb46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ Some reasons you might want to use REST framework: * [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section]. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. -* [Extensive documentation][index], and [great community support][group]. +* Extensive documentation, and [great community support][group]. * Used and trusted by internationally recognised companies including [Mozilla][mozilla], [Red Hat][redhat], [Heroku][heroku], and [Eventbrite][eventbrite]. --- @@ -85,8 +85,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Python (3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1, 2.2) +* Python (3.5, 3.6, 3.7, 3.8) +* Django (1.11, 2.0, 2.1, 2.2, 3.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 3fd560634..b020c380a 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -384,7 +384,7 @@ First, install the API documentation views. These will include the schema resour urlpatterns = [ ... - url(r'^docs/', include_docs_urls(title='My API service')) + url(r'^docs/', include_docs_urls(title='My API service'), name='api-docs'), ] Once the API documentation URLs are installed, you'll be able to include both the required JavaScript resources. Note that the ordering of these two lines is important, as the schema loading requires CoreAPI to already be installed. @@ -401,14 +401,14 @@ Once the API documentation URLs are installed, you'll be able to include both th The `coreapi` library, and the `schema` object will now both be available on the `window` instance. - const coreapi = window.coreapi - const schema = window.schema + const coreapi = window.coreapi; + const schema = window.schema; ## Instantiating a client In order to interact with the API you'll need a client instance. - var client = new coreapi.Client() + var client = new coreapi.Client(); Typically you'll also want to provide some authentication credentials when instantiating the client. @@ -421,9 +421,9 @@ the user to login, and then instantiate a client using session authentication: let auth = new coreapi.auth.SessionAuthentication({ csrfCookieName: 'csrftoken', - csrfHeaderName: 'X-CSRFToken' - }) - let client = new coreapi.Client({auth: auth}) + csrfHeaderName: 'X-CSRFToken', + }); + let client = new coreapi.Client({auth: auth}); The authentication scheme will handle including a CSRF header in any outgoing requests for unsafe HTTP methods. @@ -434,10 +434,10 @@ The `TokenAuthentication` class can be used to support REST framework's built-in `TokenAuthentication`, as well as OAuth and JWT schemes. let auth = new coreapi.auth.TokenAuthentication({ - scheme: 'JWT' - token: '' - }) - let client = new coreapi.Client({auth: auth}) + scheme: 'JWT', + token: '', + }); + let client = new coreapi.Client({auth: auth}); When using TokenAuthentication you'll probably need to implement a login flow using the CoreAPI client. @@ -448,20 +448,20 @@ request to an "obtain token" endpoint For example, using the "Django REST framework JWT" package // Setup some globally accessible state - window.client = new coreapi.Client() - window.loggedIn = false + window.client = new coreapi.Client(); + window.loggedIn = false; function loginUser(username, password) { - let action = ["api-token-auth", "obtain-token"] - let params = {username: "example", email: "example@example.com"} + let action = ["api-token-auth", "obtain-token"]; + let params = {username: "example", email: "example@example.com"}; client.action(schema, action, params).then(function(result) { // On success, instantiate an authenticated client. let auth = window.coreapi.auth.TokenAuthentication({ scheme: 'JWT', - token: result['token'] + token: result['token'], }) - window.client = coreapi.Client({auth: auth}) - window.loggedIn = true + window.client = coreapi.Client({auth: auth}); + window.loggedIn = true; }).catch(function (error) { // Handle error case where eg. user provides incorrect credentials. }) @@ -473,23 +473,23 @@ The `BasicAuthentication` class can be used to support HTTP Basic Authentication let auth = new coreapi.auth.BasicAuthentication({ username: '', - password: '' + password: '', }) - let client = new coreapi.Client({auth: auth}) + let client = new coreapi.Client({auth: auth}); ## Using the client Making requests: - let action = ["users", "list"] + let action = ["users", "list"]; client.action(schema, action).then(function(result) { // Return value is in 'result' }) Including parameters: - let action = ["users", "create"] - let params = {username: "example", email: "example@example.com"} + let action = ["users", "create"]; + let params = {username: "example", email: "example@example.com"}; client.action(schema, action, params).then(function(result) { // Return value is in 'result' }) @@ -512,12 +512,12 @@ The coreapi package is available on NPM. You'll either want to include the API schema in your codebase directly, by copying it from the `schema.js` resource, or else load the schema asynchronously. For example: - let client = new coreapi.Client() - let schema = null + let client = new coreapi.Client(); + let schema = null; client.get("https://api.example.org/").then(function(data) { // Load a CoreJSON API schema. - schema = data - console.log('schema loaded') + schema = data; + console.log('schema loaded'); }) [heroku-api]: https://devcenter.heroku.com/categories/platform-api diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 5c806ea7e..5c5872650 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -45,7 +45,11 @@ this: SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset ], - layout: "BaseLayout" + layout: "BaseLayout", + requestInterceptor: (request) => { + request.headers['X-CSRFToken'] = "{{ csrf_token }}" + return request; + } }) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index e3d21e864..f2b369615 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -29,13 +29,11 @@ REST framework provides two wrappers you can use to write API views. These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed. -The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.data` with malformed input. +The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exceptions that occur when accessing `request.data` with malformed input. ## Pulling it all together -Okay, let's go ahead and start using these new components to write a few views. - -We don't need our `JSONResponse` class in `views.py` any more, so go ahead and delete that. Once that's done we can start refactoring our views slightly. +Okay, let's go ahead and start using these new components to refactor our views slightly. from rest_framework import status from rest_framework.decorators import api_view diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index ee54816dc..505f7f91d 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -85,6 +85,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a from django.contrib.auth.models import User, Group from rest_framework import viewsets + from rest_framework import permissions from tutorial.quickstart.serializers import UserSerializer, GroupSerializer @@ -94,6 +95,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a """ queryset = User.objects.all().order_by('-date_joined') serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] class GroupViewSet(viewsets.ModelViewSet): @@ -102,6 +104,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a """ queryset = Group.objects.all() serializer_class = GroupSerializer + permission_classes = [permissions.IsAuthenticated] Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index bb17a3a11..e9d7f23bf 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -74,6 +74,12 @@ pre { white-space: pre; } +code, pre { + font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif; + font-size: 13px; +} + + /* Preserve the spacing of the navbar across different screen sizes. */ .navbar-inner { /*padding: 5px 0;*/ @@ -432,3 +438,4 @@ ul.sponsor { margin: 0 !important; display: inline-block !important; } + diff --git a/mkdocs.yml b/mkdocs.yml index 83a345a3d..484971a71 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.11 Announcement': 'community/3.11-announcement.md' - '3.10 Announcement': 'community/3.10-announcement.md' - '3.9 Announcement': 'community/3.9-announcement.md' - '3.8 Announcement': 'community/3.8-announcement.md' diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index a33248d10..14957a531 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -2,7 +2,7 @@ psycopg2-binary>=2.8.2, <2.9 markdown==3.1.1 pygments==2.4.2 -django-guardian==1.5.0 +django-guardian==2.1.0 django-filter>=2.2.0, <2.3 coreapi==2.3.1 coreschema==0.0.4 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2b96e7336..b6f3f65ce 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.10.3' +__version__ = '3.11.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' @@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601' default_app_config = 'rest_framework.apps.RestFrameworkConfig' -class RemovedInDRF311Warning(DeprecationWarning): +class RemovedInDRF312Warning(DeprecationWarning): pass -class RemovedInDRF312Warning(PendingDeprecationWarning): +class RemovedInDRF313Warning(PendingDeprecationWarning): pass diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index eb1cad9e4..30b9d84d4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -124,8 +124,23 @@ def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs): """ Mark a ViewSet method as a routable action. - Set the `detail` boolean to determine if this action should apply to - instance/detail requests or collection/list requests. + `@action`-decorated functions will be endowed with a `mapping` property, + a `MethodMapper` that can be used to add additional method-based behaviors + on the routed action. + + :param methods: A list of HTTP method names this action responds to. + Defaults to GET only. + :param detail: Required. Determines whether this action applies to + instance/detail requests or collection/list requests. + :param url_path: Define the URL segment for this action. Defaults to the + name of the method decorated. + :param url_name: Define the internal (`reverse`) URL name for this action. + Defaults to the name of the method decorated with underscores + replaced with dashes. + :param kwargs: Additional properties to set on the view. This can be used + to override viewset-level *_classes settings, equivalent to + how the `@renderer_classes` etc. decorators work for function- + based API views. """ methods = ['get'] if (methods is None) else methods methods = [method.lower() for method in methods] @@ -144,6 +159,10 @@ def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs): func.detail = detail func.url_path = url_path if url_path else func.__name__ func.url_name = url_name if url_name else func.__name__.replace('_', '-') + + # These kwargs will end up being passed to `ViewSet.as_view()` within + # the router, which eventually delegates to Django's CBV `View`, + # which assigns them as instance attributes for each request. func.kwargs = kwargs # Set descriptive arguments for viewsets diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ea8f47b2d..958bebeef 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -5,6 +5,7 @@ import functools import inspect import re import uuid +import warnings from collections import OrderedDict from collections.abc import Mapping @@ -22,19 +23,20 @@ from django.utils.dateparse import ( parse_date, parse_datetime, parse_duration, parse_time ) from django.utils.duration import duration_string -from django.utils.encoding import is_protected_type, smart_text +from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address from django.utils.timezone import utc from django.utils.translation import gettext_lazy as _ from pytz.exceptions import InvalidTimeError -from rest_framework import ISO_8601 +from rest_framework import ISO_8601, RemovedInDRF313Warning from rest_framework.compat import ProhibitNullCharactersValidator from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, json, representation from rest_framework.utils.formatting import lazy_format +from rest_framework.validators import ProhibitSurrogateCharactersValidator class empty: @@ -249,19 +251,30 @@ class CreateOnlyDefault: for create operations, but that do not return any value for update operations. """ + requires_context = True + def __init__(self, default): self.default = default - def set_context(self, serializer_field): - self.is_update = serializer_field.parent.instance is not None - if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update: - self.default.set_context(serializer_field) - - def __call__(self): - if self.is_update: + def __call__(self, serializer_field): + is_update = serializer_field.parent.instance is not None + if is_update: raise SkipField() if callable(self.default): - return self.default() + if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.13. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + RemovedInDRF313Warning, stacklevel=2 + ) + self.default.set_context(self) + + if getattr(self.default, 'requires_context', False): + return self.default(serializer_field) + else: + return self.default() return self.default def __repr__(self): @@ -269,11 +282,10 @@ class CreateOnlyDefault: class CurrentUserDefault: - def set_context(self, serializer_field): - self.user = serializer_field.context['request'].user + requires_context = True - def __call__(self): - return self.user + def __call__(self, serializer_field): + return serializer_field.context['request'].user def __repr__(self): return '%s()' % self.__class__.__name__ @@ -489,8 +501,20 @@ class Field: raise SkipField() if callable(self.default): if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.13. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + RemovedInDRF313Warning, stacklevel=2 + ) self.default.set_context(self) - return self.default() + + if getattr(self.default, 'requires_context', False): + return self.default(self) + else: + return self.default() + return self.default def validate_empty_values(self, data): @@ -551,10 +575,20 @@ class Field: errors = [] for validator in self.validators: if hasattr(validator, 'set_context'): + warnings.warn( + "Method `set_context` on validators is deprecated and will " + "no longer be called starting with 3.13. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + RemovedInDRF313Warning, stacklevel=2 + ) validator.set_context(self) try: - validator(value) + if getattr(validator, 'requires_context', False): + validator(value, self) + else: + validator(value) except ValidationError as exc: # If the validation error contains a mapping of fields to # errors then simply raise it immediately rather than @@ -572,8 +606,11 @@ class Field: Transform the *incoming* primitive data into a native value. """ raise NotImplementedError( - '{cls}.to_internal_value() must be implemented.'.format( - cls=self.__class__.__name__ + '{cls}.to_internal_value() must be implemented for field ' + '{field_name}. If you do not need to support write operations ' + 'you probably want to subclass `ReadOnlyField` instead.'.format( + cls=self.__class__.__name__, + field_name=self.field_name, ) ) @@ -582,9 +619,7 @@ class Field: Transform the *outgoing* native value into primitive data. """ raise NotImplementedError( - '{cls}.to_representation() must be implemented for field ' - '{field_name}. If you do not need to support write operations ' - 'you probably want to subclass `ReadOnlyField` instead.'.format( + '{cls}.to_representation() must be implemented for field {field_name}.'.format( cls=self.__class__.__name__, field_name=self.field_name, ) @@ -784,6 +819,7 @@ class CharField(Field): # ProhibitNullCharactersValidator is None on Django < 2.0 if ProhibitNullCharactersValidator is not None: self.validators.append(ProhibitNullCharactersValidator()) + self.validators.append(ProhibitSurrogateCharactersValidator()) def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, @@ -1049,7 +1085,7 @@ class DecimalField(Field): instance. """ - data = smart_text(data).strip() + data = smart_str(data).strip() if self.localize: data = sanitize_separators(data) @@ -1855,14 +1891,9 @@ class SerializerMethodField(Field): super().__init__(**kwargs) def bind(self, field_name, parent): - # In order to enforce a consistent style, we error if a redundant - # 'method_name' argument has been used. For example: - # my_field = serializer.SerializerMethodField(method_name='get_my_field') - default_method_name = 'get_{field_name}'.format(field_name=field_name) - - # The method name should default to `get_{field_name}`. + # The method name defaults to `get_{field_name}`. if self.method_name is None: - self.method_name = default_method_name + self.method_name = 'get_{field_name}'.format(field_name=field_name) super().bind(field_name, parent) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 9bde79b19..3a2a8fb4b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -6,7 +6,7 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db.models import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve -from django.utils.encoding import smart_text, uri_to_iri +from django.utils.encoding import smart_str, uri_to_iri from django.utils.translation import gettext_lazy as _ from rest_framework.fields import ( @@ -344,7 +344,7 @@ class HyperlinkedRelatedField(RelatedField): if data.startswith(prefix): data = '/' + data[len(prefix):] - data = uri_to_iri(data) + data = uri_to_iri(parse.unquote(data)) try: match = resolve(data) @@ -452,7 +452,7 @@ class SlugRelatedField(RelatedField): try: return self.get_queryset().get(**{self.slug_field: data}) except ObjectDoesNotExist: - self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) + self.fail('does_not_exist', slug_name=self.slug_field, value=smart_str(data)) except (TypeError, ValueError): self.fail('invalid') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 9a6f3c3c5..29ac90ea8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -16,7 +16,6 @@ from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page from django.http.multipartparser import parse_header from django.template import engines, loader -from django.test.client import encode_multipart from django.urls import NoReverseMatch from django.utils.html import mark_safe @@ -902,6 +901,8 @@ class MultiPartRenderer(BaseRenderer): BOUNDARY = 'BoUnDaRyStRiNg' def render(self, data, accepted_media_type=None, renderer_context=None): + from django.test.client import encode_multipart + if hasattr(data, 'items'): for key, value in data.items(): assert not isinstance(value, dict), ( diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d8e19a2d7..657ad67bc 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -14,15 +14,13 @@ For example, you might have a `urls.py` that looks something like this: urlpatterns = router.urls """ import itertools -import warnings from collections import OrderedDict, namedtuple from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch -from django.utils.deprecation import RenameMethodsBase -from rest_framework import RemovedInDRF311Warning, views +from rest_framework import views from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator @@ -48,27 +46,11 @@ def flatten(list_of_lists): return itertools.chain(*list_of_lists) -class RenameRouterMethods(RenameMethodsBase): - renamed_methods = ( - ('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning), - ) - - -class BaseRouter(metaclass=RenameRouterMethods): +class BaseRouter: def __init__(self): self.registry = [] - def register(self, prefix, viewset, basename=None, base_name=None): - if base_name is not None: - msg = "The `base_name` argument is pending deprecation in favor of `basename`." - warnings.warn(msg, RemovedInDRF311Warning, 2) - - assert not (basename and base_name), ( - "Do not provide both the `basename` and `base_name` arguments.") - - if basename is None: - basename = base_name - + def register(self, prefix, viewset, basename=None): if basename is None: basename = self.get_default_basename(viewset) self.registry.append((prefix, viewset, basename)) diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 3b7e7f963..027472db1 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -6,7 +6,7 @@ See schemas.__init__.py for package overview. import re from weakref import WeakKeyDictionary -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from rest_framework.settings import api_settings from rest_framework.utils import formatting @@ -82,7 +82,7 @@ class ViewInspector: method_docstring = getattr(view, method_name, None).__doc__ if method_docstring: # An explicit docstring on the method or action. - return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring))) + return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring))) else: return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description()) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 134df5043..3a7eb29a7 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -32,39 +32,30 @@ class SchemaGenerator(BaseSchemaGenerator): return info - def get_paths(self, request=None): - result = {} - - paths, view_endpoints = self._get_paths_and_endpoints(request) - - # Only generate the path prefix for paths that will be included - if not paths: - return None - - for path, method, view in view_endpoints: - if not self.has_view_permissions(path, method, view): - continue - operation = view.schema.get_operation(path, method) - # Normalise path for any provided mount url. - if path.startswith('/'): - path = path[1:] - path = urljoin(self.url or '/', path) - - result.setdefault(path, {}) - result[path][method.lower()] = operation - - return result - def get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ self._initialise_endpoints() - paths = self.get_paths(None if public else request) - if not paths: - return None + # Iterate endpoints generating per method path operations. + # TODO: …and reference components. + paths = {} + _, view_endpoints = self._get_paths_and_endpoints(None if public else request) + for path, method, view in view_endpoints: + if not self.has_view_permissions(path, method, view): + continue + operation = view.schema.get_operation(path, method) + # Normalise path for any provided mount url. + if path.startswith('/'): + path = path[1:] + path = urljoin(self.url or '/', path) + + paths.setdefault(path, {}) + paths[path][method.lower()] = operation + + # Compile final schema. schema = { 'openapi': '3.0.2', 'info': self.get_info(), @@ -268,13 +259,7 @@ class AutoSchema(ViewInspector): 'items': {}, } if not isinstance(field.child, _UnvalidatedField): - map_field = self._map_field(field.child) - items = { - "type": map_field.get('type') - } - if 'format' in map_field: - items['format'] = map_field.get('format') - mapping['items'] = items + mapping['items'] = self._map_field(field.child) return mapping # DateField and DateTimeField type is string @@ -393,7 +378,7 @@ class AutoSchema(ViewInspector): schema['writeOnly'] = True if field.allow_null: schema['nullable'] = True - if field.default and field.default != empty: # why don't they use None?! + if field.default and field.default != empty and not callable(field.default): schema['default'] = field.default if field.help_text: schema['description'] = str(field.help_text) @@ -464,7 +449,7 @@ class AutoSchema(ViewInspector): media_types.append(renderer.media_type) return media_types - def _get_serializer(self, method, path): + def _get_serializer(self, path, method): view = self.view if not hasattr(view, 'get_serializer'): diff --git a/rest_framework/schemas/utils.py b/rest_framework/schemas/utils.py index 6724eb428..60ed69829 100644 --- a/rest_framework/schemas/utils.py +++ b/rest_framework/schemas/utils.py @@ -4,7 +4,7 @@ utils.py # Shared helper functions See schemas.__init__.py for package overview. """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework.mixins import RetrieveModelMixin diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f5d9a5065..8c2486bea 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -19,7 +19,6 @@ from collections.abc import Mapping from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models -from django.db.models import DurationField as ModelDurationField from django.db.models.fields import Field as DjangoModelField from django.utils import timezone from django.utils.functional import cached_property @@ -167,13 +166,6 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): - assert not hasattr(self, 'save_object'), ( - 'Serializer `%s.%s` has old-style version 2 `.save_object()` ' - 'that is no longer compatible with REST framework 3. ' - 'Use the new-style `.create()` and `.update()` methods instead.' % - (self.__class__.__module__, self.__class__.__name__) - ) - assert hasattr(self, '_errors'), ( 'You must call `.is_valid()` before calling `.save()`.' ) @@ -217,13 +209,6 @@ class BaseSerializer(Field): return self.instance def is_valid(self, raise_exception=False): - assert not hasattr(self, 'restore_object'), ( - 'Serializer `%s.%s` has old-style version 2 `.restore_object()` ' - 'that is no longer compatible with REST framework 3. ' - 'Use the new-style `.create()` and `.update()` methods instead.' % - (self.__class__.__module__, self.__class__.__name__) - ) - assert hasattr(self, 'initial_data'), ( 'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'passed when instantiating the serializer instance.' @@ -298,18 +283,22 @@ class SerializerMetaclass(type): if isinstance(obj, Field)] fields.sort(key=lambda x: x[1]._creation_counter) - # If this class is subclassing another Serializer, add that Serializer's - # fields. Note that we loop over the bases in *reverse*. This is necessary - # in order to maintain the correct order of fields. - for base in reversed(bases): - if hasattr(base, '_declared_fields'): - fields = [ - (field_name, obj) for field_name, obj - in base._declared_fields.items() - if field_name not in attrs - ] + fields + # Ensures a base class field doesn't override cls attrs, and maintains + # field precedence when inheriting multiple parents. e.g. if there is a + # class C(A, B), and A and B both define 'field', use 'field' from A. + known = set(attrs) - return OrderedDict(fields) + def visit(name): + known.add(name) + return name + + base_fields = [ + (visit(name), f) + for base in bases if hasattr(base, '_declared_fields') + for name, f in base._declared_fields.items() if name not in known + ] + + return OrderedDict(base_fields + fields) def __new__(cls, name, bases, attrs): attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) @@ -448,7 +437,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): default = field.get_default() except SkipField: continue - defaults[field.field_name] = default + defaults[field.source] = default return defaults @@ -872,6 +861,7 @@ class ModelSerializer(Serializer): models.DateField: DateField, models.DateTimeField: DateTimeField, models.DecimalField: DecimalField, + models.DurationField: DurationField, models.EmailField: EmailField, models.Field: ModelField, models.FileField: FileField, @@ -886,11 +876,14 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, + models.UUIDField: UUIDField, models.GenericIPAddressField: IPAddressField, models.FilePathField: FilePathField, } - if ModelDurationField is not None: - serializer_field_mapping[ModelDurationField] = DurationField + if postgres_fields: + serializer_field_mapping[postgres_fields.HStoreField] = HStoreField + serializer_field_mapping[postgres_fields.ArrayField] = ListField + serializer_field_mapping[postgres_fields.JSONField] = JSONField serializer_related_field = PrimaryKeyRelatedField serializer_related_to_field = SlugRelatedField serializer_url_field = HyperlinkedIdentityField @@ -1581,19 +1574,6 @@ class ModelSerializer(Serializer): return validators -if hasattr(models, 'UUIDField'): - ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField - -# IPAddressField is deprecated in Django -if hasattr(models, 'IPAddressField'): - ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField - -if postgres_fields: - ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = HStoreField - ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField - ModelSerializer.serializer_field_mapping[postgres_fields.JSONField] = JSONField - - class HyperlinkedModelSerializer(ModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead diff --git a/rest_framework/settings.py b/rest_framework/settings.py index c4c0e7939..9eb4c5653 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -182,14 +182,19 @@ def import_from_string(val, setting_name): class APISettings: """ - A settings object, that allows API settings to be accessed as properties. - For example: + A settings object that allows REST Framework settings to be accessed as + properties. For example: from rest_framework.settings import api_settings print(api_settings.DEFAULT_RENDERER_CLASSES) Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. + + Note: + This is an internal class that is only compatible with settings namespaced + under the REST_FRAMEWORK name. It is not intended to be used by 3rd-party + apps, and test helpers like `override_settings` may not work as expected. """ def __init__(self, user_settings=None, defaults=None, import_strings=None): if user_settings: diff --git a/rest_framework/status.py b/rest_framework/status.py index 06d090733..2561d7689 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -66,6 +66,7 @@ HTTP_414_REQUEST_URI_TOO_LONG = 414 HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 HTTP_417_EXPECTATION_FAILED = 417 +HTTP_418_IM_A_TEAPOT = 418 HTTP_422_UNPROCESSABLE_ENTITY = 422 HTTP_423_LOCKED = 423 HTTP_424_FAILED_DEPENDENCY = 424 diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b90c3eead..a25880d0f 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -91,7 +91,8 @@ def get_field_kwargs(field_name, model_field): if isinstance(model_field, models.SlugField): kwargs['allow_unicode'] = model_field.allow_unicode - if isinstance(model_field, models.TextField) or (postgres_fields and isinstance(model_field, postgres_fields.JSONField)): + if isinstance(model_field, models.TextField) and not model_field.choices or \ + (postgres_fields and isinstance(model_field, postgres_fields.JSONField)): kwargs['style'] = {'base_template': 'textarea.html'} if isinstance(model_field, models.AutoField) or not model_field.editable: diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 1cbe31b5e..a5cb75a84 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -37,44 +37,39 @@ class UniqueValidator: Should be applied to an individual field on the serializer. """ message = _('This field must be unique.') + requires_context = True def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset - self.serializer_field = None self.message = message or self.message self.lookup = lookup - def set_context(self, serializer_field): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the underlying model field name. This may not be the - # same as the serializer field name if `source=<>` is set. - self.field_name = serializer_field.source_attrs[-1] - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer_field.parent, 'instance', None) - - def filter_queryset(self, value, queryset): + def filter_queryset(self, value, queryset, field_name): """ Filter the queryset to all instances matching the given attribute. """ - filter_kwargs = {'%s__%s' % (self.field_name, self.lookup): value} + filter_kwargs = {'%s__%s' % (field_name, self.lookup): value} return qs_filter(queryset, **filter_kwargs) - def exclude_current_instance(self, queryset): + def exclude_current_instance(self, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, value): + def __call__(self, value, serializer_field): + # Determine the underlying model field name. This may not be the + # same as the serializer field name if `source=<>` is set. + field_name = serializer_field.source_attrs[-1] + # Determine the existing instance, if this is an update operation. + instance = getattr(serializer_field.parent, 'instance', None) + queryset = self.queryset - queryset = self.filter_queryset(value, queryset) - queryset = self.exclude_current_instance(queryset) + queryset = self.filter_queryset(value, queryset, field_name) + queryset = self.exclude_current_instance(queryset, instance) if qs_exists(queryset): raise ValidationError(self.message, code='unique') @@ -93,69 +88,67 @@ class UniqueTogetherValidator: """ message = _('The fields {field_names} must make a unique set.') missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, fields, message=None): self.queryset = queryset self.fields = fields - self.serializer_field = None self.message = message or self.message - def set_context(self, serializer): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer, 'instance', None) - - def enforce_required_fields(self, attrs): + def enforce_required_fields(self, attrs, serializer): """ The `UniqueTogetherValidator` always forces an implied 'required' state on the fields it applies to. """ - if self.instance is not None: + if serializer.instance is not None: return missing_items = { field_name: self.missing_message for field_name in self.fields - if field_name not in attrs + if serializer.fields[field_name].source not in attrs } if missing_items: raise ValidationError(missing_items, code='required') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, serializer): """ Filter the queryset to all instances matching the given attributes. """ + # field names => field sources + sources = [ + serializer.fields[field_name].source + for field_name in self.fields + ] + # If this is an update, then any unprovided field should # have it's value set based on the existing instance attribute. - if self.instance is not None: - for field_name in self.fields: - if field_name not in attrs: - attrs[field_name] = getattr(self.instance, field_name) + if serializer.instance is not None: + for source in sources: + if source not in attrs: + attrs[source] = getattr(serializer.instance, source) # Determine the filter keyword arguments and filter the queryset. filter_kwargs = { - field_name: attrs[field_name] - for field_name in self.fields + source: attrs[source] + for source in sources } return qs_filter(queryset, **filter_kwargs) - def exclude_current_instance(self, attrs, queryset): + def exclude_current_instance(self, attrs, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, attrs): - self.enforce_required_fields(attrs) + def __call__(self, attrs, serializer): + self.enforce_required_fields(attrs, serializer) queryset = self.queryset - queryset = self.filter_queryset(attrs, queryset) - queryset = self.exclude_current_instance(attrs, queryset) + queryset = self.filter_queryset(attrs, queryset, serializer) + queryset = self.exclude_current_instance(attrs, queryset, serializer.instance) # Ignore validation if any field is None checked_values = [ @@ -174,9 +167,21 @@ class UniqueTogetherValidator: ) +class ProhibitSurrogateCharactersValidator: + message = _('Surrogate characters are not allowed: U+{code_point:X}.') + code = 'surrogate_characters_not_allowed' + + def __call__(self, value): + for surrogate_character in (ch for ch in str(value) + if 0xD800 <= ord(ch) <= 0xDFFF): + message = self.message.format(code_point=ord(surrogate_character)) + raise ValidationError(message, code=self.code) + + class BaseUniqueForValidator: message = None missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, field, date_field, message=None): self.queryset = queryset @@ -184,18 +189,6 @@ class BaseUniqueForValidator: self.date_field = date_field self.message = message or self.message - def set_context(self, serializer): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the underlying model field names. These may not be the - # same as the serializer field names if `source=<>` is set. - self.field_name = serializer.fields[self.field].source_attrs[-1] - self.date_field_name = serializer.fields[self.date_field].source_attrs[-1] - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer, 'instance', None) - def enforce_required_fields(self, attrs): """ The `UniqueForValidator` classes always force an implied @@ -209,23 +202,28 @@ class BaseUniqueForValidator: if missing_items: raise ValidationError(missing_items, code='required') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): raise NotImplementedError('`filter_queryset` must be implemented.') - def exclude_current_instance(self, attrs, queryset): + def exclude_current_instance(self, attrs, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, attrs): + def __call__(self, attrs, serializer): + # Determine the underlying model field names. These may not be the + # same as the serializer field names if `source=<>` is set. + field_name = serializer.fields[self.field].source_attrs[-1] + date_field_name = serializer.fields[self.date_field].source_attrs[-1] + self.enforce_required_fields(attrs) queryset = self.queryset - queryset = self.filter_queryset(attrs, queryset) - queryset = self.exclude_current_instance(attrs, queryset) + queryset = self.filter_queryset(attrs, queryset, field_name, date_field_name) + queryset = self.exclude_current_instance(attrs, queryset, serializer.instance) if qs_exists(queryset): message = self.message.format(date_field=self.date_field) raise ValidationError({ @@ -244,39 +242,39 @@ class BaseUniqueForValidator: class UniqueForDateValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" date.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__day' % self.date_field_name] = date.day - filter_kwargs['%s__month' % self.date_field_name] = date.month - filter_kwargs['%s__year' % self.date_field_name] = date.year + filter_kwargs[field_name] = value + filter_kwargs['%s__day' % date_field_name] = date.day + filter_kwargs['%s__month' % date_field_name] = date.month + filter_kwargs['%s__year' % date_field_name] = date.year return qs_filter(queryset, **filter_kwargs) class UniqueForMonthValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" month.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__month' % self.date_field_name] = date.month + filter_kwargs[field_name] = value + filter_kwargs['%s__month' % date_field_name] = date.month return qs_filter(queryset, **filter_kwargs) class UniqueForYearValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" year.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__year' % self.date_field_name] = date.year + filter_kwargs[field_name] = value + filter_kwargs['%s__year' % date_field_name] = date.year return qs_filter(queryset, **filter_kwargs) diff --git a/rest_framework/views.py b/rest_framework/views.py index bec10560a..69db053d6 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -7,7 +7,7 @@ from django.db import connection, models, transaction from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from django.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -56,7 +56,7 @@ def get_view_description(view, html=False): if description is None: description = view.__class__.__doc__ or '' - description = formatting.dedent(smart_text(description)) + description = formatting.dedent(smart_str(description)) if html: return formatting.markup_description(description) return description diff --git a/setup.py b/setup.py index c9d6443d5..65536885a 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ This will install the latest version of Django REST Framework which works on your version of Python. If you can't upgrade your pip (or Python), request an older version of Django REST Framework: - $ python -m pip install "django<3.10" + $ python -m pip install "djangorestframework<3.10" """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) sys.exit(1) @@ -82,7 +82,7 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=[], + install_requires=["django>=1.11"], python_requires=">=3.5", zip_safe=False, classifiers=[ diff --git a/tests/conftest.py b/tests/conftest.py index ac29e4a42..d28edeb8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,19 +67,22 @@ def pytest_configure(config): ) # guardian is optional - try: - import guardian # NOQA - except ImportError: - pass - else: - settings.ANONYMOUS_USER_ID = -1 - settings.AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'guardian.backends.ObjectPermissionBackend', - ) - settings.INSTALLED_APPS += ( - 'guardian', - ) + # Note that for the test cases we're installing a version of django-guardian + # that's only compatible with Django 2.0+. + if django.VERSION >= (2, 0, 0): + try: + import guardian # NOQA + except ImportError: + pass + else: + settings.ANONYMOUS_USER_ID = -1 + settings.AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', + ) + settings.INSTALLED_APPS += ( + 'guardian', + ) if config.getoption('--no-pkgroot'): sys.path.pop(0) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 622f78cdd..f734fd169 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -51,7 +51,9 @@ class TestFieldMapping(TestCase): (serializers.ListField(child=serializers.FloatField()), {'items': {'type': 'number'}, 'type': 'array'}), (serializers.ListField(child=serializers.CharField()), {'items': {'type': 'string'}, 'type': 'array'}), (serializers.ListField(child=serializers.IntegerField(max_value=4294967295)), - {'items': {'type': 'integer', 'format': 'int64'}, 'type': 'array'}), + {'items': {'type': 'integer', 'maximum': 4294967295, 'format': 'int64'}, 'type': 'array'}), + (serializers.ListField(child=serializers.ChoiceField(choices=[('a', 'Choice A'), ('b', 'Choice B')])), + {'items': {'enum': ['a', 'b']}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] @@ -571,6 +573,22 @@ class TestOperationIntrospection(TestCase): properties = response_schema['items']['properties'] assert properties['hstore']['type'] == 'object' + def test_serializer_callable_default(self): + path = '/' + method = 'GET' + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + responses = inspector._get_responses(path, method) + response_schema = responses['200']['content']['application/json']['schema'] + properties = response_schema['items']['properties'] + assert 'default' not in properties['uuid_field'] + def test_serializer_validators(self): path = '/' method = 'GET' @@ -643,7 +661,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/example/' in paths example_operations = paths['/example/'] @@ -660,7 +678,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/v1/example/' in paths assert '/v1/example/{id}/' in paths @@ -673,7 +691,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns, url='/api') generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/api/example/' in paths assert '/api/example/{id}/' in paths @@ -691,6 +709,15 @@ class TestGenerator(TestCase): assert 'openapi' in schema assert 'paths' in schema + def test_schema_with_no_paths(self): + patterns = [] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + + assert schema['paths'] == {} + def test_schema_information(self): """Construction of the top level dictionary.""" patterns = [ diff --git a/tests/schemas/views.py b/tests/schemas/views.py index f8d143e71..e8307ccbd 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -58,6 +58,7 @@ class ExampleSerializer(serializers.Serializer): date = serializers.DateField() datetime = serializers.DateTimeField() hstore = serializers.HStoreField() + uuid_field = serializers.UUIDField(default=uuid.uuid4) class ExampleGenericAPIView(generics.GenericAPIView): diff --git a/tests/test_fields.py b/tests/test_fields.py index 1d302b730..a4b78fd51 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -565,11 +565,10 @@ class TestCreateOnlyDefault: on the callable if possible """ class TestCallableDefault: - def set_context(self, serializer_field): - self.field = serializer_field + requires_context = True - def __call__(self): - return "success" if hasattr(self, 'field') else "failure" + def __call__(self, field=None): + return "success" if field is not None else "failure" class TestSerializer(serializers.Serializer): context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault())) @@ -759,6 +758,21 @@ class TestCharField(FieldValues): 'Null characters are not allowed.' ] + def test_surrogate_characters(self): + field = serializers.CharField() + + for code_point, expected_message in ( + (0xD800, 'Surrogate characters are not allowed: U+D800.'), + (0xDFFF, 'Surrogate characters are not allowed: U+DFFF.'), + ): + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation(chr(code_point)) + assert exc_info.value.detail[0].code == 'surrogate_characters_not_allowed' + assert str(exc_info.value.detail[0]) == expected_message + + for code_point in (0xD800 - 1, 0xDFFF + 1): + field.run_validation(chr(code_point)) + def test_iterable_validators(self): """ Ensure `validators` parameter is compatible with reasonable iterables. diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 21ec82347..fbb562792 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -89,6 +89,7 @@ class FieldOptionsModel(models.Model): default_field = models.IntegerField(default=0) descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label') choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES) + text_choices_field = models.TextField(choices=COLOR_CHOICES) class ChoicesModel(models.Model): @@ -211,6 +212,7 @@ class TestRegularFieldMappings(TestCase): default_field = IntegerField(required=False) descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) + text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) """) self.assertEqual(repr(TestSerializer()), expected) diff --git a/tests/test_relations.py b/tests/test_relations.py index c89293415..9f05e3b31 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -145,14 +145,18 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk.int -@override_settings(ROOT_URLCONF=[ +urlpatterns = [ url(r'^example/(?P.+)/$', lambda: None, name='example'), -]) +] + + +@override_settings(ROOT_URLCONF='tests.test_relations') class TestHyperlinkedRelatedField(APISimpleTestCase): def setUp(self): self.queryset = MockQueryset([ MockObject(pk=1, name='foobar'), MockObject(pk=2, name='bazABCqux'), + MockObject(pk=2, name='bazABC qux'), ]) self.field = serializers.HyperlinkedRelatedField( view_name='example', @@ -191,6 +195,10 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/') assert instance is self.queryset.items[1] + def test_hyperlinked_related_lookup_url_space_encoded_exists(self): + instance = self.field.to_internal_value('http://example.org/example/bazABC%20qux/') + assert instance is self.queryset.items[2] + 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/') diff --git a/tests/test_routers.py b/tests/test_routers.py index 0f428e2a5..ff927ff33 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,4 +1,3 @@ -import warnings from collections import namedtuple import pytest @@ -8,9 +7,7 @@ from django.db import models from django.test import TestCase, override_settings from django.urls import resolve, reverse -from rest_framework import ( - RemovedInDRF311Warning, permissions, serializers, viewsets -) +from rest_framework import permissions, serializers, viewsets from rest_framework.compat import get_regex_pattern from rest_framework.decorators import action from rest_framework.response import Response @@ -488,71 +485,3 @@ class TestViewInitkwargs(URLPatternsTestCase, TestCase): initkwargs = match.func.initkwargs assert initkwargs['basename'] == 'routertestmodel' - - -class TestBaseNameRename(TestCase): - - def test_base_name_and_basename_assertion(self): - router = SimpleRouter() - - msg = "Do not provide both the `basename` and `base_name` arguments." - with warnings.catch_warnings(record=True) as w, \ - self.assertRaisesMessage(AssertionError, msg): - warnings.simplefilter('always') - router.register('mock', MockViewSet, 'mock', base_name='mock') - - msg = "The `base_name` argument is pending deprecation in favor of `basename`." - assert len(w) == 1 - assert str(w[0].message) == msg - - def test_base_name_argument_deprecation(self): - router = SimpleRouter() - - with pytest.warns(RemovedInDRF311Warning) as w: - warnings.simplefilter('always') - router.register('mock', MockViewSet, base_name='mock') - - msg = "The `base_name` argument is pending deprecation in favor of `basename`." - assert len(w) == 1 - assert str(w[0].message) == msg - assert router.registry == [ - ('mock', MockViewSet, 'mock'), - ] - - def test_basename_argument_no_warnings(self): - router = SimpleRouter() - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - router.register('mock', MockViewSet, basename='mock') - - assert len(w) == 0 - assert router.registry == [ - ('mock', MockViewSet, 'mock'), - ] - - def test_get_default_base_name_deprecation(self): - msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`." - - # Class definition should raise a warning - with pytest.warns(RemovedInDRF311Warning) as w: - warnings.simplefilter('always') - - class CustomRouter(SimpleRouter): - def get_default_base_name(self, viewset): - return 'foo' - - assert len(w) == 1 - assert str(w[0].message) == msg - - # Deprecated method implementation should still be called - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - - router = CustomRouter() - router.register('mock', MockViewSet) - - assert len(w) == 0 - assert router.registry == [ - ('mock', MockViewSet, 'foo'), - ] diff --git a/tests/test_serializer.py b/tests/test_serializer.py index fab0472b9..a58c46b2d 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -682,3 +682,53 @@ class TestDeclaredFieldInheritance: assert len(Parent().get_fields()) == 2 assert len(Child().get_fields()) == 2 assert len(Grandchild().get_fields()) == 2 + + def test_multiple_inheritance(self): + class A(serializers.Serializer): + field = serializers.CharField() + + class B(serializers.Serializer): + field = serializers.IntegerField() + + class TestSerializer(A, B): + pass + + fields = { + name: type(f) for name, f + in TestSerializer()._declared_fields.items() + } + assert fields == { + 'field': serializers.CharField, + } + + def test_field_ordering(self): + class Base(serializers.Serializer): + f1 = serializers.CharField() + f2 = serializers.CharField() + + class A(Base): + f3 = serializers.IntegerField() + + class B(serializers.Serializer): + f3 = serializers.CharField() + f4 = serializers.CharField() + + class TestSerializer(A, B): + f2 = serializers.IntegerField() + f5 = serializers.CharField() + + fields = { + name: type(f) for name, f + in TestSerializer()._declared_fields.items() + } + + # `IntegerField`s should be the 'winners' in field name conflicts + # - `TestSerializer.f2` should override `Base.F2` + # - `A.f3` should override `B.f3` + assert fields == { + 'f1': serializers.CharField, + 'f2': serializers.IntegerField, + 'f3': serializers.IntegerField, + 'f4': serializers.CharField, + 'f5': serializers.CharField, + } diff --git a/tests/test_validators.py b/tests/test_validators.py index fe31ba235..21c00073d 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -301,6 +301,49 @@ class TestUniquenessTogetherValidation(TestCase): ] } + def test_read_only_fields_with_default_and_source(self): + class ReadOnlySerializer(serializers.ModelSerializer): + name = serializers.CharField(source='race_name', default='test', read_only=True) + + class Meta: + model = UniquenessTogetherModel + fields = ['name', 'position'] + validators = [ + UniqueTogetherValidator( + queryset=UniquenessTogetherModel.objects.all(), + fields=['name', 'position'] + ) + ] + + serializer = ReadOnlySerializer(data={'position': 1}) + assert serializer.is_valid(raise_exception=True) + + def test_writeable_fields_with_source(self): + class WriteableSerializer(serializers.ModelSerializer): + name = serializers.CharField(source='race_name') + + class Meta: + model = UniquenessTogetherModel + fields = ['name', 'position'] + validators = [ + UniqueTogetherValidator( + queryset=UniquenessTogetherModel.objects.all(), + fields=['name', 'position'] + ) + ] + + serializer = WriteableSerializer(data={'name': 'test', 'position': 1}) + assert serializer.is_valid(raise_exception=True) + + # Validation error should use seriazlier field name, not source + serializer = WriteableSerializer(data={'position': 1}) + assert not serializer.is_valid() + assert serializer.errors == { + 'name': [ + 'This field is required.' + ] + } + def test_allow_explict_override(self): """ Ensure validators can be explicitly removed.. @@ -359,10 +402,10 @@ class TestUniquenessTogetherValidation(TestCase): data = {'race_name': 'bar'} queryset = MockQueryset() + serializer = UniquenessTogetherSerializer(instance=self.instance) validator = UniqueTogetherValidator(queryset, fields=('race_name', 'position')) - validator.instance = self.instance - validator.filter_queryset(attrs=data, queryset=queryset) + validator.filter_queryset(attrs=data, queryset=queryset, serializer=serializer) assert queryset.called_with == {'race_name': 'bar', 'position': 1} @@ -586,4 +629,6 @@ class ValidatorsTests(TestCase): validator = BaseUniqueForValidator(queryset=object(), field='foo', date_field='bar') with pytest.raises(NotImplementedError): - validator.filter_queryset(attrs=None, queryset=None) + validator.filter_queryset( + attrs=None, queryset=None, field_name='', date_field_name='' + ) diff --git a/tox.ini b/tox.ini index 587c469b1..9b8069174 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = {py35,py36,py37}-django20, {py35,py36,py37}-django21 {py35,py36,py37}-django22 + {py36,py37,py38}-django30, {py36,py37,py38}-djangomaster, base,dist,lint,docs, @@ -13,6 +14,7 @@ DJANGO = 2.0: django20 2.1: django21 2.2: django22 + 3.0: django30 master: djangomaster [testenv] @@ -26,6 +28,7 @@ deps = django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 + django30: Django>=3.0,<3.1 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt