From 5b071ab35e9f3b1d3f06e90741ded0e10e8a1651 Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Mon, 1 Aug 2016 13:05:47 +0200 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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) From 54096dc22f255140f148906e85da5e4be0048f10 Mon Sep 17 00:00:00 2001 From: Corentin Smith Date: Thu, 4 Aug 2016 23:06:35 +0200 Subject: [PATCH 12/41] Add imports in validators docs (#4355) --- docs/api-guide/validators.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index a059f1197..f04c74c3c 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -64,6 +64,8 @@ It takes a single required argument, and an optional `messages` argument: This validator should be applied to *serializer fields*, like so: + from rest_framework.validators import UniqueValidator + slug = SlugField( max_length=100, validators=[UniqueValidator(queryset=BlogPost.objects.all())] @@ -80,6 +82,8 @@ It has two required arguments, and a single optional `messages` argument: The validator should be applied to *serializer classes*, like so: + from rest_framework.validators import UniqueTogetherValidator + class ExampleSerializer(serializers.Serializer): # ... class Meta: @@ -114,6 +118,8 @@ These validators can be used to enforce the `unique_for_date`, `unique_for_month The validator should be applied to *serializer classes*, like so: + from rest_framework.validators import UniqueForYearValidator + class ExampleSerializer(serializers.Serializer): # ... class Meta: @@ -183,7 +189,7 @@ It takes a single argument, which is the default value or callable that should b created_at = serializers.DateTimeField( read_only=True, - default=CreateOnlyDefault(timezone.now) + default=serializers.CreateOnlyDefault(timezone.now) ) --- From aff146ae83a64676cdcaf169832c29f4132cac6a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 10:23:40 +0100 Subject: [PATCH 13/41] Filter HEAD out from schemas (#4357) --- rest_framework/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 02960083c..b5d2e0254 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -167,7 +167,7 @@ class SchemaGenerator(object): return [ method for method in - callback.cls().allowed_methods if method != 'OPTIONS' + callback.cls().allowed_methods if method not in ('OPTIONS', 'HEAD') ] def get_key(self, path, method, callback): From 11a2468379496a4e9ed29d84d135825a927a5595 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 11:04:01 +0100 Subject: [PATCH 14/41] Access `request.user.is_authenticated` as property not method, under Django 1.10+ (#4358) * For Django >=1.10 use user.is_authenticated, not user.is_authenticated() --- rest_framework/compat.py | 6 ++++++ rest_framework/permissions.py | 9 ++++++--- rest_framework/throttling.py | 7 ++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 3143e7654..1ab1478f1 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -122,6 +122,12 @@ def _resolve_model(obj): raise ValueError("{0} is not a Django model".format(obj)) +def is_authenticated(user): + if django.VERSION < (1, 10): + return user.is_authenticated() + return user.is_authenticated + + def get_related_model(field): if django.VERSION < (1, 9): return _resolve_model(field.rel.to) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 8f5de0256..dd2d35ccd 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals from django.http import Http404 +from rest_framework.compat import is_authenticated + + SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') @@ -44,7 +47,7 @@ class IsAuthenticated(BasePermission): """ def has_permission(self, request, view): - return request.user and request.user.is_authenticated() + return request.user and is_authenticated(request.user) class IsAdminUser(BasePermission): @@ -65,7 +68,7 @@ class IsAuthenticatedOrReadOnly(BasePermission): return ( request.method in SAFE_METHODS or request.user and - request.user.is_authenticated() + is_authenticated(request.user) ) @@ -127,7 +130,7 @@ class DjangoModelPermissions(BasePermission): return ( request.user and - (request.user.is_authenticated() or not self.authenticated_users_only) and + (is_authenticated(request.user) or not self.authenticated_users_only) and request.user.has_perms(perms) ) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 1449f501b..57f24d13f 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -8,6 +8,7 @@ import time from django.core.cache import cache as default_cache from django.core.exceptions import ImproperlyConfigured +from rest_framework.compat import is_authenticated from rest_framework.settings import api_settings @@ -173,7 +174,7 @@ class AnonRateThrottle(SimpleRateThrottle): scope = 'anon' def get_cache_key(self, request, view): - if request.user.is_authenticated(): + if is_authenticated(request.user): return None # Only throttle unauthenticated requests. return self.cache_format % { @@ -193,7 +194,7 @@ class UserRateThrottle(SimpleRateThrottle): scope = 'user' def get_cache_key(self, request, view): - if request.user.is_authenticated(): + if is_authenticated(request.user): ident = request.user.pk else: ident = self.get_ident(request) @@ -241,7 +242,7 @@ class ScopedRateThrottle(SimpleRateThrottle): Otherwise generate the unique cache key by concatenating the user id with the '.throttle_scope` property of the view. """ - if request.user.is_authenticated(): + if is_authenticated(request.user): ident = request.user.pk else: ident = self.get_ident(request) From d5178c92462f6d18337fb0d85b6f230e812ed2fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 11:19:39 +0100 Subject: [PATCH 15/41] Include kwargs passed to 'as_view' when generating schemas (#4359) --- rest_framework/schemas.py | 2 ++ rest_framework/viewsets.py | 1 + tests/test_schemas.py | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index b5d2e0254..688deec88 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -195,6 +195,8 @@ class SchemaGenerator(object): Return a `coreapi.Link` instance for the given endpoint. """ view = callback.cls() + for attr, val in getattr(callback, 'initkwargs', {}).items(): + setattr(view, attr, val) fields = self.get_path_fields(path, method, callback, view) fields += self.get_serializer_fields(path, method, callback, view) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 7687448c4..2f440c567 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -97,6 +97,7 @@ class ViewSetMixin(object): # generation can pick out these bits of information from a # resolved URL. view.cls = cls + view.initkwargs = initkwargs view.suffix = initkwargs.get('suffix', None) view.actions = actions return csrf_exempt(view) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index a32b8a117..6c02c9d23 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -5,6 +5,7 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi +from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator @@ -27,12 +28,21 @@ class ExampleSerializer(serializers.Serializer): b = serializers.CharField(required=False) +class AnotherSerializer(serializers.Serializer): + c = serializers.CharField(required=True) + d = serializers.CharField(required=False) + + class ExampleViewSet(ModelViewSet): pagination_class = ExamplePagination permission_classes = [permissions.IsAuthenticatedOrReadOnly] filter_backends = [filters.OrderingFilter] serializer_class = ExampleSerializer + @detail_route(methods=['post'], serializer_class=AnotherSerializer) + def custom_action(self, request, pk): + return super(ExampleSerializer, self).retrieve(self, request) + class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -120,6 +130,16 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('pk', required=True, location='path') ] ), + 'custom_action': coreapi.Link( + url='/example/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + ), 'update': coreapi.Link( url='/example/{pk}/', action='put', From f9cf22edc88a19e83b70fc72de7844c948023f44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 12:38:19 +0100 Subject: [PATCH 16/41] Version 3.4.2 (#4360) --- docs/topics/release-notes.md | 47 ++++++++++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 025231ccf..7baff2277 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,24 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series +### 3.4.2 + +**Date**: [5th August 2016][3.4.2-milestone] + +Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) +Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) +Filter HEAD out from schemas. ([#4357][gh4357]) +extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) +Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347]) +Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) +Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) +Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) +DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) +Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) +Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) +Test cases for DictField with allow_null options. ([#4348][gh4348]) +Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) + ### 3.4.1 **Date**: [28th July 2016][3.4.1-milestone] @@ -514,6 +532,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22 [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 [3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 +[3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -943,3 +962,31 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4272]: https://github.com/tomchristie/django-rest-framework/issues/4272 [gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273 [gh4288]: https://github.com/tomchristie/django-rest-framework/issues/4288 + + +[gh3565]: https://github.com/tomchristie/django-rest-framework/issues/3565 +[gh3610]: https://github.com/tomchristie/django-rest-framework/issues/3610 +[gh4198]: https://github.com/tomchristie/django-rest-framework/issues/4198 +[gh4199]: https://github.com/tomchristie/django-rest-framework/issues/4199 +[gh4236]: https://github.com/tomchristie/django-rest-framework/issues/4236 +[gh4292]: https://github.com/tomchristie/django-rest-framework/issues/4292 +[gh4296]: https://github.com/tomchristie/django-rest-framework/issues/4296 +[gh4318]: https://github.com/tomchristie/django-rest-framework/issues/4318 +[gh4330]: https://github.com/tomchristie/django-rest-framework/issues/4330 +[gh4331]: https://github.com/tomchristie/django-rest-framework/issues/4331 +[gh4332]: https://github.com/tomchristie/django-rest-framework/issues/4332 +[gh4335]: https://github.com/tomchristie/django-rest-framework/issues/4335 +[gh4336]: https://github.com/tomchristie/django-rest-framework/issues/4336 +[gh4338]: https://github.com/tomchristie/django-rest-framework/issues/4338 +[gh4339]: https://github.com/tomchristie/django-rest-framework/issues/4339 +[gh4340]: https://github.com/tomchristie/django-rest-framework/issues/4340 +[gh4344]: https://github.com/tomchristie/django-rest-framework/issues/4344 +[gh4345]: https://github.com/tomchristie/django-rest-framework/issues/4345 +[gh4346]: https://github.com/tomchristie/django-rest-framework/issues/4346 +[gh4347]: https://github.com/tomchristie/django-rest-framework/issues/4347 +[gh4348]: https://github.com/tomchristie/django-rest-framework/issues/4348 +[gh4349]: https://github.com/tomchristie/django-rest-framework/issues/4349 +[gh4354]: https://github.com/tomchristie/django-rest-framework/issues/4354 +[gh4357]: https://github.com/tomchristie/django-rest-framework/issues/4357 +[gh4358]: https://github.com/tomchristie/django-rest-framework/issues/4358 +[gh4359]: https://github.com/tomchristie/django-rest-framework/issues/4359 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 5c61f8d7f..fb6da68ce 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.1' +__version__ = '3.4.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 35320b1f2d07fcc20b21f5ebc71629c1fc4ebb21 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 12:41:15 +0100 Subject: [PATCH 17/41] Add bullet points to release notes [ci skip] --- docs/topics/release-notes.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 7baff2277..692af57e9 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,19 +44,19 @@ You can determine your currently installed version using `pip freeze`: **Date**: [5th August 2016][3.4.2-milestone] -Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) -Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) -Filter HEAD out from schemas. ([#4357][gh4357]) -extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) -Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347]) -Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) -Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) -Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) -DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) -Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) -Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) -Test cases for DictField with allow_null options. ([#4348][gh4348]) -Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) +* Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) +* Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) +* Filter HEAD out from schemas. ([#4357][gh4357]) +* extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) +* Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347])* +* Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) +* Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) +* Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) +* DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) +* Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) +* Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) +* Test cases for DictField with allow_null options. ([#4348][gh4348]) +* Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) ### 3.4.1 From bb613c5ad19bb10a8e85a2bb3520bd4c4e6879a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 13:33:25 +0100 Subject: [PATCH 18/41] Version 3.4.3 (#4361) * Version 3.4.3 --- docs/topics/release-notes.md | 10 ++++++++++ rest_framework/__init__.py | 2 +- rest_framework/renderers.py | 11 +++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 692af57e9..6edbd2544 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series +### 3.4.3 + +**Date**: [5th August 2016][3.4.3-milestone] + +* Include fallaback for users of older TemplateHTMLRenderer internal API. ([#4361][gh4361]) + ### 3.4.2 **Date**: [5th August 2016][3.4.2-milestone] @@ -533,6 +539,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 [3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 [3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22 +[3.4.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.3+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -990,3 +997,6 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4357]: https://github.com/tomchristie/django-rest-framework/issues/4357 [gh4358]: https://github.com/tomchristie/django-rest-framework/issues/4358 [gh4359]: https://github.com/tomchristie/django-rest-framework/issues/4359 + + +[gh4361]: https://github.com/tomchristie/django-rest-framework/issues/4361 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fb6da68ce..19f83ecab 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.2' +__version__ = '3.4.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ef7747eaf..91d9e9072 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -166,7 +166,11 @@ class TemplateHTMLRenderer(BaseRenderer): template_names = self.get_template_names(response, view) template = self.resolve_template(template_names) - context = self.get_template_context(data, renderer_context) + if hasattr(self, 'resolve_context'): + # Fallback for older versions. + context = self.resolve_context(self, data, request, response) + else: + context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) def resolve_template(self, template_names): @@ -229,7 +233,10 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): if response and response.exception: request = renderer_context['request'] template = self.get_exception_template(response) - context = self.resolve_context(data, request, response) + if hasattr(self, 'resolve_context'): + context = self.resolve_context(data, request, response) + else: + context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) return data From 672e5a0f96c4e860f4eeee85f9cd26c8c4070d63 Mon Sep 17 00:00:00 2001 From: Marlon Date: Fri, 5 Aug 2016 11:57:43 -0700 Subject: [PATCH 19/41] Fix minor typo --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4b566d37e..f986f1508 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -488,7 +488,7 @@ This field is used by default with `ModelSerializer` when including field names **Signature**: `ReadOnlyField()` -For example, is `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`: +For example, if `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`: class AccountSerializer(serializers.ModelSerializer): class Meta: From febaa4db00c322d169a5e60ecd22c909e8a836c2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Aug 2016 09:28:15 +0100 Subject: [PATCH 20/41] Add import in docs. [ci skip] --- docs/api-guide/throttling.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 4eef4fd5d..51d2beef1 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -184,6 +184,8 @@ If the `.wait()` method is implemented and the request is throttled, then a `Ret The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. + import random + class RandomRateThrottle(throttling.BaseThrottle): def allow_request(self, request, view): return random.randint(1, 10) == 1 From e1768bdc161ee6c4fab422d0f47e54f9bf257ce2 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Mon, 8 Aug 2016 10:32:22 +0200 Subject: [PATCH 21/41] Fixed various typos (#4366) --- rest_framework/compat.py | 2 +- rest_framework/fields.py | 6 +++--- rest_framework/negotiation.py | 2 +- rest_framework/pagination.py | 16 ++++++++-------- rest_framework/serializers.py | 4 ++-- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/utils/field_mapping.py | 2 +- rest_framework/utils/html.py | 2 +- rest_framework/versioning.py | 4 ++-- rest_framework/viewsets.py | 2 +- tests/test_authentication.py | 2 +- tests/test_fields.py | 4 ++-- tests/test_filters.py | 6 +++--- tests/test_model_serializer.py | 2 +- tests/test_pagination.py | 6 +++--- tests/test_templatetags.py | 2 +- tests/test_testing.py | 2 +- 17 files changed, 33 insertions(+), 33 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 1ab1478f1..cee430a84 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -168,7 +168,7 @@ except ImportError: crispy_forms = None -# coreapi is optional (Note that uritemplate is a dependancy of coreapi) +# coreapi is optional (Note that uritemplate is a dependency of coreapi) try: import coreapi import uritemplate diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 704dbaf3f..b4346cd85 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -395,8 +395,8 @@ class Field(object): # determine if we should use null instead. return '' if getattr(self, 'allow_blank', False) else None elif ret == '' and not self.required: - # If the field is blank, and emptyness is valid then - # determine if we should use emptyness instead. + # If the field is blank, and emptiness is valid then + # determine if we should use emptiness instead. return '' if getattr(self, 'allow_blank', False) else empty return ret return dictionary.get(self.field_name, empty) @@ -1346,7 +1346,7 @@ class FilePathField(ChoiceField): def __init__(self, path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs): - # Defer to Django's FilePathField implmentation to get the + # Defer to Django's FilePathField implementation to get the # valid set of choices. field = DjangoFilePathField( path, match=match, recursive=recursive, allow_files=allow_files, diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 2a2b6f168..ca1b59f12 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -90,7 +90,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): def get_accept_list(self, request): """ - Given the incoming request, return a tokenised list of media + Given the incoming request, return a tokenized list of media type strings. """ header = request.META.get('HTTP_ACCEPT', '*/*') diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6ad10d860..77c10337e 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -64,10 +64,10 @@ def _get_displayed_page_numbers(current, final): This implementation gives one page to each side of the cursor, or two pages to the side when the cursor is at the edge, then - ensures that any breaks between non-continous page numbers never + ensures that any breaks between non-continuous page numbers never remove only a single page. - For an alernativative implementation which gives two pages to each side of + For an alternative implementation which gives two pages to each side of the cursor, eg. as in GitHub issue list pagination, see: https://gist.github.com/tomchristie/321140cebb1c4a558b15 @@ -476,10 +476,10 @@ class CursorPagination(BasePagination): # Determine the position of the final item following the page. if len(results) > len(self.page): - has_following_postion = True + has_following_position = True following_position = self._get_position_from_instance(results[-1], self.ordering) else: - has_following_postion = False + has_following_position = False following_position = None # If we have a reverse queryset, then the query ordering was in reverse @@ -490,14 +490,14 @@ class CursorPagination(BasePagination): if reverse: # Determine next and previous positions for reverse cursors. self.has_next = (current_position is not None) or (offset > 0) - self.has_previous = has_following_postion + self.has_previous = has_following_position if self.has_next: self.next_position = current_position if self.has_previous: self.previous_position = following_position else: # Determine next and previous positions for forward cursors. - self.has_next = has_following_postion + self.has_next = has_following_position self.has_previous = (current_position is not None) or (offset > 0) if self.has_next: self.next_position = following_position @@ -534,7 +534,7 @@ class CursorPagination(BasePagination): # our marker. break - # The item in this postion has the same position as the item + # The item in this position has the same position as the item # following it, we can't use it as a marker position, so increment # the offset and keep seeking to the previous item. compare = position @@ -582,7 +582,7 @@ class CursorPagination(BasePagination): # our marker. break - # The item in this postion has the same position as the item + # The item in this position has the same position as the item # following it, we can't use it as a marker position, so increment # the offset and keep seeking to the previous item. compare = position diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 27c8cc229..41412af8a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1383,7 +1383,7 @@ class ModelSerializer(Serializer): def get_unique_together_validators(self): """ - Determine a default set of validators for any unique_together contraints. + Determine a default set of validators for any unique_together constraints. """ model_class_inheritance_tree = ( [self.Meta.model] + @@ -1414,7 +1414,7 @@ class ModelSerializer(Serializer): def get_unique_for_date_validators(self): """ - Determine a default set of validators for the following contraints: + Determine a default set of validators for the following constraints: * unique_for_date * unique_for_month diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 05a7ce04f..225edcbee 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -189,7 +189,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru leading punctuation (opening parens) and it'll still do the right thing. If trim_url_limit is not None, the URLs in link text longer than this limit - will truncated to trim_url_limit-3 characters and appended with an elipsis. + will truncated to trim_url_limit-3 characters and appended with an ellipsis. If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 311d58317..64df9a08e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -1,6 +1,6 @@ """ Helper functions for mapping model fields to a dictionary of default -keyword arguments that should be used for their equivelent serializer fields. +keyword arguments that should be used for their equivalent serializer fields. """ import inspect diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index 121c825c7..ca062c9fc 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -14,7 +14,7 @@ def is_html_input(dictionary): def parse_html_list(dictionary, prefix=''): """ - Used to suport list values in HTML forms. + Used to support list values in HTML forms. Supports lists of primitives and/or dictionaries. * List of primitives. diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index 763a92b4c..f533ef580 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -94,7 +94,7 @@ class NamespaceVersioning(BaseVersioning): The difference is in the backend - this implementation uses Django's URL namespaces to determine the version. - An example URL conf that is namespaced into two seperate versions + An example URL conf that is namespaced into two separate versions # users/urls.py urlpatterns = [ @@ -147,7 +147,7 @@ class HostNameVersioning(BaseVersioning): invalid_version_message = _('Invalid version in hostname.') def determine_version(self, request, *args, **kwargs): - hostname, seperator, port = request.get_host().partition(':') + hostname, separator, port = request.get_host().partition(':') match = self.hostname_regex.match(hostname) if not match: return self.default_version diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 2f440c567..bd8333504 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -112,7 +112,7 @@ class ViewSetMixin(object): if method == 'options': # This is a special case as we always provide handling for the # options method in the base `View` class. - # Unlike the other explicitly defined actions, 'metadata' is implict. + # Unlike the other explicitly defined actions, 'metadata' is implicit. self.action = 'metadata' else: self.action = self.action_map.get(method) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 1f95396aa..5ef620abe 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -440,7 +440,7 @@ class FailingAuthAccessedInRenderer(TestCase): class NoAuthenticationClassesTests(TestCase): def test_permission_message_with_no_authentication_classes(self): """ - An unauthenticated request made against a view that containes no + An unauthenticated request made against a view that contains no `authentication_classes` but do contain `permissions_classes` the error code returned should be 403 with the exception's message. """ diff --git a/tests/test_fields.py b/tests/test_fields.py index 92f4548e5..60f02777a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -974,7 +974,7 @@ class TestDateField(FieldValues): class TestCustomInputFormatDateField(FieldValues): """ - Valid and invalid values for `DateField` with a cutom input format. + Valid and invalid values for `DateField` with a custom input format. """ valid_inputs = { '1 Jan 2001': datetime.date(2001, 1, 1), @@ -1041,7 +1041,7 @@ class TestDateTimeField(FieldValues): class TestCustomInputFormatDateTimeField(FieldValues): """ - Valid and invalid values for `DateTimeField` with a cutom input format. + Valid and invalid values for `DateTimeField` with a custom input format. """ valid_inputs = { '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()), diff --git a/tests/test_filters.py b/tests/test_filters.py index 175ae5b12..03d61fc37 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -711,7 +711,7 @@ class OrderingFilterTests(TestCase): serializer_class = OrderingFilterSerializer filter_backends = (filters.OrderingFilter,) ordering = ('title',) - oredering_fields = ('text',) + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('') @@ -819,7 +819,7 @@ class OrderingFilterTests(TestCase): queryset = OrderingFilterModel.objects.all() filter_backends = (filters.OrderingFilter,) ordering = ('title',) - # note: no ordering_fields and serializer_class speficied + # note: no ordering_fields and serializer_class specified def get_serializer_class(self): return OrderingFilterSerializer @@ -842,7 +842,7 @@ class OrderingFilterTests(TestCase): filter_backends = (filters.OrderingFilter,) ordering = ('title',) # note: no ordering_fields and serializer_class - # or get_serializer_class speficied + # or get_serializer_class specified view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'text'}) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index b2d336d84..a14972f04 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -136,7 +136,7 @@ class TestModelSerializer(TestCase): class TestRegularFieldMappings(TestCase): def test_regular_fields(self): """ - Model fields should map to their equivelent serializer fields. + Model fields should map to their equivalent serializer fields. """ class TestSerializer(serializers.ModelSerializer): class Meta: diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 4ea706429..f7feb4006 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -67,8 +67,8 @@ class TestPaginationIntegration: def test_setting_page_size_over_maximum(self): """ - When page_size parameter exceeds maxiumum allowable, - then it should be capped to the maxiumum. + When page_size parameter exceeds maximum allowable, + then it should be capped to the maximum. """ request = factory.get('/', {'page_size': 1000}) response = self.view(request) @@ -259,7 +259,7 @@ class TestPageNumberPaginationOverride: def setup(self): class OverriddenDjangoPaginator(DjangoPaginator): - # override the count in our overriden Django Paginator + # override the count in our overridden Django Paginator # we will only return one page, with one item count = 1 diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 746f51b7e..ac218df21 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -13,7 +13,7 @@ factory = APIRequestFactory() class TemplateTagTests(TestCase): - def test_add_query_param_with_non_latin_charactor(self): + def test_add_query_param_with_non_latin_character(self): # Ensure we don't double-escape non-latin characters # that are present in the querystring. # See #1314. diff --git a/tests/test_testing.py b/tests/test_testing.py index e6c8de22d..3adcc55f8 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -78,7 +78,7 @@ class TestAPITestClient(TestCase): response = self.client.get('/session-view/') self.assertEqual(response.data['active_session'], False) - # Subsequant requests have an active session + # Subsequent requests have an active session response = self.client.get('/session-view/') self.assertEqual(response.data['active_session'], True) From 0781182646e4b292e88ec46b74cc5b709c190753 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Aug 2016 17:48:29 +0100 Subject: [PATCH 22/41] Fix call to .resolve_context (#4371) --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 91d9e9072..371cd6ec7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -168,7 +168,7 @@ class TemplateHTMLRenderer(BaseRenderer): if hasattr(self, 'resolve_context'): # Fallback for older versions. - context = self.resolve_context(self, data, request, response) + context = self.resolve_context(data, request, response) else: context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) From 8105a4ac5abc9760ac5dea8e567f333feb6f8e2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Aug 2016 12:02:33 +0100 Subject: [PATCH 23/41] Resolve form display with ChoiceField, MultipleChoiceField and non-string choices. (#4374) * Add tests for html-form-rendering choice fields * Resolve issues with ChoiceField, MultipleChoiceField and non-string options * Ensure None template comparisons don't match string None --- .../horizontal/checkbox_multiple.html | 6 +- .../rest_framework/horizontal/radio.html | 6 +- .../rest_framework/horizontal/select.html | 4 +- .../horizontal/select_multiple.html | 4 +- .../inline/checkbox_multiple.html | 4 +- .../rest_framework/inline/radio.html | 3 +- .../rest_framework/inline/select.html | 4 +- .../inline/select_multiple.html | 3 +- .../vertical/checkbox_multiple.html | 6 +- .../rest_framework/vertical/radio.html | 5 +- .../rest_framework/vertical/select.html | 4 +- .../vertical/select_multiple.html | 3 +- rest_framework/templatetags/rest_framework.py | 15 ++++ rest_framework/utils/serializer_helpers.py | 2 +- tests/test_renderers.py | 87 +++++++++++++++++++ 15 files changed, 139 insertions(+), 17 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html index f01071297..7c7e57326 100644 --- a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %}