Merge branch 'master' into math-a3k-315_rn

This commit is contained in:
Tom Christie 2024-03-15 11:20:48 +00:00 committed by GitHub
commit 79cc4a6391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 294 additions and 235 deletions

13
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Action updates into a single larger pull request
schedule:
interval: weekly

View File

@ -20,11 +20,12 @@ jobs:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@ -61,9 +62,9 @@ jobs:
name: Test documentation links
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'

View File

@ -11,14 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: pre-commit/action@v3.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: pre-commit/action@v3.0.1

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@ -9,19 +9,25 @@ repos:
- id: check-symlinks
- id: check-toml
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 3.9.0
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-tidy-imports
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.13.0
rev: 1.16.0
hooks:
- id: blacken-docs
exclude: ^(?!docs).*$
additional_dependencies:
- black==23.1.0
- repo: https://github.com/codespell-project/codespell
# Configuration for codespell is in .codespellrc
rev: v2.2.6
hooks:
- id: codespell
exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js

View File

@ -56,7 +56,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements
* Python 3.6+
* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0.
* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.

View File

@ -303,7 +303,7 @@ Corresponds to `django.db.models.fields.DecimalField`.
* `min_value` Validate that the number provided is no less than this value.
* `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`.
* `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 loosing 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`.
#### Example usage

View File

@ -173,11 +173,9 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
# Advanced field defaults
Validators that are applied across multiple fields in the serializer can sometimes require a field input that should not be provided by the API client, but that *is* available as input to the validator.
For this purposes use `HiddenField`. This field will be present in `validated_data` but *will not* be used in the serializer output representation.
Two patterns that you may want to use for this sort of validation include:
* Using `HiddenField`. This field will be present in `validated_data` but *will not* be used in the serializer output representation.
* Using a standard field with `read_only=True`, but that also includes a `default=…` argument. This field *will* be used in the serializer output representation, but cannot be set directly by the user.
**Note:** Using a `read_only=True` field is excluded from writable fields so it won't use a `default=…` argument. Look [3.8 announcement](https://www.django-rest-framework.org/community/3.8-announcement/#altered-the-behaviour-of-read_only-plus-default-on-field).
REST framework includes a couple of defaults that may be useful in this context.
@ -189,7 +187,7 @@ A default class that can be used to represent the current user. In order to use
default=serializers.CurrentUserDefault()
)
#### CreateOnlyDefault
#### CreateOnlyDefault
A default class that can be used to *only set a default argument during create operations*. During updates the field is omitted.

View File

@ -311,7 +311,7 @@ You may need to provide custom `ViewSet` classes that do not have the full set o
To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions:
from rest_framework import mixins
from rest_framework import mixins, viewsets
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,

View File

@ -64,7 +64,7 @@ In some circumstances a Validator class or a Default class may need to access th
* Uniqueness validators need to be able to determine the name of the field to which they are applied, in order to run an appropriate database query.
* The `CurrentUserDefault` needs to be able to determine the context with which the serializer was instantiated, in order to return the current user instance.
Previous our approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13.
Our previous approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13.
Instead, validators or defaults which require the serializer context, should include a `requires_context = True` attribute on the class.

View File

