Merge branch 'master' into mjm/upgrade-pytest

This commit is contained in:
Tom Christie 2021-03-10 12:45:28 +00:00 committed by GitHub
commit d6e571d27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 236 additions and 54 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
github: encode
custom: https://fund.django-rest-framework.org/topics/funding/

10
.github/ISSUE_TEMPLATE/1-issue.md vendored Normal file
View File

@ -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.

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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. 💖

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
*.db
*~
.*
*.py.bak
/site/
/htmlcov/

View File

@ -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

View File

@ -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

View File

@ -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',
]
}
```

View File

@ -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,16 @@ 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.
## Django-Rest-Durin
[Django-Rest-Durin][django-rest-durin] is built with the idea to have one library that does token auth for multiple Web/CLI/Mobile API clients via one interface but allows different token configuration for each API Client that consumes the API. It provides support for multiple tokens per user via custom models, views, permissions that work with Django-Rest-Framework. The token expiration time can be different per API client and is customizable via the Django Admin Interface.
More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html).
[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 +458,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 +476,5 @@ 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
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin

View File

@ -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

View File

@ -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:

View File

@ -116,7 +116,7 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file.
__Note:__ when you set new permission classes via the class attribute or decorators you're telling the view to ignore the default list set in the __settings.py__ file.
Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written:
@ -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.

View File

@ -103,6 +103,16 @@ Unlike other renderers, the data passed to the `Response` does not need to be se
The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context.
---
**Note:** When used with a view that makes use of a serializer the `Response` sent for rendering may not be a dictionay and will need to be wrapped in a dict before returning to allow the TemplateHTMLRenderer to render it. For example:
```
response.data = {'results': response.data}
```
---
The template name is determined by (in order of preference):
1. An explicit `template_name` argument passed to the response.

View File

@ -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-
<!-- 3.10.0 -->
[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
<!-- 3.11.0 -->
[gh6892]: https://github.com/encode/django-rest-framework/issues/6892

View File

@ -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
@ -225,6 +227,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-rest-multiple-models][django-rest-multiple-models] - Provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request.
* [drf-typed-views][drf-typed-views] - Use Python type annotations to validate/deserialize request parameters. Inspired by API Star, Hug and FastAPI.
* [rest-framework-actions][rest-framework-actions] - Provides control over each action in ViewSets. Serializers per action, method.
### Routers
@ -362,3 +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

View File

@ -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({

View File

@ -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.

View File

@ -20,7 +20,7 @@ def _get_error_details(data, default_code=None):
Descend into a nested data structure, forcing any
lazy translation strings or strings into `ErrorDetail`.
"""
if isinstance(data, list):
if isinstance(data, (list, tuple)):
ret = [
_get_error_details(item, default_code) for item in data
]
@ -150,7 +150,9 @@ class ValidationError(APIException):
# For validation failures, we may collect many errors together,
# so the details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list):
if isinstance(detail, tuple):
detail = list(detail)
elif not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
self.detail = _get_error_details(detail, code)

View File

@ -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())

View File

@ -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:

View File

@ -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')

View File

@ -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%;
}

View File

@ -5,7 +5,7 @@
<div class="input-group">
<input type="text" class="form-control" style="width: 350px" name="{{ param }}" value="{{ term }}">
<span class="input-group-btn">
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> Search</button>
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> {% trans "Search" %}</button>
</span>
</div>
</div>

View File

@ -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)

View File

@ -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):

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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."

View File

@ -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 = (
'<fieldset>'
'<legend>Nested</legend>'
'<divclass="form-group">'
'<label>Textfield</label>'
'<inputname="nested.text_field"class="form-control"type="text"value="">'
'</div>'
'</fieldset>'
)
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed
class TestJSONBoundField:
def test_as_form_fields(self):

View File

@ -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.

View File

@ -2,6 +2,7 @@ from django.test import TestCase
from rest_framework import serializers, status
from rest_framework.decorators import api_view
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
@ -99,3 +100,12 @@ class TestValidationErrorWithCodes(TestCase):
response = view(request)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data == self.expected_response_data
class TestValidationErrorConvertsTuplesToLists(TestCase):
def test_validation_error_details(self):
error = ValidationError(detail=('message1', 'message2'))
assert isinstance(error.detail, list)
assert len(error.detail) == 2
assert str(error.detail[0]) == 'message1'
assert str(error.detail[1]) == 'message2'

View File

@ -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