mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-28 00:49:49 +03:00
Merge branch 'master' of https://github.com/encode/django-rest-framework
This commit is contained in:
commit
ad3955584a
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
117
docs/community/3.11-announcement.md
Normal file
117
docs/community/3.11-announcement.md
Normal file
|
@ -0,0 +1,117 @@
|
|||
<style>
|
||||
.promo li a {
|
||||
float: left;
|
||||
width: 130px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
margin: 10px 30px;
|
||||
padding: 150px 0 0 0;
|
||||
background-position: 0 50%;
|
||||
background-size: 130px auto;
|
||||
background-repeat: no-repeat;
|
||||
font-size: 120%;
|
||||
color: black;
|
||||
}
|
||||
.promo li {
|
||||
list-style: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
# 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.*
|
||||
|
||||
<ul class="premium-promo promo">
|
||||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||
<li><a href="https://software.esg-usa.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/esg-new-logo.png)">ESG</a></li>
|
||||
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
||||
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
||||
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li>
|
||||
<li><a href="https://lightsonsoftware.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/lightson-dark.png)">Lights On Software</a></li>
|
||||
<li><a href="https://retool.com/?utm_source=djangorest&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/retool-sidebar.png)">Retool</a></li>
|
||||
</ul>
|
||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||
|
||||
*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
|
|
@ -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.
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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: '<token>'
|
||||
})
|
||||
let client = new coreapi.Client({auth: auth})
|
||||
scheme: 'JWT',
|
||||
token: '<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: '<username>',
|
||||
password: '<password>'
|
||||
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
|
||||
|
|
|
@ -45,7 +45,11 @@ this:
|
|||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout"
|
||||
layout: "BaseLayout",
|
||||
requestInterceptor: (request) => {
|
||||
request.headers['X-CSRFToken'] = "{{ csrf_token }}"
|
||||
return request;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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), (
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 `UniqueFor<Range>Validator` 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)
|
||||
|
|
|
@ -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
|
||||
|
|
4
setup.py
4
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=[
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -145,14 +145,18 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase):
|
|||
assert representation == self.instance.pk.int
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF=[
|
||||
urlpatterns = [
|
||||
url(r'^example/(?P<name>.+)/$', 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/')
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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=''
|
||||
)
|
||||
|
|
3
tox.ini
3
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
|
||||
|
|
Loading…
Reference in New Issue
Block a user