mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-06 12:26:00 +03:00
Merge branch 'encode:master' into jsonencoder_ipaddress
This commit is contained in:
commit
0880ed9e87
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
|
@ -19,6 +19,7 @@ jobs:
|
||||||
- '3.10'
|
- '3.10'
|
||||||
- '3.11'
|
- '3.11'
|
||||||
- '3.12'
|
- '3.12'
|
||||||
|
- '3.13'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -33,10 +34,10 @@ jobs:
|
||||||
run: python -m pip install --upgrade pip setuptools virtualenv wheel
|
run: python -m pip install --upgrade pip setuptools virtualenv wheel
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python -m pip install --upgrade codecov tox
|
run: python -m pip install --upgrade tox
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
- name: Run tox targets for ${{ matrix.python-version }}
|
||||||
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-')
|
||||||
|
|
||||||
- name: Run extra tox targets
|
- name: Run extra tox targets
|
||||||
if: ${{ matrix.python-version == '3.9' }}
|
if: ${{ matrix.python-version == '3.9' }}
|
||||||
|
@ -44,8 +45,9 @@ jobs:
|
||||||
tox -e base,dist,docs
|
tox -e base,dist,docs
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
run: |
|
uses: codecov/codecov-action@v5
|
||||||
codecov -e TOXENV,DJANGO
|
with:
|
||||||
|
env_vars: TOXENV,DJANGO
|
||||||
|
|
||||||
test-docs:
|
test-docs:
|
||||||
name: Test documentation links
|
name: Test documentation links
|
||||||
|
|
|
@ -55,7 +55,7 @@ Some reasons you might want to use REST framework:
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python 3.8+
|
* Python 3.8+
|
||||||
* Django 5.0, 4.2
|
* Django 4.2, 5.0, 5.1
|
||||||
|
|
||||||
We **highly recommend** and only officially support the latest patch release of
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team).
|
**Please report security issues by emailing security@encode.io**.
|
||||||
|
|
||||||
**Please report security issues by emailing security@djangoproject.com**.
|
The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||||
|
|
||||||
The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
|
||||||
|
|
|
@ -90,6 +90,12 @@ The kind of response that will be used depends on the authentication scheme. Al
|
||||||
|
|
||||||
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
|
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
|
||||||
|
|
||||||
|
## Django 5.1+ `LoginRequiredMiddleware`
|
||||||
|
|
||||||
|
If you're running Django 5.1+ and use the [`LoginRequiredMiddleware`][login-required-middleware], please note that all views from DRF are opted-out of this middleware. This is because the authentication in DRF is based authentication and permissions classes, which may be determined after the middleware has been applied. Additionally, when the request is not authenticated, the middleware redirects the user to the login page, which is not suitable for API requests, where it's preferable to return a 401 status code.
|
||||||
|
|
||||||
|
REST framework offers an equivalent mechanism for DRF views via the global settings, `DEFAULT_AUTHENTICATION_CLASSES` and `DEFAULT_PERMISSION_CLASSES`. They should be changed accordingly if you need to enforce that API requests are logged in.
|
||||||
|
|
||||||
## Apache mod_wsgi specific configuration
|
## Apache mod_wsgi specific configuration
|
||||||
|
|
||||||
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
||||||
|
@ -484,3 +490,4 @@ More information can be found in the [Documentation](https://django-rest-durin.r
|
||||||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||||
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
||||||
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
||||||
|
[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware
|
|
@ -291,8 +291,8 @@ Corresponds to `django.db.models.fields.DecimalField`.
|
||||||
* `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`.
|
* `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`.
|
||||||
* `decimal_places` The number of decimal places to store with the number.
|
* `decimal_places` The number of decimal places to store with the number.
|
||||||
* `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`.
|
* `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`.
|
||||||
* `max_value` Validate that the number provided is no greater than this value.
|
* `max_value` Validate that the number provided is no greater than this value. Should be an integer or `Decimal` object.
|
||||||
* `min_value` Validate that the number provided is no less than this value.
|
* `min_value` Validate that the number provided is no less than this value. Should be an integer or `Decimal` object.
|
||||||
* `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
|
* `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
|
||||||
* `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
|
* `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
|
||||||
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`.
|
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`.
|
||||||
|
|
|
@ -525,7 +525,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
||||||
|
|
||||||
## LaTeX
|
## LaTeX
|
||||||
|
|
||||||
[Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble].
|
[Rest Framework Latex] provides a renderer that outputs PDFs using Lualatex. It is maintained by [Pebble (S/F Software)][mypebble].
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process
|
||||||
|
|
|
@ -142,6 +142,24 @@ The above example would now generate the following URL pattern:
|
||||||
* URL path: `^users/{pk}/change-password/$`
|
* URL path: `^users/{pk}/change-password/$`
|
||||||
* URL name: `'user-change_password'`
|
* URL name: `'user-change_password'`
|
||||||
|
|
||||||
|
### Using Django `path()` with routers
|
||||||
|
|
||||||
|
By default, the URLs created by routers use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example:
|
||||||
|
|
||||||
|
router = SimpleRouter(use_regex_path=False)
|
||||||
|
|
||||||
|
The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs:
|
||||||
|
|
||||||
|
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
lookup_field = 'my_model_id'
|
||||||
|
lookup_value_regex = '[0-9a-f]{32}'
|
||||||
|
|
||||||
|
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
lookup_field = 'my_model_uuid'
|
||||||
|
lookup_value_converter = 'uuid'
|
||||||
|
|
||||||
|
Note that path converters will be used on all URLs registered in the router, including viewset actions.
|
||||||
|
|
||||||
# API Guide
|
# API Guide
|
||||||
|
|
||||||
## SimpleRouter
|
## SimpleRouter
|
||||||
|
@ -160,30 +178,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
|
||||||
<tr><td>{prefix}/{lookup}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
|
<tr><td>{prefix}/{lookup}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
|
By default, the URLs created by `SimpleRouter` are appended with a trailing slash.
|
||||||
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
||||||
|
|
||||||
router = SimpleRouter(trailing_slash=False)
|
router = SimpleRouter(trailing_slash=False)
|
||||||
|
|
||||||
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
||||||
|
|
||||||
By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example:
|
|
||||||
|
|
||||||
router = SimpleRouter(use_regex_path=False)
|
|
||||||
|
|
||||||
**Note**: `use_regex_path=False` only works with Django 2.x or above, since this feature was introduced in 2.0.0. See [release note][simplified-routing-release-note]
|
|
||||||
|
|
||||||
|
|
||||||
The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs:
|
|
||||||
|
|
||||||
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
||||||
lookup_field = 'my_model_id'
|
|
||||||
lookup_value_regex = '[0-9a-f]{32}'
|
|
||||||
|
|
||||||
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
||||||
lookup_field = 'my_model_uuid'
|
|
||||||
lookup_value_converter = 'uuid'
|
|
||||||
|
|
||||||
## DefaultRouter
|
## DefaultRouter
|
||||||
|
|
||||||
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
||||||
|
@ -351,5 +352,4 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
|
||||||
[drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
|
[drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
|
||||||
[url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces
|
[url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces
|
||||||
[include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include
|
[include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include
|
||||||
[simplified-routing-release-note]: https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax
|
|
||||||
[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters
|
[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters
|
||||||
|
|
|
@ -233,7 +233,7 @@ Serializer classes can also include reusable validators that are applied to the
|
||||||
|
|
||||||
class EventSerializer(serializers.Serializer):
|
class EventSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
|
room_number = serializers.ChoiceField(choices=[101, 102, 103, 201])
|
||||||
date = serializers.DateField()
|
date = serializers.DateField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -460,4 +460,4 @@ Default: `None`
|
||||||
[cite]: https://www.python.org/dev/peps/pep-0020/
|
[cite]: https://www.python.org/dev/peps/pep-0020/
|
||||||
[rfc4627]: https://www.ietf.org/rfc/rfc4627.txt
|
[rfc4627]: https://www.ietf.org/rfc/rfc4627.txt
|
||||||
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
|
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
|
||||||
[strftime]: https://docs.python.org/3/library/time.html#time.strftime
|
[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
|
||||||
|
|
|
@ -25,9 +25,12 @@ The `APIRequestFactory` class supports an almost identical API to Django's stand
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
request = factory.post('/notes/', {'title': 'new idea'})
|
request = factory.post('/notes/', {'title': 'new idea'})
|
||||||
|
|
||||||
|
# Using the standard RequestFactory API to encode JSON data
|
||||||
|
request = factory.post('/notes/', {'title': 'new idea'}, content_type='application/json')
|
||||||
|
|
||||||
#### Using the `format` argument
|
#### Using the `format` argument
|
||||||
|
|
||||||
Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example:
|
Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a wide set of request formats. When using this argument, the factory will select an appropriate renderer and its configured `content_type`. For example:
|
||||||
|
|
||||||
# Create a JSON POST request
|
# Create a JSON POST request
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
@ -41,7 +44,7 @@ To support a wider set of request formats, or change the default format, [see th
|
||||||
|
|
||||||
If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example:
|
If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example:
|
||||||
|
|
||||||
request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')
|
request = factory.post('/notes/', yaml.dump({'title': 'new idea'}), content_type='application/yaml')
|
||||||
|
|
||||||
#### PUT and PATCH with form data
|
#### PUT and PATCH with form data
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
|
The rates used in `DEFAULT_THROTTLE_RATES` can be specified over a period of second, minute, hour or day. The period must be specified after the `/` separator using `s`, `m`, `h` or `d`, respectively. For increased clarity, extended units such as `second`, `minute`, `hour`, `day` or even abbreviations like `sec`, `min`, `hr` are allowed, as only the first character is relevant to identify the rate.
|
||||||
|
|
||||||
You can also set the throttling policy on a per-view or per-viewset basis,
|
You can also set the throttling policy on a per-view or per-viewset basis,
|
||||||
using the `APIView` class-based views.
|
using the `APIView` class-based views.
|
||||||
|
|
|
@ -48,7 +48,7 @@ If we open up the Django shell using `manage.py shell` we can now
|
||||||
CustomerReportSerializer():
|
CustomerReportSerializer():
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField(label='ID', read_only=True)
|
||||||
time_raised = DateTimeField(read_only=True)
|
time_raised = DateTimeField(read_only=True)
|
||||||
reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
|
reference = CharField(max_length=20, validators=[UniqueValidator(queryset=CustomerReportRecord.objects.all())])
|
||||||
description = CharField(style={'type': 'textarea'})
|
description = CharField(style={'type': 'textarea'})
|
||||||
|
|
||||||
The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field.
|
The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field.
|
||||||
|
|
|
@ -128,6 +128,8 @@ You may inspect these attributes to adjust behavior based on the current action.
|
||||||
permission_classes = [IsAdminUser]
|
permission_classes = [IsAdminUser]
|
||||||
return [permission() for permission in permission_classes]
|
return [permission() for permission in permission_classes]
|
||||||
|
|
||||||
|
**Note**: the `action` attribute is not available in the `get_parsers`, `get_authenticators` and `get_content_negotiator` methods, as it is set _after_ they are called in the framework lifecycle. If you override one of these methods and try to access the `action` attribute in them, you will get an `AttributeError` error.
|
||||||
|
|
||||||
## Marking extra actions for routing
|
## Marking extra actions for routing
|
||||||
|
|
||||||
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
|
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
|
||||||
|
|
|
@ -7,6 +7,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
|
||||||
|
|
||||||
* [https://www.djangoproject.com/community/jobs/][djangoproject-website]
|
* [https://www.djangoproject.com/community/jobs/][djangoproject-website]
|
||||||
* [https://www.python.org/jobs/][python-org-jobs]
|
* [https://www.python.org/jobs/][python-org-jobs]
|
||||||
|
* [https://django.on-remote.com][django-on-remote]
|
||||||
* [https://djangogigs.com][django-gigs-com]
|
* [https://djangogigs.com][django-gigs-com]
|
||||||
* [https://djangojobs.net/jobs/][django-jobs-net]
|
* [https://djangojobs.net/jobs/][django-jobs-net]
|
||||||
* [https://findwork.dev/django-rest-framework-jobs][findwork-dev]
|
* [https://findwork.dev/django-rest-framework-jobs][findwork-dev]
|
||||||
|
@ -26,6 +27,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
|
||||||
|
|
||||||
[djangoproject-website]: https://www.djangoproject.com/community/jobs/
|
[djangoproject-website]: https://www.djangoproject.com/community/jobs/
|
||||||
[python-org-jobs]: https://www.python.org/jobs/
|
[python-org-jobs]: https://www.python.org/jobs/
|
||||||
|
[django-on-remote]: https://django.on-remote.com/
|
||||||
[django-gigs-com]: https://djangogigs.com
|
[django-gigs-com]: https://djangogigs.com
|
||||||
[django-jobs-net]: https://djangojobs.net/jobs/
|
[django-jobs-net]: https://djangojobs.net/jobs/
|
||||||
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs
|
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs
|
||||||
|
|
|
@ -12,7 +12,7 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
<img src="../../img/books/tsd-cover.png"/>
|
<img src="../../img/books/tsd-cover.png"/>
|
||||||
</a>
|
</a>
|
||||||
<a class="book-cover" href="https://djangoforapis.com">
|
<a class="book-cover" href="https://djangoforapis.com">
|
||||||
<img src="../../img/books/dfa-cover.jpg"/>
|
<img src="../../img/books/dfa-40-cover.jpg"/>
|
||||||
</a>
|
</a>
|
||||||
<a class="book-cover" href="https://books.agiliq.com/projects/django-api-polls-tutorial/en/latest/">
|
<a class="book-cover" href="https://books.agiliq.com/projects/django-api-polls-tutorial/en/latest/">
|
||||||
<img src="../../img/books/bda-cover.png"/>
|
<img src="../../img/books/bda-cover.png"/>
|
||||||
|
@ -28,7 +28,6 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
|
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
|
||||||
* [Django REST Framework - An Introduction][drf-an-intro]
|
* [Django REST Framework - An Introduction][drf-an-intro]
|
||||||
* [Django REST Framework Tutorial][drf-tutorial]
|
* [Django REST Framework Tutorial][drf-tutorial]
|
||||||
* [Django REST Framework Course][django-rest-framework-course]
|
|
||||||
* [Building a RESTful API with Django REST Framework][building-a-restful-api-with-drf]
|
* [Building a RESTful API with Django REST Framework][building-a-restful-api-with-drf]
|
||||||
* [Getting Started with Django REST Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs]
|
* [Getting Started with Django REST Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs]
|
||||||
* [End to End Web App with Django REST Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs]
|
* [End to End Web App with Django REST Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs]
|
||||||
|
@ -39,8 +38,10 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
* [Check Credentials Using Django REST Framework][check-credentials-using-django-rest-framework]
|
* [Check Credentials Using Django REST Framework][check-credentials-using-django-rest-framework]
|
||||||
* [Creating a Production Ready API with Python and Django REST Framework – Part 1][creating-a-production-ready-api-with-python-and-drf-part1]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 1][creating-a-production-ready-api-with-python-and-drf-part1]
|
||||||
* [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2]
|
||||||
* [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 3][creating-a-production-ready-api-with-python-and-drf-part3]
|
||||||
* [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 4][creating-a-production-ready-api-with-python-and-drf-part4]
|
||||||
|
* [Django Polls Tutorial API][django-polls-api]
|
||||||
|
* [Django REST Framework Tutorial: Todo API][django-rest-framework-todo-api]
|
||||||
* [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog]
|
* [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog]
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
|
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
|
||||||
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
|
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
|
||||||
* [Django REST Framework: Schemas, Hypermedia & Client Libraries][pycon-uk-2016]
|
* [Django REST Framework: Schemas, Hypermedia & Client Libraries][pycon-uk-2016]
|
||||||
|
* [Finally Understand Authentication in Django REST Framework][django-con-2018]
|
||||||
|
|
||||||
### Tutorials
|
### Tutorials
|
||||||
|
|
||||||
|
@ -103,7 +104,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/
|
[api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/
|
||||||
[cdrf.co]:http://www.cdrf.co
|
[cdrf.co]:http://www.cdrf.co
|
||||||
[medium-django-rest-framework]: https://medium.com/django-rest-framework
|
[medium-django-rest-framework]: https://medium.com/django-rest-framework
|
||||||
[django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework
|
|
||||||
[pycon-uk-2016]: https://www.youtube.com/watch?v=FjmiGh7OqVg
|
[pycon-uk-2016]: https://www.youtube.com/watch?v=FjmiGh7OqVg
|
||||||
[django-under-hood-2014]: https://www.youtube.com/watch?v=3cSsbe-tA0E
|
[django-under-hood-2014]: https://www.youtube.com/watch?v=3cSsbe-tA0E
|
||||||
[integrating-pandas-drf-and-bokeh]: https://web.archive.org/web/20180104205117/http://machinalis.com/blog/pandas-django-rest-framework-bokeh/
|
[integrating-pandas-drf-and-bokeh]: https://web.archive.org/web/20180104205117/http://machinalis.com/blog/pandas-django-rest-framework-bokeh/
|
||||||
|
@ -115,10 +115,12 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[chatbot-using-drf-part1]: https://chatbotslife.com/chatbot-using-django-rest-framework-api-ai-slack-part-1-3-69c7e38b7b1e#.g2aceuncf
|
[chatbot-using-drf-part1]: https://chatbotslife.com/chatbot-using-django-rest-framework-api-ai-slack-part-1-3-69c7e38b7b1e#.g2aceuncf
|
||||||
[new-django-admin-with-drf-and-emberjs]: https://blog.levit.be/new-django-admin-with-emberjs-what-are-the-news/
|
[new-django-admin-with-drf-and-emberjs]: https://blog.levit.be/new-django-admin-with-emberjs-what-are-the-news/
|
||||||
[drf-schema]: https://drf-schema-adapter.readthedocs.io/en/latest/
|
[drf-schema]: https://drf-schema-adapter.readthedocs.io/en/latest/
|
||||||
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/
|
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/posts/creating-production-ready-api-python-django-rest-framework-part-1/
|
||||||
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
|
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
|
||||||
[django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/
|
[creating-a-production-ready-api-with-python-and-drf-part3]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-3/
|
||||||
[django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/
|
[creating-a-production-ready-api-with-python-and-drf-part4]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-4/
|
||||||
|
[django-polls-api]: https://learndjango.com/tutorials/django-polls-tutorial-api
|
||||||
|
[django-rest-framework-todo-api]: https://learndjango.com/tutorials/django-rest-framework-tutorial-todo-api
|
||||||
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
|
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
|
||||||
[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
|
[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
|
||||||
[drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww
|
[drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww
|
||||||
|
@ -135,3 +137,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/
|
[django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/
|
||||||
[doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/
|
[doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/
|
||||||
[developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/
|
[developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/
|
||||||
|
[django-con-2018]: https://youtu.be/pY-oje5b5Qk?si=AOU6tLi0IL1_pVzq
|
BIN
docs/img/books/dfa-40-cover.jpg
Normal file
BIN
docs/img/books/dfa-40-cover.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 755 KiB |
|
@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
|
|
||||||
REST framework requires the following:
|
REST framework requires the following:
|
||||||
|
|
||||||
* Django (4.2, 5.0)
|
* Django (4.2, 5.0, 5.1)
|
||||||
* Python (3.8, 3.9, 3.10, 3.11, 3.12)
|
* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13)
|
||||||
|
|
||||||
We **highly recommend** and only officially support the latest patch release of
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
@ -196,9 +196,7 @@ For priority support please sign up for a [professional or premium sponsorship p
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team).
|
**Please report security issues by emailing security@encode.io**.
|
||||||
|
|
||||||
**Please report security issues by emailing security@djangoproject.com**.
|
|
||||||
|
|
||||||
The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,11 @@ By default, the API will return the format specified by the headers, which in th
|
||||||
To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf:
|
To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# ...
|
# ...
|
||||||
url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
|
path("api-auth/", include("rest_framework.urls", namespace="rest_framework"))
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ For API clients the most appropriate of these will typically be to use the `Acce
|
||||||
[cite]: https://youtu.be/Wa0VfS2q94Y
|
[cite]: https://youtu.be/Wa0VfS2q94Y
|
||||||
[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation
|
[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation
|
||||||
[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
|
[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
|
||||||
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
[transifex-project]: https://explore.transifex.com/django-rest-framework-1/django-rest-framework/
|
||||||
[django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
[django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
||||||
[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference
|
[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference
|
||||||
[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS
|
[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS
|
||||||
|
|
|
@ -321,7 +321,7 @@ You can install httpie using pip:
|
||||||
|
|
||||||
Finally, we can get a list of all of the snippets:
|
Finally, we can get a list of all of the snippets:
|
||||||
|
|
||||||
http http://127.0.0.1:8000/snippets/ --unsorted
|
http GET http://127.0.0.1:8000/snippets/ --unsorted
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
...
|
...
|
||||||
|
@ -354,7 +354,7 @@ Finally, we can get a list of all of the snippets:
|
||||||
|
|
||||||
Or we can get a particular snippet by referencing its id:
|
Or we can get a particular snippet by referencing its id:
|
||||||
|
|
||||||
http http://127.0.0.1:8000/snippets/2/ --unsorted
|
http GET http://127.0.0.1:8000/snippets/2/ --unsorted
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
...
|
...
|
||||||
|
|
|
@ -23,9 +23,5 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||||
ISO_8601 = 'iso-8601'
|
ISO_8601 = 'iso-8601'
|
||||||
|
|
||||||
|
|
||||||
class RemovedInDRF316Warning(DeprecationWarning):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RemovedInDRF317Warning(PendingDeprecationWarning):
|
class RemovedInDRF317Warning(PendingDeprecationWarning):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -986,10 +986,10 @@ class DecimalField(Field):
|
||||||
self.max_value = max_value
|
self.max_value = max_value
|
||||||
self.min_value = min_value
|
self.min_value = min_value
|
||||||
|
|
||||||
if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
|
if self.max_value is not None and not isinstance(self.max_value, (int, decimal.Decimal)):
|
||||||
warnings.warn("max_value should be a Decimal instance.")
|
warnings.warn("max_value should be an integer or Decimal instance.")
|
||||||
if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal):
|
if self.min_value is not None and not isinstance(self.min_value, (int, decimal.Decimal)):
|
||||||
warnings.warn("min_value should be a Decimal instance.")
|
warnings.warn("min_value should be an integer or Decimal instance.")
|
||||||
|
|
||||||
if self.max_digits is not None and self.decimal_places is not None:
|
if self.max_digits is not None and self.decimal_places is not None:
|
||||||
self.max_whole_digits = self.max_digits - self.decimal_places
|
self.max_whole_digits = self.max_digits - self.decimal_places
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
||||||
# Omid Zarin <zarinpy@gmail.com>, 2019
|
# Omid Zarin <zarinpy@gmail.com>, 2019
|
||||||
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
||||||
|
# Sina Amini <general.sina.amini.20@gmail.com>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
|
||||||
|
|
||||||
#: fields.py:816
|
#: fields.py:816
|
||||||
msgid "Enter a valid email address."
|
msgid "Enter a valid email address."
|
||||||
msgstr "پست الکترونیکی صحبح وارد کنید."
|
msgstr "پست الکترونیکی صحیح وارد کنید."
|
||||||
|
|
||||||
#: fields.py:827
|
#: fields.py:827
|
||||||
msgid "This value does not match the required pattern."
|
msgid "This value does not match the required pattern."
|
||||||
|
|
Binary file not shown.
|
@ -7,6 +7,7 @@
|
||||||
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
||||||
# Omid Zarin <zarinpy@gmail.com>, 2019
|
# Omid Zarin <zarinpy@gmail.com>, 2019
|
||||||
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
||||||
|
# Sina Amini <general.sina.amini.20@gmail.com>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
|
||||||
|
|
||||||
#: fields.py:816
|
#: fields.py:816
|
||||||
msgid "Enter a valid email address."
|
msgid "Enter a valid email address."
|
||||||
msgstr "پست الکترونیکی صحبح وارد کنید."
|
msgstr "پست الکترونیکی صحیح وارد کنید."
|
||||||
|
|
||||||
#: fields.py:827
|
#: fields.py:827
|
||||||
msgid "This value does not match the required pattern."
|
msgid "This value does not match the required pattern."
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 2015
|
# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 2015
|
||||||
# Hugo Leonardo Chalhoub Mendonça <hugoleonardocm@live.com>, 2015
|
# Hugo Leonardo Chalhoub Mendonça <hugoleonardocm@live.com>, 2015
|
||||||
# Jonatas Baldin <jonatas.baldin@gmail.com>, 2017
|
# Jonatas Baldin <jonatas.baldin@gmail.com>, 2017
|
||||||
|
# Gabriel Mitelman Tkacz <gmtkacz@proton.me>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -106,11 +107,11 @@ msgstr "Ocorreu um erro de servidor."
|
||||||
|
|
||||||
#: exceptions.py:142
|
#: exceptions.py:142
|
||||||
msgid "Invalid input."
|
msgid "Invalid input."
|
||||||
msgstr ""
|
msgstr "Entrada inválida"
|
||||||
|
|
||||||
#: exceptions.py:161
|
#: exceptions.py:161
|
||||||
msgid "Malformed request."
|
msgid "Malformed request."
|
||||||
msgstr "Pedido malformado."
|
msgstr "Requisição malformada."
|
||||||
|
|
||||||
#: exceptions.py:167
|
#: exceptions.py:167
|
||||||
msgid "Incorrect authentication credentials."
|
msgid "Incorrect authentication credentials."
|
||||||
|
@ -149,12 +150,12 @@ msgstr "Pedido foi limitado."
|
||||||
#: exceptions.py:224
|
#: exceptions.py:224
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected available in {wait} second."
|
msgid "Expected available in {wait} second."
|
||||||
msgstr ""
|
msgstr "Disponível em {wait} segundo."
|
||||||
|
|
||||||
#: exceptions.py:225
|
#: exceptions.py:225
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected available in {wait} seconds."
|
msgid "Expected available in {wait} seconds."
|
||||||
msgstr ""
|
msgstr "Disponível em {wait} segundos."
|
||||||
|
|
||||||
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
|
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
|
||||||
#: validators.py:183
|
#: validators.py:183
|
||||||
|
@ -167,15 +168,15 @@ msgstr "Este campo não pode ser nulo."
|
||||||
|
|
||||||
#: fields.py:701
|
#: fields.py:701
|
||||||
msgid "Must be a valid boolean."
|
msgid "Must be a valid boolean."
|
||||||
msgstr ""
|
msgstr "Deve ser um valor booleano válido."
|
||||||
|
|
||||||
#: fields.py:766
|
#: fields.py:766
|
||||||
msgid "Not a valid string."
|
msgid "Not a valid string."
|
||||||
msgstr ""
|
msgstr "Não é uma string válida."
|
||||||
|
|
||||||
#: fields.py:767
|
#: fields.py:767
|
||||||
msgid "This field may not be blank."
|
msgid "This field may not be blank."
|
||||||
msgstr "Este campo não pode ser em branco."
|
msgstr "Este campo não pode estar em branco."
|
||||||
|
|
||||||
#: fields.py:768 fields.py:1881
|
#: fields.py:768 fields.py:1881
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -205,7 +206,7 @@ msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
|
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
|
||||||
"or hyphens."
|
"or hyphens."
|
||||||
msgstr ""
|
msgstr "Digite um \"slug\" válido que consista de letras Unicode, números, sublinhados ou hífens."
|
||||||
|
|
||||||
#: fields.py:854
|
#: fields.py:854
|
||||||
msgid "Enter a valid URL."
|
msgid "Enter a valid URL."
|
||||||
|
@ -213,7 +214,7 @@ msgstr "Entrar um URL válido."
|
||||||
|
|
||||||
#: fields.py:867
|
#: fields.py:867
|
||||||
msgid "Must be a valid UUID."
|
msgid "Must be a valid UUID."
|
||||||
msgstr ""
|
msgstr "Deve ser um UUID válido."
|
||||||
|
|
||||||
#: fields.py:903
|
#: fields.py:903
|
||||||
msgid "Enter a valid IPv4 or IPv6 address."
|
msgid "Enter a valid IPv4 or IPv6 address."
|
||||||
|
@ -271,11 +272,11 @@ msgstr "Necessário uma data e hora mas recebeu uma data."
|
||||||
#: fields.py:1150
|
#: fields.py:1150
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid datetime for the timezone \"{timezone}\"."
|
msgid "Invalid datetime for the timezone \"{timezone}\"."
|
||||||
msgstr ""
|
msgstr "Data e hora inválidas para o fuso horário \"{timezone}\"."
|
||||||
|
|
||||||
#: fields.py:1151
|
#: fields.py:1151
|
||||||
msgid "Datetime value out of range."
|
msgid "Datetime value out of range."
|
||||||
msgstr ""
|
msgstr "Valor de data e hora fora do intervalo."
|
||||||
|
|
||||||
#: fields.py:1236
|
#: fields.py:1236
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -299,7 +300,7 @@ msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format}
|
||||||
#: fields.py:1399 fields.py:1456
|
#: fields.py:1399 fields.py:1456
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "\"{input}\" is not a valid choice."
|
msgid "\"{input}\" is not a valid choice."
|
||||||
msgstr "\"{input}\" não é um escolha válido."
|
msgstr "\"{input}\" não é um escolha válida."
|
||||||
|
|
||||||
#: fields.py:1402
|
#: fields.py:1402
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -309,7 +310,7 @@ msgstr "Mais de {count} itens..."
|
||||||
#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
|
#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected a list of items but got type \"{input_type}\"."
|
msgid "Expected a list of items but got type \"{input_type}\"."
|
||||||
msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"."
|
msgstr "Esperava uma lista de itens, mas recebeu tipo \"{input_type}\"."
|
||||||
|
|
||||||
#: fields.py:1458
|
#: fields.py:1458
|
||||||
msgid "This selection may not be empty."
|
msgid "This selection may not be empty."
|
||||||
|
@ -356,21 +357,21 @@ msgstr "Esta lista não pode estar vazia."
|
||||||
#: fields.py:1605
|
#: fields.py:1605
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has at least {min_length} elements."
|
msgid "Ensure this field has at least {min_length} elements."
|
||||||
msgstr ""
|
msgstr "Certifique-se de que este campo tenha pelo menos {min_length} elementos."
|
||||||
|
|
||||||
#: fields.py:1606
|
#: fields.py:1606
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has no more than {max_length} elements."
|
msgid "Ensure this field has no more than {max_length} elements."
|
||||||
msgstr ""
|
msgstr "Certifique-se de que este campo não tenha mais que {max_length} elementos."
|
||||||
|
|
||||||
#: fields.py:1682
|
#: fields.py:1682
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected a dictionary of items but got type \"{input_type}\"."
|
msgid "Expected a dictionary of items but got type \"{input_type}\"."
|
||||||
msgstr "Esperado um dicionário de itens mas recebeu tipo \"{input_type}\"."
|
msgstr "Esperava um dicionário de itens mas recebeu tipo \"{input_type}\"."
|
||||||
|
|
||||||
#: fields.py:1683
|
#: fields.py:1683
|
||||||
msgid "This dictionary may not be empty."
|
msgid "This dictionary may not be empty."
|
||||||
msgstr ""
|
msgstr "Este dicionário não pode estar vazio."
|
||||||
|
|
||||||
#: fields.py:1755
|
#: fields.py:1755
|
||||||
msgid "Value must be valid JSON."
|
msgid "Value must be valid JSON."
|
||||||
|
@ -382,7 +383,7 @@ msgstr "Buscar"
|
||||||
|
|
||||||
#: filters.py:50
|
#: filters.py:50
|
||||||
msgid "A search term."
|
msgid "A search term."
|
||||||
msgstr ""
|
msgstr "Um termo de busca."
|
||||||
|
|
||||||
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
|
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
|
||||||
msgid "Ordering"
|
msgid "Ordering"
|
||||||
|
@ -390,7 +391,7 @@ msgstr "Ordenando"
|
||||||
|
|
||||||
#: filters.py:181
|
#: filters.py:181
|
||||||
msgid "Which field to use when ordering the results."
|
msgid "Which field to use when ordering the results."
|
||||||
msgstr ""
|
msgstr "Qual campo usar ao ordenar os resultados."
|
||||||
|
|
||||||
#: filters.py:287
|
#: filters.py:287
|
||||||
msgid "ascending"
|
msgid "ascending"
|
||||||
|
@ -402,11 +403,11 @@ msgstr "descendente"
|
||||||
|
|
||||||
#: pagination.py:174
|
#: pagination.py:174
|
||||||
msgid "A page number within the paginated result set."
|
msgid "A page number within the paginated result set."
|
||||||
msgstr ""
|
msgstr "Um número de página dentro do conjunto de resultados paginado."
|
||||||
|
|
||||||
#: pagination.py:179 pagination.py:372 pagination.py:590
|
#: pagination.py:179 pagination.py:372 pagination.py:590
|
||||||
msgid "Number of results to return per page."
|
msgid "Number of results to return per page."
|
||||||
msgstr ""
|
msgstr "Número de resultados a serem retornados por página."
|
||||||
|
|
||||||
#: pagination.py:189
|
#: pagination.py:189
|
||||||
msgid "Invalid page."
|
msgid "Invalid page."
|
||||||
|
@ -414,11 +415,11 @@ msgstr "Página inválida."
|
||||||
|
|
||||||
#: pagination.py:374
|
#: pagination.py:374
|
||||||
msgid "The initial index from which to return the results."
|
msgid "The initial index from which to return the results."
|
||||||
msgstr ""
|
msgstr "O índice inicial a partir do qual retornar os resultados."
|
||||||
|
|
||||||
#: pagination.py:581
|
#: pagination.py:581
|
||||||
msgid "The pagination cursor value."
|
msgid "The pagination cursor value."
|
||||||
msgstr ""
|
msgstr "O valor do cursor de paginação."
|
||||||
|
|
||||||
#: pagination.py:583
|
#: pagination.py:583
|
||||||
msgid "Invalid cursor"
|
msgid "Invalid cursor"
|
||||||
|
@ -432,7 +433,7 @@ msgstr "Pk inválido \"{pk_value}\" - objeto não existe."
|
||||||
#: relations.py:247
|
#: relations.py:247
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Incorrect type. Expected pk value, received {data_type}."
|
msgid "Incorrect type. Expected pk value, received {data_type}."
|
||||||
msgstr "Tipo incorreto. Esperado valor pk, recebeu {data_type}."
|
msgstr "Tipo incorreto. Esperava valor pk, recebeu {data_type}."
|
||||||
|
|
||||||
#: relations.py:280
|
#: relations.py:280
|
||||||
msgid "Invalid hyperlink - No URL match."
|
msgid "Invalid hyperlink - No URL match."
|
||||||
|
@ -462,20 +463,20 @@ msgstr "Valor inválido."
|
||||||
|
|
||||||
#: schemas/utils.py:32
|
#: schemas/utils.py:32
|
||||||
msgid "unique integer value"
|
msgid "unique integer value"
|
||||||
msgstr ""
|
msgstr "valor inteiro único"
|
||||||
|
|
||||||
#: schemas/utils.py:34
|
#: schemas/utils.py:34
|
||||||
msgid "UUID string"
|
msgid "UUID string"
|
||||||
msgstr ""
|
msgstr "string UUID"
|
||||||
|
|
||||||
#: schemas/utils.py:36
|
#: schemas/utils.py:36
|
||||||
msgid "unique value"
|
msgid "unique value"
|
||||||
msgstr ""
|
msgstr "valor único"
|
||||||
|
|
||||||
#: schemas/utils.py:38
|
#: schemas/utils.py:38
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "A {value_type} identifying this {name}."
|
msgid "A {value_type} identifying this {name}."
|
||||||
msgstr ""
|
msgstr "Um {value_type} que identifica este {name}."
|
||||||
|
|
||||||
#: serializers.py:337
|
#: serializers.py:337
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -485,7 +486,7 @@ msgstr "Dado inválido. Necessário um dicionário mas recebeu {datatype}."
|
||||||
#: templates/rest_framework/admin.html:116
|
#: templates/rest_framework/admin.html:116
|
||||||
#: templates/rest_framework/base.html:136
|
#: templates/rest_framework/base.html:136
|
||||||
msgid "Extra Actions"
|
msgid "Extra Actions"
|
||||||
msgstr ""
|
msgstr "Ações Extras"
|
||||||
|
|
||||||
#: templates/rest_framework/admin.html:130
|
#: templates/rest_framework/admin.html:130
|
||||||
#: templates/rest_framework/base.html:150
|
#: templates/rest_framework/base.html:150
|
||||||
|
@ -540,7 +541,7 @@ msgstr "Os campos {field_names} devem criar um set único."
|
||||||
#: validators.py:171
|
#: validators.py:171
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Surrogate characters are not allowed: U+{code_point:X}."
|
msgid "Surrogate characters are not allowed: U+{code_point:X}."
|
||||||
msgstr ""
|
msgstr "Caracteres substitutos não são permitidos: U+{code_point:X}."
|
||||||
|
|
||||||
#: validators.py:243
|
#: validators.py:243
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
|
|
@ -353,12 +353,12 @@ msgstr "列表字段不能为空值。"
|
||||||
#: fields.py:1605
|
#: fields.py:1605
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has at least {min_length} elements."
|
msgid "Ensure this field has at least {min_length} elements."
|
||||||
msgstr ""
|
msgstr "请确保这个字段至少包含 {min_length} 个元素。"
|
||||||
|
|
||||||
#: fields.py:1606
|
#: fields.py:1606
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has no more than {max_length} elements."
|
msgid "Ensure this field has no more than {max_length} elements."
|
||||||
msgstr ""
|
msgstr "请确保这个字段不能超过 {max_length} 个元素。"
|
||||||
|
|
||||||
#: fields.py:1682
|
#: fields.py:1682
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -367,7 +367,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。"
|
||||||
|
|
||||||
#: fields.py:1683
|
#: fields.py:1683
|
||||||
msgid "This dictionary may not be empty."
|
msgid "This dictionary may not be empty."
|
||||||
msgstr ""
|
msgstr "这个字典可能不是空的。"
|
||||||
|
|
||||||
#: fields.py:1755
|
#: fields.py:1755
|
||||||
msgid "Value must be valid JSON."
|
msgid "Value must be valid JSON."
|
||||||
|
|
|
@ -171,6 +171,10 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
|
|
||||||
def get_template_context(self, data, renderer_context):
|
def get_template_context(self, data, renderer_context):
|
||||||
response = renderer_context['response']
|
response = renderer_context['response']
|
||||||
|
# in case a ValidationError is caught the data parameter may be a list
|
||||||
|
# see rest_framework.views.exception_handler
|
||||||
|
if isinstance(data, list):
|
||||||
|
return {'details': data, 'status_code': response.status_code}
|
||||||
if response.exception:
|
if response.exception:
|
||||||
data['status_code'] = response.status_code
|
data['status_code'] = response.status_code
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -217,7 +217,8 @@ class Request:
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
if not _hasattr(self, '_full_data'):
|
if not _hasattr(self, '_full_data'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
return self._full_data
|
return self._full_data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -420,13 +421,14 @@ class Request:
|
||||||
_request = self.__getattribute__("_request")
|
_request = self.__getattribute__("_request")
|
||||||
return getattr(_request, attr)
|
return getattr(_request, attr)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return self.__getattribute__(attr)
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def POST(self):
|
def POST(self):
|
||||||
# Ensure that request.POST uses our request parsing.
|
# Ensure that request.POST uses our request parsing.
|
||||||
if not _hasattr(self, '_data'):
|
if not _hasattr(self, '_data'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
if is_form_media_type(self.content_type):
|
if is_form_media_type(self.content_type):
|
||||||
return self._data
|
return self._data
|
||||||
return QueryDict('', encoding=self._request._encoding)
|
return QueryDict('', encoding=self._request._encoding)
|
||||||
|
@ -437,7 +439,8 @@ class Request:
|
||||||
# Different from the other two cases, which are not valid property
|
# Different from the other two cases, which are not valid property
|
||||||
# names on the WSGIRequest class.
|
# names on the WSGIRequest class.
|
||||||
if not _hasattr(self, '_files'):
|
if not _hasattr(self, '_files'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
def force_plaintext_errors(self, value):
|
def force_plaintext_errors(self, value):
|
||||||
|
|
|
@ -79,8 +79,9 @@ class ViewInspector:
|
||||||
view = self.view
|
view = self.view
|
||||||
|
|
||||||
method_name = getattr(view, 'action', method.lower())
|
method_name = getattr(view, 'action', method.lower())
|
||||||
method_docstring = getattr(view, method_name, None).__doc__
|
method_func = getattr(view, method_name, None)
|
||||||
if method_docstring:
|
method_docstring = method_func.__doc__
|
||||||
|
if method_func and method_docstring:
|
||||||
# An explicit docstring on the method or action.
|
# An explicit docstring on the method or action.
|
||||||
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
|
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -11,9 +11,7 @@ from django.core.validators import (
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from rest_framework import (
|
from rest_framework import exceptions, renderers, serializers
|
||||||
RemovedInDRF316Warning, exceptions, renderers, serializers
|
|
||||||
)
|
|
||||||
from rest_framework.compat import inflection, uritemplate
|
from rest_framework.compat import inflection, uritemplate
|
||||||
from rest_framework.fields import _UnvalidatedField, empty
|
from rest_framework.fields import _UnvalidatedField, empty
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
@ -721,11 +719,3 @@ class AutoSchema(ViewInspector):
|
||||||
path = path[1:]
|
path = path[1:]
|
||||||
|
|
||||||
return [path.split('/')[0].replace('_', '-')]
|
return [path.split('/')[0].replace('_', '-')]
|
||||||
|
|
||||||
def _get_reference(self, serializer):
|
|
||||||
warnings.warn(
|
|
||||||
"Method `_get_reference()` has been renamed to `get_reference()`. "
|
|
||||||
"The old name will be removed in DRF v3.16.",
|
|
||||||
RemovedInDRF316Warning, stacklevel=2
|
|
||||||
)
|
|
||||||
return self.get_reference(serializer)
|
|
||||||
|
|
|
@ -1490,6 +1490,8 @@ class ModelSerializer(Serializer):
|
||||||
default = timezone.now
|
default = timezone.now
|
||||||
elif unique_constraint_field.has_default():
|
elif unique_constraint_field.has_default():
|
||||||
default = unique_constraint_field.default
|
default = unique_constraint_field.default
|
||||||
|
elif unique_constraint_field.null:
|
||||||
|
default = None
|
||||||
else:
|
else:
|
||||||
default = empty
|
default = empty
|
||||||
|
|
||||||
|
|
|
@ -150,15 +150,19 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
"""
|
"""
|
||||||
Encode the data returning a two tuple of (bytes, content_type)
|
Encode the data returning a two tuple of (bytes, content_type)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return ('', content_type)
|
return (b'', content_type)
|
||||||
|
|
||||||
assert format is None or content_type is None, (
|
assert format is None or content_type is None, (
|
||||||
'You may not set both `format` and `content_type`.'
|
'You may not set both `format` and `content_type`.'
|
||||||
)
|
)
|
||||||
|
|
||||||
if content_type:
|
if content_type:
|
||||||
|
try:
|
||||||
|
data = self._encode_json(data, content_type)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Content type specified explicitly, treat data as a raw bytestring
|
# Content type specified explicitly, treat data as a raw bytestring
|
||||||
ret = force_bytes(data, settings.DEFAULT_CHARSET)
|
ret = force_bytes(data, settings.DEFAULT_CHARSET)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.urls import URLResolver, include, path, re_path, register_converter
|
from django.urls import URLResolver, include, path, re_path, register_converter
|
||||||
|
from django.urls.converters import get_converters
|
||||||
from django.urls.resolvers import RoutePattern
|
from django.urls.resolvers import RoutePattern
|
||||||
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
def _get_format_path_converter(suffix_kwarg, allowed):
|
def _get_format_path_converter(allowed):
|
||||||
if allowed:
|
if allowed:
|
||||||
if len(allowed) == 1:
|
if len(allowed) == 1:
|
||||||
allowed_pattern = allowed[0]
|
allowed_pattern = allowed[0]
|
||||||
|
@ -23,11 +24,14 @@ def _get_format_path_converter(suffix_kwarg, allowed):
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return '.' + value + '/'
|
return '.' + value + '/'
|
||||||
|
|
||||||
|
return FormatSuffixConverter
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_converter_name(allowed):
|
||||||
converter_name = 'drf_format_suffix'
|
converter_name = 'drf_format_suffix'
|
||||||
if allowed:
|
if allowed:
|
||||||
converter_name += '_' + '_'.join(allowed)
|
converter_name += '_' + '_'.join(allowed)
|
||||||
|
return converter_name
|
||||||
return converter_name, FormatSuffixConverter
|
|
||||||
|
|
||||||
|
|
||||||
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None):
|
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None):
|
||||||
|
@ -104,8 +108,10 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||||
else:
|
else:
|
||||||
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
|
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
|
||||||
|
|
||||||
converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed)
|
converter_name = _generate_converter_name(allowed)
|
||||||
register_converter(suffix_converter, converter_name)
|
if converter_name not in get_converters():
|
||||||
|
suffix_converter = _get_format_path_converter(allowed)
|
||||||
|
register_converter(suffix_converter, converter_name)
|
||||||
|
|
||||||
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
|
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
|
||||||
|
|
||||||
|
|
|
@ -159,17 +159,18 @@ class UniqueTogetherValidator:
|
||||||
queryset = self.filter_queryset(attrs, queryset, serializer)
|
queryset = self.filter_queryset(attrs, queryset, serializer)
|
||||||
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
|
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
|
||||||
|
|
||||||
|
checked_names = [
|
||||||
|
serializer.fields[field_name].source for field_name in self.fields
|
||||||
|
]
|
||||||
# Ignore validation if any field is None
|
# Ignore validation if any field is None
|
||||||
if serializer.instance is None:
|
if serializer.instance is None:
|
||||||
checked_values = [
|
checked_values = [attrs[field_name] for field_name in checked_names]
|
||||||
value for field, value in attrs.items() if field in self.fields
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
# Ignore validation if all field values are unchanged
|
# Ignore validation if all field values are unchanged
|
||||||
checked_values = [
|
checked_values = [
|
||||||
value
|
attrs[field_name]
|
||||||
for field, value in attrs.items()
|
for field_name in checked_names
|
||||||
if field in self.fields and value != getattr(serializer.instance, field)
|
if attrs[field_name] != getattr(serializer.instance, field_name)
|
||||||
]
|
]
|
||||||
|
|
||||||
if checked_values and None not in checked_values and qs_exists(queryset):
|
if checked_values and None not in checked_values and qs_exists(queryset):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Provides an APIView class that is the base of all views in REST framework.
|
Provides an APIView class that is the base of all views in REST framework.
|
||||||
"""
|
"""
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import connections, models
|
from django.db import connections, models
|
||||||
|
@ -139,6 +140,11 @@ class APIView(View):
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
view.initkwargs = initkwargs
|
view.initkwargs = initkwargs
|
||||||
|
|
||||||
|
# Exempt all DRF views from Django's LoginRequiredMiddleware. Users should set
|
||||||
|
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||||
|
if DJANGO_VERSION >= (5, 1):
|
||||||
|
view.login_required = False
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
# all other authentication is CSRF exempt.
|
# all other authentication is CSRF exempt.
|
||||||
return csrf_exempt(view)
|
return csrf_exempt(view)
|
||||||
|
|
|
@ -19,6 +19,7 @@ automatically.
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from inspect import getmembers
|
from inspect import getmembers
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from django.utils.decorators import classonlymethod
|
from django.utils.decorators import classonlymethod
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
@ -136,6 +137,12 @@ class ViewSetMixin:
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
view.initkwargs = initkwargs
|
view.initkwargs = initkwargs
|
||||||
view.actions = actions
|
view.actions = actions
|
||||||
|
|
||||||
|
# Exempt from Django's LoginRequiredMiddleware. Users should set
|
||||||
|
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||||
|
if DJANGO_VERSION >= (5, 1):
|
||||||
|
view.login_required = False
|
||||||
|
|
||||||
return csrf_exempt(view)
|
return csrf_exempt(view)
|
||||||
|
|
||||||
def initialize_request(self, request, *args, **kwargs):
|
def initialize_request(self, request, *args, **kwargs):
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -91,6 +91,7 @@ setup(
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 4.2',
|
'Framework :: Django :: 4.2',
|
||||||
'Framework :: Django :: 5.0',
|
'Framework :: Django :: 5.0',
|
||||||
|
'Framework :: Django :: 5.1',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
|
@ -101,6 +102,7 @@ setup(
|
||||||
'Programming Language :: Python :: 3.10',
|
'Programming Language :: Python :: 3.10',
|
||||||
'Programming Language :: Python :: 3.11',
|
'Programming Language :: Python :: 3.11',
|
||||||
'Programming Language :: Python :: 3.12',
|
'Programming Language :: Python :: 3.12',
|
||||||
|
'Programming Language :: Python :: 3.13',
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
'Topic :: Internet :: WWW/HTTP',
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
],
|
],
|
||||||
|
|
|
@ -1245,13 +1245,13 @@ class TestMinMaxDecimalField(FieldValues):
|
||||||
'20.0': Decimal('20.0'),
|
'20.0': Decimal('20.0'),
|
||||||
}
|
}
|
||||||
invalid_inputs = {
|
invalid_inputs = {
|
||||||
'9.9': ['Ensure this value is greater than or equal to 10.'],
|
'9.9': ['Ensure this value is greater than or equal to 10.0.'],
|
||||||
'20.1': ['Ensure this value is less than or equal to 20.'],
|
'20.1': ['Ensure this value is less than or equal to 20.0.'],
|
||||||
}
|
}
|
||||||
outputs = {}
|
outputs = {}
|
||||||
field = serializers.DecimalField(
|
field = serializers.DecimalField(
|
||||||
max_digits=3, decimal_places=1,
|
max_digits=3, decimal_places=1,
|
||||||
min_value=10, max_value=20
|
min_value=10.0, max_value=20.0
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_warning_when_not_decimal_types(self, caplog):
|
def test_warning_when_not_decimal_types(self, caplog):
|
||||||
|
@ -1260,14 +1260,14 @@ class TestMinMaxDecimalField(FieldValues):
|
||||||
|
|
||||||
serializers.DecimalField(
|
serializers.DecimalField(
|
||||||
max_digits=3, decimal_places=1,
|
max_digits=3, decimal_places=1,
|
||||||
min_value=10, max_value=20
|
min_value=10.0, max_value=20.0
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(w) == 2
|
assert len(w) == 2
|
||||||
assert all(issubclass(i.category, UserWarning) for i in w)
|
assert all(issubclass(i.category, UserWarning) for i in w)
|
||||||
|
|
||||||
assert 'max_value should be a Decimal instance' in str(w[0].message)
|
assert 'max_value should be an integer or Decimal instance' in str(w[0].message)
|
||||||
assert 'min_value should be a Decimal instance' in str(w[1].message)
|
assert 'min_value should be an integer or Decimal instance' in str(w[1].message)
|
||||||
|
|
||||||
|
|
||||||
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.urls import path
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.renderers import TemplateHTMLRenderer
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
@ -34,10 +35,17 @@ def not_found(request):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def validation_error(request):
|
||||||
|
raise ValidationError('error')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', example),
|
path('', example),
|
||||||
path('permission_denied', permission_denied),
|
path('permission_denied', permission_denied),
|
||||||
path('not_found', not_found),
|
path('not_found', not_found),
|
||||||
|
path('validation_error', validation_error),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,6 +99,12 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
self.assertEqual(response.content, b"403 Forbidden")
|
self.assertEqual(response.content, b"403 Forbidden")
|
||||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
|
def test_validation_error_html_view(self):
|
||||||
|
response = self.client.get('/validation_error')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.content, b"400 Bad Request")
|
||||||
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
# 2 tests below are based on order of if statements in corresponding method
|
# 2 tests below are based on order of if statements in corresponding method
|
||||||
# of TemplateHTMLRenderer
|
# of TemplateHTMLRenderer
|
||||||
def test_get_template_names_returns_own_template_name(self):
|
def test_get_template_names_returns_own_template_name(self):
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import django
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework.decorators import action, api_view
|
||||||
from rest_framework.request import is_form_media_type
|
from rest_framework.request import is_form_media_type
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.routers import SimpleRouter
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
|
||||||
class PostView(APIView):
|
class PostView(APIView):
|
||||||
|
@ -16,9 +23,39 @@ class PostView(APIView):
|
||||||
return Response(data=request.data, status=200)
|
return Response(data=request.data, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class GetAPIView(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
return Response(data="OK", status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def get_func_view(request):
|
||||||
|
return Response(data="OK", status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class ListViewSet(GenericViewSet):
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
response = Response()
|
||||||
|
response.view = self
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action(detail=False, url_path='list-action')
|
||||||
|
def list_action(self, request, *args, **kwargs):
|
||||||
|
response = Response()
|
||||||
|
response.view = self
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
router = SimpleRouter()
|
||||||
|
router.register(r'view-set', ListViewSet, basename='view_set')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
|
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
|
||||||
path('post', PostView.as_view()),
|
path('post', PostView.as_view()),
|
||||||
|
path('get', GetAPIView.as_view()),
|
||||||
|
path('get-func', get_func_view),
|
||||||
|
path('api/', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,3 +111,38 @@ class TestMiddleware(APITestCase):
|
||||||
|
|
||||||
response = self.client.post('/post', {'foo': 'bar'}, format='json')
|
response = self.client.post('/post', {'foo': 'bar'}, format='json')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(django.VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
@override_settings(
|
||||||
|
ROOT_URLCONF='tests.test_middleware',
|
||||||
|
MIDDLEWARE=(
|
||||||
|
# Needed for AuthenticationMiddleware
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
# Needed for LoginRequiredMiddleware
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.LoginRequiredMiddleware',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class TestLoginRequiredMiddlewareCompat(APITestCase):
|
||||||
|
"""
|
||||||
|
Django's 5.1+ LoginRequiredMiddleware should NOT apply to DRF views.
|
||||||
|
|
||||||
|
Instead, users should put IsAuthenticated in their
|
||||||
|
DEFAULT_PERMISSION_CLASSES setting.
|
||||||
|
"""
|
||||||
|
def test_class_based_view(self):
|
||||||
|
response = self.client.get('/get')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_function_based_view(self):
|
||||||
|
response = self.client.get('/get-func')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_viewset_list(self):
|
||||||
|
response = self.client.get('/api/view-set/')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_viewset_list_action(self):
|
||||||
|
response = self.client.get('/api/view-set/list-action/')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
|
@ -56,3 +56,17 @@ class TestPrefetchRelatedUpdates(TestCase):
|
||||||
'email': 'tom@example.com'
|
'email': 'tom@example.com'
|
||||||
}
|
}
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
|
||||||
|
def test_can_update_without_queryset_on_class_view(self):
|
||||||
|
class UserUpdateWithoutQuerySet(generics.UpdateAPIView):
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return User.objects.get(pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
request = factory.patch('/', {'username': 'new'})
|
||||||
|
response = UserUpdateWithoutQuerySet.as_view()(request, pk=self.user.pk)
|
||||||
|
assert response.data['id'] == self.user.id
|
||||||
|
assert response.data['username'] == 'new'
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
assert self.user.username == 'new'
|
||||||
|
|
|
@ -126,6 +126,25 @@ class TestContentParsing(TestCase):
|
||||||
request.parsers = (PlainTextParser(), )
|
request.parsers = (PlainTextParser(), )
|
||||||
assert request.data == content
|
assert request.data == content
|
||||||
|
|
||||||
|
def test_calling_data_fails_when_attribute_error_is_raised(self):
|
||||||
|
"""
|
||||||
|
Ensure attribute errors raised when parsing are properly re-raised.
|
||||||
|
"""
|
||||||
|
expected_message = "Internal error"
|
||||||
|
|
||||||
|
class BrokenParser:
|
||||||
|
media_type = "application/json"
|
||||||
|
|
||||||
|
def parse(self, *args, **kwargs):
|
||||||
|
raise AttributeError(expected_message)
|
||||||
|
|
||||||
|
http_request = factory.post('/', data={}, format="json")
|
||||||
|
request = Request(http_request)
|
||||||
|
request.parsers = (BrokenParser,)
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(WrappedAttributeError, expected_message):
|
||||||
|
request.data
|
||||||
|
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
authentication_classes = (SessionAuthentication,)
|
authentication_classes = (SessionAuthentication,)
|
||||||
|
|
|
@ -8,9 +8,11 @@ from django.shortcuts import redirect
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from rest_framework import fields, serializers
|
from rest_framework import fields, parsers, renderers, serializers, status
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import (
|
||||||
|
api_view, parser_classes, renderer_classes
|
||||||
|
)
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.test import (
|
from rest_framework.test import (
|
||||||
APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate
|
APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate
|
||||||
|
@ -50,6 +52,18 @@ class BasicSerializer(serializers.Serializer):
|
||||||
flag = fields.BooleanField(default=lambda: True)
|
flag = fields.BooleanField(default=lambda: True)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
@parser_classes((parsers.JSONParser,))
|
||||||
|
def post_json_view(request):
|
||||||
|
return Response(request.data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['DELETE'])
|
||||||
|
@renderer_classes((renderers.JSONRenderer, ))
|
||||||
|
def delete_json_view(request):
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def post_view(request):
|
def post_view(request):
|
||||||
serializer = BasicSerializer(data=request.data)
|
serializer = BasicSerializer(data=request.data)
|
||||||
|
@ -62,7 +76,9 @@ urlpatterns = [
|
||||||
path('session-view/', session_view),
|
path('session-view/', session_view),
|
||||||
path('redirect-view/', redirect_view),
|
path('redirect-view/', redirect_view),
|
||||||
path('redirect-view/<int:code>/', redirect_307_308_view),
|
path('redirect-view/<int:code>/', redirect_307_308_view),
|
||||||
path('post-view/', post_view)
|
path('post-json-view/', post_json_view),
|
||||||
|
path('delete-json-view/', delete_json_view),
|
||||||
|
path('post-view/', post_view),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -236,6 +252,22 @@ class TestAPITestClient(TestCase):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data == {"flag": True}
|
assert response.data == {"flag": True}
|
||||||
|
|
||||||
|
def test_post_encodes_data_based_on_json_content_type(self):
|
||||||
|
data = {'data': True}
|
||||||
|
response = self.client.post(
|
||||||
|
'/post-json-view/',
|
||||||
|
data=data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == data
|
||||||
|
|
||||||
|
def test_delete_based_on_format(self):
|
||||||
|
response = self.client.delete('/delete-json-view/', format='json')
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
assert response.data is None
|
||||||
|
|
||||||
|
|
||||||
class TestAPIRequestFactory(TestCase):
|
class TestAPIRequestFactory(TestCase):
|
||||||
def test_csrf_exempt_by_default(self):
|
def test_csrf_exempt_by_default(self):
|
||||||
|
|
|
@ -441,6 +441,14 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
serializer = NullUniquenessTogetherSerializer(data=data)
|
serializer = NullUniquenessTogetherSerializer(data=data)
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
|
|
||||||
|
def test_ignore_validation_for_missing_nullable_fields(self):
|
||||||
|
data = {
|
||||||
|
'date': datetime.date(2000, 1, 1),
|
||||||
|
'race_name': 'Paris Marathon',
|
||||||
|
}
|
||||||
|
serializer = NullUniquenessTogetherSerializer(data=data)
|
||||||
|
assert serializer.is_valid(), serializer.errors
|
||||||
|
|
||||||
def test_do_not_ignore_validation_for_null_fields(self):
|
def test_do_not_ignore_validation_for_null_fields(self):
|
||||||
# None values that are not on fields part of the uniqueness constraint
|
# None values that are not on fields part of the uniqueness constraint
|
||||||
# do not cause the instance to skip validation.
|
# do not cause the instance to skip validation.
|
||||||
|
@ -469,6 +477,28 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert not mock.called
|
assert not mock.called
|
||||||
|
|
||||||
|
@patch("rest_framework.validators.qs_exists")
|
||||||
|
def test_unique_together_with_source(self, mock_qs_exists):
|
||||||
|
class UniqueTogetherWithSourceSerializer(serializers.ModelSerializer):
|
||||||
|
name = serializers.CharField(source="race_name")
|
||||||
|
pos = serializers.IntegerField(source="position")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UniquenessTogetherModel
|
||||||
|
fields = ["name", "pos"]
|
||||||
|
|
||||||
|
data = {"name": "Paris Marathon", "pos": 1}
|
||||||
|
instance = UniquenessTogetherModel.objects.create(
|
||||||
|
race_name="Paris Marathon", position=1
|
||||||
|
)
|
||||||
|
serializer = UniqueTogetherWithSourceSerializer(data=data)
|
||||||
|
assert not serializer.is_valid()
|
||||||
|
assert mock_qs_exists.called
|
||||||
|
mock_qs_exists.reset_mock()
|
||||||
|
serializer = UniqueTogetherWithSourceSerializer(data=data, instance=instance)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert not mock_qs_exists.called
|
||||||
|
|
||||||
def test_filter_queryset_do_not_skip_existing_attribute(self):
|
def test_filter_queryset_do_not_skip_existing_attribute(self):
|
||||||
"""
|
"""
|
||||||
filter_queryset should add value from existing instance attribute
|
filter_queryset should add value from existing instance attribute
|
||||||
|
@ -517,12 +547,30 @@ class UniqueConstraintModel(models.Model):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueConstraintNullableModel(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
age = models.IntegerField(null=True)
|
||||||
|
tag = models.CharField(max_length=100, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
# Unique constraint on 2 nullable fields
|
||||||
|
models.UniqueConstraint(name='unique_constraint', fields=('age', 'tag'))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UniqueConstraintSerializer(serializers.ModelSerializer):
|
class UniqueConstraintSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UniqueConstraintModel
|
model = UniqueConstraintModel
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueConstraintNullableSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UniqueConstraintNullableModel
|
||||||
|
fields = ('title', 'age', 'tag')
|
||||||
|
|
||||||
|
|
||||||
class TestUniqueConstraintValidation(TestCase):
|
class TestUniqueConstraintValidation(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.instance = UniqueConstraintModel.objects.create(
|
self.instance = UniqueConstraintModel.objects.create(
|
||||||
|
@ -589,6 +637,12 @@ class TestUniqueConstraintValidation(TestCase):
|
||||||
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
|
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
|
||||||
assert ids_in_qs == {frozenset([1]), frozenset([3])}
|
assert ids_in_qs == {frozenset([1]), frozenset([3])}
|
||||||
|
|
||||||
|
def test_nullable_unique_constraint_fields_are_not_required(self):
|
||||||
|
serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
|
||||||
|
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||||
|
result = serializer.save()
|
||||||
|
self.assertIsInstance(result, UniqueConstraintNullableModel)
|
||||||
|
|
||||||
|
|
||||||
# Tests for `UniqueForDateValidator`
|
# Tests for `UniqueForDateValidator`
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import copy
|
import copy
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
@ -136,3 +138,13 @@ class TestCustomSettings(TestCase):
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.data == {'error': 'SyntaxError'}
|
assert response.data == {'error': 'SyntaxError'}
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
class TestLoginRequiredMiddlewareCompat(TestCase):
|
||||||
|
def test_class_based_view_opted_out(self):
|
||||||
|
class_based_view = BasicView.as_view()
|
||||||
|
assert class_based_view.login_required is False
|
||||||
|
|
||||||
|
def test_function_based_view_opted_out(self):
|
||||||
|
assert basic_view.login_required is False
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import unittest
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
@ -196,6 +198,11 @@ class InitializeViewSetsTestCase(TestCase):
|
||||||
assert get.view.action == 'list_action'
|
assert get.view.action == 'list_action'
|
||||||
assert head.view.action == 'list_action'
|
assert head.view.action == 'list_action'
|
||||||
|
|
||||||
|
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
def test_login_required_middleware_compat(self):
|
||||||
|
view = ActionViewSet.as_view(actions={'get': 'list'})
|
||||||
|
assert view.login_required is False
|
||||||
|
|
||||||
|
|
||||||
class GetExtraActionsTests(TestCase):
|
class GetExtraActionsTests(TestCase):
|
||||||
|
|
||||||
|
|
17
tox.ini
17
tox.ini
|
@ -1,9 +1,10 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py38,py39}-{django42}
|
{py38,py39}-{django42}
|
||||||
{py310}-{django42,django50,djangomain}
|
{py310}-{django42,django50,django51,djangomain}
|
||||||
{py311}-{django42,django50,djangomain}
|
{py311}-{django42,django50,django51,djangomain}
|
||||||
{py312}-{django42,django50,djangomain}
|
{py312}-{django42,django50,django51,djangomain}
|
||||||
|
{py313}-{django51,djangomain}
|
||||||
base
|
base
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
@ -17,6 +18,7 @@ setenv =
|
||||||
deps =
|
deps =
|
||||||
django42: Django>=4.2,<5.0
|
django42: Django>=4.2,<5.0
|
||||||
django50: Django>=5.0,<5.1
|
django50: Django>=5.0,<5.1
|
||||||
|
django51: Django>=5.1,<5.2
|
||||||
djangomain: https://github.com/django/django/archive/main.tar.gz
|
djangomain: https://github.com/django/django/archive/main.tar.gz
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-optionals.txt
|
-rrequirements/requirements-optionals.txt
|
||||||
|
@ -42,12 +44,6 @@ deps =
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-documentation.txt
|
-rrequirements/requirements-documentation.txt
|
||||||
|
|
||||||
[testenv:py38-djangomain]
|
|
||||||
ignore_outcome = true
|
|
||||||
|
|
||||||
[testenv:py39-djangomain]
|
|
||||||
ignore_outcome = true
|
|
||||||
|
|
||||||
[testenv:py310-djangomain]
|
[testenv:py310-djangomain]
|
||||||
ignore_outcome = true
|
ignore_outcome = true
|
||||||
|
|
||||||
|
@ -56,3 +52,6 @@ ignore_outcome = true
|
||||||
|
|
||||||
[testenv:py312-djangomain]
|
[testenv:py312-djangomain]
|
||||||
ignore_outcome = true
|
ignore_outcome = true
|
||||||
|
|
||||||
|
[testenv:py313-djangomain]
|
||||||
|
ignore_outcome = true
|
||||||
|
|
Loading…
Reference in New Issue
Block a user