Merge branch 'master' into patch-1

This commit is contained in:
Tom Christie 2022-06-08 13:55:04 +01:00 committed by GitHub
commit 5bc835f9ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 159 additions and 35 deletions

View File

@ -21,14 +21,14 @@ The initial aim is to provide a single full-time position on REST framework.
[![][sentry-img]][sentry-url] [![][sentry-img]][sentry-url]
[![][stream-img]][stream-url] [![][stream-img]][stream-url]
[![][rollbar-img]][rollbar-url] [![][spacinov-img]][spacinov-url]
[![][esg-img]][esg-url]
[![][retool-img]][retool-url] [![][retool-img]][retool-url]
[![][bitio-img]][bitio-url] [![][bitio-img]][bitio-url]
[![][posthog-img]][posthog-url] [![][posthog-img]][posthog-url]
[![][cryptapi-img]][cryptapi-url] [![][cryptapi-img]][cryptapi-url]
[![][fezto-img]][fezto-url]
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [ESG][esg-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], and [CryptAPI][cryptapi-url]. Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], and [FEZTO][fezto-url].
--- ---
@ -55,7 +55,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (3.6, 3.7, 3.8, 3.9, 3.10) * Python (3.6, 3.7, 3.8, 3.9, 3.10)
* Django (2.2, 3.0, 3.1, 3.2, 4.0) * Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.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.
@ -194,21 +194,21 @@ Please see the [security policy][security-policy].
[sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png [sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png
[stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png [stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png
[rollbar-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png [spacinov-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/spacinov-readme.png
[esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png
[retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/retool-readme.png [retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/retool-readme.png
[bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/bitio-readme.png [bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/bitio-readme.png
[posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/posthog-readme.png [posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/posthog-readme.png
[cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png [cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png
[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png
[sentry-url]: https://getsentry.com/welcome/ [sentry-url]: https://getsentry.com/welcome/
[stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage [stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage
[rollbar-url]: https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial [spacinov-url]: https://www.spacinov.com/
[esg-url]: https://software.esg-usa.com/
[retool-url]: https://retool.com/?utm_source=djangorest&utm_medium=sponsorship [retool-url]: https://retool.com/?utm_source=djangorest&utm_medium=sponsorship
[bitio-url]: https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship [bitio-url]: https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship
[posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship [posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship
[cryptapi-url]: https://cryptapi.io [cryptapi-url]: https://cryptapi.io
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
[oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit [oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit

View File

@ -260,6 +260,15 @@ Set as `handler400`:
handler400 = 'rest_framework.exceptions.bad_request' handler400 = 'rest_framework.exceptions.bad_request'
# Third party packages
The following third-party packages are also available.
## DRF Standardized Errors
The [drf-standardized-errors][drf-standardized-errors] package provides an exception handler that generates the same format for all 4xx and 5xx responses. It is a drop-in replacement for the default exception handler and allows customizing the error response format without rewriting the whole exception handler. The standardized error response format is easier to document and easier to handle by API consumers.
[cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/ [cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/
[authentication]: authentication.md [authentication]: authentication.md
[django-custom-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views [django-custom-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors

View File

@ -165,7 +165,7 @@ In order to customize the top-level schema, subclass
as an argument to the `generateschema` command or `get_schema_view()` helper as an argument to the `generateschema` command or `get_schema_view()` helper
function. function.
### get_schema(self, request) ### get_schema(self, request=None, public=False)
Returns a dictionary that represents the OpenAPI schema: Returns a dictionary that represents the OpenAPI schema:
@ -313,6 +313,11 @@ Computes the component's name from the serializer.
You may see warnings if your API has duplicate component names. If so you can override `get_component_name()` or pass the `component_name` `__init__()` kwarg (see below) to provide different names. You may see warnings if your API has duplicate component names. If so you can override `get_component_name()` or pass the `component_name` `__init__()` kwarg (see below) to provide different names.
#### `get_reference()`
Returns a reference to the serializer component. This may be useful if you override `get_schema()`.
#### `map_serializer()` #### `map_serializer()`
Maps serializers to their OpenAPI representations. Maps serializers to their OpenAPI representations.

View File

@ -148,6 +148,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons. * [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons.
* [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models.. * [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models..
* [fast-drf] - A model based library for making API development faster and easier. * [fast-drf] - A model based library for making API development faster and easier.
* [django-requestlogs] - Providing middleware and other helpers for audit logging for REST framework.
* [drf-standardized-errors][drf-standardized-errors] - DRF exception handler to standardize error responses for all API endpoints.
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
@ -237,3 +239,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[graphwrap]: https://github.com/PaulGilmartin/graph_wrap [graphwrap]: https://github.com/PaulGilmartin/graph_wrap
[rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions [rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions
[fast-drf]: https://github.com/iashraful/fast-drf [fast-drf]: https://github.com/iashraful/fast-drf
[django-requestlogs]: https://github.com/Raekkeri/django-requestlogs
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -68,16 +68,16 @@ continued development by **[signing up for a paid plan][funding]**.
<ul class="premium-promo promo"> <ul class="premium-promo promo">
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li> <li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li> <li><a href="https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://software.esg-usa.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/esg-new-logo.png)">ESG</a></li> <li><a href="https://www.spacinov.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/spacinov.png)">Spacinov</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
<li><a href="https://retool.com/?utm_source=djangorest&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/retool-sidebar.png)">Retool</a></li> <li><a href="https://retool.com/?utm_source=djangorest&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/retool-sidebar.png)">Retool</a></li>
<li><a href="https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/bitio_logo_gold_background.png)">bit.io</a></li> <li><a href="https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/bitio_logo_gold_background.png)">bit.io</a></li>
<li><a href="https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/135996800-d49fe024-32d9-441a-98d9-4c7596287a67.png)">PostHog</a></li> <li><a href="https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/135996800-d49fe024-32d9-441a-98d9-4c7596287a67.png)">PostHog</a></li>
<li><a href="https://cryptapi.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cryptapi.png)">CryptAPI</a></li> <li><a href="https://cryptapi.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cryptapi.png)">CryptAPI</a></li>
<li><a href="https://www.fezto.xyz/?utm_source=DjangoRESTFramework" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/fezto.png)">FEZTO</a></li>
</ul> </ul>
<div style="clear: both; padding-bottom: 20px;"></div> <div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), and [CryptAPI](https://cryptapi.io).* *Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), and [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework).*
--- ---
@ -86,7 +86,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Python (3.6, 3.7, 3.8, 3.9, 3.10) * Python (3.6, 3.7, 3.8, 3.9, 3.10)
* Django (2.2, 3.0, 3.1, 3.2, 4.0) * Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.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.

View File

@ -27,7 +27,6 @@ from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_str from django.utils.encoding import is_protected_type, smart_str
from django.utils.formats import localize_input, sanitize_separators from django.utils.formats import localize_input, sanitize_separators
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.timezone import utc
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pytz.exceptions import InvalidTimeError from pytz.exceptions import InvalidTimeError
@ -63,6 +62,9 @@ def is_simple_callable(obj):
""" """
True if the object is a callable that takes no arguments. True if the object is a callable that takes no arguments.
""" """
if not callable(obj):
return False
# Bail early since we cannot inspect built-in function signatures. # Bail early since we cannot inspect built-in function signatures.
if inspect.isbuiltin(obj): if inspect.isbuiltin(obj):
raise BuiltinSignatureError( raise BuiltinSignatureError(
@ -1190,7 +1192,7 @@ class DateTimeField(Field):
except InvalidTimeError: except InvalidTimeError:
self.fail('make_aware', timezone=field_timezone) self.fail('make_aware', timezone=field_timezone)
elif (field_timezone is None) and timezone.is_aware(value): elif (field_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, utc) return timezone.make_naive(value, datetime.timezone.utc)
return value return value
def default_timezone(self): def default_timezone(self):

View File

@ -636,7 +636,7 @@ class AutoSchema(ViewInspector):
""" """
return self.get_serializer(path, method) return self.get_serializer(path, method)
def _get_reference(self, serializer): def get_reference(self, serializer):
return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))} return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
def get_request_body(self, path, method): def get_request_body(self, path, method):
@ -650,7 +650,7 @@ class AutoSchema(ViewInspector):
if not isinstance(serializer, serializers.Serializer): if not isinstance(serializer, serializers.Serializer):
item_schema = {} item_schema = {}
else: else:
item_schema = self._get_reference(serializer) item_schema = self.get_reference(serializer)
return { return {
'content': { 'content': {
@ -674,7 +674,7 @@ class AutoSchema(ViewInspector):
if not isinstance(serializer, serializers.Serializer): if not isinstance(serializer, serializers.Serializer):
item_schema = {} item_schema = {}
else: else:
item_schema = self._get_reference(serializer) item_schema = self.get_reference(serializer)
if is_list_view(path, method, self.view): if is_list_view(path, method, self.view):
response_schema = { response_schema = {
@ -808,3 +808,11 @@ class AutoSchema(ViewInspector):
RemovedInDRF314Warning, stacklevel=2 RemovedInDRF314Warning, stacklevel=2
) )
return self.allows_filters(path, method) return self.allows_filters(path, method)
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.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_reference(serializer)

View File

@ -217,15 +217,9 @@ def get_field_kwargs(field_name, model_field):
] ]
if getattr(model_field, 'unique', False): if getattr(model_field, 'unique', False):
unique_error_message = model_field.error_messages.get('unique', None)
if unique_error_message:
unique_error_message = unique_error_message % {
'model_name': model_field.model._meta.verbose_name,
'field_label': model_field.verbose_name
}
validator = UniqueValidator( validator = UniqueValidator(
queryset=model_field.model._default_manager, queryset=model_field.model._default_manager,
message=unique_error_message) message=get_unique_error_message(model_field))
validator_kwarg.append(validator) validator_kwarg.append(validator)
if validator_kwarg: if validator_kwarg:
@ -281,7 +275,9 @@ def get_relation_kwargs(field_name, relation_info):
if model_field.validators: if model_field.validators:
kwargs['validators'] = model_field.validators kwargs['validators'] = model_field.validators
if getattr(model_field, 'unique', False): if getattr(model_field, 'unique', False):
validator = UniqueValidator(queryset=model_field.model._default_manager) validator = UniqueValidator(
queryset=model_field.model._default_manager,
message=get_unique_error_message(model_field))
kwargs['validators'] = kwargs.get('validators', []) + [validator] kwargs['validators'] = kwargs.get('validators', []) + [validator]
if to_many and not model_field.blank: if to_many and not model_field.blank:
kwargs['allow_empty'] = False kwargs['allow_empty'] = False
@ -300,3 +296,13 @@ def get_url_kwargs(model_field):
return { return {
'view_name': get_detail_view_name(model_field) 'view_name': get_detail_view_name(model_field)
} }
def get_unique_error_message(model_field):
unique_error_message = model_field.error_messages.get('unique', None)
if unique_error_message:
unique_error_message = unique_error_message % {
'model_name': model_field.model._meta.verbose_name,
'field_label': model_field.verbose_name
}
return unique_error_message

View File

@ -94,6 +94,7 @@ setup(
'Framework :: Django :: 3.1', 'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2', 'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0', 'Framework :: Django :: 4.0',
'Framework :: Django :: 4.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',

View File

@ -1,7 +1,8 @@
from django.urls import path from django.urls import path
from .views import MockView from .views import BasicModelWithUsersViewSet, MockView
urlpatterns = [ urlpatterns = [
path('', MockView.as_view()), path('', MockView.as_view()),
path('basicviewset', BasicModelWithUsersViewSet.as_view({'get': 'list'})),
] ]

View File

@ -0,0 +1,8 @@
from rest_framework.serializers import ModelSerializer
from tests.models import BasicModelWithUsers
class BasicSerializer(ModelSerializer):
class Meta:
model = BasicModelWithUsers
fields = '__all__'

View File

@ -1,8 +1,35 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from rest_framework.permissions import IsAuthenticated
from rest_framework.test import APIClient from rest_framework.test import APIClient
from .views import BasicModelWithUsersViewSet, OrganizationPermissions
@override_settings(ROOT_URLCONF='tests.browsable_api.no_auth_urls')
class AnonymousUserTests(TestCase):
"""Tests correct handling of anonymous user request on endpoints with IsAuthenticated permission class."""
def setUp(self):
self.client = APIClient(enforce_csrf_checks=True)
def tearDown(self):
self.client.logout()
def test_get_raises_typeerror_when_anonymous_user_in_queryset_filter(self):
with self.assertRaises(TypeError):
self.client.get('/basicviewset')
def test_get_returns_http_forbidden_when_anonymous_user(self):
old_permissions = BasicModelWithUsersViewSet.permission_classes
BasicModelWithUsersViewSet.permission_classes = [IsAuthenticated, OrganizationPermissions]
response = self.client.get('/basicviewset')
BasicModelWithUsersViewSet.permission_classes = old_permissions
self.assertEqual(response.status_code, 403)
@override_settings(ROOT_URLCONF='tests.browsable_api.auth_urls') @override_settings(ROOT_URLCONF='tests.browsable_api.auth_urls')
class DropdownWithAuthTests(TestCase): class DropdownWithAuthTests(TestCase):

View File

@ -1,6 +1,16 @@
from rest_framework import authentication, renderers from rest_framework import authentication, renderers
from rest_framework.permissions import BasePermission
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from ..models import BasicModelWithUsers
from .serializers import BasicSerializer
class OrganizationPermissions(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_staff or (request.user == obj.owner.organization_user.user)
class MockView(APIView): class MockView(APIView):
@ -9,3 +19,15 @@ class MockView(APIView):
def get(self, request): def get(self, request):
return Response({'a': 1, 'b': 2, 'c': 3}) return Response({'a': 1, 'b': 2, 'c': 3})
class BasicModelWithUsersViewSet(ModelViewSet):
queryset = BasicModelWithUsers.objects.all()
serializer_class = BasicSerializer
permission_classes = [OrganizationPermissions]
# permission_classes = [IsAuthenticated, OrganizationPermissions]
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_queryset(self):
qs = super().get_queryset().filter(users=self.request.user)
return qs

View File

@ -1,5 +1,6 @@
import uuid import uuid
from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -33,6 +34,10 @@ class ManyToManySource(RESTFrameworkModel):
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources') targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
class BasicModelWithUsers(RESTFrameworkModel):
users = models.ManyToManyField(User)
# ForeignKey # ForeignKey
class ForeignKeyTarget(RESTFrameworkModel): class ForeignKeyTarget(RESTFrameworkModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

View File

@ -1,15 +1,16 @@
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import utc
from rest_framework.compat import coreapi from rest_framework.compat import coreapi
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from rest_framework.utils.serializer_helpers import ReturnList from rest_framework.utils.serializer_helpers import ReturnList
utc = timezone.utc
class MockList: class MockList:
def tolist(self): def tolist(self):

View File

@ -9,7 +9,7 @@ import pytz
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.http import QueryDict from django.http import QueryDict
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.timezone import activate, deactivate, override, utc from django.utils.timezone import activate, deactivate, override
import rest_framework import rest_framework
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
@ -17,6 +17,8 @@ from rest_framework.fields import (
BuiltinSignatureError, DjangoImageField, is_simple_callable BuiltinSignatureError, DjangoImageField, is_simple_callable
) )
utc = datetime.timezone.utc
# Tests for helper functions. # Tests for helper functions.
# --------------------------- # ---------------------------
@ -73,6 +75,10 @@ class TestIsSimpleCallable:
assert is_simple_callable(valid_vargs_kwargs) assert is_simple_callable(valid_vargs_kwargs)
assert not is_simple_callable(invalid) assert not is_simple_callable(invalid)
@pytest.mark.parametrize('obj', (True, None, "str", b'bytes', 123, 1.23))
def test_not_callable(self, obj):
assert not is_simple_callable(obj)
def test_4602_regression(self): def test_4602_regression(self):
from django.db import models from django.db import models

View File

@ -12,6 +12,7 @@ import sys
import tempfile import tempfile
from collections import OrderedDict from collections import OrderedDict
import django
import pytest import pytest
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@ -452,11 +453,14 @@ class TestPosgresFieldsMapping(TestCase):
model = ArrayFieldModel model = ArrayFieldModel
fields = ['array_field', 'array_field_with_blank'] fields = ['array_field', 'array_field_with_blank']
validators = ""
if django.VERSION < (4, 1):
validators = ", validators=[<django.core.validators.MaxLengthValidator object>]"
expected = dedent(""" expected = dedent("""
TestSerializer(): TestSerializer():
array_field = ListField(allow_empty=False, child=CharField(label='Array field', validators=[<django.core.validators.MaxLengthValidator object>])) array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s))
array_field_with_blank = ListField(child=CharField(label='Array field with blank', validators=[<django.core.validators.MaxLengthValidator object>]), required=False) array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False)
""") """ % (validators, validators))
self.assertEqual(repr(TestSerializer()), expected) self.assertEqual(repr(TestSerializer()), expected)
@pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField') @pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField')

View File

@ -42,6 +42,12 @@ class RelatedModelSerializer(serializers.ModelSerializer):
fields = ('username', 'email') fields = ('username', 'email')
class RelatedModelUserSerializer(serializers.ModelSerializer):
class Meta:
model = RelatedModel
fields = ('user',)
class AnotherUniquenessModel(models.Model): class AnotherUniquenessModel(models.Model):
code = models.IntegerField(unique=True) code = models.IntegerField(unique=True)
@ -83,6 +89,13 @@ class TestUniquenessValidation(TestCase):
assert not serializer.is_valid() assert not serializer.is_valid()
assert serializer.errors == {'username': ['uniqueness model with this username already exists.']} assert serializer.errors == {'username': ['uniqueness model with this username already exists.']}
def test_relation_is_not_unique(self):
RelatedModel.objects.create(user=self.instance)
data = {'user': self.instance.pk}
serializer = RelatedModelUserSerializer(data=data)
assert not serializer.is_valid()
assert serializer.errors == {'user': ['related model with this user already exists.']}
def test_is_unique(self): def test_is_unique(self):
data = {'username': 'other'} data = {'username': 'other'}
serializer = UniquenessSerializer(data=data) serializer = UniquenessSerializer(data=data)

View File

@ -3,7 +3,7 @@ envlist =
{py36,py37,py38,py39}-django22, {py36,py37,py38,py39}-django22,
{py36,py37,py38,py39}-django31, {py36,py37,py38,py39}-django31,
{py36,py37,py38,py39,py310}-django32, {py36,py37,py38,py39,py310}-django32,
{py38,py39,py310}-{django40,djangomain}, {py38,py39,py310}-{django40,django41,djangomain},
base,dist,docs, base,dist,docs,
[travis:env] [travis:env]
@ -12,6 +12,7 @@ DJANGO =
3.1: django31 3.1: django31
3.2: django32 3.2: django32
4.0: django40 4.0: django40
4.1: django41
main: djangomain main: djangomain
[testenv] [testenv]
@ -24,7 +25,8 @@ deps =
django22: Django>=2.2,<3.0 django22: Django>=2.2,<3.0
django31: Django>=3.1,<3.2 django31: Django>=3.1,<3.2
django32: Django>=3.2,<4.0 django32: Django>=3.2,<4.0
django40: Django>=4.0,<5.0 django40: Django>=4.0,<4.1
django41: Django>=4.1a1,<4.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