Merge branch 'master' into update_irc_server

This commit is contained in:
Tom Christie 2021-08-06 16:46:20 +01:00 committed by GitHub
commit c1c8b9f3de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 83 additions and 35 deletions

View File

@ -105,7 +105,7 @@ The TemplateHTMLRenderer will create a `RequestContext`, using the `response.dat
--- ---
**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: **Note:** When used with a view that makes use of a serializer the `Response` sent for rendering may not be a dictionary 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} response.data = {'results': response.data}
@ -528,7 +528,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. [Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble].
[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/#the-rendering-process [cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md [conneg]: content-negotiation.md
[html-and-forms]: ../topics/html-and-forms.md [html-and-forms]: ../topics/html-and-forms.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers

View File

@ -605,13 +605,13 @@ For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`.
For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`. For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`.
### `serializer_url_field` ### `.serializer_url_field`
The serializer field class that should be used for any `url` field on the serializer. The serializer field class that should be used for any `url` field on the serializer.
Defaults to `serializers.HyperlinkedIdentityField` Defaults to `serializers.HyperlinkedIdentityField`
### `serializer_choice_field` ### `.serializer_choice_field`
The serializer field class that should be used for any choice fields on the serializer. The serializer field class that should be used for any choice fields on the serializer.

View File

@ -186,7 +186,7 @@ Framework.
## Support ## Support
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.libera.chat`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.libera.chat`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/). For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/).
@ -257,7 +257,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[funding]: community/funding.md [funding]: community/funding.md
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[botbot]: https://botbot.me/freenode/restframework/
[stack-overflow]: https://stackoverflow.com/ [stack-overflow]: https://stackoverflow.com/
[django-rest-framework-tag]: https://stackoverflow.com/questions/tagged/django-rest-framework [django-rest-framework-tag]: https://stackoverflow.com/questions/tagged/django-rest-framework
[security-mail]: mailto:rest-framework-security@googlegroups.com [security-mail]: mailto:rest-framework-security@googlegroups.com

View File

