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}
|
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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user