@ -28,10 +28,10 @@ Our requirements are now:
* Python 3.6+
* Django 4.1, 4.0, 3.2, 3.1, 3.0
## `raise_exceptions` argument for `is_valid` is now keyword-only.
## `raise_exception` argument for `is_valid` is now keyword-only.
Calling `serializer_instance.is_valid(True)` is no longer acceptable syntax.
If you'd like to use the `raise_exceptions` argument, you must use it as a
If you'd like to use the `raise_exception` argument, you must use it as a
keyword argument.
See Pull Request [#7952](https://github.com/encode/django-rest-framework/pull/7952) for more details.

View File

@ -64,14 +64,10 @@ from rest_framework.schemas import get_schema_view
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
schema_view = get_schema_view(
title='Example API',
renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]
title="Example API", renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]
)
urlpatterns = [
path('swagger/', schema_view),
...
]
urlpatterns = [path("swagger/", schema_view), ...]
```
There have been a large number of fixes to the schema generation. These should

View File

@ -131,7 +131,7 @@ Date: 22nd September 2022
* Stop calling `set_context` on Validators. [[#8589](https://github.com/encode/django-rest-framework/pull/8589)]
* Return `NotImplemented` from `ErrorDetails.__ne__`. [[#8538](https://github.com/encode/django-rest-framework/pull/8538)]
* Don't evaluate `DateTimeField.default_timezone` when a custom timezone is set. [[#8531](https://github.com/encode/django-rest-framework/pull/8531)]
* Make relative URLs clickable in Browseable API. [[#8464](https://github.com/encode/django-rest-framework/pull/8464)]
* Make relative URLs clickable in Browsable API. [[#8464](https://github.com/encode/django-rest-framework/pull/8464)]
* Support `ManyRelatedField` falling back to the default value when the attribute specified by dot notation doesn't exist. Matches `ManyRelatedField.get_attribute` to `Field.get_attribute`. [[#7574](https://github.com/encode/django-rest-framework/pull/7574)]
* Make `schemas.openapi.get_reference` public. [[#7515](https://github.com/encode/django-rest-framework/pull/7515)]
* Make `ReturnDict` support `dict` union operators on Python 3.9 and later. [[#8302](https://github.com/encode/django-rest-framework/pull/8302)]
@ -149,7 +149,7 @@ Date: 15th December 2021
Date: 13th December 2021
* Django 4.0 compatability. [#8178]
* Django 4.0 compatibility. [#8178]
* Add `max_length` and `min_length` options to `ListSerializer`. [#8165]
* Add `get_request_serializer` and `get_response_serializer` hooks to `AutoSchema`. [#7424]
* Fix OpenAPI representation of null-able read only fields. [#8116]
@ -1030,7 +1030,7 @@ See the [release announcement][3.6-release].
* description.py codes and tests removal. ([#4153][gh4153])
* Wrap guardian.VERSION in tuple. ([#4149][gh4149])
* Refine validator for fields with <source=> kwargs. ([#4146][gh4146])
* Fix None values representation in childs of ListField, DictField. ([#4118][gh4118])
* Fix None values representation in children of ListField, DictField. ([#4118][gh4118])
* Resolve TimeField representation for midnight value. ([#4107][gh4107])
* Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. ([#4106][gh4106])
* TimeField render returns None instead of 00:00:00. ([#4105][gh4105])
@ -1038,7 +1038,7 @@ See the [release announcement][3.6-release].
* Prevent raising exception when limit is 0. ([#4098][gh4098])
* TokenAuthentication: Allow custom keyword in the header. ([#4097][gh4097])
* Handle incorrectly padded HTTP basic auth header. ([#4090][gh4090])
* LimitOffset pagination crashes Browseable API when limit=0. ([#4079][gh4079])
* LimitOffset pagination crashes Browsable API when limit=0. ([#4079][gh4079])
* Fixed DecimalField arbitrary precision support. ([#4075][gh4075])
* Added support for custom CSRF cookie names. ([#4049][gh4049])
* Fix regression introduced by #4035. ([#4041][gh4041])

View File

@ -152,6 +152,11 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [drf-standardized-errors][drf-standardized-errors] - DRF exception handler to standardize error responses for all API endpoints.
* [drf-api-action][drf-api-action] - uses the power of DRF also as a library functions
### Customization
* [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5.
* [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design.
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
[new-repo]: https://github.com/new
@ -243,3 +248,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-requestlogs]: https://github.com/Raekkeri/django-requestlogs
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors
[drf-api-action]: https://github.com/Ori-Roza/drf-api-action
[drf-redesign]: https://github.com/youzarsiph/drf-redesign
[drf-material]: https://github.com/youzarsiph/drf-material

BIN
docs/img/rfm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
docs/img/rfr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2)
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.

View File

@ -15,6 +15,18 @@ If you include fully-qualified URLs in your resource output, they will be 'urliz
By default, the API will return the format specified by the headers, which in the case of the browser is HTML. The format can be specified using `?format=` in the request, so you can look at the raw JSON response in a browser by adding `?format=json` to the URL. There are helpful extensions for viewing JSON in [Firefox][ffjsonview] and [Chrome][chromejsonview].
## Authentication
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
urlpatterns = [
# ...
url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
]
```
## Customizing
The browsable API is built with [Twitter's Bootstrap][bootstrap] (v 3.4.1), making it easy to customize the look-and-feel.
@ -65,6 +77,27 @@ For more specific CSS tweaks than simply overriding the default bootstrap theme
---
### Third party packages for customization
You can use a third party package for customization, rather than doing it by yourself. Here is 2 packages for customizing the API:
* [rest-framework-redesign][rest-framework-redesign] - A package for customizing the API using Bootstrap 5. Modern and sleek design, it comes with the support for dark mode.
* [rest-framework-material][rest-framework-material] - Material design for Django REST Framework.
---
![Django REST Framework Redesign][rfr]
*Screenshot of the rest-framework-redesign*
---
![Django REST Framework Material][rfm]
*Screenshot of the rest-framework-material*
---
### Blocks
All of the blocks available in the browsable API base template that can be used in your `api.html`.
@ -162,3 +195,7 @@ There are [a variety of packages for autocomplete widgets][autocomplete-packages
[bcomponentsnav]: https://getbootstrap.com/2.3.2/components.html#navbar
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[rest-framework-redesign]: https://github.com/youzarsiph/rest-framework-redesign
[rest-framework-material]: https://github.com/youzarsiph/rest-framework-material
[rfr]: ../img/rfr.png
[rfm]: ../img/rfm.png

View File

@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
Let's take our current set of views, and refactor them into view sets.
First of all let's refactor our `UserList` and `UserDetail` classes into a single `UserViewSet` class. We can remove the two view classes, and replace them with a single ViewSet class:
First of all let's refactor our `UserList` and `UserDetail` classes into a single `UserViewSet` class. In the `snippets/views.py` file, we can remove the two view classes and replace them with a single ViewSet class:
from rest_framework import viewsets

View File

@ -105,7 +105,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
queryset = Group.objects.all().order_by('name')
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]
@ -132,8 +132,6 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
urlpatterns += router.urls
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.

View File

@ -169,6 +169,21 @@ else:
}
if django.VERSION >= (5, 1):
# Django 5.1+: use the stock ip_address_validators function
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
# 1) the list of validators and 2) the error message. Starting from
# Django 5.1 ip_address_validators only returns the list of validators
from django.core.validators import ip_address_validators
else:
# Django <= 5.1: create a compatibility shim for ip_address_validators
from django.core.validators import \
ip_address_validators as _ip_address_validators
def ip_address_validators(protocol, unpack_ipv4):
return _ip_address_validators(protocol, unpack_ipv4)[0]
# `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':')

View File

@ -36,7 +36,7 @@ def api_view(http_method_names=None):
# WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
# api_view applied without (method_names)
assert not(isinstance(http_method_names, types.FunctionType)), \
assert not isinstance(http_method_names, types.FunctionType), \
'@api_view missing list of allowed HTTP methods'
# api_view applied with eg. string instead of list of strings

View File

@ -16,7 +16,7 @@ from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import (
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, ProhibitNullCharactersValidator, RegexValidator,
URLValidator, ip_address_validators
URLValidator
)
from django.forms import FilePathField as DjangoFilePathField
from django.forms import ImageField as DjangoImageField
@ -36,6 +36,7 @@ except ImportError:
pytz = None
from rest_framework import ISO_8601
from rest_framework.compat import ip_address_validators
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, json, representation
@ -866,7 +867,7 @@ class IPAddressField(CharField):
self.protocol = protocol.lower()
self.unpack_ipv4 = (self.protocol == 'both')
super().__init__(**kwargs)
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
validators = ip_address_validators(protocol, self.unpack_ipv4)
self.validators.extend(validators)
def to_internal_value(self, data):

View File

@ -21,14 +21,14 @@ from rest_framework.settings import api_settings
def search_smart_split(search_terms):
"""generator that first splits string by spaces, leaving quoted phrases togheter,
"""generator that first splits string by spaces, leaving quoted phrases together,
then it splits non-quoted phrases by commas.
"""
for term in smart_split(search_terms):
# trim commas to avoid bad matching for quoted phrases
term = term.strip(',')
if term.startswith(('"', "'")) and term[0] == term[-1]:
# quoted phrases are kept togheter without any other split
# quoted phrases are kept together without any other split
yield unescape_string_literal(term)
else:
# non-quoted tokens are split by comma, keeping only non-empty ones

View File

@ -102,12 +102,12 @@ class EndpointEnumerator:
Given a URL conf regex, return a URI template string.
"""
# ???: Would it be feasible to adjust this such that we generate the
# path, plus the kwargs, plus the type from the convertor, such that we
# path, plus the kwargs, plus the type from the converter, such that we
# could feed that straight into the parameter schema object?
path = simplify_regex(path_regex)
# Strip Django 2.0 convertors as they are incompatible with uritemplate format
# Strip Django 2.0 converters as they are incompatible with uritemplate format
return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
def should_include_endpoint(self, path, callback):

