diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 954fb3bb9..f13b7ba94 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -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} @@ -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]. -[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 [html-and-forms]: ../topics/html-and-forms.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index f05fe7e7e..13c0c8710 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -605,13 +605,13 @@ For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. 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. 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. diff --git a/docs/index.md b/docs/index.md index 78c81c52e..641800b93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -186,7 +186,7 @@ Framework. ## 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/). @@ -257,7 +257,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [funding]: community/funding.md [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[botbot]: https://botbot.me/freenode/restframework/ [stack-overflow]: https://stackoverflow.com/ [django-rest-framework-tag]: https://stackoverflow.com/questions/tagged/django-rest-framework [security-mail]: mailto:rest-framework-security@googlegroups.com diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e4be54751..5cafed555 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -320,7 +320,7 @@ class Field: default_empty_html = empty 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, label=None, help_text=None, style=None, 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)) 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): """ Validate that the input is a decimal number and return a Decimal @@ -1063,9 +1068,6 @@ 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(): @@ -1161,14 +1163,14 @@ class DateTimeField(Field): } 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: self.format = format if input_formats is not None: self.input_formats = input_formats if default_timezone is not None: self.timezone = default_timezone - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def enforce_timezone(self, value): """ @@ -1247,12 +1249,12 @@ class DateField(Field): } 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: self.format = format if input_formats is not None: self.input_formats = input_formats - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) @@ -1313,12 +1315,12 @@ class TimeField(Field): } 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: self.format = format if input_formats is not None: self.input_formats = input_formats - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS) @@ -1468,9 +1470,9 @@ class MultipleChoiceField(ChoiceField): } default_empty_html = [] - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.allow_empty = kwargs.pop('allow_empty', True) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def get_value(self, 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}).'), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.max_length = kwargs.pop('max_length', None) self.allow_empty_file = kwargs.pop('allow_empty_file', False) if 'use_url' in kwargs: self.use_url = kwargs.pop('use_url') - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): try: @@ -1576,9 +1578,9 @@ class ImageField(FileField): ), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): # Image validation is a bit grungy, so we'll just outright @@ -1593,8 +1595,8 @@ class ImageField(FileField): # Composite field types... class _UnvalidatedField(Field): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.allow_blank = True self.allow_null = True @@ -1615,7 +1617,7 @@ class ListField(Field): '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.allow_empty = kwargs.pop('allow_empty', True) self.max_length = kwargs.pop('max_length', None) @@ -1627,7 +1629,7 @@ class ListField(Field): "Remove `source=` from the field declaration." ) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.child.bind(field_name='', parent=self) if self.max_length is not None: 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.'), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) @@ -1702,7 +1704,7 @@ class DictField(Field): "Remove `source=` from the field declaration." ) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.child.bind(field_name='', parent=self) def get_value(self, dictionary): @@ -1751,8 +1753,8 @@ class DictField(Field): class HStoreField(DictField): child = CharField(allow_blank=True, allow_null=True) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) assert isinstance(self.child, CharField), ( "The `child` argument must be an instance of `CharField`, " "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 _is_jsonfield = True - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.binary = kwargs.pop('binary', False) self.encoder = kwargs.pop('encoder', None) self.decoder = kwargs.pop('decoder', None) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def get_value(self, dictionary): if html.is_html_input(dictionary) and self.field_name in dictionary: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 49eec8259..9ea57f1af 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1326,9 +1326,8 @@ class ModelSerializer(Serializer): """ if extra_kwargs.get('read_only', False): for attr in [ - 'required', 'default', 'allow_blank', 'allow_null', - 'min_length', 'max_length', 'min_value', 'max_value', - 'validators', 'queryset' + 'required', 'default', 'allow_blank', 'min_length', + 'max_length', 'min_value', 'max_value', 'validators', 'queryset' ]: kwargs.pop(attr, None) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index aef20670e..daa035a3f 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -2,6 +2,7 @@ import uuid import warnings import pytest +from django.db import models from django.test import RequestFactory, TestCase, override_settings from django.urls import path 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 '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.') class TestOperationIntrospection(TestCase): diff --git a/tests/test_fields.py b/tests/test_fields.py index 78a9effb8..2d4cc44ae 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -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): field = serializers.DecimalField( max_value=100, min_value=0, @@ -1986,6 +2010,11 @@ class TestListField(FieldValues): field.to_internal_value(input_value) 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): """