diff --git a/README.md b/README.md index bf4016751..e6e43047e 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ The initial aim is to provide a single full-time position on REST framework. [![][sentry-img]][sentry-url] [![][stream-img]][stream-url] -[![][rollbar-img]][rollbar-url] -[![][esg-img]][esg-url] +[![][spacinov-img]][spacinov-url] [![][retool-img]][retool-url] [![][bitio-img]][bitio-url] [![][posthog-img]][posthog-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 * 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 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 [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 -[esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png +[spacinov-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/spacinov-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 [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 +[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png [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 -[rollbar-url]: https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial -[esg-url]: https://software.esg-usa.com/ +[spacinov-url]: https://www.spacinov.com/ [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 [posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship [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 [oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 2f23e1718..fca9374d0 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -120,6 +120,14 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401 ## TokenAuthentication +--- + +**Note:** The token authentication provided by Django REST framework is a fairly simple implementation. + +For an implementation which allows more than one token per user, has some tighter security implementation details, and supports token expiry, please see the [Django REST Knox][django-rest-knox] third party package. + +--- + This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: @@ -129,11 +137,9 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica 'rest_framework.authtoken' ] ---- +Make sure to run `manage.py migrate` after changing your settings. -**Note:** Make sure to run `manage.py migrate` after changing your settings. The `rest_framework.authtoken` app provides Django database migrations. - ---- +The `rest_framework.authtoken` app provides Django database migrations. You'll also need to create tokens for your users. @@ -146,7 +152,7 @@ For clients to authenticate, the token key should be included in the `Authorizat Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b -**Note:** If you want to use a different keyword in the header, such as `Bearer`, simply subclass `TokenAuthentication` and set the `keyword` class variable. +*If you want to use a different keyword in the header, such as `Bearer`, simply subclass `TokenAuthentication` and set the `keyword` class variable.* If successfully authenticated, `TokenAuthentication` provides the following credentials. @@ -355,6 +361,10 @@ The following example will authenticate any incoming request as the user given b The following third-party packages are also available. +## django-rest-knox + +[Django-rest-knox][django-rest-knox] library provides models and views to handle token-based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into). + ## Django OAuth Toolkit The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. @@ -422,11 +432,7 @@ There are currently two forks of this project. ## drf-social-oauth2 -[Drf-social-oauth2][drf-social-oauth2] is a framework that helps you authenticate with major social oauth2 vendors, such as Facebook, Google, Twitter, Orcid, etc. It generates tokens in a JWTed way with an easy setup. - -## django-rest-knox - -[Django-rest-knox][django-rest-knox] library provides models and views to handle token-based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into). +[Drf-social-oauth2][drf-social-oauth2] is a framework that helps you authenticate with major social oauth2 vendors, such as Facebook, Google, Twitter, Orcid, etc. It generates tokens in a JWTed way with an easy setup. ## drfpasswordless diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index e62a7e4f9..347541d56 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -260,6 +260,15 @@ Set as `handler400`: 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/ [authentication]: authentication.md [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 diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 5b9688dca..e9ef5c6b6 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -42,7 +42,7 @@ Set to false if this field is not required to be present during deserialization. Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation. -Defaults to `True`. +Defaults to `True`. If you're using [Model Serializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) default value will be `False` if you have specified `blank=True` or `default` or `null=True` at your field in your `Model`. ### `default` diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 4547253b0..9c8295b85 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -19,7 +19,7 @@ Relational fields are used to represent model relationships. They can be applie --- -**Note:** REST Framework does not attempt to automatically optimize querysets passed to serializers in terms of `select_related` and `prefetch_related` since it would be too much magic. A serializer with a field spanning an orm relation through its source attribute could require an additional database hit to fetch related object from the database. It is the programmer's responsibility to optimize queries to avoid additional database hits which could occur while using such a serializer. +**Note:** REST Framework does not attempt to automatically optimize querysets passed to serializers in terms of `select_related` and `prefetch_related` since it would be too much magic. A serializer with a field spanning an orm relation through its source attribute could require an additional database hit to fetch related objects from the database. It is the programmer's responsibility to optimize queries to avoid additional database hits which could occur while using such a serializer. For example, the following serializer would lead to a database hit each time evaluating the tracks field if it is not prefetched: diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8d8594eee..70c05fdde 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -338,5 +338,5 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions [drf-extensions-nested-viewsets]: https://chibisov.github.io/drf-extensions/docs/#nested-routes [drf-extensions-collection-level-controllers]: https://chibisov.github.io/drf-extensions/docs/#collection-level-controllers [drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name -[url-namespace-docs]: https://docs.djangoproject.com/en/1.11/topics/http/urls/#url-namespaces -[include-api-reference]: https://docs.djangoproject.com/en/2.0/ref/urls/#include +[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 diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index b9de6745f..004e2d3ce 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -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 function. -### get_schema(self, request) +### get_schema(self, request=None, public=False) 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. +#### `get_reference()` + +Returns a reference to the serializer component. This may be useful if you override `get_schema()`. + + #### `map_serializer()` Maps serializers to their OpenAPI representations. diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 719378247..b87522197 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -106,6 +106,12 @@ If you need to use a cache other than `'default'`, you can do so by creating a c You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. +## A note on concurrency + +The built-in throttle implementations are open to [race conditions][race], so under high concurrency they may allow a few extra requests through. + +If your project relies on guaranteeing the number of requests during concurrent requests, you will need to implement your own throttle class. See [issue #5181][gh5181] for more details. + --- # API Reference @@ -214,3 +220,5 @@ The following is an example of a rate throttle, that will randomly throttle 1 in [identifying-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster [cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches [cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache +[gh5181]: https://github.com/encode/django-rest-framework/issues/5181 +[race]: https://en.wikipedia.org/wiki/Race_condition#Data_race diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index e25421f50..9513b13d1 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -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-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. +* [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 [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 [rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions [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 diff --git a/docs/img/premium/bitio-readme.png b/docs/img/premium/bitio-readme.png index c47118cc6..d5d6259e6 100644 Binary files a/docs/img/premium/bitio-readme.png and b/docs/img/premium/bitio-readme.png differ diff --git a/docs/img/premium/cryptapi-readme.png b/docs/img/premium/cryptapi-readme.png index 163f6a9ea..10839b13b 100644 Binary files a/docs/img/premium/cryptapi-readme.png and b/docs/img/premium/cryptapi-readme.png differ diff --git a/docs/img/premium/fezto-readme.png b/docs/img/premium/fezto-readme.png new file mode 100644 index 000000000..7cc3be6e6 Binary files /dev/null and b/docs/img/premium/fezto-readme.png differ diff --git a/docs/img/premium/posthog-readme.png b/docs/img/premium/posthog-readme.png index 9ca8b0ecf..0fc09f0b8 100644 Binary files a/docs/img/premium/posthog-readme.png and b/docs/img/premium/posthog-readme.png differ diff --git a/docs/img/premium/retool-readme.png b/docs/img/premium/retool-readme.png index 56adba04d..971563427 100644 Binary files a/docs/img/premium/retool-readme.png and b/docs/img/premium/retool-readme.png differ diff --git a/docs/img/premium/sentry-readme.png b/docs/img/premium/sentry-readme.png index 420e8ee87..3c8858aca 100644 Binary files a/docs/img/premium/sentry-readme.png and b/docs/img/premium/sentry-readme.png differ diff --git a/docs/img/premium/spacinov-readme.png b/docs/img/premium/spacinov-readme.png new file mode 100644 index 000000000..20e925211 Binary files /dev/null and b/docs/img/premium/spacinov-readme.png differ diff --git a/docs/img/premium/stream-readme.png b/docs/img/premium/stream-readme.png index 15da6ba71..a6a7317b7 100644 Binary files a/docs/img/premium/stream-readme.png and b/docs/img/premium/stream-readme.png differ diff --git a/docs/index.md b/docs/index.md index e58f24df8..2f44fae9a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,16 +68,16 @@ continued development by **[signing up for a paid plan][funding]**.
-*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: * 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 each Python and Django series. diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 18066f056..e12becbd0 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -112,8 +112,8 @@ Here's our re-wired `snippets/urls.py` file. # Create a router and register our viewsets with it. router = DefaultRouter() - router.register(r'snippets', views.SnippetViewSet,basename="snippets") - router.register(r'users', views.UserViewSet,basename="users") + router.register(r'snippets', views.SnippetViewSet,basename="snippet") + router.register(r'users', views.UserViewSet,basename="user") # The API URLs are now determined automatically by the router. urlpatterns = [ diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index ad4928730..cf2dc26e8 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,2 +1,3 @@ # MkDocs to build our documentation. mkdocs>=1.1.2,<1.2 +jinja2>=2.10,<3.1.0 # contextfilter has been renamed diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d7e7816ce..31e5b994c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -27,7 +27,6 @@ from django.utils.duration import duration_string from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address -from django.utils.timezone import utc from django.utils.translation import gettext_lazy as _ from pytz.exceptions import InvalidTimeError @@ -63,6 +62,9 @@ def is_simple_callable(obj): """ 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. if inspect.isbuiltin(obj): raise BuiltinSignatureError( @@ -1190,7 +1192,7 @@ class DateTimeField(Field): except InvalidTimeError: self.fail('make_aware', timezone=field_timezone) 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 def default_timezone(self): diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c98700784..bdedd43b8 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -10,7 +10,7 @@ from django.utils.encoding import smart_str, uri_to_iri from django.utils.translation import gettext_lazy as _ from rest_framework.fields import ( - Field, empty, get_attribute, is_simple_callable, iter_options + Field, SkipField, empty, get_attribute, is_simple_callable, iter_options ) from rest_framework.reverse import reverse from rest_framework.settings import api_settings @@ -535,7 +535,30 @@ class ManyRelatedField(Field): if hasattr(instance, 'pk') and instance.pk is None: return [] - relationship = get_attribute(instance, self.source_attrs) + try: + relationship = get_attribute(instance, self.source_attrs) + except (KeyError, AttributeError) as exc: + if self.default is not empty: + return self.get_default() + if self.allow_null: + return None + if not self.required: + raise SkipField() + msg = ( + 'Got {exc_type} when attempting to get a value for field ' + '`{field}` on serializer `{serializer}`.\nThe serializer ' + 'field might be named incorrectly and not match ' + 'any attribute or key on the `{instance}` instance.\n' + 'Original exception text was: {exc}.'.format( + exc_type=type(exc).__name__, + field=self.field_name, + serializer=self.parent.__class__.__name__, + instance=instance.__class__.__name__, + exc=exc + ) + ) + raise type(exc)(msg) + return relationship.all() if hasattr(relationship, 'all') else relationship def to_representation(self, iterable): diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 5e9d59f8b..122846376 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -636,7 +636,7 @@ class AutoSchema(ViewInspector): """ 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))} def get_request_body(self, path, method): @@ -650,7 +650,7 @@ class AutoSchema(ViewInspector): if not isinstance(serializer, serializers.Serializer): item_schema = {} else: - item_schema = self._get_reference(serializer) + item_schema = self.get_reference(serializer) return { 'content': { @@ -674,7 +674,7 @@ class AutoSchema(ViewInspector): if not isinstance(serializer, serializers.Serializer): item_schema = {} else: - item_schema = self._get_reference(serializer) + item_schema = self.get_reference(serializer) if is_list_view(path, method, self.view): response_schema = { @@ -808,3 +808,11 @@ class AutoSchema(ViewInspector): RemovedInDRF314Warning, stacklevel=2 ) 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) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index db0e9c95c..ccd9430b4 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -218,7 +218,7 @@ def format_value(value): return template.render(context) elif isinstance(value, str): if ( - (value.startswith('http:') or value.startswith('https:')) and not + (value.startswith('http:') or value.startswith('https:') or value.startswith('/')) and not re.search(r'\s', value) ): return mark_safe('{value}'.format(value=escape(value))) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 4f8a4f192..673821b5e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -217,15 +217,9 @@ def get_field_kwargs(field_name, model_field): ] 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( queryset=model_field.model._default_manager, - message=unique_error_message) + message=get_unique_error_message(model_field)) validator_kwarg.append(validator) if validator_kwarg: @@ -281,7 +275,9 @@ def get_relation_kwargs(field_name, relation_info): if model_field.validators: kwargs['validators'] = model_field.validators 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] if to_many and not model_field.blank: kwargs['allow_empty'] = False @@ -300,3 +296,13 @@ def get_url_kwargs(model_field): return { '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 diff --git a/setup.py b/setup.py index 3c3761c86..cb6708c6e 100755 --- a/setup.py +++ b/setup.py @@ -94,6 +94,7 @@ setup( 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py index 65701c065..33491ad92 100644 --- a/tests/browsable_api/no_auth_urls.py +++ b/tests/browsable_api/no_auth_urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import MockView +from .views import BasicModelWithUsersViewSet, MockView urlpatterns = [ path('', MockView.as_view()), + path('basicviewset', BasicModelWithUsersViewSet.as_view({'get': 'list'})), ] diff --git a/tests/browsable_api/serializers.py b/tests/browsable_api/serializers.py new file mode 100644 index 000000000..e8a1cdef8 --- /dev/null +++ b/tests/browsable_api/serializers.py @@ -0,0 +1,8 @@ +from rest_framework.serializers import ModelSerializer +from tests.models import BasicModelWithUsers + + +class BasicSerializer(ModelSerializer): + class Meta: + model = BasicModelWithUsers + fields = '__all__' diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 17644c2ac..a76d11fe3 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -1,8 +1,35 @@ from django.contrib.auth.models import User from django.test import TestCase, override_settings +from rest_framework.permissions import IsAuthenticated 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') class DropdownWithAuthTests(TestCase): diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py index e1cf13a1e..e73967bf8 100644 --- a/tests/browsable_api/views.py +++ b/tests/browsable_api/views.py @@ -1,6 +1,16 @@ from rest_framework import authentication, renderers +from rest_framework.permissions import BasePermission from rest_framework.response import Response 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): @@ -9,3 +19,15 @@ class MockView(APIView): def get(self, request): 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 diff --git a/tests/models.py b/tests/models.py index afe649760..666e9f003 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,6 @@ import uuid +from django.contrib.auth.models import User from django.db import models from django.utils.translation import gettext_lazy as _ @@ -33,6 +34,10 @@ class ManyToManySource(RESTFrameworkModel): targets = models.ManyToManyField(ManyToManyTarget, related_name='sources') +class BasicModelWithUsers(RESTFrameworkModel): + users = models.ManyToManyField(User) + + # ForeignKey class ForeignKeyTarget(RESTFrameworkModel): name = models.CharField(max_length=100) diff --git a/tests/test_encoders.py b/tests/test_encoders.py index c104dd5a5..953e5564b 100644 --- a/tests/test_encoders.py +++ b/tests/test_encoders.py @@ -1,15 +1,16 @@ -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from decimal import Decimal from uuid import uuid4 import pytest from django.test import TestCase -from django.utils.timezone import utc from rest_framework.compat import coreapi from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.serializer_helpers import ReturnList +utc = timezone.utc + class MockList: def tolist(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index 7a5304a82..cbec79119 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,7 +9,7 @@ import pytz from django.core.exceptions import ValidationError as DjangoValidationError from django.http import QueryDict 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 from rest_framework import exceptions, serializers @@ -17,6 +17,8 @@ from rest_framework.fields import ( BuiltinSignatureError, DjangoImageField, is_simple_callable ) +utc = datetime.timezone.utc + # Tests for helper functions. # --------------------------- @@ -73,6 +75,10 @@ class TestIsSimpleCallable: assert is_simple_callable(valid_vargs_kwargs) 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): from django.db import models @@ -1859,9 +1865,9 @@ class TestMultipleChoiceField(FieldValues): def test_against_partial_and_full_updates(self): field = serializers.MultipleChoiceField(choices=(('a', 'a'), ('b', 'b'))) field.partial = False - assert field.get_value(QueryDict({})) == [] + assert field.get_value(QueryDict('')) == [] field.partial = True - assert field.get_value(QueryDict({})) == rest_framework.fields.empty + assert field.get_value(QueryDict('')) == rest_framework.fields.empty class TestEmptyMultipleChoiceField(FieldValues): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 7da1b41ae..419eae632 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -12,6 +12,7 @@ import sys import tempfile from collections import OrderedDict +import django import pytest from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder @@ -452,11 +453,14 @@ class TestPosgresFieldsMapping(TestCase): model = ArrayFieldModel fields = ['array_field', 'array_field_with_blank'] + validators = "" + if django.VERSION < (4, 1): + validators = ", validators=[]" expected = dedent(""" TestSerializer(): - array_field = ListField(allow_empty=False, child=CharField(label='Array field', validators=[])) - array_field_with_blank = ListField(child=CharField(label='Array field with blank', validators=[]), required=False) - """) + array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s)) + array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False) + """ % (validators, validators)) self.assertEqual(repr(TestSerializer()), expected) @pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField') @@ -1021,6 +1025,73 @@ class Issue2704TestCase(TestCase): assert serializer.data == expected +class Issue7550FooModel(models.Model): + text = models.CharField(max_length=100) + bar = models.ForeignKey( + 'Issue7550BarModel', null=True, blank=True, on_delete=models.SET_NULL, + related_name='foos', related_query_name='foo') + + +class Issue7550BarModel(models.Model): + pass + + +class Issue7550TestCase(TestCase): + + def test_dotted_source(self): + + class _FooSerializer(serializers.ModelSerializer): + class Meta: + model = Issue7550FooModel + fields = ('id', 'text') + + class FooSerializer(serializers.ModelSerializer): + other_foos = _FooSerializer(source='bar.foos', many=True) + + class Meta: + model = Issue7550BarModel + fields = ('id', 'other_foos') + + bar = Issue7550BarModel.objects.create() + foo_a = Issue7550FooModel.objects.create(bar=bar, text='abc') + foo_b = Issue7550FooModel.objects.create(bar=bar, text='123') + + assert FooSerializer(foo_a).data == { + 'id': foo_a.id, + 'other_foos': [ + { + 'id': foo_a.id, + 'text': foo_a.text, + }, + { + 'id': foo_b.id, + 'text': foo_b.text, + }, + ], + } + + def test_dotted_source_with_default(self): + + class _FooSerializer(serializers.ModelSerializer): + class Meta: + model = Issue7550FooModel + fields = ('id', 'text') + + class FooSerializer(serializers.ModelSerializer): + other_foos = _FooSerializer(source='bar.foos', default=[], many=True) + + class Meta: + model = Issue7550FooModel + fields = ('id', 'other_foos') + + foo = Issue7550FooModel.objects.create(bar=None, text='abc') + + assert FooSerializer(foo).data == { + 'id': foo.id, + 'other_foos': [], + } + + class DecimalFieldModel(models.Model): decimal_field = models.DecimalField( max_digits=3, diff --git a/tests/test_validators.py b/tests/test_validators.py index bccbe1514..39490ac86 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -42,6 +42,12 @@ class RelatedModelSerializer(serializers.ModelSerializer): fields = ('username', 'email') +class RelatedModelUserSerializer(serializers.ModelSerializer): + class Meta: + model = RelatedModel + fields = ('user',) + + class AnotherUniquenessModel(models.Model): code = models.IntegerField(unique=True) @@ -83,6 +89,13 @@ class TestUniquenessValidation(TestCase): assert not serializer.is_valid() 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): data = {'username': 'other'} serializer = UniquenessSerializer(data=data) diff --git a/tox.ini b/tox.ini index a41176d72..c275a0abe 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py36,py37,py38,py39}-django22, {py36,py37,py38,py39}-django31, {py36,py37,py38,py39,py310}-django32, - {py38,py39,py310}-{django40,djangomain}, + {py38,py39,py310}-{django40,django41,djangomain}, base,dist,docs, [travis:env] @@ -12,6 +12,7 @@ DJANGO = 3.1: django31 3.2: django32 4.0: django40 + 4.1: django41 main: djangomain [testenv] @@ -24,7 +25,8 @@ deps = django22: Django>=2.2,<3.0 django31: Django>=3.1,<3.2 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 -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt