mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-09 23:04:47 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
130c7714e3
|
@ -49,7 +49,9 @@ Defaults to `False`
|
|||
|
||||
### `default`
|
||||
|
||||
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behaviour is to not populate the attribute at all.
|
||||
|
||||
The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned.
|
||||
|
||||
May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context).
|
||||
|
||||
|
|
|
@ -241,7 +241,6 @@ For more details on using filter sets see the [django-filter documentation][djan
|
|||
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
|
||||
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
|
||||
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
|
||||
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -435,7 +435,8 @@ class Field(object):
|
|||
return `empty`, indicating that no value should be set in the
|
||||
validated data for this field.
|
||||
"""
|
||||
if self.default is empty:
|
||||
if self.default is empty or getattr(self.root, 'partial', False):
|
||||
# No default, or this is a partial update.
|
||||
raise SkipField()
|
||||
if callable(self.default):
|
||||
if hasattr(self.default, 'set_context'):
|
||||
|
@ -804,7 +805,10 @@ class IPAddressField(CharField):
|
|||
self.validators.extend(validators)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data and ':' in data:
|
||||
if not isinstance(data, six.string_types):
|
||||
self.fail('invalid', value=data)
|
||||
|
||||
if ':' in data:
|
||||
try:
|
||||
if self.protocol in ('both', 'ipv6'):
|
||||
return clean_ipv6_address(data, self.unpack_ipv4)
|
||||
|
@ -952,7 +956,7 @@ class DecimalField(Field):
|
|||
if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
|
||||
self.fail('invalid')
|
||||
|
||||
return self.validate_precision(value)
|
||||
return self.quantize(self.validate_precision(value))
|
||||
|
||||
def validate_precision(self, value):
|
||||
"""
|
||||
|
@ -1015,7 +1019,8 @@ class DecimalField(Field):
|
|||
context.prec = self.max_digits
|
||||
return value.quantize(
|
||||
decimal.Decimal('.1') ** self.decimal_places,
|
||||
context=context)
|
||||
context=context
|
||||
)
|
||||
|
||||
|
||||
# Date & time fields...
|
||||
|
|
|
@ -118,6 +118,10 @@ class FileUploadParser(BaseParser):
|
|||
Parser for file upload data.
|
||||
"""
|
||||
media_type = '*/*'
|
||||
errors = {
|
||||
'unhandled': 'FileUpload parse error - none of upload handlers can handle the stream',
|
||||
'no_filename': 'Missing filename. Request should include a Content-Disposition header with a filename parameter.',
|
||||
}
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
|
@ -134,6 +138,9 @@ class FileUploadParser(BaseParser):
|
|||
upload_handlers = request.upload_handlers
|
||||
filename = self.get_filename(stream, media_type, parser_context)
|
||||
|
||||
if not filename:
|
||||
raise ParseError(self.errors['no_filename'])
|
||||
|
||||
# Note that this code is extracted from Django's handling of
|
||||
# file uploads in MultiPartParser.
|
||||
content_type = meta.get('HTTP_CONTENT_TYPE',
|
||||
|
@ -146,7 +153,7 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
# See if the handler will want to take care of the parsing.
|
||||
for handler in upload_handlers:
|
||||
result = handler.handle_raw_input(None,
|
||||
result = handler.handle_raw_input(stream,
|
||||
meta,
|
||||
content_length,
|
||||
None,
|
||||
|
@ -178,10 +185,10 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
for index, handler in enumerate(upload_handlers):
|
||||
file_obj = handler.file_complete(counters[index])
|
||||
if file_obj:
|
||||
if file_obj is not None:
|
||||
return DataAndFiles({}, {'file': file_obj})
|
||||
raise ParseError("FileUpload parse error - "
|
||||
"none of upload handlers can handle the stream")
|
||||
|
||||
raise ParseError(self.errors['unhandled'])
|
||||
|
||||
def get_filename(self, stream, media_type, parser_context):
|
||||
"""
|
||||
|
|
|
@ -166,13 +166,14 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
template_names = self.get_template_names(response, view)
|
||||
template = self.resolve_template(template_names)
|
||||
|
||||
context = self.resolve_context(data, request, response)
|
||||
context = self.get_template_context(data, renderer_context)
|
||||
return template_render(template, context, request=request)
|
||||
|
||||
def resolve_template(self, template_names):
|
||||
return loader.select_template(template_names)
|
||||
|
||||
def resolve_context(self, data, request, response):
|
||||
def get_template_context(self, data, renderer_context):
|
||||
response = renderer_context['response']
|
||||
if response.exception:
|
||||
data['status_code'] = response.status_code
|
||||
return data
|
||||
|
|
|
@ -206,6 +206,9 @@ class SchemaGenerator(object):
|
|||
else:
|
||||
encoding = None
|
||||
|
||||
if self.url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=urlparse.urljoin(self.url, path),
|
||||
action=method.lower(),
|
||||
|
|
|
@ -1324,9 +1324,8 @@ class ModelSerializer(Serializer):
|
|||
# Update `extra_kwargs` with any new options.
|
||||
for key, value in uniqueness_extra_kwargs.items():
|
||||
if key in extra_kwargs:
|
||||
extra_kwargs[key].update(value)
|
||||
else:
|
||||
extra_kwargs[key] = value
|
||||
value.update(extra_kwargs[key])
|
||||
extra_kwargs[key] = value
|
||||
|
||||
return extra_kwargs, hidden_fields
|
||||
|
||||
|
|
|
@ -32,13 +32,22 @@ def dedent(content):
|
|||
unindented text on the initial line.
|
||||
"""
|
||||
content = force_text(content)
|
||||
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||
for line in content.splitlines()[1:] if line.lstrip()]
|
||||
whitespace_counts = [
|
||||
len(line) - len(line.lstrip(' '))
|
||||
for line in content.splitlines()[1:] if line.lstrip()
|
||||
]
|
||||
tab_counts = [
|
||||
len(line) - len(line.lstrip('\t'))
|
||||
for line in content.splitlines()[1:] if line.lstrip()
|
||||
]
|
||||
|
||||
# unindent the content if needed
|
||||
if whitespace_counts:
|
||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
elif tab_counts:
|
||||
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.test import TestCase
|
|||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from rest_framework.compat import apply_markdown
|
||||
from rest_framework.utils.formatting import dedent
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
|
@ -120,3 +121,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
|
||||
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
|
||||
self.assertTrue(gte_21_match or lt_21_match)
|
||||
|
||||
|
||||
def test_dedent_tabs():
|
||||
assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string'
|
||||
|
|
|
@ -663,6 +663,7 @@ class TestIPAddressField(FieldValues):
|
|||
'127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'],
|
||||
'2001:::9652': ['Enter a valid IPv4 or IPv6 address.'],
|
||||
'2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'],
|
||||
1000: ['Enter a valid IPv4 or IPv6 address.'],
|
||||
}
|
||||
outputs = {}
|
||||
field = serializers.IPAddressField()
|
||||
|
@ -911,6 +912,26 @@ class TestLocalizedDecimalField(TestCase):
|
|||
self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types))
|
||||
|
||||
|
||||
class TestQuantizedValueForDecimal(TestCase):
|
||||
def test_int_quantized_value_for_decimal(self):
|
||||
field = serializers.DecimalField(max_digits=4, decimal_places=2)
|
||||
value = field.to_internal_value(12).as_tuple()
|
||||
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
|
||||
self.assertEqual(value, expected_digit_tuple)
|
||||
|
||||
def test_string_quantized_value_for_decimal(self):
|
||||
field = serializers.DecimalField(max_digits=4, decimal_places=2)
|
||||
value = field.to_internal_value('12').as_tuple()
|
||||
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
|
||||
self.assertEqual(value, expected_digit_tuple)
|
||||
|
||||
def test_part_precision_string_quantized_value_for_decimal(self):
|
||||
field = serializers.DecimalField(max_digits=4, decimal_places=2)
|
||||
value = field.to_internal_value('12.0').as_tuple()
|
||||
expected_digit_tuple = (0, (1, 2, 0, 0), -2)
|
||||
self.assertEqual(value, expected_digit_tuple)
|
||||
|
||||
|
||||
class TestNoDecimalPlaces(FieldValues):
|
||||
valid_inputs = {
|
||||
'0.12345': Decimal('0.12345'),
|
||||
|
@ -1573,6 +1594,29 @@ class TestDictField(FieldValues):
|
|||
"Remove `source=` from the field declaration."
|
||||
)
|
||||
|
||||
def test_allow_null(self):
|
||||
"""
|
||||
If `allow_null=True` then `None` is a valid input.
|
||||
"""
|
||||
field = serializers.DictField(allow_null=True)
|
||||
output = field.run_validation(None)
|
||||
assert output is None
|
||||
|
||||
|
||||
class TestDictFieldWithNullChild(FieldValues):
|
||||
"""
|
||||
Values for `ListField` with allow_null CharField as child.
|
||||
"""
|
||||
valid_inputs = [
|
||||
({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
|
||||
]
|
||||
invalid_inputs = [
|
||||
]
|
||||
outputs = [
|
||||
({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
|
||||
]
|
||||
field = serializers.DictField(child=serializers.CharField(allow_null=True))
|
||||
|
||||
|
||||
class TestUnvalidatedDictField(FieldValues):
|
||||
"""
|
||||
|
|
|
@ -976,3 +976,22 @@ class TestModelFieldValues(TestCase):
|
|||
source = OneToOneSourceTestModel(target=target)
|
||||
serializer = ExampleSerializer(source)
|
||||
self.assertEqual(serializer.data, {'target': 1})
|
||||
|
||||
|
||||
class TestUniquenessOverride(TestCase):
|
||||
def test_required_not_overwritten(self):
|
||||
class TestModel(models.Model):
|
||||
field_1 = models.IntegerField(null=True)
|
||||
field_2 = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = (('field_1', 'field_2'),)
|
||||
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TestModel
|
||||
extra_kwargs = {'field_1': {'required': False}}
|
||||
|
||||
fields = TestSerializer().fields
|
||||
self.assertFalse(fields['field_1'].required)
|
||||
self.assertTrue(fields['field_2'].required)
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
from django import forms
|
||||
from django.core.files.uploadhandler import MemoryFileUploadHandler
|
||||
from django.core.files.uploadhandler import (
|
||||
MemoryFileUploadHandler, TemporaryFileUploadHandler
|
||||
)
|
||||
from django.test import TestCase
|
||||
from django.utils.six.moves import StringIO
|
||||
|
||||
|
@ -63,8 +66,9 @@ class TestFileUploadParser(TestCase):
|
|||
parser = FileUploadParser()
|
||||
self.stream.seek(0)
|
||||
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
|
||||
with self.assertRaises(ParseError):
|
||||
with pytest.raises(ParseError) as excinfo:
|
||||
parser.parse(self.stream, None, self.parser_context)
|
||||
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
|
||||
|
||||
def test_parse_missing_filename_multiple_upload_handlers(self):
|
||||
"""
|
||||
|
@ -78,8 +82,23 @@ class TestFileUploadParser(TestCase):
|
|||
MemoryFileUploadHandler()
|
||||
)
|
||||
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
|
||||
with self.assertRaises(ParseError):
|
||||
with pytest.raises(ParseError) as excinfo:
|
||||
parser.parse(self.stream, None, self.parser_context)
|
||||
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
|
||||
|
||||
def test_parse_missing_filename_large_file(self):
|
||||
"""
|
||||
Parse raw file upload when filename is missing with TemporaryFileUploadHandler.
|
||||
"""
|
||||
parser = FileUploadParser()
|
||||
self.stream.seek(0)
|
||||
self.parser_context['request'].upload_handlers = (
|
||||
TemporaryFileUploadHandler(),
|
||||
)
|
||||
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
|
||||
with pytest.raises(ParseError) as excinfo:
|
||||
parser.parse(self.stream, None, self.parser_context)
|
||||
assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.'
|
||||
|
||||
def test_get_filename(self):
|
||||
parser = FileUploadParser()
|
||||
|
|
|
@ -309,3 +309,31 @@ class TestCacheSerializerData:
|
|||
pickled = pickle.dumps(serializer.data)
|
||||
data = pickle.loads(pickled)
|
||||
assert data == {'field1': 'a', 'field2': 'b'}
|
||||
|
||||
|
||||
class TestDefaultInclusions:
|
||||
def setup(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
char = serializers.CharField(read_only=True, default='abc')
|
||||
integer = serializers.IntegerField()
|
||||
self.Serializer = ExampleSerializer
|
||||
|
||||
def test_default_should_included_on_create(self):
|
||||
serializer = self.Serializer(data={'integer': 456})
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {'char': 'abc', 'integer': 456}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_default_should_be_included_on_update(self):
|
||||
instance = MockObject(char='def', integer=123)
|
||||
serializer = self.Serializer(instance, data={'integer': 456})
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {'char': 'abc', 'integer': 456}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_default_should_not_be_included_on_partial_update(self):
|
||||
instance = MockObject(char='def', integer=123)
|
||||
serializer = self.Serializer(instance, data={'integer': 456}, partial=True)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == {'integer': 456}
|
||||
assert serializer.errors == {}
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -16,8 +16,8 @@ setenv =
|
|||
PYTHONWARNINGS=once
|
||||
deps =
|
||||
django18: Django==1.8.14
|
||||
django19: Django==1.9.8
|
||||
django110: Django==1.10rc1
|
||||
django19: Django==1.9.9
|
||||
django110: Django==1.10
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
|
Loading…
Reference in New Issue
Block a user