View File

@ -84,7 +84,7 @@ class SchemaGenerator(BaseSchemaGenerator):
continue
if components_schemas[k] == components[k]:
continue
warnings.warn('Schema component "{}" has been overriden with a different value.'.format(k))
warnings.warn('Schema component "{}" has been overridden with a different value.'.format(k))
components_schemas.update(components)

View File

@ -76,6 +76,7 @@ LIST_SERIALIZER_KWARGS = (
'instance', 'data', 'partial', 'context', 'allow_null',
'max_length', 'min_length'
)
LIST_SERIALIZER_KWARGS_REMOVE = ('allow_empty', 'min_length', 'max_length')
ALL_FIELDS = '__all__'
@ -145,19 +146,12 @@ class BaseSerializer(Field):
kwargs['child'] = cls()
return CustomListSerializer(*args, **kwargs)
"""
allow_empty = kwargs.pop('allow_empty', None)
max_length = kwargs.pop('max_length', None)
min_length = kwargs.pop('min_length', None)
child_serializer = cls(*args, **kwargs)
list_kwargs = {
'child': child_serializer,
}
if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty
if max_length is not None:
list_kwargs['max_length'] = max_length
if min_length is not None:
list_kwargs['min_length'] = min_length
list_kwargs = {}
for key in LIST_SERIALIZER_KWARGS_REMOVE:
value = kwargs.pop(key, None)
if value is not None:
list_kwargs[key] = value
list_kwargs['child'] = cls(*args, **kwargs)
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
@ -609,12 +603,6 @@ class ListSerializer(BaseSerializer):
self.min_length = kwargs.pop('min_length', None)
assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
instance = kwargs.get('instance', [])
data = kwargs.get('data', [])
if instance and data:
assert len(data) == len(instance), 'Data and instance should have same length'
super().__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self)
@ -700,13 +688,7 @@ class ListSerializer(BaseSerializer):
ret = []
errors = []
for idx, item in enumerate(data):
if (
hasattr(self, 'instance')
and self.instance
and len(self.instance) > idx
):
self.child.instance = self.instance[idx]
for item in data:
try:
validated = self.run_child_validation(item)
except ValidationError as exc:

View File

@ -116,7 +116,7 @@ DEFAULTS = {
'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True,
# Browseable API
# Browsable API
'HTML_SELECT_CUTOFF': 1000,
'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",

View File

@ -3,6 +3,12 @@ function replaceDocument(docString) {
doc.write(docString);
doc.close();
if (window.djdt) {
// If Django Debug Toolbar is available, reinitialize it so that
// it can show updated panels from new `docString`.
window.addEventListener("load", djdt.init);
}
}
function doAjaxSubmit(e) {

View File

@ -42,7 +42,7 @@
<ul class="nav navbar-nav pull-right">
{% block userlinks %}
{% if user.is_authenticated %}
{% optional_logout request user %}
{% optional_logout request user csrf_token %}
{% else %}
{% optional_login request %}
{% endif %}

View File

@ -46,7 +46,7 @@
<ul class="nav navbar-nav pull-right">
{% block userlinks %}
{% if user.is_authenticated %}
{% optional_logout request user %}
{% optional_logout request user csrf_token %}
{% else %}
{% optional_login request %}
{% endif %}

View File

@ -119,7 +119,7 @@ def optional_docs_login(request):
@register.simple_tag
def optional_logout(request, user):
def optional_logout(request, user, csrf_token):
"""
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
@ -135,11 +135,16 @@ def optional_logout(request, user):
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href='{href}?next={next}'>Log out</a></li>
<form id="logoutForm" method="post" action="{href}?next={next}">
<input type="hidden" name="csrfmiddlewaretoken" value="{csrf_token}">
</form>
<li>
<a href="#" onclick='document.getElementById("logoutForm").submit()'>Log out</a>
</li>
</ul>
</li>"""
snippet = format_html(snippet, user=escape(user), href=logout_url, next=escape(request.path))
snippet = format_html(snippet, user=escape(user), href=logout_url,
next=escape(request.path), csrf_token=csrf_token)
return mark_safe(snippet)

View File

@ -27,8 +27,8 @@ def smart_repr(value):
if isinstance(value, models.Manager):
return manager_repr(value)
if isinstance(value, Promise) and value._delegate_text:
value = force_str(value)
if isinstance(value, Promise):
value = force_str(value, strings_only=True)
value = repr(value)

View File

@ -160,10 +160,19 @@ class UniqueTogetherValidator:
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
# Ignore validation if any field is None
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
if None not in checked_values and qs_exists(queryset):
if serializer.instance is None:
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
else:
# Ignore validation if all field values are unchanged
checked_values = [
value
for field, value in attrs.items()
if field in self.fields and value != getattr(serializer.instance, field)
]
if checked_values and None not in checked_values and qs_exists(queryset):
field_names = ', '.join(self.fields)
message = self.message.format(field_names=field_names)
raise ValidationError(message, code='unique')

View File

@ -29,3 +29,8 @@ include = rest_framework/*,tests/*
exclude_lines =
pragma: no cover
raise NotImplementedError
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = */kickstarter-announcement.md,*.js,*.map,*.po
ignore-words-list = fo,malcom,ser

View File

@ -96,6 +96,7 @@ setup(
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
@ -107,6 +108,7 @@ setup(
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Internet :: WWW/HTTP',
],

View File

@ -65,6 +65,12 @@ class DropdownWithAuthTests(TestCase):
content = response.content.decode()
assert '>Log in<' in content
def test_dropdown_contains_logout_form(self):
self.client.login(username=self.username, password=self.password)
response = self.client.get('/')
content = response.content.decode()
assert '<form id="logoutForm" method="post" action="/auth/logout/?next=/">' in content
@override_settings(ROOT_URLCONF='tests.browsable_api.no_auth_urls')
class NoDropdownWithoutAuthTests(TestCase):

View File

@ -8,7 +8,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.urls import path
from rest_framework.compat import uritemplate, yaml
from rest_framework.compat import coreapi, uritemplate, yaml
from rest_framework.management.commands import generateschema
from rest_framework.utils import formatting, json
from rest_framework.views import APIView
@ -91,6 +91,7 @@ class GenerateSchemaTests(TestCase):
os.remove(path)
@pytest.mark.skipif(yaml is None, reason='PyYAML is required.')
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_coreapi_renders_default_schema_with_custom_title_url_and_description(self):
expected_out = """info:
@ -113,6 +114,7 @@ class GenerateSchemaTests(TestCase):
self.assertIn(formatting.dedent(expected_out), self.out.getvalue())
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_coreapi_renders_openapi_json_schema(self):
expected_out = {
@ -142,6 +144,7 @@ class GenerateSchemaTests(TestCase):
self.assertDictEqual(out_json, expected_out)
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_renders_corejson_schema(self):
expected_out = """{"_type":"document","":{"list":{"_type":"link","url":"/","action":"get"}}}"""

View File

@ -1347,7 +1347,7 @@ class TestGenerator(TestCase):
assert len(w) == 1
assert issubclass(w[-1].category, UserWarning)
assert 'has been overriden with a different value.' in str(w[-1].message)
assert 'has been overridden with a different value.' in str(w[-1].message)
assert 'components' in schema
assert 'schemas' in schema['components']

View File

@ -1538,7 +1538,8 @@ class TestNoOutputFormatDateTimeField(FieldValues):
field = serializers.DateTimeField(format=None)
class TestNaiveDateTimeField(FieldValues):
@override_settings(TIME_ZONE='UTC', USE_TZ=False)
class TestNaiveDateTimeField(FieldValues, TestCase):
"""
Valid and invalid values for `DateTimeField` with naive datetimes.
"""

View File

@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
import datetime
import decimal
import json # noqa
import re
import sys
import tempfile
@ -169,33 +170,32 @@ class TestRegularFieldMappings(TestCase):
model = RegularFieldsModel
fields = '__all__'
expected = dedent("""
TestSerializer():
auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField()
boolean_field = BooleanField(default=False, required=False)
char_field = CharField(max_length=100)
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
date_field = DateField()
datetime_field = DateTimeField()
decimal_field = DecimalField(decimal_places=1, max_digits=3)
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
null_boolean_field = BooleanField(allow_null=True, default=False, required=False)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(allow_unicode=False, max_length=100)
small_integer_field = IntegerField()
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
file_field = FileField(max_length=100)
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
file_path_field = FilePathField(path=%r)
expected = dedent(r"""
TestSerializer\(\):
auto_field = IntegerField\(read_only=True\)
big_integer_field = IntegerField\(.*\)
boolean_field = BooleanField\(default=False, required=False\)
char_field = CharField\(max_length=100\)
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
date_field = DateField\(\)
datetime_field = DateTimeField\(\)
decimal_field = DecimalField\(decimal_places=1, max_digits=3\)
email_field = EmailField\(max_length=100\)
float_field = FloatField\(\)
integer_field = IntegerField\(.*\)
null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
positive_integer_field = IntegerField\(.*\)
positive_small_integer_field = IntegerField\(.*\)
slug_field = SlugField\(allow_unicode=False, max_length=100\)
small_integer_field = IntegerField\(.*\)
text_field = CharField\(max_length=100, style={'base_template': 'textarea.html'}\)
file_field = FileField\(max_length=100\)
time_field = TimeField\(\)
url_field = URLField\(max_length=100\)
custom_field = ModelField\(model_field=<tests.test_model_serializer.CustomField: custom_field>\)
file_path_field = FilePathField\(path=%r\)
""" % tempfile.gettempdir())
self.assertEqual(repr(TestSerializer()), expected)
assert re.search(expected, repr(TestSerializer())) is not None
def test_field_options(self):
class TestSerializer(serializers.ModelSerializer):
@ -203,19 +203,19 @@ class TestRegularFieldMappings(TestCase):
model = FieldOptionsModel
fields = '__all__'
expected = dedent("""
TestSerializer():
id = IntegerField(label='ID', read_only=True)
value_limit_field = IntegerField(max_value=10, min_value=1)
length_limit_field = CharField(max_length=12, min_length=3)
blank_field = CharField(allow_blank=True, max_length=10, required=False)
null_field = IntegerField(allow_null=True, required=False)
default_field = IntegerField(default=0, required=False)
descriptive_field = IntegerField(help_text='Some help text', label='A label')
choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
expected = dedent(r"""
TestSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
value_limit_field = IntegerField\(max_value=10, min_value=1\)
length_limit_field = CharField\(max_length=12, min_length=3\)
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
null_field = IntegerField\(allow_null=True,.*required=False\)
default_field = IntegerField\(default=0,.*required=False\)
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
""")
self.assertEqual(repr(TestSerializer()), expected)
assert re.search(expected, repr(TestSerializer())) is not None
def test_nullable_boolean_field_choices(self):
class NullableBooleanChoicesModel(models.Model):
@ -1334,12 +1334,12 @@ class TestFieldSource(TestCase):
}
}
expected = dedent("""
TestSerializer():
number_field = IntegerField(source='integer_field')
expected = dedent(r"""
TestSerializer\(\):
number_field = IntegerField\(.*source='integer_field'\)
""")
self.maxDiff = None
self.assertEqual(repr(TestSerializer()), expected)
assert re.search(expected, repr(TestSerializer())) is not None
class Issue6110TestModel(models.Model):

View File

@ -132,7 +132,7 @@ urlpatterns = [
]
# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ...
# TODO: Clean tests below - remove duplicates with above, better unit testing, ...
@override_settings(ROOT_URLCONF='tests.test_response')
class RendererIntegrationTests(TestCase):
"""

View File

@ -2,7 +2,6 @@ import inspect
import pickle
import re
import sys
import unittest
from collections import ChainMap
from collections.abc import Mapping
@ -784,63 +783,3 @@ class TestSetValueMethod:
ret = {'a': 1}
self.s.set_value(ret, ['x', 'y'], 2)
assert ret == {'a': 1, 'x': {'y': 2}}
class MyClass(models.Model):
name = models.CharField(max_length=100)
value = models.CharField(max_length=100, blank=True)
app_label = "test"
@property
def is_valid(self):
return self.name == 'valid'
class MyClassSerializer(serializers.ModelSerializer):
class Meta:
model = MyClass
fields = ('id', 'name', 'value')
def validate_value(self, value):
if value and not self.instance.is_valid:
raise serializers.ValidationError(
'Status cannot be set for invalid instance')
return value
class TestMultipleObjectsValidation(unittest.TestCase):
def setUp(self):
self.objs = [
MyClass(name='valid'),
MyClass(name='invalid'),
MyClass(name='other'),
]
def test_multiple_objects_are_validated_separately(self):
serializer = MyClassSerializer(
data=[{'value': 'set', 'id': instance.id} for instance in
self.objs],
instance=self.objs,
many=True,
partial=True,
)
assert not serializer.is_valid()
assert serializer.errors == [
{},
{'value': ['Status cannot be set for invalid instance']},
{'value': ['Status cannot be set for invalid instance']}
]
def test_exception_raised_when_data_and_instance_length_different(self):
with self.assertRaises(AssertionError):
MyClassSerializer(
data=[{'value': 'set', 'id': instance.id} for instance in
self.objs],
instance=self.objs[:-1],
many=True,
partial=True,
)

View File

@ -223,7 +223,7 @@ class TestNotRequiredNestedSerializerWithMany:
input_data = {}
serializer = self.Serializer(data=input_data)
# request is empty, therefor 'nested' should not be in serializer.data
# request is empty, therefore 'nested' should not be in serializer.data
assert serializer.is_valid()
assert 'nested' not in serializer.validated_data
@ -237,7 +237,7 @@ class TestNotRequiredNestedSerializerWithMany:
input_data = QueryDict('')
serializer = self.Serializer(data=input_data)
# the querydict is empty, therefor 'nested' should not be in serializer.data
# the querydict is empty, therefore 'nested' should not be in serializer.data
assert serializer.is_valid()
assert 'nested' not in serializer.validated_data

View File

@ -192,7 +192,7 @@ class ThrottlingTests(TestCase):
if expect is not None:
assert response['Retry-After'] == expect
else:
assert not'Retry-After' in response
assert 'Retry-After' not in response
def test_seconds_fields(self):
"""

View File

@ -1,7 +1,9 @@
import datetime
from unittest.mock import MagicMock
import re
from unittest.mock import MagicMock, patch
import pytest
from django import VERSION as django_version
from django.db import DataError, models
from django.test import TestCase
@ -112,11 +114,15 @@ class TestUniquenessValidation(TestCase):
def test_doesnt_pollute_model(self):
instance = AnotherUniquenessModel.objects.create(code='100')
serializer = AnotherUniquenessSerializer(instance)
assert AnotherUniquenessModel._meta.get_field('code').validators == []
assert all(
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
)
# Accessing data shouldn't effect validators on the model
serializer.data
assert AnotherUniquenessModel._meta.get_field('code').validators == []
assert all(
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
)
def test_related_model_is_unique(self):
data = {'username': 'Existing', 'email': 'new-email@example.com'}
@ -193,15 +199,15 @@ class TestUniquenessTogetherValidation(TestCase):
def test_repr(self):
serializer = UniquenessTogetherSerializer()
expected = dedent("""
UniquenessTogetherSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100, required=True)
position = IntegerField(required=True)
expected = dedent(r"""
UniquenessTogetherSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(max_length=100, required=True\)
position = IntegerField\(.*required=True\)
class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('race_name', 'position'\)\)>\]
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None
def test_is_not_unique_together(self):
"""
@ -282,13 +288,13 @@ class TestUniquenessTogetherValidation(TestCase):
read_only_fields = ('race_name',)
serializer = ReadOnlyFieldSerializer()
expected = dedent("""
ReadOnlyFieldSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(read_only=True)
position = IntegerField(required=True)
expected = dedent(r"""
ReadOnlyFieldSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(read_only=True\)
position = IntegerField\(.*required=True\)
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None
def test_read_only_fields_with_default(self):
"""
@ -366,14 +372,14 @@ class TestUniquenessTogetherValidation(TestCase):
fields = ['name', 'position']
serializer = TestSerializer()
expected = dedent("""
TestSerializer():
name = CharField(source='race_name')
position = IntegerField()
expected = dedent(r"""
TestSerializer\(\):
name = CharField\(source='race_name'\)
position = IntegerField\(.*\)
class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('name', 'position'))>]
validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('name', 'position'\)\)>\]
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None
def test_default_validator_with_multiple_fields_with_same_source(self):
class TestSerializer(serializers.ModelSerializer):
@ -411,13 +417,13 @@ class TestUniquenessTogetherValidation(TestCase):
validators = []
serializer = NoValidatorsSerializer()
expected = dedent("""
NoValidatorsSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100)
position = IntegerField()
expected = dedent(r"""
NoValidatorsSerializer\(\):
id = IntegerField\(label='ID', read_only=True.*\)
race_name = CharField\(max_length=100\)
position = IntegerField\(.*\)
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None
def test_ignore_validation_for_null_fields(self):
# None values that are on fields which are part of the uniqueness
@ -447,6 +453,22 @@ class TestUniquenessTogetherValidation(TestCase):
serializer = NullUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()
def test_ignore_validation_for_unchanged_fields(self):
"""
If all fields in the unique together constraint are unchanged,
then the instance should skip uniqueness validation.
"""
instance = UniquenessTogetherModel.objects.create(
race_name="Paris Marathon", position=1
)
data = {"race_name": "Paris Marathon", "position": 1}
serializer = UniquenessTogetherSerializer(data=data, instance=instance)
with patch(
"rest_framework.validators.qs_exists"
) as mock:
assert serializer.is_valid()
assert not mock.called
def test_filter_queryset_do_not_skip_existing_attribute(self):
"""
filter_queryset should add value from existing instance attribute
@ -524,16 +546,16 @@ class TestUniqueConstraintValidation(TestCase):
# the order of validators isn't deterministic so delete
# fancy_conditions field that has two of them
del serializer.fields['fancy_conditions']
expected = dedent("""
UniqueConstraintSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100, required=True)
position = IntegerField(required=True)
global_id = IntegerField(validators=[<UniqueValidator(queryset=UniqueConstraintModel.objects.all())>])
expected = dedent(r"""
UniqueConstraintSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(max_length=100, required=True\)
position = IntegerField\(.*required=True\)
global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintModel.objects.all\(\)\)>\]\)
class Meta:
validators = [<UniqueTogetherValidator(queryset=<QuerySet [<UniqueConstraintModel: UniqueConstraintModel object (1)>, <UniqueConstraintModel: UniqueConstraintModel object (2)>]>, fields=('race_name', 'position'))>]
validators = \[<UniqueTogetherValidator\(queryset=<QuerySet \[<UniqueConstraintModel: UniqueConstraintModel object \(1\)>, <UniqueConstraintModel: UniqueConstraintModel object \(2\)>\]>, fields=\('race_name', 'position'\)\)>\]
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None
def test_unique_together_field(self):
"""
@ -553,15 +575,18 @@ class TestUniqueConstraintValidation(TestCase):
UniqueConstraint with single field must be transformed into
field's UniqueValidator
"""
# Django 5 includes Max and Min values validators for IntergerField
extra_validators_qty = 2 if django_version[0] >= 5 else 0
#
serializer = UniqueConstraintSerializer()
assert len(serializer.validators) == 1
validators = serializer.fields['global_id'].validators
assert len(validators) == 1
assert len(validators) == 1 + extra_validators_qty
assert validators[0].queryset == UniqueConstraintModel.objects
validators = serializer.fields['fancy_conditions'].validators
assert len(validators) == 2
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators}
assert len(validators) == 2 + extra_validators_qty
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])}

View File

@ -4,7 +4,8 @@ envlist =
{py36,py37,py38,py39}-django31
{py36,py37,py38,py39,py310}-django32
{py38,py39,py310}-{django40,django41,django42,djangomain}
{py311}-{django41,django42,djangomain}
{py311}-{django41,django42,django50,djangomain}
{py312}-{django42,djanggo50,djangomain}
base
dist
docs
@ -22,9 +23,11 @@ deps =
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2
django42: Django>=4.2,<5.0
django50: Django>=5.0,<5.1
djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
setuptools
[testenv:base]
; Ensure optional dependencies are not required
@ -57,3 +60,6 @@ ignore_outcome = true
[testenv:py311-djangomain]
ignore_outcome = true
[testenv:py312-djangomain]
ignore_outcome = true