@ -320,7 +320,7 @@ class Field:
default_empty_html = empty default_empty_html = empty
initial = None initial = None
def __init__(self, read_only=False, write_only=False, def __init__(self, *, read_only=False, write_only=False,
required=None, default=empty, initial=empty, source=None, required=None, default=empty, initial=empty, source=None,
label=None, help_text=None, style=None, label=None, help_text=None, style=None,
error_messages=None, validators=None, allow_null=False): error_messages=None, validators=None, allow_null=False):
@ -1046,6 +1046,11 @@ class DecimalField(Field):
'Invalid rounding option %s. Valid values for rounding are: %s' % (rounding, valid_roundings)) 'Invalid rounding option %s. Valid values for rounding are: %s' % (rounding, valid_roundings))
self.rounding = rounding self.rounding = rounding
def validate_empty_values(self, data):
if smart_str(data).strip() == '' and self.allow_null:
return (True, None)
return super().validate_empty_values(data)
def to_internal_value(self, data): def to_internal_value(self, data):
""" """
Validate that the input is a decimal number and return a Decimal Validate that the input is a decimal number and return a Decimal
@ -1063,9 +1068,6 @@ class DecimalField(Field):
try: try:
value = decimal.Decimal(data) value = decimal.Decimal(data)
except decimal.DecimalException: except decimal.DecimalException:
if data == '' and self.allow_null:
return None
self.fail('invalid') self.fail('invalid')
if value.is_nan(): if value.is_nan():
@ -1161,14 +1163,14 @@ class DateTimeField(Field):
} }
datetime_parser = datetime.datetime.strptime datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, default_timezone=None, **kwargs):
if format is not empty: if format is not empty:
self.format = format self.format = format
if input_formats is not None: if input_formats is not None:
self.input_formats = input_formats self.input_formats = input_formats
if default_timezone is not None: if default_timezone is not None:
self.timezone = default_timezone self.timezone = default_timezone
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def enforce_timezone(self, value): def enforce_timezone(self, value):
""" """
@ -1247,12 +1249,12 @@ class DateField(Field):
} }
datetime_parser = datetime.datetime.strptime datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, **kwargs):
if format is not empty: if format is not empty:
self.format = format self.format = format
if input_formats is not None: if input_formats is not None:
self.input_formats = input_formats self.input_formats = input_formats
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
@ -1313,12 +1315,12 @@ class TimeField(Field):
} }
datetime_parser = datetime.datetime.strptime datetime_parser = datetime.datetime.strptime
def __init__(self, format=empty, input_formats=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, **kwargs):
if format is not empty: if format is not empty:
self.format = format self.format = format
if input_formats is not None: if input_formats is not None:
self.input_formats = input_formats self.input_formats = input_formats
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS) input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
@ -1468,9 +1470,9 @@ class MultipleChoiceField(ChoiceField):
} }
default_empty_html = [] default_empty_html = []
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self.allow_empty = kwargs.pop('allow_empty', True) self.allow_empty = kwargs.pop('allow_empty', True)
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def get_value(self, dictionary): def get_value(self, dictionary):
if self.field_name not in dictionary: if self.field_name not in dictionary:
@ -1527,12 +1529,12 @@ class FileField(Field):
'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
} }
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
self.allow_empty_file = kwargs.pop('allow_empty_file', False) self.allow_empty_file = kwargs.pop('allow_empty_file', False)
if 'use_url' in kwargs: if 'use_url' in kwargs:
self.use_url = kwargs.pop('use_url') self.use_url = kwargs.pop('use_url')
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
try: try:
@ -1576,9 +1578,9 @@ class ImageField(FileField):
), ),
} }
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
# Image validation is a bit grungy, so we'll just outright # Image validation is a bit grungy, so we'll just outright
@ -1593,8 +1595,8 @@ class ImageField(FileField):
# Composite field types... # Composite field types...
class _UnvalidatedField(Field): class _UnvalidatedField(Field):
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self.allow_blank = True self.allow_blank = True
self.allow_null = True self.allow_null = True
@ -1615,7 +1617,7 @@ class ListField(Field):
'max_length': _('Ensure this field has no more than {max_length} elements.') 'max_length': _('Ensure this field has no more than {max_length} elements.')
} }
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True) self.allow_empty = kwargs.pop('allow_empty', True)
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
@ -1627,7 +1629,7 @@ class ListField(Field):
"Remove `source=` from the field declaration." "Remove `source=` from the field declaration."
) )
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
if self.max_length is not None: if self.max_length is not None:
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length) message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
@ -1692,7 +1694,7 @@ class DictField(Field):
'empty': _('This dictionary may not be empty.'), 'empty': _('This dictionary may not be empty.'),
} }
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True) self.allow_empty = kwargs.pop('allow_empty', True)
@ -1702,7 +1704,7 @@ class DictField(Field):
"Remove `source=` from the field declaration." "Remove `source=` from the field declaration."
) )
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
def get_value(self, dictionary): def get_value(self, dictionary):
@ -1751,8 +1753,8 @@ class DictField(Field):
class HStoreField(DictField): class HStoreField(DictField):
child = CharField(allow_blank=True, allow_null=True) child = CharField(allow_blank=True, allow_null=True)
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super().__init__(*args, **kwargs) super().__init__(**kwargs)
assert isinstance(self.child, CharField), ( assert isinstance(self.child, CharField), (
"The `child` argument must be an instance of `CharField`, " "The `child` argument must be an instance of `CharField`, "
"as the hstore extension stores values as strings." "as the hstore extension stores values as strings."
@ -1767,11 +1769,11 @@ class JSONField(Field):
# Workaround for isinstance calls when importing the field isn't possible # Workaround for isinstance calls when importing the field isn't possible
_is_jsonfield = True _is_jsonfield = True
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
self.binary = kwargs.pop('binary', False) self.binary = kwargs.pop('binary', False)
self.encoder = kwargs.pop('encoder', None) self.encoder = kwargs.pop('encoder', None)
self.decoder = kwargs.pop('decoder', None) self.decoder = kwargs.pop('decoder', None)
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def get_value(self, dictionary): def get_value(self, dictionary):
if html.is_html_input(dictionary) and self.field_name in dictionary: if html.is_html_input(dictionary) and self.field_name in dictionary:

View File

@ -1326,9 +1326,8 @@ class ModelSerializer(Serializer):
""" """
if extra_kwargs.get('read_only', False): if extra_kwargs.get('read_only', False):
for attr in [ for attr in [
'required', 'default', 'allow_blank', 'allow_null', 'required', 'default', 'allow_blank', 'min_length',
'min_length', 'max_length', 'min_value', 'max_value', 'max_length', 'min_value', 'max_value', 'validators', 'queryset'
'validators', 'queryset'
]: ]:
kwargs.pop(attr, None) kwargs.pop(attr, None)

View File

@ -2,6 +2,7 @@ import uuid
import warnings import warnings
import pytest import pytest
from django.db import models
from django.test import RequestFactory, TestCase, override_settings from django.test import RequestFactory, TestCase, override_settings
from django.urls import path from django.urls import path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -110,6 +111,24 @@ class TestFieldMapping(TestCase):
assert data['properties']['default_false']['default'] is False, "default must be false" assert data['properties']['default_false']['default'] is False, "default must be false"
assert 'default' not in data['properties']['without_default'], "default must not be defined" assert 'default' not in data['properties']['without_default'], "default must not be defined"
def test_nullable_fields(self):
class Model(models.Model):
rw_field = models.CharField(null=True)
ro_field = models.CharField(null=True)
class Serializer(serializers.ModelSerializer):
class Meta:
model = Model
fields = ["rw_field", "ro_field"]
read_only_fields = ["ro_field"]
inspector = AutoSchema()
data = inspector.map_serializer(Serializer())
assert data['properties']['rw_field']['nullable'], "rw_field nullable must be true"
assert data['properties']['ro_field']['nullable'], "ro_field nullable must be true"
assert data['properties']['ro_field']['readOnly'], "ro_field read_only must be true"
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.') @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')
class TestOperationIntrospection(TestCase): class TestOperationIntrospection(TestCase):

View File

@ -1163,6 +1163,30 @@ class TestMinMaxDecimalField(FieldValues):
) )
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
"""
Check that empty string ('', ' ') is acceptable value for the DecimalField
if allow_null=True and there are max/min validators
"""
valid_inputs = {
None: None,
'': None,
' ': None,
' ': None,
5: Decimal('5'),
'0': Decimal('0'),
'10': Decimal('10'),
}
invalid_inputs = {
-1: ['Ensure this value is greater than or equal to 0.'],
11: ['Ensure this value is less than or equal to 10.'],
}
outputs = {
None: '',
}
field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, min_value=0, max_value=10)
class TestNoMaxDigitsDecimalField(FieldValues): class TestNoMaxDigitsDecimalField(FieldValues):
field = serializers.DecimalField( field = serializers.DecimalField(
max_value=100, min_value=0, max_value=100, min_value=0,
@ -1986,6 +2010,11 @@ class TestListField(FieldValues):
field.to_internal_value(input_value) field.to_internal_value(input_value)
assert exc_info.value.detail == ['Expected a list of items but got type "dict".'] assert exc_info.value.detail == ['Expected a list of items but got type "dict".']
def test_constructor_misuse_raises(self):
# Test that `ListField` can only be instantiated with keyword arguments
with pytest.raises(TypeError):
serializers.ListField(serializers.CharField())
class TestNestedListField(FieldValues): class TestNestedListField(FieldValues):
""" """