mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-27 08:29:59 +03:00
Merge branch 'master' into patch-4
This commit is contained in:
commit
4b7cf3f7da
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
|||
github: encode
|
||||
custom: https://fund.django-rest-framework.org/topics/funding/
|
||||
|
|
10
.github/ISSUE_TEMPLATE/1-issue.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/1-issue.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Issue
|
||||
about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Raised initially as discussion #...
|
||||
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
|
||||
- [ ] I have reduced the issue to the simplest possible case.
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/encode/django-rest-framework/discussions
|
||||
about: >
|
||||
The "Discussions" forum is where you want to start. 💖
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,6 +2,8 @@
|
|||
*.db
|
||||
*~
|
||||
.*
|
||||
*.py.bak
|
||||
|
||||
|
||||
/site/
|
||||
/htmlcov/
|
||||
|
|
13
.travis.yml
13
.travis.yml
|
@ -10,19 +10,21 @@ matrix:
|
|||
- { python: "3.6", env: DJANGO=2.2 }
|
||||
- { python: "3.6", env: DJANGO=3.0 }
|
||||
- { python: "3.6", env: DJANGO=3.1 }
|
||||
- { python: "3.6", env: DJANGO=master }
|
||||
- { python: "3.6", env: DJANGO=3.2 }
|
||||
|
||||
- { python: "3.7", env: DJANGO=2.2 }
|
||||
- { python: "3.7", env: DJANGO=3.0 }
|
||||
- { python: "3.7", env: DJANGO=3.1 }
|
||||
- { python: "3.7", env: DJANGO=master }
|
||||
- { python: "3.7", env: DJANGO=3.2 }
|
||||
|
||||
- { python: "3.8", env: DJANGO=3.0 }
|
||||
- { python: "3.8", env: DJANGO=3.1 }
|
||||
- { python: "3.8", env: DJANGO=master }
|
||||
- { python: "3.8", env: DJANGO=3.2 }
|
||||
- { python: "3.8", env: DJANGO=main }
|
||||
|
||||
- { python: "3.9", env: DJANGO=3.1 }
|
||||
- { python: "3.9", env: DJANGO=master }
|
||||
- { python: "3.9", env: DJANGO=3.2 }
|
||||
- { python: "3.9", env: DJANGO=main }
|
||||
|
||||
- { python: "3.8", env: TOXENV=base }
|
||||
- { python: "3.8", env: TOXENV=lint }
|
||||
|
@ -37,7 +39,8 @@ matrix:
|
|||
- tox # test sdist
|
||||
|
||||
allow_failures:
|
||||
- env: DJANGO=master
|
||||
- env: DJANGO=main
|
||||
- env: DJANGO=3.2
|
||||
|
||||
install:
|
||||
- pip install tox tox-travis
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
## Checklist
|
||||
|
||||
- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
|
||||
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
|
||||
- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
|
||||
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
|
||||
- [ ] I have reduced the issue to the simplest possible case.
|
||||
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
## Expected behavior
|
||||
|
||||
## Actual behavior
|
|
@ -113,7 +113,7 @@ router.register(r'users', UserViewSet)
|
|||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -131,7 +131,7 @@ REST_FRAMEWORK = {
|
|||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# or allow read-only access for unauthenticated users.
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -357,7 +357,7 @@ The following third party packages are also available.
|
|||
|
||||
## Django OAuth Toolkit
|
||||
|
||||
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
|
||||
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
|
||||
|
||||
#### Installation & configuration
|
||||
|
||||
|
@ -432,6 +432,16 @@ There are currently two forks of this project.
|
|||
|
||||
[drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's own TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number.
|
||||
|
||||
## django-rest-authemail
|
||||
|
||||
[django-rest-authemail][django-rest-authemail] provides a RESTful API interface for user signup and authentication. Email addresses are used for authentication, rather than usernames. API endpoints are available for signup, signup email verification, login, logout, password reset, password reset verification, email change, email change verification, password change, and user detail. A fully-functional example project and detailed instructions are included.
|
||||
|
||||
## Django-Rest-Durin
|
||||
|
||||
[Django-Rest-Durin][django-rest-durin] is built with the idea to have one library that does token auth for multiple Web/CLI/Mobile API clients via one interface but allows different token configuration for each API Client that consumes the API. It provides support for multiple tokens per user via custom models, views, permissions that work with Django-Rest-Framework. The token expiration time can be different per API client and is customizable via the Django Admin Interface.
|
||||
|
||||
More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html).
|
||||
|
||||
[cite]: https://jacobian.org/writing/rest-worst-practices/
|
||||
[http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||
[http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||
|
@ -448,7 +458,7 @@ There are currently two forks of this project.
|
|||
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
|
||||
[oauth-1.0a]: https://oauth.net/core/1.0a/
|
||||
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
|
||||
[evonove]: https://github.com/evonove/
|
||||
[jazzband]: https://github.com/jazzband/
|
||||
[oauthlib]: https://github.com/idan/oauthlib
|
||||
[djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt
|
||||
[etoccalino]: https://github.com/etoccalino/
|
||||
|
@ -466,3 +476,5 @@ There are currently two forks of this project.
|
|||
[django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2
|
||||
[django-rest-knox]: https://github.com/James1345/django-rest-knox
|
||||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
||||
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
||||
|
|
|
@ -13,13 +13,13 @@ provided in Django.
|
|||
|
||||
Django provides a [`method_decorator`][decorator] to use
|
||||
decorators with class based views. This can be used with
|
||||
other cache decorators such as [`cache_page`][page] and
|
||||
[`vary_on_cookie`][cookie].
|
||||
other cache decorators such as [`cache_page`][page],
|
||||
[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers].
|
||||
|
||||
```python
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from django.views.decorators.vary import vary_on_cookie, vary_on_headers
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
@ -27,8 +27,7 @@ from rest_framework import viewsets
|
|||
|
||||
|
||||
class UserViewSet(viewsets.ViewSet):
|
||||
|
||||
# Cache requested url for each user for 2 hours
|
||||
# With cookie: cache requested url for each user for 2 hours
|
||||
@method_decorator(cache_page(60*60*2))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def list(self, request, format=None):
|
||||
|
@ -38,8 +37,18 @@ class UserViewSet(viewsets.ViewSet):
|
|||
return Response(content)
|
||||
|
||||
|
||||
class PostView(APIView):
|
||||
class ProfileView(APIView):
|
||||
# With auth: cache requested url for each user for 2 hours
|
||||
@method_decorator(cache_page(60*60*2))
|
||||
@method_decorator(vary_on_headers("Authorization",))
|
||||
def get(self, request, format=None):
|
||||
content = {
|
||||
'user_feed': request.user.get_user_feed()
|
||||
}
|
||||
return Response(content)
|
||||
|
||||
|
||||
class PostView(APIView):
|
||||
# Cache page for the requested url
|
||||
@method_decorator(cache_page(60*60*2))
|
||||
def get(self, request, format=None):
|
||||
|
@ -55,4 +64,5 @@ class PostView(APIView):
|
|||
|
||||
[page]: https://docs.djangoproject.com/en/dev/topics/cache/#the-per-view-cache
|
||||
[cookie]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_cookie
|
||||
[headers]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_headers
|
||||
[decorator]: https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/#decorating-the-class
|
||||
|
|
|
@ -222,7 +222,7 @@ By default this exception results in a response with the HTTP status code "429 T
|
|||
The `ValidationError` exception is slightly different from the other `APIException` classes:
|
||||
|
||||
* The `detail` argument is mandatory, not optional.
|
||||
* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure.
|
||||
* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. By using a dictionary, you can specify field-level errors while performing object-level validation in the `validate()` method of a serializer. For example. `raise serializers.ValidationError({'name': 'Please enter a valid name.'})`
|
||||
* By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')`
|
||||
|
||||
The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument:
|
||||
|
|
|
@ -75,7 +75,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
|
|||
by filtering against a `username` query parameter in the URL.
|
||||
"""
|
||||
queryset = Purchase.objects.all()
|
||||
username = self.request.query_params.get('username', None)
|
||||
username = self.request.query_params.get('username')
|
||||
if username is not None:
|
||||
queryset = queryset.filter(purchaser__username=username)
|
||||
return queryset
|
||||
|
|
|
@ -70,6 +70,8 @@ For performance reasons the generic views will not automatically apply object le
|
|||
|
||||
Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view.
|
||||
|
||||
Because the `get_object()` method is not called, object level permissions from the `has_object_permission()` method **are not applied** when creating objects. In order to restrict object creation you need to implement the permission check either in your Serializer class or override the `perform_create()` method of your ViewSet class.
|
||||
|
||||
## Setting the permission policy
|
||||
|
||||
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
|
||||
|
@ -116,7 +118,7 @@ Or, if you're using the `@api_view` decorator with function based views.
|
|||
}
|
||||
return Response(content)
|
||||
|
||||
__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file.
|
||||
__Note:__ when you set new permission classes via the class attribute or decorators you're telling the view to ignore the default list set in the __settings.py__ file.
|
||||
|
||||
Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written:
|
||||
|
||||
|
@ -169,7 +171,7 @@ This permission is suitable if you want to your API to allow read permissions to
|
|||
|
||||
## DjangoModelPermissions
|
||||
|
||||
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
|
||||
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
|
||||
|
||||
* `POST` requests require the user to have the `add` permission on the model.
|
||||
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
|
||||
|
@ -179,12 +181,6 @@ The default behaviour can also be overridden to support custom model permissions
|
|||
|
||||
To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
|
||||
|
||||
#### Using with views that do not include a `queryset` attribute.
|
||||
|
||||
If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sentinel queryset, so that this class can determine the required permissions. For example:
|
||||
|
||||
queryset = User.objects.none() # Required for DjangoModelPermissions
|
||||
|
||||
## DjangoModelPermissionsOrAnonReadOnly
|
||||
|
||||
Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API.
|
||||
|
@ -278,6 +274,30 @@ Note that the generic views will check the appropriate object level permissions,
|
|||
|
||||
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details.
|
||||
|
||||
# Overview of access restriction methods
|
||||
|
||||
REST framework offers three different methods to customize access restrictions on a case-by-case basis. These apply in different scenarios and have different effects and limitations.
|
||||
|
||||
* `queryset`/`get_queryset()`: Limits the general visibility of existing objects from the database. The queryset limits which objects will be listed and which objects can be modified or deleted. The `get_queryset()` method can apply different querysets based on the current action.
|
||||
* `permission_classes`/`get_permissions()`: General permission checks based on the current action, request and targeted object. Object level permissions can only be applied to retrieve, modify and deletion actions. Permission checks for list and create will be applied to the entire object type. (In case of list: subject to restrictions in the queryset.)
|
||||
* `serializer_class`/`get_serializer()`: Instance level restrictions that apply to all objects on input and output. The serializer may have access to the request context. The `get_serializer()` method can apply different serializers based on the current action.
|
||||
|
||||
The following table lists the access restriction methods and the level of control they offer over which actions.
|
||||
|
||||
| | `queryset` | `permission_classes` | `serializer_class` |
|
||||
|------------------------------------|------------|----------------------|--------------------|
|
||||
| Action: list | global | no | object-level* |
|
||||
| Action: create | no | global | object-level |
|
||||
| Action: retrieve | global | object-level | object-level |
|
||||
| Action: update | global | object-level | object-level |
|
||||
| Action: partial_update | global | object-level | object-level |
|
||||
| Action: destroy | global | object-level | no |
|
||||
| Can reference action in decision | no** | yes | no** |
|
||||
| Can reference request in decision | no** | yes | yes |
|
||||
|
||||
\* A Serializer class should not raise PermissionDenied in a list action, or the entire list would not be returned. <br>
|
||||
\** The `get_*()` methods have access to the current view and can return different Serializer or QuerySet instances based on the request or action.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
|
|
@ -177,6 +177,8 @@ Date: 28th September 2020
|
|||
* Don't strict disallow redundant `SerializerMethodField` field name arguments.
|
||||
* Don't render extra actions in browable API if not authenticated.
|
||||
* Strip null characters from search parameters.
|
||||
* Deprecate the `detail_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=True)` instead. [gh6687]
|
||||
* Deprecate the `list_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=False)` instead. [gh6687]
|
||||
|
||||
## 3.9.x series
|
||||
|
||||
|
@ -2270,6 +2272,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
<!-- 3.10.0 -->
|
||||
[gh6680]: https://github.com/encode/django-rest-framework/issues/6680
|
||||
[gh6317]: https://github.com/encode/django-rest-framework/issues/6317
|
||||
[gh6687]: https://github.com/encode/django-rest-framework/issues/6687
|
||||
|
||||
<!-- 3.11.0 -->
|
||||
[gh6892]: https://github.com/encode/django-rest-framework/issues/6892
|
||||
|
|
|
@ -190,6 +190,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc.
|
||||
* [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF.
|
||||
* [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers.
|
||||
* [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses.
|
||||
|
||||
### Permissions
|
||||
|
||||
|
@ -214,17 +215,19 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [drf-action-serializer][drf-action-serializer] - Serializer providing per-action fields config for use with ViewSets to prevent having to write multiple serializers.
|
||||
* [djangorestframework-dataclasses][djangorestframework-dataclasses] - Serializer providing automatic field generation for Python dataclasses, like the built-in ModelSerializer does for models.
|
||||
* [django-restql][django-restql] - Turn your REST API into a GraphQL like API(It allows clients to control which fields will be sent in a response, uses GraphQL like syntax, supports read and write on both flat and nested fields).
|
||||
* [graphwrap][graphwrap] - Transform your REST API into a fully compliant GraphQL API with just two lines of code. Leverages [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/) to dynamically build, at runtime, a GraphQL ObjectType for each view in your API.
|
||||
|
||||
### Serializer fields
|
||||
|
||||
* [drf-compound-fields][drf-compound-fields] - Provides "compound" serializer fields, such as lists of simple values.
|
||||
* [django-extra-fields][django-extra-fields] - Provides extra serializer fields.
|
||||
* [drf-extra-fields][drf-extra-fields] - Provides extra serializer fields.
|
||||
* [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs].
|
||||
|
||||
### Views
|
||||
|
||||
* [django-rest-multiple-models][django-rest-multiple-models] - Provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request.
|
||||
* [drf-typed-views][drf-typed-views] - Use Python type annotations to validate/deserialize request parameters. Inspired by API Star, Hug and FastAPI.
|
||||
* [rest-framework-actions][rest-framework-actions] - Provides control over each action in ViewSets. Serializers per action, method.
|
||||
|
||||
### Routers
|
||||
|
||||
|
@ -308,7 +311,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[djangorestframework-gis]: https://github.com/djangonauts/django-rest-framework-gis
|
||||
[djangorestframework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
|
||||
[drf-compound-fields]: https://github.com/estebistec/drf-compound-fields
|
||||
[django-extra-fields]: https://github.com/Hipo/drf-extra-fields
|
||||
[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields
|
||||
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
[wq.db.rest]: https://wq.io/docs/about-rest
|
||||
|
@ -362,3 +365,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[django-elasticsearch-dsl-drf]: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf
|
||||
[django-api-client]: https://github.com/rhenter/django-api-client
|
||||
[drf-psq]: https://github.com/drf-psq/drf-psq
|
||||
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
||||
[graphwrap]: https://github.com/PaulGilmartin/graph_wrap
|
||||
[rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions
|
||||
|
|
|
@ -453,7 +453,7 @@ For example, using the "Django REST framework JWT" package
|
|||
|
||||
function loginUser(username, password) {
|
||||
let action = ["api-token-auth", "obtain-token"];
|
||||
let params = {username: "example", email: "example@example.com"};
|
||||
let params = {username: username, password: password};
|
||||
client.action(schema, action, params).then(function(result) {
|
||||
// On success, instantiate an authenticated client.
|
||||
let auth = window.coreapi.auth.TokenAuthentication({
|
||||
|
|
|
@ -202,7 +202,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `op
|
|||
meta = self.metadata_class()
|
||||
data = meta.determine_metadata(request, self)
|
||||
data.pop('description')
|
||||
return data
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
See [the Metadata docs][metadata-docs] for more details.
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ We can change the default list style to use pagination, by modifying our `tutori
|
|||
|
||||
Note that settings in REST framework are all namespaced into a single dictionary setting, named `REST_FRAMEWORK`, which helps keep them well separated from your other project settings.
|
||||
|
||||
We could also customize the pagination style if we needed too, but in this case we'll just stick with the default.
|
||||
We could also customize the pagination style if we needed to, but in this case we'll just stick with the default.
|
||||
|
||||
## Browsing the API
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Pytest for running the tests.
|
||||
|
||||
pytest>=6.2.1,<6.3
|
||||
pytest-django>=4.1.0,<4.2
|
||||
pytest-cov>=2.7.1
|
||||
six>=1.14.0
|
||||
pytest-cov>=2.10.1
|
||||
|
||||
|
|
|
@ -704,7 +704,7 @@ class BooleanField(Field):
|
|||
initial = False
|
||||
TRUE_VALUES = {
|
||||
't', 'T',
|
||||
'y', 'Y', 'yes', 'YES',
|
||||
'y', 'Y', 'yes', 'Yes', 'YES',
|
||||
'true', 'True', 'TRUE',
|
||||
'on', 'On', 'ON',
|
||||
'1', 1,
|
||||
|
@ -712,7 +712,7 @@ class BooleanField(Field):
|
|||
}
|
||||
FALSE_VALUES = {
|
||||
'f', 'F',
|
||||
'n', 'N', 'no', 'NO',
|
||||
'n', 'N', 'no', 'No', 'NO',
|
||||
'false', 'False', 'FALSE',
|
||||
'off', 'Off', 'OFF',
|
||||
'0', 0, 0.0,
|
||||
|
@ -1063,6 +1063,9 @@ class DecimalField(Field):
|
|||
try:
|
||||
value = decimal.Decimal(data)
|
||||
except decimal.DecimalException:
|
||||
if data == '' and self.allow_null:
|
||||
return None
|
||||
|
||||
self.fail('invalid')
|
||||
|
||||
if value.is_nan():
|
||||
|
@ -1112,6 +1115,12 @@ class DecimalField(Field):
|
|||
def to_representation(self, value):
|
||||
coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING)
|
||||
|
||||
if value is None:
|
||||
if coerce_to_string:
|
||||
return ''
|
||||
else:
|
||||
return None
|
||||
|
||||
if not isinstance(value, decimal.Decimal):
|
||||
value = decimal.Decimal(str(value).strip())
|
||||
|
||||
|
@ -1755,6 +1764,9 @@ class JSONField(Field):
|
|||
'invalid': _('Value must be valid JSON.')
|
||||
}
|
||||
|
||||
# Workaround for isinstance calls when importing the field isn't possible
|
||||
_is_jsonfield = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.binary = kwargs.pop('binary', False)
|
||||
self.encoder = kwargs.pop('encoder', None)
|
||||
|
|
|
@ -226,10 +226,20 @@ class OrderingFilter(BaseFilterBackend):
|
|||
)
|
||||
raise ImproperlyConfigured(msg % self.__class__.__name__)
|
||||
|
||||
model_class = queryset.model
|
||||
model_property_names = [
|
||||
# 'pk' is a property added in Django's Model class, however it is valid for ordering.
|
||||
attr for attr in dir(model_class) if isinstance(getattr(model_class, attr), property) and attr != 'pk'
|
||||
]
|
||||
|
||||
return [
|
||||
(field.source.replace('.', '__') or field_name, field.label)
|
||||
for field_name, field in serializer_class(context=context).fields.items()
|
||||
if not getattr(field, 'write_only', False) and not field.source == '*'
|
||||
if (
|
||||
not getattr(field, 'write_only', False) and
|
||||
not field.source == '*' and
|
||||
field.source not in model_property_names
|
||||
)
|
||||
]
|
||||
|
||||
def get_valid_fields(self, queryset, view, context={}):
|
||||
|
|
|
@ -198,9 +198,7 @@ class PageNumberPagination(BasePagination):
|
|||
return None
|
||||
|
||||
paginator = self.django_paginator_class(queryset, page_size)
|
||||
page_number = request.query_params.get(self.page_query_param, 1)
|
||||
if page_number in self.last_page_strings:
|
||||
page_number = paginator.num_pages
|
||||
page_number = self.get_page_number(request, paginator)
|
||||
|
||||
try:
|
||||
self.page = paginator.page(page_number)
|
||||
|
@ -217,6 +215,12 @@ class PageNumberPagination(BasePagination):
|
|||
self.request = request
|
||||
return list(self.page)
|
||||
|
||||
def get_page_number(self, request, paginator):
|
||||
page_number = request.query_params.get(self.page_query_param, 1)
|
||||
if page_number in self.last_page_strings:
|
||||
page_number = paginator.num_pages
|
||||
return page_number
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
('count', self.page.paginator.count),
|
||||
|
@ -376,11 +380,11 @@ class LimitOffsetPagination(BasePagination):
|
|||
template = 'rest_framework/pagination/numbers.html'
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
self.count = self.get_count(queryset)
|
||||
self.limit = self.get_limit(request)
|
||||
if self.limit is None:
|
||||
return None
|
||||
|
||||
self.count = self.get_count(queryset)
|
||||
self.offset = self.get_offset(request)
|
||||
self.request = request
|
||||
if self.count > self.limit and self.template is not None:
|
||||
|
|
|
@ -259,6 +259,8 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
data = self.pk_field.to_internal_value(data)
|
||||
queryset = self.get_queryset()
|
||||
try:
|
||||
if isinstance(data, bool):
|
||||
raise TypeError
|
||||
return queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
self.fail('does_not_exist', pk_value=data)
|
||||
|
|
|
@ -1063,7 +1063,8 @@ class OpenAPIRenderer(BaseRenderer):
|
|||
class JSONOpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi+json'
|
||||
charset = None
|
||||
encoder_class = encoders.JSONEncoder
|
||||
format = 'openapi-json'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return json.dumps(data, indent=2).encode('utf-8')
|
||||
return json.dumps(data, cls=self.encoder_class, indent=2).encode('utf-8')
|
||||
|
|
|
@ -40,7 +40,7 @@ td.nested > table {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
form select, form input, form textarea {
|
||||
form select, form input:not([type=checkbox]), form textarea {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="input-group">
|
||||
<input type="text" class="form-control" style="width: 350px" name="{{ param }}" value="{{ term }}">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> Search</button>
|
||||
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> {% trans "Search" %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -79,7 +79,7 @@ if requests is not None:
|
|||
"""
|
||||
raw_kwargs = {}
|
||||
|
||||
def start_response(wsgi_status, wsgi_headers):
|
||||
def start_response(wsgi_status, wsgi_headers, exc_info=None):
|
||||
status, _, reason = wsgi_status.partition(' ')
|
||||
raw_kwargs['status'] = int(status)
|
||||
raw_kwargs['reason'] = reason
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections import OrderedDict
|
||||
from collections.abc import MutableMapping
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
@ -87,7 +87,12 @@ class JSONBoundField(BoundField):
|
|||
# value will be a JSONString, rather than a JSON primitive.
|
||||
if not getattr(value, 'is_json_string', False):
|
||||
try:
|
||||
value = json.dumps(self.value, sort_keys=True, indent=4)
|
||||
value = json.dumps(
|
||||
self.value,
|
||||
sort_keys=True,
|
||||
indent=4,
|
||||
separators=(',', ': '),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return self.__class__(self._field, value, self.errors, self._prefix)
|
||||
|
@ -101,7 +106,7 @@ class NestedBoundField(BoundField):
|
|||
"""
|
||||
|
||||
def __init__(self, field, value, errors, prefix=''):
|
||||
if value is None or value == '':
|
||||
if value is None or value == '' or not isinstance(value, Mapping):
|
||||
value = {}
|
||||
super().__init__(field, value, errors, prefix)
|
||||
|
||||
|
@ -115,6 +120,8 @@ class NestedBoundField(BoundField):
|
|||
error = self.errors.get(key) if isinstance(self.errors, dict) else None
|
||||
if hasattr(field, 'fields'):
|
||||
return NestedBoundField(field, value, error, prefix=self.name + '.')
|
||||
elif getattr(field, '_is_jsonfield', False):
|
||||
return JSONBoundField(field, value, error, prefix=self.name + '.')
|
||||
return BoundField(field, value, error, prefix=self.name + '.')
|
||||
|
||||
def as_form_field(self):
|
||||
|
|
|
@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework.
|
|||
"""
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import connection, models, transaction
|
||||
from django.db import connections, models
|
||||
from django.http import Http404
|
||||
from django.http.response import HttpResponseBase
|
||||
from django.utils.cache import cc_delim_re, patch_vary_headers
|
||||
|
@ -63,9 +63,9 @@ def get_view_description(view, html=False):
|
|||
|
||||
|
||||
def set_rollback():
|
||||
atomic_requests = connection.settings_dict.get('ATOMIC_REQUESTS', False)
|
||||
if atomic_requests and connection.in_atomic_block:
|
||||
transaction.set_rollback(True)
|
||||
for db in connections.all():
|
||||
if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block:
|
||||
db.set_rollback(True)
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
|
|
|
@ -24,6 +24,10 @@ def pytest_configure(config):
|
|||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:'
|
||||
},
|
||||
'secondary': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:'
|
||||
}
|
||||
},
|
||||
SITE_ID=1,
|
||||
|
|
|
@ -11,7 +11,8 @@ from rest_framework.authtoken.views import obtain_auth_token
|
|||
from rest_framework.compat import uritemplate
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser
|
||||
from rest_framework.renderers import (
|
||||
BaseRenderer, BrowsableAPIRenderer, JSONRenderer, OpenAPIRenderer
|
||||
BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer,
|
||||
OpenAPIRenderer
|
||||
)
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator
|
||||
|
@ -992,6 +993,19 @@ class TestGenerator(TestCase):
|
|||
assert 'openapi' in schema
|
||||
assert 'paths' in schema
|
||||
|
||||
def test_schema_rendering_to_json(self):
|
||||
patterns = [
|
||||
path('example/', views.ExampleGenericAPIView.as_view()),
|
||||
]
|
||||
generator = SchemaGenerator(patterns=patterns)
|
||||
|
||||
request = create_request('/')
|
||||
schema = generator.get_schema(request=request)
|
||||
ret = JSONOpenAPIRenderer().render(schema)
|
||||
|
||||
assert b'"openapi": "' in ret
|
||||
assert b'"default": "0.0"' in ret
|
||||
|
||||
def test_schema_with_no_paths(self):
|
||||
patterns = []
|
||||
generator = SchemaGenerator(patterns=patterns)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.validators import (
|
||||
DecimalValidator, MaxLengthValidator, MaxValueValidator,
|
||||
|
@ -59,6 +60,7 @@ class DocStringExampleDetailView(APIView):
|
|||
class ExampleSerializer(serializers.Serializer):
|
||||
date = serializers.DateField()
|
||||
datetime = serializers.DateTimeField()
|
||||
duration = serializers.DurationField(default=timedelta())
|
||||
hstore = serializers.HStoreField()
|
||||
uuid_field = serializers.UUIDField(default=uuid.uuid4)
|
||||
|
||||
|
|
|
@ -130,6 +130,41 @@ class DBTransactionAPIExceptionTests(TestCase):
|
|||
assert BasicModel.objects.count() == 0
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
)
|
||||
class MultiDBTransactionAPIExceptionTests(TestCase):
|
||||
databases = '__all__'
|
||||
|
||||
def setUp(self):
|
||||
self.view = APIExceptionView.as_view()
|
||||
connections.databases['default']['ATOMIC_REQUESTS'] = True
|
||||
connections.databases['secondary']['ATOMIC_REQUESTS'] = True
|
||||
|
||||
def tearDown(self):
|
||||
connections.databases['default']['ATOMIC_REQUESTS'] = False
|
||||
connections.databases['secondary']['ATOMIC_REQUESTS'] = False
|
||||
|
||||
def test_api_exception_rollback_transaction(self):
|
||||
"""
|
||||
Transaction is rollbacked by our transaction atomic block.
|
||||
"""
|
||||
request = factory.post('/')
|
||||
num_queries = 4 if connection.features.can_release_savepoints else 3
|
||||
with self.assertNumQueries(num_queries):
|
||||
# 1 - begin savepoint
|
||||
# 2 - insert
|
||||
# 3 - rollback savepoint
|
||||
# 4 - release savepoint
|
||||
with transaction.atomic(), transaction.atomic(using='secondary'):
|
||||
response = self.view(request)
|
||||
assert transaction.get_rollback()
|
||||
assert transaction.get_rollback(using='secondary')
|
||||
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
assert BasicModel.objects.count() == 0
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
connection.features.uses_savepoints,
|
||||
"'atomic' requires transactions and savepoints."
|
||||
|
|
|
@ -91,6 +91,10 @@ class TestSimpleBoundField:
|
|||
assert rendered_packed == expected_packed
|
||||
|
||||
|
||||
class CustomJSONField(serializers.JSONField):
|
||||
pass
|
||||
|
||||
|
||||
class TestNestedBoundField:
|
||||
def test_nested_empty_bound_field(self):
|
||||
class Nested(serializers.Serializer):
|
||||
|
@ -117,14 +121,31 @@ class TestNestedBoundField:
|
|||
class Nested(serializers.Serializer):
|
||||
bool_field = serializers.BooleanField()
|
||||
null_field = serializers.IntegerField(allow_null=True)
|
||||
json_field = serializers.JSONField()
|
||||
custom_json_field = CustomJSONField()
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
nested = Nested()
|
||||
|
||||
serializer = ExampleSerializer(data={'nested': {'bool_field': False, 'null_field': None}})
|
||||
serializer = ExampleSerializer(
|
||||
data={'nested': {
|
||||
'bool_field': False, 'null_field': None,
|
||||
'json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'},
|
||||
'custom_json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'},
|
||||
}})
|
||||
assert serializer.is_valid()
|
||||
assert serializer['nested']['bool_field'].as_form_field().value == ''
|
||||
assert serializer['nested']['null_field'].as_form_field().value == ''
|
||||
assert serializer['nested']['json_field'].as_form_field().value == '''{
|
||||
"bool_item": true,
|
||||
"number": 1,
|
||||
"text_item": "text"
|
||||
}'''
|
||||
assert serializer['nested']['custom_json_field'].as_form_field().value == '''{
|
||||
"bool_item": true,
|
||||
"number": 1,
|
||||
"text_item": "text"
|
||||
}'''
|
||||
|
||||
def test_rendering_nested_fields_with_none_value(self):
|
||||
from rest_framework.renderers import HTMLFormRenderer
|
||||
|
@ -163,6 +184,33 @@ class TestNestedBoundField:
|
|||
rendered_packed = ''.join(rendered.split())
|
||||
assert rendered_packed == expected_packed
|
||||
|
||||
def test_rendering_nested_fields_with_not_mappable_value(self):
|
||||
from rest_framework.renderers import HTMLFormRenderer
|
||||
|
||||
class Nested(serializers.Serializer):
|
||||
text_field = serializers.CharField()
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
nested = Nested()
|
||||
|
||||
serializer = ExampleSerializer(data={'nested': 1})
|
||||
assert not serializer.is_valid()
|
||||
renderer = HTMLFormRenderer()
|
||||
for field in serializer:
|
||||
rendered = renderer.render_field(field, {})
|
||||
expected_packed = (
|
||||
'<fieldset>'
|
||||
'<legend>Nested</legend>'
|
||||
'<divclass="form-group">'
|
||||
'<label>Textfield</label>'
|
||||
'<inputname="nested.text_field"class="form-control"type="text"value="">'
|
||||
'</div>'
|
||||
'</fieldset>'
|
||||
)
|
||||
|
||||
rendered_packed = ''.join(rendered.split())
|
||||
assert rendered_packed == expected_packed
|
||||
|
||||
|
||||
class TestJSONBoundField:
|
||||
def test_as_form_fields(self):
|
||||
|
|
|
@ -1090,6 +1090,9 @@ class TestDecimalField(FieldValues):
|
|||
'2E+1': Decimal('20'),
|
||||
}
|
||||
invalid_inputs = (
|
||||
(None, ["This field may not be null."]),
|
||||
('', ["A valid number is required."]),
|
||||
(' ', ["A valid number is required."]),
|
||||
('abc', ["A valid number is required."]),
|
||||
(Decimal('Nan'), ["A valid number is required."]),
|
||||
(Decimal('Snan'), ["A valid number is required."]),
|
||||
|
@ -1115,6 +1118,32 @@ class TestDecimalField(FieldValues):
|
|||
field = serializers.DecimalField(max_digits=3, decimal_places=1)
|
||||
|
||||
|
||||
class TestAllowNullDecimalField(FieldValues):
|
||||
valid_inputs = {
|
||||
None: None,
|
||||
'': None,
|
||||
' ': None,
|
||||
}
|
||||
invalid_inputs = {}
|
||||
outputs = {
|
||||
None: '',
|
||||
}
|
||||
field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True)
|
||||
|
||||
|
||||
class TestAllowNullNoStringCoercionDecimalField(FieldValues):
|
||||
valid_inputs = {
|
||||
None: None,
|
||||
'': None,
|
||||
' ': None,
|
||||
}
|
||||
invalid_inputs = {}
|
||||
outputs = {
|
||||
None: None,
|
||||
}
|
||||
field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, coerce_to_string=False)
|
||||
|
||||
|
||||
class TestMinMaxDecimalField(FieldValues):
|
||||
"""
|
||||
Valid and invalid values for `DecimalField` with min and max limits.
|
||||
|
|
|
@ -424,6 +424,10 @@ class OrderingFilterModel(models.Model):
|
|||
title = models.CharField(max_length=20, verbose_name='verbose title')
|
||||
text = models.CharField(max_length=100)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.title + ": " + self.text
|
||||
|
||||
|
||||
class OrderingFilterRelatedModel(models.Model):
|
||||
related_object = models.ForeignKey(OrderingFilterModel, related_name="relateds", on_delete=models.CASCADE)
|
||||
|
@ -436,6 +440,17 @@ class OrderingFilterSerializer(serializers.ModelSerializer):
|
|||
fields = '__all__'
|
||||
|
||||
|
||||
class OrderingFilterSerializerWithModelProperty(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OrderingFilterModel
|
||||
fields = (
|
||||
"id",
|
||||
"title",
|
||||
"text",
|
||||
"description"
|
||||
)
|
||||
|
||||
|
||||
class OrderingDottedRelatedSerializer(serializers.ModelSerializer):
|
||||
related_text = serializers.CharField(source='related_object.text')
|
||||
related_title = serializers.CharField(source='related_object.title')
|
||||
|
@ -551,6 +566,42 @@ class OrderingFilterTests(TestCase):
|
|||
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||
]
|
||||
|
||||
def test_ordering_without_ordering_fields(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
serializer_class = OrderingFilterSerializerWithModelProperty
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
|
||||
# Model field ordering works fine.
|
||||
request = factory.get('/', {'ordering': 'text'})
|
||||
response = view(request)
|
||||
assert response.data == [
|
||||
{'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'},
|
||||
{'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'},
|
||||
{'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'},
|
||||
]
|
||||
|
||||
# `incorrectfield` ordering works fine.
|
||||
request = factory.get('/', {'ordering': 'foobar'})
|
||||
response = view(request)
|
||||
assert response.data == [
|
||||
{'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'},
|
||||
{'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'},
|
||||
{'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'},
|
||||
]
|
||||
|
||||
# `description` is a Model property, which should be ignored.
|
||||
request = factory.get('/', {'ordering': 'description'})
|
||||
response = view(request)
|
||||
assert response.data == [
|
||||
{'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'},
|
||||
{'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'},
|
||||
{'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'},
|
||||
]
|
||||
|
||||
def test_default_ordering(self):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = OrderingFilterModel.objects.all()
|
||||
|
|
|
@ -107,6 +107,12 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase):
|
|||
msg = excinfo.value.detail[0]
|
||||
assert msg == 'Incorrect type. Expected pk value, received BadType.'
|
||||
|
||||
def test_pk_related_lookup_bool(self):
|
||||
with pytest.raises(serializers.ValidationError) as excinfo:
|
||||
self.field.to_internal_value(True)
|
||||
msg = excinfo.value.detail[0]
|
||||
assert msg == 'Incorrect type. Expected pk value, received bool.'
|
||||
|
||||
def test_pk_representation(self):
|
||||
representation = self.field.to_representation(self.instance)
|
||||
assert representation == self.instance.pk
|
||||
|
|
9
tox.ini
9
tox.ini
|
@ -3,7 +3,8 @@ envlist =
|
|||
{py35,py36,py37}-django22,
|
||||
{py36,py37,py38}-django30,
|
||||
{py36,py37,py38,py39}-django31,
|
||||
{py36,py37,py38,py39}-djangomaster,
|
||||
{py36,py37,py38,py39}-django32,
|
||||
{py38,py39}-djangomain,
|
||||
base,dist,lint,docs,
|
||||
|
||||
[travis:env]
|
||||
|
@ -11,7 +12,8 @@ DJANGO =
|
|||
2.2: django22
|
||||
3.0: django30
|
||||
3.1: django31
|
||||
master: djangomaster
|
||||
3.2: django32
|
||||
main: djangomain
|
||||
|
||||
[testenv]
|
||||
commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning runtests.py --fast --coverage {posargs}
|
||||
|
@ -23,7 +25,8 @@ deps =
|
|||
django22: Django>=2.2,<3.0
|
||||
django30: Django>=3.0,<3.1
|
||||
django31: Django>=3.1,<3.2
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
django32: Django>=3.2a1,<4.0
|
||||
djangomain: https://github.com/django/django/archive/main.tar.gz
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user