From 74fec7eeb4e7e2e593ed5e2213020024264681ce Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 28 Jan 2014 14:30:46 +0000 Subject: [PATCH 01/26] Import force_bytes on django >= 1.5 --- rest_framework/compat.py | 2 +- rest_framework/tests/test_compat.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/test_compat.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b69749feb..d283e2f5d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -457,7 +457,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import FakePayload try: # In 1.5 the test client uses force_bytes - from django.utils.encoding import force_bytes_or_smart_bytes + from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes except ImportError: # In 1.3 and 1.4 the test client just uses smart_str from django.utils.encoding import smart_str as force_bytes_or_smart_bytes diff --git a/rest_framework/tests/test_compat.py b/rest_framework/tests/test_compat.py new file mode 100644 index 000000000..4916d19b8 --- /dev/null +++ b/rest_framework/tests/test_compat.py @@ -0,0 +1,13 @@ +import django +from django.test import TestCase + + +class TestCompat(TestCase): + def test_force_bytes_or_smart_bytes(self): + from rest_framework.compat import force_bytes_or_smart_bytes + if django.VERSION >= (1, 5): + from django.utils.encoding import force_bytes + self.assertEqual(force_bytes_or_smart_bytes, force_bytes) + else: + from django.utils.encoding import smart_str + self.assertEqual(force_bytes_or_smart_bytes, smart_str) From 78e4468f0367cc2a3a5cc6f3570a791ad67c90d9 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 28 Jan 2014 15:54:50 +0000 Subject: [PATCH 02/26] Add file upload test for APIRequestFactory Remove test_compat --- rest_framework/tests/test_compat.py | 13 ------------- rest_framework/tests/test_testing.py | 9 +++++++++ 2 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 rest_framework/tests/test_compat.py diff --git a/rest_framework/tests/test_compat.py b/rest_framework/tests/test_compat.py deleted file mode 100644 index 4916d19b8..000000000 --- a/rest_framework/tests/test_compat.py +++ /dev/null @@ -1,13 +0,0 @@ -import django -from django.test import TestCase - - -class TestCompat(TestCase): - def test_force_bytes_or_smart_bytes(self): - from rest_framework.compat import force_bytes_or_smart_bytes - if django.VERSION >= (1, 5): - from django.utils.encoding import force_bytes - self.assertEqual(force_bytes_or_smart_bytes, force_bytes) - else: - from django.utils.encoding import smart_str - self.assertEqual(force_bytes_or_smart_bytes, smart_str) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 48b8956b5..71bd8b552 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -1,6 +1,8 @@ # -- coding: utf-8 -- from __future__ import unicode_literals +from io import BytesIO + from django.contrib.auth.models import User from django.test import TestCase from rest_framework.compat import patterns, url @@ -143,3 +145,10 @@ class TestAPIRequestFactory(TestCase): force_authenticate(request, user=user) response = view(request) self.assertEqual(response.data['user'], 'example') + + def test_upload_file(self): + # This is a 1x1 black png + simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82') + simple_png.name = 'test.png' + factory = APIRequestFactory() + factory.post('/', data={'image': simple_png}) From 9f0ead95976c379957faf7d3f02eb52bf80a2e17 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 30 Jan 2014 17:32:05 +0000 Subject: [PATCH 03/26] Remove TODO note, since it hasn't been TODONE. --- rest_framework/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index e863af6dd..78010fa03 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -112,7 +112,6 @@ class APIView(View): @property def default_response_headers(self): - # TODO: deprecate? # TODO: Only vary by accept if multiple renderers return { 'Allow': ', '.join(self.allowed_methods), From 18f26ff5cc193726956d97f1a7d5ced5e6c0f4ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 30 Jan 2014 17:47:55 +0000 Subject: [PATCH 04/26] Only add 'Vary: Accept' header when there is more than one possible renderer. --- rest_framework/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 78010fa03..02a6e25a9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -112,11 +112,13 @@ class APIView(View): @property def default_response_headers(self): - # TODO: Only vary by accept if multiple renderers - return { + headers = { 'Allow': ', '.join(self.allowed_methods), - 'Vary': 'Accept' } + if len(self.renderer_classes) > 1: + headers['Vary'] = 'Accept' + return headers + def http_method_not_allowed(self, request, *args, **kwargs): """ From 0043f30cab86f50b61ce265635d503c8212848c4 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Fri, 31 Jan 2014 09:12:45 +0000 Subject: [PATCH 05/26] Use bytes BOUNDARY on django < 1.5 Django's encode_multipart was updated in django 1.5 to work internally with unicode and convert to bytes. In django >= 1.5 we therefore need to pass the BOUNDARY as unicode. In django < 1.5 we still need to pass it as bytes. --- rest_framework/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2fdd33376..e8afc26d7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals import copy import json +import django from django import forms from django.core.exceptions import ImproperlyConfigured from django.http.multipartparser import parse_header @@ -597,7 +598,7 @@ class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' format = 'multipart' charset = 'utf-8' - BOUNDARY = 'BoUnDaRyStRiNg' + BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg' def render(self, data, accepted_media_type=None, renderer_context=None): return encode_multipart(self.BOUNDARY, data) From 77ced39e6c02cd375ac6df0c00f78846ac5cbec5 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 31 Jan 2014 12:26:45 -0500 Subject: [PATCH 06/26] Fix doc for custom exception sample The way to provide a default detail for APIException is to define a `default_detail` attribute on the subclass. Defining a `detail` attribute without `default_detail` will not work, and will result in empty detail instead. --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 221df679d..4e8b823ce 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -94,7 +94,7 @@ For example, if your API relies on a third party service that may sometimes be u class ServiceUnavailable(APIException): status_code = 503 - detail = 'Service temporarily unavailable, try again later.' + default_detail = 'Service temporarily unavailable, try again later.' ## ParseError From e437854a44249806478c40f1e36244863e5ded3d Mon Sep 17 00:00:00 2001 From: meoooh Date: Sat, 1 Feb 2014 15:02:11 +0900 Subject: [PATCH 07/26] Update fields.md --- docs/api-guide/fields.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index c136509b3..0b50aa959 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -104,6 +104,7 @@ A serializer definition that looked like this: expired = serializers.Field(source='has_expired') class Meta: + model = Account fields = ('url', 'owner', 'name', 'expired') Would produce output similar to: From a33eb4177e78a65d9e07cec18198b48fa11acca1 Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Sun, 2 Feb 2014 21:21:08 -0800 Subject: [PATCH 08/26] fixed typo (wrong "its") --- docs/api-guide/fields.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0b50aa959..93f992e66 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -126,7 +126,7 @@ A field that supports both read and write operations. By itself `WritableField` ## ModelField -A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field. +A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to its associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field. The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))` @@ -308,7 +308,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. -The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation. +The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into its initial representation. ## Examples From 40b148a2e427ffbedbc04c47235f07bf98d7e520 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Mon, 3 Feb 2014 14:54:44 +0000 Subject: [PATCH 09/26] Viewsets docs typo The docstring in the example said "update" instead of "create". --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 4fdd9364d..23b16575f 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -225,7 +225,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope mixins.RetrieveModelMixin, viewsets.GenericViewSet): """ - A viewset that provides `retrieve`, `update`, and `list` actions. + A viewset that provides `retrieve`, `create`, and `list` actions. To use it, override the class and set the `.queryset` and `.serializer_class` attributes. From b182b9e246fb0c74801bea46b0d82e7384451165 Mon Sep 17 00:00:00 2001 From: juroe Date: Tue, 4 Feb 2014 11:56:41 +0100 Subject: [PATCH 10/26] Fixes typo (Implicit instead of Implict). --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 536b040bc..38b5089a8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -501,7 +501,7 @@ class BaseSerializer(WritableField): else: many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=3) @@ -563,7 +563,7 @@ class BaseSerializer(WritableField): else: many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=2) From ba8a0bac538adc8d6897ece14648e03ce7441b24 Mon Sep 17 00:00:00 2001 From: RicterZ Date: Tue, 4 Feb 2014 23:41:13 +0800 Subject: [PATCH 11/26] Fixed a bug backticks fix you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. to you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup. --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index e23b2c74f..fb927ea8b 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -119,7 +119,7 @@ For example: self.check_object_permissions(self.request, obj) return obj -Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. +Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup. #### `get_filter_backends(self)` From f8cda8adbd7db4cd60b1dbdcd4bb5debc64ba572 Mon Sep 17 00:00:00 2001 From: Matthew King Date: Sat, 1 Feb 2014 16:56:23 -0500 Subject: [PATCH 12/26] Generate random token directly --- rest_framework/authtoken/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 024f62bfe..8eac2cc49 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,5 +1,5 @@ -import uuid -import hmac +import binascii +import os from hashlib import sha1 from django.conf import settings from django.db import models @@ -34,8 +34,7 @@ class Token(models.Model): return super(Token, self).save(*args, **kwargs) def generate_key(self): - unique = uuid.uuid4() - return hmac.new(unique.bytes, digestmod=sha1).hexdigest() + return binascii.hexlify(os.urandom(20)) def __unicode__(self): return self.key From 41eb313e1c18051614809e2040e6ac8584936962 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Sun, 9 Feb 2014 01:01:05 +0400 Subject: [PATCH 13/26] update regex for matching URLs, fixes issue #1386 --- rest_framework/templatetags/rest_framework.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 83c046f99..7a70fd460 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -6,7 +6,7 @@ from django.utils.encoding import iri_to_uri from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe from rest_framework.compat import urlparse, force_text, six, smart_urlquote -import re, string +import re register = template.Library() @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@\[\]]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') @@ -211,7 +211,6 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru safe_input = isinstance(text, SafeData) words = word_split_re.split(force_text(text)) for i, word in enumerate(words): - match = None if '.' in word or '@' in word or ':' in word: # Deal with punctuation. lead, middle, trail = '', word, '' From 35f4908e48cc18e94be239f8065c95e87b2fb007 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Sun, 9 Feb 2014 02:46:25 +0400 Subject: [PATCH 14/26] issue #1386 * regex for matching URLs was rewritten * added unittests --- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/tests/test_templatetags.py | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 7a70fd460..8a0e11ba7 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@\[\]]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^\w[^@\[\]\:\/,]+\.(com|edu|gov|int|mil|net|org)(:\d{2,5})?(/(\w[^@\[\]\:\,]+)?)?$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 609a9e089..0c2259b99 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework.test import APIRequestFactory -from rest_framework.templatetags.rest_framework import add_query_param +from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links factory = APIRequestFactory() @@ -17,3 +17,39 @@ class TemplateTagTests(TestCase): json_url = add_query_param(request, "format", "json") self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) self.assertIn("format=json", json_url) + + +class Issue1386Tests(TestCase): + """ + Covers #1386 + """ + + def test_issue_1386(self): + """ + Test function urlize_quoted_links with different args + """ + correct_urls = [ + "asdf.com/zxvc", + "asdf.net", + "www.as_df.org", + "as.d8f.ghj8.gov", + "www.a-op.s.d.edu/asdf/dfff_908/", + "cd8fr.com:80/hello", + "cdfr.com:808/hello", + "cdfr.com:8080/hello", + "cdfr.com:44808/hello/asdf/", + ] + for i in correct_urls: + res = urlize_quoted_links(i) + self.assertGreater(len(res), len(i)) + self.assertIn(i, res) + + incorrect_urls = [ + "mailto://asdf@fdf.com", + "asdf://asdf.com", + "asdf.netnet", + "asdf:[/p]zxcv.com" # example from issue #1386 + ] + for i in incorrect_urls: + res = urlize_quoted_links(i) + self.assertEqual(i, res) \ No newline at end of file From 97b7c25987c3bfa096a084dc671fc24816b08f87 Mon Sep 17 00:00:00 2001 From: Hassan Shamim Date: Mon, 10 Feb 2014 12:54:56 -0600 Subject: [PATCH 15/26] Replace 'detail' with 'default_detail' in Exceptions guide and APIException class docstring. --- docs/api-guide/exceptions.md | 4 ++-- rest_framework/exceptions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 4e8b823ce..66e181737 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -18,7 +18,7 @@ The handled exceptions are: In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error. -By default all error responses will include a key `details` in the body of the response, but other keys may also be included. +By default all error responses will include a key `detail` in the body of the response, but other keys may also be included. For example, the following request: @@ -86,7 +86,7 @@ Note that the exception handler will only be called for responses generated by r The **base class** for all exceptions raised inside REST framework. -To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class. +To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class. For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so: diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 4276625af..0ac5866ef 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -12,7 +12,7 @@ import math class APIException(Exception): """ Base class for REST framework exceptions. - Subclasses should provide `.status_code` and `.detail` properties. + Subclasses should provide `.status_code` and `.default_detail` properties. """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = '' From 95670933d7954a99e02f0e19f285d8740ab5e449 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 11 Feb 2014 14:44:56 +0100 Subject: [PATCH 16/26] Test and quick fix for #1257 --- rest_framework/serializers.py | 1 + rest_framework/tests/test_serializer.py | 29 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 38b5089a8..10256d479 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -893,6 +893,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ + and field.required \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 75d6e7859..6b1e333e4 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -71,6 +71,15 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem +class ActionItemSerializerOptionalFields(serializers.ModelSerializer): + """ + Intended to test that fields with `required=False` are excluded from validation. + """ + title = serializers.CharField(required=False) + + class Meta: + model = ActionItem + fields = ('title',) class ActionItemSerializerCustomRestore(serializers.ModelSerializer): @@ -288,7 +297,13 @@ class BasicTests(TestCase): serializer.save() self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') - + def test_fields_marked_as_not_required_are_excluded_from_validation(self): + """ + Check that fields with `required=False` are included in list of exclusions. + """ + serializer = ActionItemSerializerOptionalFields(self.actionitem) + exclusions = serializer.get_validation_exclusions() + self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded') class DictStyleSerializer(serializers.Serializer): @@ -1808,14 +1823,14 @@ class SerializerDefaultTrueBoolean(TestCase): self.assertEqual(serializer.data['cat'], False) self.assertEqual(serializer.data['dog'], False) - + class BoolenFieldTypeTest(TestCase): ''' Ensure the various Boolean based model fields are rendered as the proper field type - + ''' - + def setUp(self): ''' Setup an ActionItemSerializer for BooleanTesting @@ -1831,11 +1846,11 @@ class BoolenFieldTypeTest(TestCase): ''' bfield = self.serializer.get_fields()['done'] self.assertEqual(type(bfield), fields.BooleanField) - + def test_nullbooleanfield_type(self): ''' - Test that BooleanField is infered from models.NullBooleanField - + Test that BooleanField is infered from models.NullBooleanField + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 ''' bfield = self.serializer.get_fields()['started'] From f1016441f5b9f6c95530d2ec306f90aa45762831 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 11 Feb 2014 19:52:32 +0100 Subject: [PATCH 17/26] Test and fix for #1210. World's lowest hanging fruit. --- rest_framework/fields.py | 3 ++- rest_framework/tests/test_fields.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2f475d6ea..05daaab76 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -477,7 +477,8 @@ class URLField(CharField): type_label = 'url' def __init__(self, **kwargs): - kwargs['validators'] = [validators.URLValidator()] + if not 'validators' in kwargs: + kwargs['validators'] = [validators.URLValidator()] super(URLField, self).__init__(**kwargs) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 5c96bce92..e127feef9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -860,7 +860,9 @@ class SlugFieldTests(TestCase): class URLFieldTests(TestCase): """ - Tests for URLField attribute values + Tests for URLField attribute values. + + (Includes test for #1210, checking that validators can be overridden.) """ class URLFieldModel(RESTFrameworkModel): @@ -902,6 +904,11 @@ class URLFieldTests(TestCase): self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20) + def test_validators_can_be_overridden(self): + url_field = serializers.URLField(validators=[]) + validators = url_field.validators + self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators') + class FieldMetadata(TestCase): def setUp(self): From 0cb08ac7076da05bec797144263c472f507958b6 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Wed, 12 Feb 2014 23:17:05 +0100 Subject: [PATCH 18/26] Fixed Testing docs section --- docs/topics/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 30d292f80..5a5d1a80b 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -60,7 +60,7 @@ To run the tests, clone the repository, and then: # Setup the virtual environment virtualenv env - env/bin/activate + source env/bin/activate pip install -r requirements.txt pip install -r optionals.txt From d00ea3bcac5d622c586b267d18aef4700657f269 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 18:59:05 +0400 Subject: [PATCH 19/26] change regex back, issue #1386 --- rest_framework/templatetags/rest_framework.py | 8 ++++++-- rest_framework/tests/test_templatetags.py | 12 +++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 8a0e11ba7..0fcc53463 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^\w[^@\[\]\:\/,]+\.(com|edu|gov|int|mil|net|org)(:\d{2,5})?(/(\w[^@\[\]\:\,]+)?)?$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') @@ -234,7 +234,11 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru if simple_url_re.match(middle): url = smart_urlquote(middle) elif simple_url_2_re.match(middle): - url = smart_urlquote('http://%s' % middle) + # ValueError("Invalid IPv6 URL") can be raised here, see issue #1386 + try: + url = smart_urlquote('http://%s' % middle) + except ValueError: + pass elif not ':' in middle and simple_email_re.match(middle): local, domain = middle.rsplit('@', 1) try: diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 0c2259b99..999c718ac 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -29,26 +29,20 @@ class Issue1386Tests(TestCase): Test function urlize_quoted_links with different args """ correct_urls = [ - "asdf.com/zxvc", + "asdf.com", "asdf.net", "www.as_df.org", "as.d8f.ghj8.gov", - "www.a-op.s.d.edu/asdf/dfff_908/", - "cd8fr.com:80/hello", - "cdfr.com:808/hello", - "cdfr.com:8080/hello", - "cdfr.com:44808/hello/asdf/", ] for i in correct_urls: res = urlize_quoted_links(i) - self.assertGreater(len(res), len(i)) + self.assertNotEqual(res, i) self.assertIn(i, res) incorrect_urls = [ "mailto://asdf@fdf.com", - "asdf://asdf.com", "asdf.netnet", - "asdf:[/p]zxcv.com" # example from issue #1386 + "asdf:[/p]zxcv.com", # example from issue #1386 ] for i in incorrect_urls: res = urlize_quoted_links(i) From 08ec23268dbb4a40000b6c4bf877f5563a4ba57b Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 19:39:53 +0400 Subject: [PATCH 20/26] (I hope) tests are fixed, issue #1386 --- rest_framework/tests/test_templatetags.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 999c718ac..d4da0c23b 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -42,8 +42,10 @@ class Issue1386Tests(TestCase): incorrect_urls = [ "mailto://asdf@fdf.com", "asdf.netnet", - "asdf:[/p]zxcv.com", # example from issue #1386 ] for i in incorrect_urls: res = urlize_quoted_links(i) - self.assertEqual(i, res) \ No newline at end of file + self.assertEqual(i, res) + + # example from issue #1386, this shouldn't raise an exception + _ = urlize_quoted_links("asdf:[/p]zxcv.com") From dbd993d108b51bebbf9fd8d567d1c782cf941404 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 20:14:47 +0400 Subject: [PATCH 21/26] wrapper for smart_urlquote, issue #1386 --- rest_framework/templatetags/rest_framework.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 0fcc53463..beb8c5b0e 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -189,6 +189,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net simple_email_re = re.compile(r'^\S+@\S+\.\S+$') +def smart_urlquote_wrapper(matched_url): + """ + Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can + be raised here, see issue #1386 + """ + try: + return smart_urlquote(matched_url) + except ValueError: + return None + + @register.filter def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True): """ @@ -232,13 +243,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru url = None nofollow_attr = ' rel="nofollow"' if nofollow else '' if simple_url_re.match(middle): - url = smart_urlquote(middle) + url = smart_urlquote_wrapper(middle) elif simple_url_2_re.match(middle): - # ValueError("Invalid IPv6 URL") can be raised here, see issue #1386 - try: - url = smart_urlquote('http://%s' % middle) - except ValueError: - pass + url = smart_urlquote_wrapper('http://%s' % middle) elif not ':' in middle and simple_email_re.match(middle): local, domain = middle.rsplit('@', 1) try: From 723d3a509bf196b1317205eb9bb425f84bbbb22b Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 21:24:29 +0400 Subject: [PATCH 22/26] credit, issue #1386, PR #1897 --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d4c00bc4e..f4a3e6555 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -182,6 +182,7 @@ The following people have helped make REST framework great. * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] * Philip Forget - [philipforget] +* Artem Mezhenin - [amezhenin] Many thanks to everyone who's contributed to the project. From 45d89b5d110939ecbbbbc03ee51bd8ea78cc41dd Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 21:25:35 +0400 Subject: [PATCH 23/26] credit, issue #1386, PR #1397 --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d4c00bc4e..f4a3e6555 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -182,6 +182,7 @@ The following people have helped make REST framework great. * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] * Philip Forget - [philipforget] +* Artem Mezhenin - [amezhenin] Many thanks to everyone who's contributed to the project. From aaa58852326ecf98785de853a71c8a6f19a0cb7e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Feb 2014 17:40:00 +0000 Subject: [PATCH 24/26] Update credits --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index f4a3e6555..5f0dc7522 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -401,3 +401,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ian-foote]: https://github.com/ian-foote [chuckharmston]: https://github.com/chuckharmston [philipforget]: https://github.com/philipforget +[amezhenin]: https://github.com/amezhenin From 821f8488023cb5161eb0f69b9121f6d956c39baf Mon Sep 17 00:00:00 2001 From: Vita Smid Date: Fri, 14 Feb 2014 10:44:02 +0100 Subject: [PATCH 25/26] Minor typos fixed in api-guide/testing.md (request -> response). --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 4a8a91682..72c339613 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -218,12 +218,12 @@ You can use any of REST framework's test case classes as you would for the regul When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. -For example, it's easier to inspect `request.data`: +For example, it's easier to inspect `response.data`: response = self.client.get('/users/4/') self.assertEqual(response.data, {'id': 4, 'username': 'lauren'}) -Instead of inspecting the result of parsing `request.content`: +Instead of inspecting the result of parsing `response.content`: response = self.client.get('/users/4/') self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) From 6f4c2c6f0536bdf596534c295e411e17be14aab7 Mon Sep 17 00:00:00 2001 From: Bo Peng Date: Fri, 14 Feb 2014 13:47:06 -0600 Subject: [PATCH 26/26] Update throttling.md Added comma to make DEFAULT_THROTTLE_CLASSES a tuple in example, for copy&paste to work nicely. --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index fc1525df6..b7c320f01 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -150,7 +150,7 @@ For example, given the following views... REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': ( - 'rest_framework.throttling.ScopedRateThrottle' + 'rest_framework.throttling.ScopedRateThrottle', ), 'DEFAULT_THROTTLE_RATES': { 'contacts': '1000/day',