From 5b071ab35e9f3b1d3f06e90741ded0e10e8a1651 Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Mon, 1 Aug 2016 13:05:47 +0200 Subject: [PATCH 01/11] Remove note about Django 1.3 (#4334) Remove note about Django 1.3 --- docs/api-guide/filtering.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 8664dcc8a..0ccd51dd3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -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. --- From e997713313c36808ac7052a218b253087622e179 Mon Sep 17 00:00:00 2001 From: jsurloppe Date: Mon, 1 Aug 2016 15:14:55 +0200 Subject: [PATCH 02/11] urljoin with leading slash remove part of path (#4332) --- rest_framework/schemas.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 41dc82da1..02960083c 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -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(), From aa349fe76729dbea1b8becf1846ce58c70871f35 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 16:14:26 +0100 Subject: [PATCH 03/11] Handle non-string input for IP fields (#4338) --- rest_framework/fields.py | 5 ++++- tests/test_fields.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 46d7ed09b..69cf9740b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -804,7 +804,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) diff --git a/tests/test_fields.py b/tests/test_fields.py index 1149cc4b3..24ff25588 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -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() From 46a44e52aa8a0eae82cc9c1e290a83ecf156f81a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 17:15:41 +0100 Subject: [PATCH 04/11] Quantize incoming digitals (#4339) --- rest_framework/fields.py | 5 +++-- tests/test_fields.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 69cf9740b..cbf12010c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -955,7 +955,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): """ @@ -1018,7 +1018,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... diff --git a/tests/test_fields.py b/tests/test_fields.py index 24ff25588..105a51973 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -912,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'), From 3ef3fee92627d832962d1e5aed02c19a3eaa554b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 18:44:58 +0100 Subject: [PATCH 05/11] Descriptive error from FileUploadParser when filename not included. (#4340) * Descriptive error from FileUploadParser when filename not included. * Consistent handling of upload filenames --- rest_framework/parsers.py | 15 +++++++++++---- tests/test_parsers.py | 25 ++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index ab74a6e58..238382364 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -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): """ diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 1e0f2e17f..f3af6817f 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -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() From 296e47a9f8f5303d7862b68db7277aa87886e8a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 10:23:56 +0100 Subject: [PATCH 06/11] Update from Django 1.10 beta to Django 1.10 (#4344) --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 89655aee2..1e8a7e5c4 100644 --- a/tox.ini +++ b/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 From e37619f7410bcfc472db56635aa573b33f83e92a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 13:05:12 +0100 Subject: [PATCH 07/11] Serializer defaults should not be included in partial updates. (#4346) Serializer default values should not be included in partial updates --- docs/api-guide/fields.md | 4 +++- rest_framework/fields.py | 3 ++- tests/test_serializer.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index a7ad4f70c..4b566d37e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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). diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cbf12010c..704dbaf3f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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'): diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 741c6ab17..4e9080909 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -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 == {} From 9f5e841daf8e086de2b2b90153356a52ce783283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Fleschenberg?= Date: Tue, 2 Aug 2016 14:11:41 +0200 Subject: [PATCH 08/11] Change template context generation in TemplateHTMLRenderer (#4236) - Change the name of ``resolve_context()`` to ``get_template_context()``. - Pass the renderer context to this method, to give subclasses more flexibility when overriding. --- rest_framework/renderers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e313998d1..ef7747eaf 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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 From bda16a518a03ca79c912c8b90adfce3626cf1069 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 13:33:14 +0100 Subject: [PATCH 09/11] Dedent tabs. (#4347) --- rest_framework/utils/formatting.py | 13 +++++++++++-- tests/test_description.py | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 67aabd3f1..ca5b33c5e 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -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() diff --git a/tests/test_description.py b/tests/test_description.py index 1683e106b..fcb88287b 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -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' From 5500b265bceb1d964725d3fd13f60429b834dbf4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 14:14:36 +0100 Subject: [PATCH 10/11] Test cases for DictField with allow_null options (#4348) --- tests/test_fields.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index 105a51973..92f4548e5 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1594,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): """ From a9a097496ec86d4b5e7db756fe51a39a57f5b370 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 14:33:15 +0100 Subject: [PATCH 11/11] extra_kwargs takes precedence over uniqueness kwargs (#4349) --- rest_framework/serializers.py | 5 ++--- tests/test_model_serializer.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d5e6b66ed..27c8cc229 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -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 diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 2cf6cb04c..b2d336d84 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -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)