mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-27 08:29:59 +03:00
Merge branch 'master' into update_irc_server
This commit is contained in:
commit
c1c8b9f3de
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue
Block a user