This commit is contained in:
Asif Saif Uddin (Auvi) 2020-01-25 22:10:33 +06:00
commit ad3955584a
46 changed files with 607 additions and 376 deletions

View File

@ -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 }

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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`.

View File

@ -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.

View File

@ -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/

View File

@ -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

View 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

View File

@ -1,9 +1,5 @@
# Release Notes
> Release Early, Release Often
>
> &mdash; 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.

View File

@ -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/

View File

@ -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.

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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`.

View File

@ -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;
}

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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), (

View File

@ -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))

View File

@ -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())

View File

@ -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'):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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=[

View File

@ -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)

View File

@ -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 = [

View File

@ -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):

View File

@ -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.

View File

@ -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)

View File

@ -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/')

View File

@ -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'),
]

View File

@ -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,
}

View File

@ -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=''
)

View File

@ -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