diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d7c23d635..5a830ca53 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: encode custom: https://fund.django-rest-framework.org/topics/funding/ diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md new file mode 100644 index 000000000..0da154953 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -0,0 +1,10 @@ +--- +name: Issue +about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏 +--- + +## Checklist + +- [ ] Raised initially as discussion #... +- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) +- [ ] I have reduced the issue to the simplest possible case. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..382fc521a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: +- name: Discussions + url: https://github.com/encode/django-rest-framework/discussions + about: > + The "Discussions" forum is where you want to start. 💖 diff --git a/.gitignore b/.gitignore index 41768084c..82e885ede 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.db *~ .* +*.py.bak + /site/ /htmlcov/ diff --git a/.travis.yml b/.travis.yml index 7a820766e..f9f22336f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,18 +10,20 @@ matrix: - { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=3.0 } - { python: "3.6", env: DJANGO=3.1 } - - { python: "3.6", env: DJANGO=master } + - { python: "3.6", env: DJANGO=3.2 } - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=3.0 } - { python: "3.7", env: DJANGO=3.1 } - - { python: "3.7", env: DJANGO=master } + - { python: "3.7", env: DJANGO=3.2 } - { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=3.1 } + - { python: "3.8", env: DJANGO=3.2 } - { python: "3.8", env: DJANGO=master } - { python: "3.9", env: DJANGO=3.1 } + - { python: "3.9", env: DJANGO=3.2 } - { python: "3.9", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } @@ -38,6 +40,7 @@ matrix: allow_failures: - env: DJANGO=master + - env: DJANGO=3.2 install: - pip install tox tox-travis diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 566bf9543..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -## Checklist - -- [ ] I have verified that that issue exists against the `master` branch of Django REST framework. -- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. -- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.) -- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) -- [ ] I have reduced the issue to the simplest possible case. -- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.) - -## Steps to reproduce - -## Expected behavior - -## Actual behavior diff --git a/README.md b/README.md index 8af1466f8..305f92389 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ router.register(r'users', UserViewSet) # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), ] ``` @@ -131,7 +131,7 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', ] } ``` diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index da932a06c..d13c5a2f0 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -357,7 +357,7 @@ The following third party packages are also available. ## 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 [Evonove][evonove] 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**. +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**. #### Installation & configuration @@ -432,6 +432,10 @@ There are currently two forks of this project. [drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's own TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number. +## django-rest-authemail + +[django-rest-authemail][django-rest-authemail] provides a RESTful API interface for user signup and authentication. Email addresses are used for authentication, rather than usernames. API endpoints are available for signup, signup email verification, login, logout, password reset, password reset verification, email change, email change verification, password change, and user detail. A fully-functional example project and detailed instructions are included. + [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -448,7 +452,7 @@ There are currently two forks of this project. [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [oauth-1.0a]: https://oauth.net/core/1.0a/ [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit -[evonove]: https://github.com/evonove/ +[jazzband]: https://github.com/jazzband/ [oauthlib]: https://github.com/idan/oauthlib [djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt [etoccalino]: https://github.com/etoccalino/ @@ -466,3 +470,4 @@ There are currently two forks of this project. [django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2 [django-rest-knox]: https://github.com/James1345/django-rest-knox [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 96517b15e..ab4f82cd2 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -13,13 +13,13 @@ provided in Django. Django provides a [`method_decorator`][decorator] to use decorators with class based views. This can be used with -other cache decorators such as [`cache_page`][page] and -[`vary_on_cookie`][cookie]. +other cache decorators such as [`cache_page`][page], +[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers]. ```python from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page -from django.views.decorators.vary import vary_on_cookie +from django.views.decorators.vary import vary_on_cookie, vary_on_headers from rest_framework.response import Response from rest_framework.views import APIView @@ -27,8 +27,7 @@ from rest_framework import viewsets class UserViewSet(viewsets.ViewSet): - - # Cache requested url for each user for 2 hours + # With cookie: cache requested url for each user for 2 hours @method_decorator(cache_page(60*60*2)) @method_decorator(vary_on_cookie) def list(self, request, format=None): @@ -38,8 +37,18 @@ class UserViewSet(viewsets.ViewSet): return Response(content) -class PostView(APIView): +class ProfileView(APIView): + # With auth: cache requested url for each user for 2 hours + @method_decorator(cache_page(60*60*2)) + @method_decorator(vary_on_headers("Authorization",)) + def get(self, request, format=None): + content = { + 'user_feed': request.user.get_user_feed() + } + return Response(content) + +class PostView(APIView): # Cache page for the requested url @method_decorator(cache_page(60*60*2)) def get(self, request, format=None): @@ -55,4 +64,5 @@ class PostView(APIView): [page]: https://docs.djangoproject.com/en/dev/topics/cache/#the-per-view-cache [cookie]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_cookie +[headers]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_headers [decorator]: https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/#decorating-the-class diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index fbf3097e0..e62a7e4f9 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -222,7 +222,7 @@ By default this exception results in a response with the HTTP status code "429 T The `ValidationError` exception is slightly different from the other `APIException` classes: * The `detail` argument is mandatory, not optional. -* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. +* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. By using a dictionary, you can specify field-level errors while performing object-level validation in the `validate()` method of a serializer. For example. `raise serializers.ValidationError({'name': 'Please enter a valid name.'})` * By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')` The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument: diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index ade146257..f694d6be5 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -169,7 +169,7 @@ This permission is suitable if you want to your API to allow read permissions to ## DjangoModelPermissions -This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. +This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. @@ -179,12 +179,6 @@ The default behaviour can also be overridden to support custom model permissions To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. -#### Using with views that do not include a `queryset` attribute. - -If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sentinel queryset, so that this class can determine the required permissions. For example: - - queryset = User.objects.none() # Required for DjangoModelPermissions - ## DjangoModelPermissionsOrAnonReadOnly Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index c981b9ac9..49fb655b0 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -177,6 +177,8 @@ Date: 28th September 2020 * Don't strict disallow redundant `SerializerMethodField` field name arguments. * Don't render extra actions in browable API if not authenticated. * Strip null characters from search parameters. +* Deprecate the `detail_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=True)` instead. [gh6687] +* Deprecate the `list_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=False)` instead. [gh6687] ## 3.9.x series @@ -2270,6 +2272,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6680]: https://github.com/encode/django-rest-framework/issues/6680 [gh6317]: https://github.com/encode/django-rest-framework/issues/6317 +[gh6687]: https://github.com/encode/django-rest-framework/issues/6687 [gh6892]: https://github.com/encode/django-rest-framework/issues/6892 diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 694e462af..838122cbe 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -190,6 +190,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. * [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF. * [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers. +* [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses. ### Permissions @@ -214,6 +215,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-action-serializer][drf-action-serializer] - Serializer providing per-action fields config for use with ViewSets to prevent having to write multiple serializers. * [djangorestframework-dataclasses][djangorestframework-dataclasses] - Serializer providing automatic field generation for Python dataclasses, like the built-in ModelSerializer does for models. * [django-restql][django-restql] - Turn your REST API into a GraphQL like API(It allows clients to control which fields will be sent in a response, uses GraphQL like syntax, supports read and write on both flat and nested fields). +* [graphwrap][graphwrap] - Transform your REST API into a fully compliant GraphQL API with just two lines of code. Leverages [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/) to dynamically build, at runtime, a GraphQL ObjectType for each view in your API. ### Serializer fields @@ -363,4 +365,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-elasticsearch-dsl-drf]: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf [django-api-client]: https://github.com/rhenter/django-api-client [drf-psq]: https://github.com/drf-psq/drf-psq +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail +[graphwrap]: https://github.com/PaulGilmartin/graph_wrap [rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 9b61eaf42..b9f5e3ecd 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -453,7 +453,7 @@ For example, using the "Django REST framework JWT" package function loginUser(username, password) { let action = ["api-token-auth", "obtain-token"]; - let params = {username: "example", email: "example@example.com"}; + let params = {username: username, password: password}; client.action(schema, action, params).then(function(result) { // On success, instantiate an authenticated client. let auth = window.coreapi.auth.TokenAuthentication({ diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index cd7e5098f..5eabeee7b 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -202,7 +202,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `op meta = self.metadata_class() data = meta.determine_metadata(request, self) data.pop('description') - return data + return Response(data=data, status=status.HTTP_200_OK) See [the Metadata docs][metadata-docs] for more details. diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index ad246e857..99463560e 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -2,3 +2,4 @@ pytest>=5.4.1,<5.5 pytest-django>=3.9.0,<3.10 pytest-cov>=2.7.1 +six>=1.14.0 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fdfba13f2..d91299484 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -704,7 +704,7 @@ class BooleanField(Field): initial = False TRUE_VALUES = { 't', 'T', - 'y', 'Y', 'yes', 'YES', + 'y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE', 'on', 'On', 'ON', '1', 1, @@ -712,7 +712,7 @@ class BooleanField(Field): } FALSE_VALUES = { 'f', 'F', - 'n', 'N', 'no', 'NO', + 'n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE', 'off', 'Off', 'OFF', '0', 0, 0.0, @@ -1063,6 +1063,9 @@ class DecimalField(Field): try: value = decimal.Decimal(data) except decimal.DecimalException: + if data == '' and self.allow_null: + return None + self.fail('invalid') if value.is_nan(): @@ -1112,6 +1115,12 @@ class DecimalField(Field): def to_representation(self, value): coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING) + if value is None: + if coerce_to_string: + return '' + else: + return None + if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value).strip()) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 60a57c8e4..0f0aa9ccf 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -198,7 +198,7 @@ class PageNumberPagination(BasePagination): return None paginator = self.django_paginator_class(queryset, page_size) - page_number = request.query_params.get(self.page_query_param, 1) + page_number = self.get_page_number(request) if page_number in self.last_page_strings: page_number = paginator.num_pages @@ -217,6 +217,9 @@ class PageNumberPagination(BasePagination): self.request = request return list(self.page) + def get_page_number(self, request): + return request.query_params.get(self.page_query_param, 1) + def get_paginated_response(self, data): return Response(OrderedDict([ ('count', self.page.paginator.count), @@ -376,11 +379,11 @@ class LimitOffsetPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' def paginate_queryset(self, queryset, request, view=None): - self.count = self.get_count(queryset) self.limit = self.get_limit(request) if self.limit is None: return None + self.count = self.get_count(queryset) self.offset = self.get_offset(request) self.request = request if self.count > self.limit and self.template is not None: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3c4be8aeb..5b7ba8a8c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1063,7 +1063,8 @@ class OpenAPIRenderer(BaseRenderer): class JSONOpenAPIRenderer(BaseRenderer): media_type = 'application/vnd.oai.openapi+json' charset = None + encoder_class = encoders.JSONEncoder format = 'openapi-json' def render(self, data, media_type=None, renderer_context=None): - return json.dumps(data, indent=2).encode('utf-8') + return json.dumps(data, cls=self.encoder_class, indent=2).encode('utf-8') diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 86fef1773..51ca3ba19 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -40,7 +40,7 @@ td.nested > table { margin: 0; } -form select, form input, form textarea { +form select, form input:not([type=checkbox]), form textarea { width: 90%; } diff --git a/rest_framework/templates/rest_framework/filters/search.html b/rest_framework/templates/rest_framework/filters/search.html index edb28d45d..065c3889a 100644 --- a/rest_framework/templates/rest_framework/filters/search.html +++ b/rest_framework/templates/rest_framework/filters/search.html @@ -5,7 +5,7 @@
- +
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index b18fbe0df..cd0373adc 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from django.utils.encoding import force_str @@ -101,7 +101,7 @@ class NestedBoundField(BoundField): """ def __init__(self, field, value, errors, prefix=''): - if value is None or value == '': + if value is None or value == '' or not isinstance(value, Mapping): value = {} super().__init__(field, value, errors, prefix) diff --git a/rest_framework/views.py b/rest_framework/views.py index d1b5e4ed9..5b0622069 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework. """ from django.conf import settings from django.core.exceptions import PermissionDenied -from django.db import connection, models, transaction +from django.db import connections, models from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers @@ -63,9 +63,9 @@ def get_view_description(view, html=False): def set_rollback(): - atomic_requests = connection.settings_dict.get('ATOMIC_REQUESTS', False) - if atomic_requests and connection.in_atomic_block: - transaction.set_rollback(True) + for db in connections.all(): + if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block: + db.set_rollback(True) def exception_handler(exc, context): diff --git a/tests/conftest.py b/tests/conftest.py index ac29e4a42..cc32cc637 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,10 @@ def pytest_configure(config): 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' + }, + 'secondary': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' } }, SITE_ID=1, diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 542c377b1..871eb1b30 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -11,7 +11,8 @@ from rest_framework.authtoken.views import obtain_auth_token from rest_framework.compat import uritemplate from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import ( - BaseRenderer, BrowsableAPIRenderer, JSONRenderer, OpenAPIRenderer + BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer, + OpenAPIRenderer ) from rest_framework.request import Request from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator @@ -992,6 +993,19 @@ class TestGenerator(TestCase): assert 'openapi' in schema assert 'paths' in schema + def test_schema_rendering_to_json(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = JSONOpenAPIRenderer().render(schema) + + assert b'"openapi": "' in ret + assert b'"default": "0.0"' in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 18b3beae4..f1ed0bd4e 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -1,4 +1,5 @@ import uuid +from datetime import timedelta from django.core.validators import ( DecimalValidator, MaxLengthValidator, MaxValueValidator, @@ -59,6 +60,7 @@ class DocStringExampleDetailView(APIView): class ExampleSerializer(serializers.Serializer): date = serializers.DateField() datetime = serializers.DateTimeField() + duration = serializers.DurationField(default=timedelta()) hstore = serializers.HStoreField() uuid_field = serializers.UUIDField(default=uuid.uuid4) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 15b41e02f..beda5cba1 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -130,6 +130,41 @@ class DBTransactionAPIExceptionTests(TestCase): assert BasicModel.objects.count() == 0 +@unittest.skipUnless( + connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints." +) +class MultiDBTransactionAPIExceptionTests(TestCase): + databases = '__all__' + + def setUp(self): + self.view = APIExceptionView.as_view() + connections.databases['default']['ATOMIC_REQUESTS'] = True + connections.databases['secondary']['ATOMIC_REQUESTS'] = True + + def tearDown(self): + connections.databases['default']['ATOMIC_REQUESTS'] = False + connections.databases['secondary']['ATOMIC_REQUESTS'] = False + + def test_api_exception_rollback_transaction(self): + """ + Transaction is rollbacked by our transaction atomic block. + """ + request = factory.post('/') + num_queries = 4 if connection.features.can_release_savepoints else 3 + with self.assertNumQueries(num_queries): + # 1 - begin savepoint + # 2 - insert + # 3 - rollback savepoint + # 4 - release savepoint + with transaction.atomic(), transaction.atomic(using='secondary'): + response = self.view(request) + assert transaction.get_rollback() + assert transaction.get_rollback(using='secondary') + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert BasicModel.objects.count() == 0 + + @unittest.skipUnless( connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py index dc5ab542f..dec8793c3 100644 --- a/tests/test_bound_fields.py +++ b/tests/test_bound_fields.py @@ -163,6 +163,33 @@ class TestNestedBoundField: rendered_packed = ''.join(rendered.split()) assert rendered_packed == expected_packed + def test_rendering_nested_fields_with_not_mappable_value(self): + from rest_framework.renderers import HTMLFormRenderer + + class Nested(serializers.Serializer): + text_field = serializers.CharField() + + class ExampleSerializer(serializers.Serializer): + nested = Nested() + + serializer = ExampleSerializer(data={'nested': 1}) + assert not serializer.is_valid() + renderer = HTMLFormRenderer() + for field in serializer: + rendered = renderer.render_field(field, {}) + expected_packed = ( + '
' + 'Nested' + '' + '' + '' + '' + '
' + ) + + rendered_packed = ''.join(rendered.split()) + assert rendered_packed == expected_packed + class TestJSONBoundField: def test_as_form_fields(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index fdd570d8a..5842553f0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1090,6 +1090,9 @@ class TestDecimalField(FieldValues): '2E+1': Decimal('20'), } invalid_inputs = ( + (None, ["This field may not be null."]), + ('', ["A valid number is required."]), + (' ', ["A valid number is required."]), ('abc', ["A valid number is required."]), (Decimal('Nan'), ["A valid number is required."]), (Decimal('Snan'), ["A valid number is required."]), @@ -1115,6 +1118,32 @@ class TestDecimalField(FieldValues): field = serializers.DecimalField(max_digits=3, decimal_places=1) +class TestAllowNullDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: '', + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True) + + +class TestAllowNullNoStringCoercionDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: None, + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, coerce_to_string=False) + + class TestMinMaxDecimalField(FieldValues): """ Valid and invalid values for `DecimalField` with min and max limits. diff --git a/tox.ini b/tox.ini index df6387d5e..544bab163 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,8 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38}-django30, {py36,py37,py38,py39}-django31, - {py36,py37,py38,py39}-djangomaster, + {py36,py37,py38,py39}-django32, + {py38,py39}-djangomaster, base,dist,lint,docs, [travis:env] @@ -11,6 +12,7 @@ DJANGO = 2.2: django22 3.0: django30 3.1: django31 + 3.2: django32 master: djangomaster [testenv] @@ -23,6 +25,7 @@ deps = django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 + django32: Django>=3.2a1,<4.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt