From 735c75abb9e612fa0f1e6bdfa4283610752056d2 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Fri, 18 Oct 2013 21:10:49 -0400 Subject: [PATCH 001/236] add test case around ensuring proper field inference for boolean model field types --- rest_framework/tests/models.py | 1 + rest_framework/tests/test_serializer.py | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 1598ecd94..32a726c0b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel): class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) + started = models.NullBooleanField(default=False) done = models.BooleanField(default=False) info = CustomField(default='---', max_length=12) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a4749..a7ea3ac0b 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1720,3 +1720,35 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + +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 + ''' + data = { + 'title': 'b' * 201, + } + self.serializer = ActionItemSerializer(data=data) + + def test_booleanfield_type(self): + ''' + Test that BooleanField is infered from models.BooleanField + ''' + 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 + + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 + ''' + bfield = self.serializer.get_fields()['started'] + self.assertEqual(type(bfield), fields.BooleanField) From 17a00be830a3b8861be5014e94850f7a8f4ecd25 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Fri, 18 Oct 2013 21:13:20 -0400 Subject: [PATCH 002/236] This fix results in models.NullBooleanField rendering as a checkbox in the browsable API --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058b..f30f1fd8b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -606,6 +606,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, models.FileField: FileField, models.ImageField: ImageField, } From 7a87893b962d155f5e9d06bd0001a7f994419a2d Mon Sep 17 00:00:00 2001 From: Chris Guethle Date: Thu, 24 Oct 2013 09:40:43 -0500 Subject: [PATCH 003/236] reworked APIException, pushing some of the status_code and detail management up. Also, makes the APIException useful in isolation (defaults to status code 500) --- rest_framework/exceptions.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 425a72149..2bd21de3b 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -13,47 +13,40 @@ class APIException(Exception): Base class for REST framework exceptions. Subclasses should provide `.status_code` and `.detail` properties. """ - pass + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "" + + def __init__(self, detail=None, status_code=None): + self.status_code = status_code or self.status_code + self.detail = detail or self.default_detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = 'Malformed request.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Incorrect authentication credentials.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Authentication credentials were not provided.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = 'You do not have permission to perform this action.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED default_detail = "Method '%s' not allowed." def __init__(self, method, detail=None): - self.detail = (detail or self.default_detail) % method + super(MethodNotAllowed, self).__init__((detail or self.default_detail) % method) class NotAcceptable(APIException): @@ -61,7 +54,7 @@ class NotAcceptable(APIException): default_detail = "Could not satisfy the request's Accept header" def __init__(self, detail=None, available_renderers=None): - self.detail = detail or self.default_detail + super(NotAcceptable, self).__init__(detail) self.available_renderers = available_renderers @@ -70,19 +63,19 @@ class UnsupportedMediaType(APIException): default_detail = "Unsupported media type '%s' in request." def __init__(self, media_type, detail=None): - self.detail = (detail or self.default_detail) % media_type + super(UnsupportedMediaType, self).__init__((detail or self.default_detail) % media_type) class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS - default_detail = "Request was throttled." + default_detail = 'Request was throttled.' extra_detail = "Expected available in %d second%s." def __init__(self, wait=None, detail=None): + super(Throttled, self).__init__(detail) + import math self.wait = wait and math.ceil(wait) or None if wait is not None: - format = detail or self.default_detail + self.extra_detail - self.detail = format % (self.wait, self.wait != 1 and 's' or '') - else: - self.detail = detail or self.default_detail + format = self.detail + self.extra_detail + self.detail = format % (self.wait, self.wait != 1 and 's' or '') \ No newline at end of file From 5239362951956f130a1ef91d12d6b7d680220104 Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Thu, 14 Nov 2013 18:02:07 -0500 Subject: [PATCH 004/236] pass oauth_timestamp to oauth_provider --- rest_framework/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cf001a24d..bca542ebb 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -281,7 +281,8 @@ class OAuthAuthentication(BaseAuthentication): """ Checks nonce of request, and return True if valid. """ - return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + return oauth_provider_store.check_nonce(request, oauth_request, + oauth_request['oauth_nonce'], oauth_request['oauth_timestamp']) class OAuth2Authentication(BaseAuthentication): From 4b947a6a2af83592d1542d8984b9e5013efd0b1e Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Fri, 15 Nov 2013 12:18:22 -0500 Subject: [PATCH 005/236] update requirements for django-oauth-plus to 2.2.1 --- .travis.yml | 2 +- tox.ini | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index bcf1bae0e..18fe66abd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - pip install $DJANGO - pip install defusedxml==0.3 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" diff --git a/tox.ini b/tox.ini index 1fa0a958f..77766d20b 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ basepython = python2.7 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 @@ -32,7 +32,7 @@ basepython = python2.6 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 @@ -54,7 +54,7 @@ basepython = python2.7 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -64,7 +64,7 @@ basepython = python2.6 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -74,7 +74,7 @@ basepython = python2.7 deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -84,7 +84,7 @@ basepython = python2.6 deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -94,7 +94,7 @@ basepython = python2.7 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -104,7 +104,7 @@ basepython = python2.6 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 From b86765d9c02388f6bb82dbb5824a005e8fe73dec Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Fri, 15 Nov 2013 12:25:32 -0500 Subject: [PATCH 006/236] add auth param to request client calls --- rest_framework/tests/test_authentication.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index a44813b69..fe11423dc 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -362,7 +362,8 @@ class OAuthTests(TestCase): def test_post_form_with_urlencoded_parameters(self): """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @@ -424,7 +425,8 @@ class OAuthTests(TestCase): read_write_access_token.resource.is_readonly = False read_write_access_token.resource.save() params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth-with-scope/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') From ad7aa8fe485580e1bdff53d39fe3ea282d908a04 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 17 Nov 2013 01:27:16 +0100 Subject: [PATCH 007/236] =?UTF-8?q?Fixed=20the=20nested=20model=20serializ?= =?UTF-8?q?ers=20in=20case=20of=20the=20related=5Fname=20isn=E2=80=99t=20s?= =?UTF-8?q?et.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 2 +- .../tests/test_serializer_nested.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f0..e20e582ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -872,7 +872,7 @@ class ModelSerializer(Serializer): # Reverse fk or one-to-one relations for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: related_data[field_name] = attrs.pop(field_name) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 029f8bffd..7114a0603 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -6,6 +6,7 @@ Doesn't cover model serializers. from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers +from . import models class WritableNestedSerializerBasicTests(TestCase): @@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): serializer = self.AlbumSerializer(instance=original, data=data) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected) + + +class NestedModelSerializerUpdateTests(TestCase): + def test_second_nested_level(self): + john = models.Person.objects.create(name="john") + + post = john.blogpost_set.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.BlogPostComment + + class BlogPostSerializer(serializers.ModelSerializer): + comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') + class Meta: + model = models.BlogPost + fields = ('id', 'title', 'comments') + + class PersonSerializer(serializers.ModelSerializer): + posts = BlogPostSerializer(many=True, source='blogpost_set') + class Meta: + model = models.Person + fields = ('id', 'name', 'age', 'posts') + + serialize = PersonSerializer(instance=john) + deserialize = PersonSerializer(data=serialize.data, instance=john) + self.assertTrue(deserialize.is_valid()) + + result = deserialize.object + result.save() + self.assertEqual(result.id, john.id) + From a8b15f4290f4bad17d0dd599b8d5c29c155b89e5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 15:06:39 +0100 Subject: [PATCH 008/236] =?UTF-8?q?Another=20fix=20for=20nested=20writable?= =?UTF-8?q?=20serializers=20in=20case=20of=20the=20related=5Fname=20isn?= =?UTF-8?q?=E2=80=99t=20set=20on=20the=20ForeignKey.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e20e582ec..5059c71b3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -940,11 +940,12 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): + related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + fk_field = related_fields[accessor_name].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) From 263281d71d0425d7bb9b4ebbdf1811ef637ee60a Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Thu, 21 Nov 2013 20:09:48 +0000 Subject: [PATCH 009/236] Fix issue #1231: JSONEncoder doesn't handle dict-like objects Check for __getitem__ and then attempt to convert to a dict. The check for __getitem__ is there as there's no universal way to check if an object is a mapping type, but this is a likely proxy --- rest_framework/tests/test_renderers.py | 57 ++++++++++++++++++++++++++ rest_framework/utils/encoders.py | 6 +++ 2 files changed, 63 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..18da6ef85 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -18,6 +18,9 @@ from rest_framework.test import APIRequestFactory import datetime import pickle import re +import UserDict +import collections +import json DUMMYSTATUS = status.HTTP_200_OK @@ -244,6 +247,60 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') + def test_render_userdict_obj(self): + class DictLike(UserDict.DictMixin): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def keys(self): + return self._dict.keys() + x = DictLike() + x['a'] = 1 + x['b'] = "string value" + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + + def test_render_dict_abc_obj(self): + class Dict(collections.MutableMapping): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def __iter__(self): + return self._dict.__iter__() + def __len__(self): + return self._dict.__len__() + + x = Dict() + x['key'] = 'string value' + x[2] = 3 + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + + + def test_render_obj_with_getitem(self): + class DictLike(object): + def __init__(self): + self._dict = {} + def set(self, value): + self._dict = dict(value) + def __getitem__(self, key): + return self._dict[key] + + x = DictLike() + x.set({'a': 1, 'b': 'string'}) + with self.assertRaises(TypeError): + JSONRenderer().render(x) + def test_without_content_type_args(self): """ Test basic JSON rendering. diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35ad206b9..22b1ab3de 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -44,6 +44,12 @@ class JSONEncoder(json.JSONEncoder): return str(o) elif hasattr(o, 'tolist'): return o.tolist() + elif hasattr(o, '__getitem__'): + try: + return dict(o) + except KeyError: + # Couldn't convert to a dict, fall through + pass elif hasattr(o, '__iter__'): return [i for i in o] return super(JSONEncoder, self).default(o) From 6af31ed3945fd051a6e8c08851d7a656637d1f00 Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Fri, 22 Nov 2013 10:59:48 +0000 Subject: [PATCH 010/236] Remove u from literals --- rest_framework/tests/test_renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 18da6ef85..9c9c7452e 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -263,7 +263,7 @@ class JSONRendererTests(TestCase): x['a'] = 1 x['b'] = "string value" ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) def test_render_dict_abc_obj(self): class Dict(collections.MutableMapping): @@ -284,7 +284,7 @@ class JSONRendererTests(TestCase): x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): From a38d9d5b24501ae0e279c9afbea08e423112ba34 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 09:33:47 +0000 Subject: [PATCH 011/236] Add choices to options metadata for ChoiceField. --- rest_framework/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3b..80eff66c4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -514,6 +514,11 @@ class ChoiceField(WritableField): choices = property(_get_choices, _set_choices) + def metadata(self): + data = super(ChoiceField, self).metadata() + data['choices'] = self.choices + return data + def validate(self, value): """ Validates that the input is in self.choices. From 2484fc914159571a3867c2dae2d9b51314f4581d Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 17:10:16 +0000 Subject: [PATCH 012/236] Add more context to the ChoiceField metadata. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 80eff66c4..1657e57f3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = self.choices + data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] return data def validate(self, value): From 8d09f56061a3ee82e31fb646cfa84484ae525f88 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Wed, 27 Nov 2013 11:00:15 +0000 Subject: [PATCH 013/236] Add unittests for ChoiceField metadata. Rename 'name' to 'display_name'. --- rest_framework/fields.py | 2 +- rest_framework/tests/test_fields.py | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1657e57f3..0fca718e7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] + data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices] return data def validate(self, value): diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ab2cceacd..5c96bce92 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) def test_invalid_choice_model(self): - s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): """ - Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + Test that the 'empty' value is correctly passed and used depending on + the 'null' property on the model field. """ - s = ChoiceFieldModelSerializer(data={'choice' : ''}) + s = ChoiceFieldModelSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], '') - s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + s = ChoiceFieldModelWithNullSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], None) @@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.from_native(''), None) self.assertEqual(f.from_native(None), None) + def test_metadata_choices(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES] + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + + def test_metadata_choices_not_required(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} + for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES] + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + class EmailFieldTests(TestCase): """ From 2dce8d7a8a0e64f84994b6ac437e2d96920f094e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:23:49 +0200 Subject: [PATCH 014/236] Recommend using Pillow instead of PIL. --- docs/api-guide/fields.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 03c5af32a..b0dedd394 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires the `PIL` package. +Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. Signature and validation is the same as with `FileField`. @@ -345,3 +345,4 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime +[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html From b8f8fb7779dc01b5117e468345aaf99304f807ac Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:26:49 +0200 Subject: [PATCH 015/236] Updated the assertion message of the ImageField. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3b..463d296f6 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -966,7 +966,7 @@ class ImageField(FileField): return None from rest_framework.compat import Image - assert Image is not None, 'PIL must be installed for ImageField support' + assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.' # We need to get a file object for PIL. We might have a path or we might # have to read the data into memory. From c46106c96158a99eb2ff29c464a2fa60aff23122 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 14:47:37 +0200 Subject: [PATCH 016/236] Rephrased documentation changes according to feedback on IRC. --- docs/api-guide/fields.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b0dedd394..e05c03061 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. +Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained. Signature and validation is the same as with `FileField`. @@ -345,4 +345,3 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime -[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html From 850cd83ba709e863598f8eec3d6551ef3bc3801c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 2 Dec 2013 11:44:04 +0100 Subject: [PATCH 017/236] Fix TemplateHTMLRenderer example --- docs/api-guide/renderers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 858e2f072..f30fa26a4 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -167,14 +167,14 @@ The template name is determined by (in order of preference): An example of a view that uses `TemplateHTMLRenderer`: - class UserDetail(generics.RetrieveUserAPIView): + class UserDetail(generics.RetrieveAPIView): """ A view that returns a templated HTML representations of a given user. """ queryset = User.objects.all() renderer_classes = (TemplateHTMLRenderer,) - def get(self, request, *args, **kwargs) + def get(self, request, *args, **kwargs): self.object = self.get_object() return Response({'user': self.object}, template_name='user_detail.html') From 699ec7236b326c97a98c6058280b822c701393fe Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Tue, 3 Dec 2013 00:07:41 +0000 Subject: [PATCH 018/236] Adds pre_delete and post_delete hooks on --- docs/api-guide/generic-views.md | 4 +++- rest_framework/generics.py | 12 ++++++++++++ rest_framework/mixins.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index b92427244..83c3e45f6 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -163,12 +163,14 @@ For example: return 20 return 100 -**Save hooks**: +**Save / deletion hooks**: The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes. * `pre_save(self, obj)` - A hook that is called before saving an object. * `post_save(self, obj, created=False)` - A hook that is called after saving an object. +* `pre_delete(self, obj)` - A hook that is called before deleting an object. +* `post_delete(self, obj)` - A hook that is called after deleting an object. The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7cb80a84c..fd411ad38 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -344,6 +344,18 @@ class GenericAPIView(views.APIView): """ pass + def pre_delete(self, obj): + """ + Placeholder method for calling before deleting an object. + """ + pass + + def post_delete(self, obj): + """ + Placeholder method for calling after saving an object. + """ + pass + def metadata(self, request): """ Return a dictionary of metadata about the view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 79f79c30c..43950c4bf 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -192,5 +192,7 @@ class DestroyModelMixin(object): """ def destroy(self, request, *args, **kwargs): obj = self.get_object() + self.pre_delete(obj) obj.delete() + self.post_delete(obj) return Response(status=status.HTTP_204_NO_CONTENT) From fe4c7d4000675a1720e7d3e02c3014a693d835aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:26:58 +0000 Subject: [PATCH 019/236] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0759bd9d1..c7e24a5ee 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 From c1d9a96df03ea81454d7d0e3583c68e270ed5043 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:58:05 +0000 Subject: [PATCH 020/236] Catch errors during parsing and set empty .DATA/.FILES before re-raising. --- rest_framework/request.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index b883d0d4f..9b551aa80 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -356,7 +356,16 @@ class Request(object): if not parser: raise exceptions.UnsupportedMediaType(media_type) - parsed = parser.parse(stream, media_type, self.parser_context) + try: + parsed = parser.parse(stream, media_type, self.parser_context) + except: + # If we get an exception during parsing, fill in empty data and + # re-raise. Ensures we don't simply repeat the error when + # attempting to render the browsable renderer response, or when + # logging the request or similar. + self._data = QueryDict('', self._request._encoding) + self._files = MultiValueDict() + raise # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. From b92c911cf66805f7826713c68f4e6704b2ad4589 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:05:19 +0000 Subject: [PATCH 021/236] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c7e24a5ee..f9ad55f76 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. From 9f1918e41e1b8dcfa621b00788bab865f2fc31aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:06:57 +0000 Subject: [PATCH 022/236] Added @ian-foote, for work on #1250. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6c9c034f..3395cd9e2 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -179,6 +179,7 @@ The following people have helped make REST framework great. * Yamila Moreno - [yamila-moreno] * Rob Hudson - [robhudson] * Alex Good - [alexjg] +* Ian Foote - [ian-foote] Many thanks to everyone who's contributed to the project. @@ -394,3 +395,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [yamila-moreno]: https://github.com/yamila-moreno [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg +[ian-foote]: https://github.com/ian-foote From 774298f145d18292b76f2bd90469e25c1950b1af Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:18:35 +0000 Subject: [PATCH 023/236] First pass at a test for ParseErrors breaking the browsable API --- rest_framework/tests/test_renderers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..549e763b1 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -88,6 +88,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -219,6 +220,12 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_parse_error_renderers_browsable_api(self): + """Invalid data should still render the browsable API correctly.""" + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') + self.assertContains(resp.content, 'Mock GET View') + self.assertEqual(resp.status_code, status.HTTP_400_) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 38d78b21c0a7c68c205ebe6e79433ca51fe609ce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:55:11 +0000 Subject: [PATCH 024/236] Remove Content-Type header from empty responses. Fixes #1196 --- docs/topics/release-notes.md | 1 + rest_framework/response.py | 4 ++++ rest_framework/tests/test_renderers.py | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f9ad55f76..e6085f592 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3e..1dc6abcf6 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse): assert charset, 'renderer returned unicode, and did not specify ' \ 'a charset value.' return bytes(ret.encode(charset)) + + if not ret: + del self['Content-Type'] + return ret @property diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..f7de8fd72 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -64,11 +64,16 @@ class MockView(APIView): class MockGETView(APIView): - def get(self, request, **kwargs): return Response({'foo': ['bar', 'baz']}) +class EmptyGETView(APIView): + renderer_classes = (JSONRenderer,) + + def get(self, request, **kwargs): + return Response(status=status.HTTP_204_NO_CONTENT) + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -90,6 +95,7 @@ urlpatterns = patterns('', url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^empty$', EmptyGETView.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) ) @@ -219,6 +225,16 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_204_no_content_responses_have_no_content_type_set(self): + """ + Regression test for #1196 + + https://github.com/tomchristie/django-rest-framework/issues/1196 + """ + resp = self.client.get('/empty') + self.assertEqual(resp.get('Content-Type', None), None) + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 3c3906e278d5e707ab1fd72bdbcb79649777df33 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 08:51:34 +0000 Subject: [PATCH 025/236] Clarify wording, fixes #1133. --- 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 1062cb32c..4fdd9364d 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, #### Example -Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: class AccountViewSet(viewsets.ModelViewSet): """ From de5b9e39dd4c21f4dcceb7cf13c7366b0fb74ec9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 14:59:09 +0000 Subject: [PATCH 026/236] First pass on contribution guide --- docs/img/travis-status.png | Bin 0 -> 10023 bytes docs/index.md | 1 + docs/template.html | 1 + docs/topics/contributing.md | 91 ++++++++++++++++++++++++------------ 4 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 docs/img/travis-status.png diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png new file mode 100644 index 0000000000000000000000000000000000000000..fec98cf9b2ba728a532df7b50b89f505053e9012 GIT binary patch literal 10023 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 zm{-Browser enhancements
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • +
  • Contributing to REST framework
  • 2.0 Announcement
  • 2.2 Announcement
  • 2.3 Announcement
  • diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 123e4a8a1..2b18c4f68 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -6,19 +6,27 @@ There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. -# Community +## Community -If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users. +The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter. +If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +## Code of conduct + +Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. + +Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. + +The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. + # Issues -It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. +It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. Some tips on good issue reporting: @@ -26,30 +34,61 @@ Some tips on good issue reporting: * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +## Triaging issues +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to -* TODO: Triage +* Read through the ticket - does it make sense, is it missing any context that would help explain it better? +* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? +* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? +* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? # Development +To start developing on Django REST framework, clone the repo: -* git clone & PYTHONPATH -* Pep8 -* Recommend editor that runs pep8 + git clone git@github.com:tomchristie/django-rest-framework.git -### Pull requests +Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. -* Make pull requests early -* Describe branching +## Testing -### Managing compatibility issues +To run the tests, clone the repository, and then: -* Describe compat module + # Setup the virtual environment + virtualenv env + env/bin/activate + pip install -r requirements.txt + pip install -r optionals.txt -# Testing + # Run the tests + rest_framework/runtests/runtests.py -* Running the tests -* tox +You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: + + tox + +## Pull requests + +It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. + +It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. + +It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. + +GitHub's documentation for working on pull requests is [available here][pull-requests]. + +Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. + +Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. + +![Travis status][travis-status] + +*Above: Travis build notifications* + +## Managing compatibility issues + +Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. # Documentation @@ -77,7 +116,7 @@ Some other tips: * Keep paragraphs reasonably short. * Use double spacing after the end of sentences. -* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'. +* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. ## Markdown style @@ -118,25 +157,19 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- - **Note:** Make sure you do this thing. + **Note:** A useful documentation note. --- -# Third party packages - -* Django reusable app - -# Core committers - -* Still use pull reqs -* Credits - [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html -[github]: https://github.com/tomchristie/django-rest-framework -[django-packages]: https://www.djangopackages.com/grids/g/api/ +[code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [so-filter]: http://stackexchange.com/filters/66475/rest-framework [issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open +[pep-8]: http://www.python.org/dev/peps/pep-0008/ +[travis-status]: ../img/travis-status.png +[pull-requests]: https://help.github.com/articles/using-pull-requests +[tox]: http://tox.readthedocs.org/en/latest/ [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ From f2682537e0fa91bb415be1a64e6bc85275129141 Mon Sep 17 00:00:00 2001 From: Drew Kowalik Date: Wed, 4 Dec 2013 16:10:05 -0800 Subject: [PATCH 027/236] fix broken documentation links --- docs/api-guide/views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 15581e098..194a7a6b3 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html -[settings]: api-guide/settings.md -[throttling]: api-guide/throttling.md +[settings]: settings.md +[throttling]: throttling.md From f8088bedef04c5bc487bdc764ac54d1f18f42c26 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 09:01:00 +0000 Subject: [PATCH 028/236] Upgrade JSONP security warning. --- docs/api-guide/renderers.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index f30fa26a4..cf2005691 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`. -**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. +--- + +**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. + +The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions. + +--- **.media_type**: `application/javascript` @@ -419,6 +425,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [cors]: http://www.w3.org/TR/cors/ [cors-docs]: ../topics/ajax-csrf-cors.md +[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use [testing]: testing.md [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven From 1f8069c0a9740297e7b5d5fa0c81830c876d7240 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 11:05:25 +0000 Subject: [PATCH 029/236] Boilerplate cuteness --- rest_framework/__init__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index de82fef51..b6a4d3a04 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,6 +1,20 @@ -__version__ = '2.3.9' +""" +______ _____ _____ _____ __ _ +| ___ \ ___/ ___|_ _| / _| | | +| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ +| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / +| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < +\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| +""" -VERSION = __version__ # synonym +__title__ = 'Django REST framework' +__version__ = '2.3.9' +__author__ = 'Tom Christie' +__license__ = 'BSD 2-Clause' +__copyright__ = 'Copyright 2011-2013 Tom Christie' + +# Version synonym +VERSION = __version__ # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' From 79596dc613bbf24aac7b5c56179cbc5c46eacdf3 Mon Sep 17 00:00:00 2001 From: Justin Davis Date: Thu, 5 Dec 2013 13:17:23 -0800 Subject: [PATCH 030/236] fix setup.py with new __init__.py boilerplate --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 26d072837..1a487f178 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def get_version(package): Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() - return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) + return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_packages(package): From cf6c11bd4b7e7fdaa1de659d69792030e565412a Mon Sep 17 00:00:00 2001 From: Chuck Harmston Date: Fri, 6 Dec 2013 14:00:23 -0600 Subject: [PATCH 031/236] Raise appropriate error in serializer when making a partial update to set a required RelatedField to null (issue #1158) --- rest_framework/serializers.py | 5 ++++- rest_framework/tests/test_serializer.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f0..44e4b04b1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -896,7 +896,10 @@ class ModelSerializer(Serializer): # Update an existing instance... if instance is not None: for key, val in attrs.items(): - setattr(instance, key, val) + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # ...or create a new instance else: diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a4749..eca467ee2 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -558,6 +558,29 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + def test_foreign_key_is_null_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test that a null foreign key does not pass validation + """ + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) + self.assertFalse(photo_serializer.is_valid()) + self.assertTrue('album' in photo_serializer.errors) + self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + def test_foreign_key_with_partial(self): """ Test ModelSerializer validation with partial=True From 51359e461299905c6cd359000941f9da3d561f7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:42:52 +0000 Subject: [PATCH 032/236] Added @chuckharmston for kickass bug squashing in #1272 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 3395cd9e2..1a838421d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -180,6 +180,7 @@ The following people have helped make REST framework great. * Rob Hudson - [robhudson] * Alex Good - [alexjg] * Ian Foote - [ian-foote] +* Chuck Harmston - [chuckharmston] Many thanks to everyone who's contributed to the project. @@ -396,3 +397,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg [ian-foote]: https://github.com/ian-foote +[chuckharmston]: https://github.com/chuckharmston From 85d9eb0f7ed3ef66a25a443b34ead914a506462c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:47:26 +0000 Subject: [PATCH 033/236] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e6085f592..2df2cf931 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. From 910de38a9c8cd03243e738c8f4adcbade8a4d7d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 22:13:50 +0000 Subject: [PATCH 034/236] Version 2.3.10 --- docs/api-guide/status-codes.md | 21 ++++++++++++++++++ docs/topics/release-notes.md | 5 ++++- rest_framework/__init__.py | 2 +- rest_framework/status.py | 17 +++++++++++++++ rest_framework/tests/test_status.py | 33 +++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_status.py diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index 409f659b2..64c464349 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc The full set of HTTP status codes included in the `status` module is listed below. +The module also includes a set of helper functions for testing if a status code is in a given range. + + from rest_framework import status + from rest_framework.test import APITestCase + + class ExampleTestCase(APITestCase): + def test_url_root(self): + url = reverse('index') + response = self.client.get(url) + self.assertTrue(status.is_success(response.status_code)) + + For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616] and [RFC 6585][rfc6585]. @@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s HTTP_505_HTTP_VERSION_NOT_SUPPORTED HTTP_511_NETWORK_AUTHENTICATION_REQUIRED +## Helper functions + +The following helper functions are available for identifying the category of the response code. + + is_informational() # 1xx + is_success() # 2xx + is_redirect() # 3xx + is_client_error() # 4xx + is_server_error() # 5xx [rfc2324]: http://www.ietf.org/rfc/rfc2324.txt [rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 2df2cf931..b080ad436 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,10 +40,13 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.10 + +**Date**: 6th December 2013 * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Added status code category helper functions. * Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b6a4d3a04..f5483b9d6 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.9' +__version__ = '2.3.10' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2013 Tom Christie' diff --git a/rest_framework/status.py b/rest_framework/status.py index b9f249f9f..764353711 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585 """ from __future__ import unicode_literals + +def is_informational(code): + return code >= 100 and code <= 199 + +def is_success(code): + return code >= 200 and code <= 299 + +def is_redirect(code): + return code >= 300 and code <= 399 + +def is_client_error(code): + return code >= 400 and code <= 499 + +def is_server_error(code): + return code >= 500 and code <= 599 + + HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py new file mode 100644 index 000000000..7b1bdae31 --- /dev/null +++ b/rest_framework/tests/test_status.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.status import ( + is_informational, is_success, is_redirect, is_client_error, is_server_error +) + + +class TestStatus(TestCase): + def test_status_categories(self): + self.assertFalse(is_informational(99)) + self.assertTrue(is_informational(100)) + self.assertTrue(is_informational(199)) + self.assertFalse(is_informational(200)) + + self.assertFalse(is_success(199)) + self.assertTrue(is_success(200)) + self.assertTrue(is_success(299)) + self.assertFalse(is_success(300)) + + self.assertFalse(is_redirect(299)) + self.assertTrue(is_redirect(300)) + self.assertTrue(is_redirect(399)) + self.assertFalse(is_redirect(400)) + + self.assertFalse(is_client_error(399)) + self.assertTrue(is_client_error(400)) + self.assertTrue(is_client_error(499)) + self.assertFalse(is_client_error(500)) + + self.assertFalse(is_server_error(499)) + self.assertTrue(is_server_error(500)) + self.assertTrue(is_server_error(599)) + self.assertFalse(is_server_error(600)) \ No newline at end of file From db19fba50d65c1093efa25bd5ed1230b6404c8ca Mon Sep 17 00:00:00 2001 From: Andy Wilson Date: Fri, 6 Dec 2013 22:31:07 -0600 Subject: [PATCH 035/236] update installation example to work with django 1.6 looks like django.conf.urls.defaults was deprecated as of django 1.6 --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3e5adbc4a..badd6f60a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -100,7 +100,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_ We're ready to create our API now. Here's our project's root `urls.py` module: - from django.conf.urls.defaults import url, patterns, include + from django.conf.urls import url, patterns, include from django.contrib.auth.models import User, Group from rest_framework import viewsets, routers From 3399158d62416af56201eac63cc20d8934f08de2 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 8 Dec 2013 11:40:40 -0500 Subject: [PATCH 036/236] RelatedField is function of serializer class --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index b9d96b5e3..556429bb9 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o For example, the following serializer. class AlbumSerializer(serializers.ModelSerializer): - tracks = RelatedField(many=True) + tracks = serializers.RelatedField(many=True) class Meta: model = Album From b8732d21652cf6b6e3c3e9807594b508be6583f8 Mon Sep 17 00:00:00 2001 From: Rustam Lalkaka Date: Sun, 8 Dec 2013 19:34:24 -0500 Subject: [PATCH 037/236] Minor grammar fix -- 'team' is singular --- docs/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/template.html b/docs/template.html index 5a0bdbfd4..c065237a5 100644 --- a/docs/template.html +++ b/docs/template.html @@ -172,7 +172,7 @@

    -

    The team behind REST framework are launching a new API service.

    +

    The team behind REST framework is launching a new API service.

    If you want to be first in line when we start issuing invitations, please sign up here:

    From 06d8a31e132c99a9645e26b5def3a1d9b9585c24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:34:08 +0000 Subject: [PATCH 038/236] Catch and mask ParseErrors that occur during rendering of the BrowsableAPI. --- rest_framework/renderers.py | 9 +++++++-- rest_framework/request.py | 2 +- rest_framework/tests/test_renderers.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fe4f43d48..2fdd33376 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,6 +20,7 @@ from rest_framework.compat import StringIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml +from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders @@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ if request.method == method: - data = request.DATA - files = request.FILES + try: + data = request.DATA + files = request.FILES + except ParseError: + data = None + files = None else: data = None files = None diff --git a/rest_framework/request.py b/rest_framework/request.py index 9b551aa80..fcea25083 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -362,7 +362,7 @@ class Request(object): # If we get an exception during parsing, fill in empty data and # re-raise. Ensures we don't simply repeat the error when # attempting to render the browsable renderer response, or when - # logging the request or similar. + # logging the request or similar. self._data = QueryDict('', self._request._encoding) self._files = MultiValueDict() raise diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 549e763b1..10aa4248b 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -69,6 +69,12 @@ class MockGETView(APIView): return Response({'foo': ['bar', 'baz']}) +class MockPOSTView(APIView): + + def post(self, request, **kwargs): + return Response({'foo': request.DATA}) + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -88,7 +94,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), - url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), + url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -224,8 +230,8 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertContains(resp.content, 'Mock GET View') - self.assertEqual(resp.status_code, status.HTTP_400_) + self.assertIn('Mock Post', resp.content) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 4e9385e709bcee87456a99839841ecf6b56f337a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:37:13 +0000 Subject: [PATCH 039/236] Drop unneeded assert --- rest_framework/tests/test_renderers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 10aa4248b..f4818eef2 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -230,7 +230,6 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertIn('Mock Post', resp.content) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' From e80b353085c460c5ab7e9b4d22249f01176c5eb1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:10:51 +0000 Subject: [PATCH 040/236] Add notes to contributing docs --- docs/topics/contributing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 2b18c4f68..4dd2718f2 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -33,6 +33,8 @@ Some tips on good issue reporting: * When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. +* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. ## Triaging issues @@ -42,6 +44,7 @@ Getting involved in triaging incoming issues is a good way to start contributing * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? * If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? * If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. # Development From 23369650e3502305ebf5d682e141c7d47db89111 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:14:21 +0000 Subject: [PATCH 041/236] Add notes to contributing docs --- 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 4dd2718f2..b3ab52bbf 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -10,7 +10,7 @@ There are many ways you can contribute to Django REST framework. We'd like it t The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. From e6f6bb5c7e3e882b0215c981e2f2b6a576820100 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:42:09 +0000 Subject: [PATCH 042/236] Add notes to contributing docs --- docs/topics/contributing.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index b3ab52bbf..77c60fb47 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,6 +164,16 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- +## Third party packages + +New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. + +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. + +We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework @@ -176,3 +186,4 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ +[django-reusable-app]: https://github.com/dabapps/django-reusable-app From c1be503308e755d72aae2c9695739bd33631e18b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:46:18 +0000 Subject: [PATCH 043/236] Add notes to contributing docs --- docs/topics/contributing.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 77c60fb47..906950bb5 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,16 +164,20 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- -## Third party packages +# Third party packages New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. +## Getting started + If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. -Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. - We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. +## Linking to your package + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework From ddd17c69e7abdd70448fa0f2f2a807d600b3391d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:24:10 +0000 Subject: [PATCH 044/236] Fix compat issues for #1231 --- rest_framework/compat.py | 7 ++++++ rest_framework/tests/test_renderers.py | 31 ++++++-------------------- rest_framework/utils/encoders.py | 3 +-- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 581e29fc7..05bd99e0c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -69,6 +69,13 @@ try: except ImportError: import urlparse +# UserDict moves in Python 3 +try: + from UserDict import UserDict + from UserDict import DictMixin +except ImportError: + from collections import UserDict + from collections import MutableMapping as DictMixin # Try to import PIL in either of the two ways it can end up installed. try: diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index d720bc51a..2ae8ae18c 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -15,12 +15,11 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory +from collections import MutableMapping import datetime +import json import pickle import re -import UserDict -import collections -import json DUMMYSTATUS = status.HTTP_200_OK @@ -277,26 +276,8 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') - def test_render_userdict_obj(self): - class DictLike(UserDict.DictMixin): - def __init__(self): - self._dict = dict() - def __getitem__(self, key): - return self._dict.__getitem__(key) - def __setitem__(self, key, value): - return self._dict.__setitem__(key, value) - def __delitem__(self, key): - return self._dict.__delitem__(key) - def keys(self): - return self._dict.keys() - x = DictLike() - x['a'] = 1 - x['b'] = "string value" - ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) - def test_render_dict_abc_obj(self): - class Dict(collections.MutableMapping): + class Dict(MutableMapping): def __init__(self): self._dict = dict() def __getitem__(self, key): @@ -309,13 +290,15 @@ class JSONRendererTests(TestCase): return self._dict.__iter__() def __len__(self): return self._dict.__len__() + def keys(self): + return self._dict.keys() x = Dict() x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) - + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): class DictLike(object): diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 22b1ab3de..3ac920c6f 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -47,8 +47,7 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(o, '__getitem__'): try: return dict(o) - except KeyError: - # Couldn't convert to a dict, fall through + except: pass elif hasattr(o, '__iter__'): return [i for i in o] From 2c898bd9018e40a8d0e4718fb2a0e3672e64782c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:27:10 +0000 Subject: [PATCH 045/236] Update release notes --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b080ad436..d3f85e6ee 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* JSON renderer now deals with objects that implement a dict-like interface. + ### 2.3.10 **Date**: 6th December 2013 From de319f3e28d27d71fffce7c8f12c23363d5c25eb Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 9 Dec 2013 09:53:16 +0000 Subject: [PATCH 046/236] Fix typo "Not" -> "Note" --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4c3fb9d33..6fc25f57a 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -425,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_ fields = ('url', 'account_name', 'users', 'created') lookup_field = 'slug' -Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. +Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example: From 9ba7be959c2a2fea989527c590ce833df5925e63 Mon Sep 17 00:00:00 2001 From: Maxim Kamenkov Date: Mon, 9 Dec 2013 20:33:06 +0200 Subject: [PATCH 047/236] Added REST Condition to 3rd party permissions packages list. --- docs/api-guide/permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 871de84ec..60624b630 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components. +## REST Condition + +The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md @@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t [filtering]: filtering.md [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions [composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions +[rest-condition]: https://github.com/caxap/rest_condition From 785a42cd5aee9e96f9b780ff144fa13c16189748 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:38:43 +0000 Subject: [PATCH 048/236] Tweak REST condition text. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 60624b630..6a0f48f44 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -232,7 +232,7 @@ The [Composed Permissions][composed-permissions] package provides a simple way t ## REST Condition -The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. +The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md From 3a1c40f81488c241cb64860d6cc510f8e71c0c40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:46:44 +0000 Subject: [PATCH 049/236] Refine model manager behavior so as not to use the behavior in incorrect cases. Closes #1205 --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 44e4b04b1..0d35fb327 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -412,7 +412,13 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() if self.source == '*': if value: From 40164fcc6241489033d7015d429cbe0afc674d37 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:49:54 +0000 Subject: [PATCH 050/236] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d3f85e6ee..e186893e1 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. +* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 From c09ad1bedc8559b2e6eadf0e5b8f3732af2d9d29 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:53:38 +0000 Subject: [PATCH 051/236] Remove incorrect apostrophe --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e186893e1..1ddd83519 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,7 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. -* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. +* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 From d3a118c728729f63f7e7c7c39b62a7932ca06391 Mon Sep 17 00:00:00 2001 From: Alan Justino Date: Tue, 10 Dec 2013 17:14:17 -0200 Subject: [PATCH 052/236] SimpleRouter.get_lookup_regex got lookup_prefix This allows @alanjds/drf-nested-routers to not duplicate/monkeypatch work made here --- rest_framework/routers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 3fee1e494..7915991d9 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -184,18 +184,18 @@ class SimpleRouter(BaseRouter): bound_methods[method] = action return bound_methods - def get_lookup_regex(self, viewset): + def get_lookup_regex(self, viewset, lookup_prefix=''): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. """ if self.trailing_slash: - base_regex = '(?P<{lookup_field}>[^/]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' else: # Don't consume `.json` style suffixes - base_regex = '(?P<{lookup_field}>[^/.]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' lookup_field = getattr(viewset, 'lookup_field', 'pk') - return base_regex.format(lookup_field=lookup_field) + return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) def get_urls(self): """ From 7382f8c6adc17c9feb02d028f7791af632d6dd3b Mon Sep 17 00:00:00 2001 From: David Ray Date: Tue, 10 Dec 2013 14:56:07 -0500 Subject: [PATCH 053/236] Update routers.md Reference to ```DefaultRouter``` should be ```SimpleRouter``` --- docs/api-guide/routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index fb48197e9..8151e60f2 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo ## Usage -Here's an example of a simple URL conf, that uses `DefaultRouter`. +Here's an example of a simple URL conf, that uses `SimpleRouter`. from rest_framework import routers From 5acefd3b17e498af756fa48e27d7f8ce19322c7a Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:55:54 +0000 Subject: [PATCH 054/236] Add full required imports to Generating Tokens example Previously we were missing User and post_save. --- docs/api-guide/authentication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1a1c68b84..ef77e02c5 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,6 +162,8 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. + from django.contrib.auth.models import User + from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token From 4f473f0b9e918f2e071da0c84bd9b584c00ac919 Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:56:56 +0000 Subject: [PATCH 055/236] Use get_user_model instead of User in Generating Tokens example Because that's a better way of doing it. --- docs/api-guide/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ef77e02c5..53efc49a0 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,12 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. - from django.contrib.auth.models import User + from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token - @receiver(post_save, sender=User) + @receiver(post_save, sender=get_user_model()) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) From df2d9034c2a5a07dc3aa5455db892ee94cbed467 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Dec 2013 23:10:31 +0000 Subject: [PATCH 056/236] Add third party packages --- docs/api-guide/filtering.md | 9 +++++++++ docs/api-guide/routers.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index a0132ffcb..0e02a2a7d 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -360,6 +360,14 @@ For example, you might need to restrict users to only being able to see objects We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API. +# Third party packages + +The following third party packages provide additional filter implementations. + +## Django REST framework chain + +The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. + [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html @@ -368,3 +376,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views, [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields +[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f2..9001b8597 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,13 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third Party Packages + +The following third party packages provide router implementations that extend the default functionality provided by REST framework. + +## DRF Nested Routers + +The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. + [cite]: http://guides.rubyonrails.org/routing.html +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers From 73e8536e0d38f6677ac30aa2b3ba80563961191f Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Thu, 12 Dec 2013 21:45:44 -0600 Subject: [PATCH 057/236] third-party package: wq.db --- docs/api-guide/routers.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f2..ed9031144 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,18 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third party packages + +The following third party packages are also available. + +## wq.db + +[wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration. + + from wq.db.rest import app + from .models import MyModel + app.router.register_model(MyModel) + [cite]: http://guides.rubyonrails.org/routing.html +[wq.db]: http://wq.io/wq.db +[wq.db-router]: http://wq.io/docs/app.py From 0453cbd56bf5c553412b61a2a5e5522a2d44a419 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 11:09:54 +0000 Subject: [PATCH 058/236] Clean up implementation --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34f315318..40caa1f38 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -949,7 +949,11 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): - related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship From ca244ad614e2f6fb4fef1dc9987be996d2624303 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 15:30:59 +0000 Subject: [PATCH 059/236] Expanded notes in quickstart. Closes #1127. Closes #1128. --- docs/index.md | 2 +- docs/tutorial/quickstart.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index badd6f60a..04804fa79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,7 +112,7 @@ Here's our project's root `urls.py` module: model = Group - # Routers provide an easy way of automatically determining the URL conf + # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() router.register(r'users', UserViewSet) router.register(r'groups', GroupViewSet) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 80bb9abb4..8bf8c7f5c 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. +Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute. + +For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications. + ## URLs Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... @@ -169,6 +173,7 @@ Great, that was easy! If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide]. +[readme-example-api]: ../#example [image]: ../img/quickstart.png [tutorial]: 1-serialization.md [guide]: ../#api-guide From 90edcbf938ed8d6f3b783372c17e60bbf0761b61 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 13 Dec 2013 12:41:44 -0500 Subject: [PATCH 060/236] Fix default values always being False for browsable API This fixes a bug that was introduced in 28ff6fb [1] for the browsable API, specifically with how it handled default values for boolean fields. Previously, it had a global default for boolean fields set to `False`, which was different than the standard None that was used elsewhere. Because this only needed to be done for the browsable API, a fix was put into place that only set the default to `False` when form data was passed into the serializer. This had the unintended side effect of overriding any default set on the boolean field. This fixes #1101 [2] by only overriding the default if the default is `None`, which is the default for all fields. [1]: https://github.com/tomchristie/django-rest-framework/commit/28ff6fb1ec02b7a04c4a0db54885f3735b6dd43f [2]: https://github.com/tomchristie/django-rest-framework/issues/1101 --- rest_framework/fields.py | 2 +- rest_framework/tests/test_serializer.py | 39 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5a4f04a5b..f1de447c7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -428,7 +428,7 @@ class BooleanField(WritableField): def field_from_native(self, data, files, field_name, into): # HTML checkboxes do not explicitly represent unchecked as `False` # we deal with that here... - if isinstance(data, QueryDict): + if isinstance(data, QueryDict) and self.default is None: self.default = False return super(BooleanField, self).field_from_native( diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index eca467ee2..14d1c664e 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1743,3 +1743,42 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + + +class DefaultTrueBooleanModel(models.Model): + cat = models.BooleanField(default=True) + dog = models.BooleanField(default=False) + + +class SerializerDefaultTrueBoolean(TestCase): + + def setUp(self): + super(SerializerDefaultTrueBoolean, self).setUp() + + class DefaultTrueBooleanSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultTrueBooleanModel + fields = ('cat', 'dog') + + self.default_true_boolean_serializer = DefaultTrueBooleanSerializer + + def test_enabled_as_false(self): + serializer = self.default_true_boolean_serializer(data={'cat': False, + 'dog': False}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + def test_enabled_as_true(self): + serializer = self.default_true_boolean_serializer(data={'cat': True, + 'dog': True}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], True) + self.assertEqual(serializer.data['dog'], True) + + def test_enabled_partial(self): + serializer = self.default_true_boolean_serializer(data={'cat': False}, + partial=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) From 87b99d1ac8ce212dd0751f597e3279d80d4fcad1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:17:26 +0000 Subject: [PATCH 061/236] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 1ddd83519..54865053e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * JSON renderer now deals with objects that implement a dict-like interface. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. +* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. ### 2.3.10 From 193af483efca2aaaebc186301b88f5735c87e638 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:22:56 +0000 Subject: [PATCH 062/236] Add notes on lookup_prefix argument and why it's there even though unused by the default implementations. --- rest_framework/routers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 7915991d9..97b35c10a 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -188,6 +188,12 @@ class SimpleRouter(BaseRouter): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. + + Note that lookup_prefix is not used directly inside REST rest_framework + itself, but is required in order to nicely support nested router + implementations, such as drf-nested-routers. + + https://github.com/alanjds/drf-nested-routers """ if self.trailing_slash: base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' From 39dbea4da43f829863d395d5f2ee158837f2afe2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:27:17 +0000 Subject: [PATCH 063/236] Links to drf-nested-routers --- docs/api-guide/relations.md | 11 +++++++++++ docs/api-guide/routers.md | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 556429bb9..1b089c541 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -442,7 +442,18 @@ In the 2.4 release, these parts of the API will be removed entirely. For more details see the [2.2 release announcement][2.2-announcement]. +--- + +# Third Party Packages + +The following third party packages are also available. + +## DRF Nested Routers + +The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. + [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 54fae65f5..895589db9 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -158,9 +158,6 @@ The following third party packages are also available. The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. -[cite]: http://guides.rubyonrails.org/routing.html -[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers - ## wq.db The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration. @@ -171,5 +168,6 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an app.router.register_model(MyModel) [cite]: http://guides.rubyonrails.org/routing.html +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db]: http://wq.io/wq.db [wq.db-router]: http://wq.io/docs/app.py From a87c55a93a7ca380bc49425cc6df00f4eba99aa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 21:57:07 +0000 Subject: [PATCH 064/236] Compat fixes for django-oauth-plus versions 2.0-2.2.1 --- rest_framework/authentication.py | 7 ++++--- rest_framework/compat.py | 15 +++++++++++++++ rest_framework/tests/test_authentication.py | 20 ++++++++++---------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index bca542ebb..e491ce5f9 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -9,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import oauth, oauth_provider, oauth_provider_store -from rest_framework.compat import oauth2_provider, provider_now +from rest_framework.compat import oauth2_provider, provider_now, check_nonce from rest_framework.authtoken.models import Token @@ -281,8 +281,9 @@ class OAuthAuthentication(BaseAuthentication): """ Checks nonce of request, and return True if valid. """ - return oauth_provider_store.check_nonce(request, oauth_request, - oauth_request['oauth_nonce'], oauth_request['oauth_timestamp']) + oauth_nonce = oauth_request['oauth_nonce'] + oauth_timestamp = oauth_request['oauth_timestamp'] + return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp) class OAuth2Authentication(BaseAuthentication): diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 05bd99e0c..88211becb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -7,6 +7,7 @@ versions of django/python, and compatibility wrappers around optional packages. from __future__ import unicode_literals import django +import inspect from django.core.exceptions import ImproperlyConfigured from django.conf import settings @@ -536,9 +537,23 @@ except ImportError: try: import oauth_provider from oauth_provider.store import store as oauth_provider_store + + # check_nonce's calling signature in django-oauth-plus changes sometime + # between versions 2.0 and 2.2.1 + def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp): + check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args + if 'timestamp' in check_nonce_args: + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce, oauth_timestamp + ) + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce + ) + except (ImportError, ImproperlyConfigured): oauth_provider = None oauth_provider_store = None + check_nonce = None # OAuth 2 support is optional try: diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index fe11423dc..f072b81b7 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -249,7 +249,7 @@ class OAuthTests(TestCase): def setUp(self): # these imports are here because oauth is optional and hiding them in try..except block or compat # could obscure problems if something breaks - from oauth_provider.models import Consumer, Resource + from oauth_provider.models import Consumer, Scope from oauth_provider.models import Token as OAuthToken from oauth_provider import consts @@ -269,8 +269,8 @@ class OAuthTests(TestCase): self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, name='example', user=self.user, status=self.consts.ACCEPTED) - self.resource = Resource.objects.create(name="resource name", url="api/") - self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, + self.scope = Scope.objects.create(name="resource name", url="api/") + self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True ) @@ -398,10 +398,10 @@ class OAuthTests(TestCase): @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @unittest.skipUnless(oauth, 'oauth2 not installed') def test_get_form_with_readonly_resource_passing_auth(self): - """Ensure POSTing with a readonly resource instead of a write scope fails""" + """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.get('/oauth-with-scope/', params) self.assertEqual(response.status_code, 200) @@ -411,8 +411,8 @@ class OAuthTests(TestCase): def test_post_form_with_readonly_resource_failing_auth(self): """Ensure POSTing with a readonly resource instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.post('/oauth-with-scope/', params) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) @@ -422,8 +422,8 @@ class OAuthTests(TestCase): def test_post_form_with_write_resource_passing_auth(self): """Ensure POSTing with a write resource succeed""" read_write_access_token = self.token - read_write_access_token.resource.is_readonly = False - read_write_access_token.resource.save() + read_write_access_token.scope.is_readonly = False + read_write_access_token.scope.save() params = self._create_authorization_url_parameters() auth = self._create_authorization_header() response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) From 54d3c6a725358ee5bb3e07450fb5efbaf957e1b7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 21:59:47 +0000 Subject: [PATCH 065/236] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 54865053e..f171b1f50 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. +* Fix compatiblity with newer versions of `django-oauth-plus`. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. * Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. From f78b3187df11925caf07f9d86eb868de906c60c8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 22:01:19 +0000 Subject: [PATCH 066/236] Added @philipforget for work on #1232. Thanks :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 1a838421d..d4c00bc4e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -181,6 +181,7 @@ The following people have helped make REST framework great. * Alex Good - [alexjg] * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] +* Philip Forget - [philipforget] Many thanks to everyone who's contributed to the project. @@ -398,3 +399,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexjg]: https://github.com/alexjg [ian-foote]: https://github.com/ian-foote [chuckharmston]: https://github.com/chuckharmston +[philipforget]: https://github.com/philipforget From 6b6b255684e8cfc25bf91168b46e9ab6512b800a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 14 Dec 2013 20:42:58 +0000 Subject: [PATCH 067/236] Add note on pagination bugfix. Closes #1293. --- docs/topics/release-notes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f171b1f50..d1ace1164 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -89,6 +89,19 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. +* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*] + +--- + +[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. + + class DisablePaginationMixin(object): + def get_paginate_by(self, queryset=None): + if self.request.QUERY_PARAMS['self.paginate_by_param'] == '0': + return None + return super(DisablePaginationMixin, self).get_paginate_by(queryset) + +--- ### 2.3.7 From 69fef838cce33b9079640f83cc03edc30f56f5f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 14 Dec 2013 21:05:11 +0000 Subject: [PATCH 068/236] Update django-oauth-plus version --- optionals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optionals.txt b/optionals.txt index 4ebfceab4..96f4b2f44 100644 --- a/optionals.txt +++ b/optionals.txt @@ -2,6 +2,6 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-filter>=0.5.4 -django-oauth-plus>=2.0 +django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 From 4a134eefa2c3b71aa1dc6a4ec94716fe41dca8f5 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Mon, 16 Dec 2013 15:51:43 +1300 Subject: [PATCH 069/236] Fix expansion of writable nested serializers where the inner fields have source set. --- rest_framework/serializers.py | 4 +++- rest_framework/tests/test_serializer.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 40caa1f38..d9313342c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -422,7 +422,9 @@ class BaseSerializer(WritableField): if self.source == '*': if value: - into.update(value) + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 14d1c664e..7808ba1ae 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer): model = Person +class NestedSerializerWithRenamedField(serializers.Serializer): + renamed_info = serializers.Field(source='info') + + +class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer): + nested = NestedSerializerWithRenamedField(source='*') + + class Meta: + model = Person + + class PersonSerializerInvalidReadOnly(serializers.ModelSerializer): """ Testing for #652. @@ -456,6 +467,20 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_with_inner_source_fields(self): + """ + Tests that a serializer with source="*" correctly expands the + it's fields into the outer serializer even if they have their + own 'source' parameters. + """ + + serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={ + 'name': 'marko', + 'nested': {'renamed_info': 'hi'}}, + ) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.errors, {}) + class CustomValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): From fc2dee844ab0ca77928f296f13777bf01d94e6fd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 08:59:10 +0000 Subject: [PATCH 070/236] Don't import compat.py from authtoken.models. Closes #1297 --- rest_framework/authtoken/models.py | 8 +++++++- rest_framework/compat.py | 7 ------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7601f5b79..024f62bfe 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,11 +1,17 @@ import uuid import hmac from hashlib import sha1 -from rest_framework.compat import AUTH_USER_MODEL from django.conf import settings from django.db import models +# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist. +# Note that we don't perform this code in the compat module due to +# bug report #1297 +# See: https://github.com/tomchristie/django-rest-framework/issues/1297 +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + + class Token(models.Model): """ The default authorization token model. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 88211becb..b69749feb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -104,13 +104,6 @@ def get_concrete_model(model_cls): return model_cls -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): - AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: - AUTH_USER_MODEL = 'auth.User' - - if django.VERSION >= (1, 5): from django.views.generic import View else: From 31dd160256a86c261099602dadb1163811a41e23 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 11:59:14 +0000 Subject: [PATCH 071/236] Typo --- 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 906950bb5..30d292f80 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -10,7 +10,7 @@ There are many ways you can contribute to Django REST framework. We'd like it t The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. From f0f7a91b3157790aac8c568e3349dde8dca1fac5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 11:59:37 +0000 Subject: [PATCH 072/236] Add CONTRIBUTING.md --- CONTRIBUTING.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..e0544a479 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,193 @@ +# Contributing to REST framework + +> The world can only really be changed one piece at a time. The art is picking that piece. +> +> — [Tim Berners-Lee][cite] + +There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. + +## Community + +The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. + +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. + +Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. + +When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. + +## Code of conduct + +Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. + +Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. + +The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. + +# Issues + +It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. + +Some tips on good issue reporting: + +* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. +* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. +* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. +* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. + +## Triaging issues + +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to + +* Read through the ticket - does it make sense, is it missing any context that would help explain it better? +* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? +* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? +* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. + +# Development + +To start developing on Django REST framework, clone the repo: + + git clone git@github.com:tomchristie/django-rest-framework.git + +Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. + +## Testing + +To run the tests, clone the repository, and then: + + # Setup the virtual environment + virtualenv env + env/bin/activate + pip install -r requirements.txt + pip install -r optionals.txt + + # Run the tests + rest_framework/runtests/runtests.py + +You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: + + tox + +## Pull requests + +It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. + +It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. + +It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. + +GitHub's documentation for working on pull requests is [available here][pull-requests]. + +Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. + +Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. + +![Travis status][travis-status] + +*Above: Travis build notifications* + +## Managing compatibility issues + +Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. + +# Documentation + +The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs]. + +There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended. + +## Building the documentation + +To build the documentation, simply run the `mkdocs.py` script. + + ./mkdocs.py + +This will build the html output into the `html` directory. + +You can build the documentation and open a preview in a browser window by using the `-p` flag. + + ./mkdocs.py -p + +## Language style + +Documentation should be in American English. The tone of the documentation is very important - try to stick to a simple, plain, objective and well-balanced style where possible. + +Some other tips: + +* Keep paragraphs reasonably short. +* Use double spacing after the end of sentences. +* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. + +## Markdown style + +There are a couple of conventions you should follow when working on the documentation. + +##### 1. Headers + +Headers should use the hash style. For example: + + ### Some important topic + +The underline style should not be used. **Don't do this:** + + Some important topic + ==================== + +##### 2. Links + +Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. + + Here is a link to [some other thing][other-thing]. + + More text... + + [other-thing]: http://example.com/other/thing + +This style helps keep the documentation source consistent and readable. + +If you are hyperlinking to another REST framework document, you should use a relative link, and link to the `.md` suffix. For example: + + [authentication]: ../api-guide/authentication.md + +Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages. + +##### 3. Notes + +If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: + + --- + + **Note:** A useful documentation note. + + --- + +# Third party packages + +New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. + +## Getting started + +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. + +We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. + +## Linking to your package + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. + +[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html +[code-of-conduct]: https://www.djangoproject.com/conduct/ +[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[so-filter]: http://stackexchange.com/filters/66475/rest-framework +[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open +[pep-8]: http://www.python.org/dev/peps/pep-0008/ +[travis-status]: https://raw.github.com/tomchristie/django-rest-framework/master/docs/img/travis-status.png +[pull-requests]: https://help.github.com/articles/using-pull-requests +[tox]: http://tox.readthedocs.org/en/latest/ +[markdown]: http://daringfireball.net/projects/markdown/basics +[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs +[mou]: http://mouapp.com/ +[django-reusable-app]: https://github.com/dabapps/django-reusable-app From 802648045476f91f1c4b6a9f507cc08625194c2c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 17 Dec 2013 10:30:23 +0100 Subject: [PATCH 073/236] Use the BytesIO for buffering bytes and import the one from the compat module. --- docs/tutorial/1-serialization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e1c0009c3..4d4e7258f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -183,9 +183,9 @@ At this point we've translated the model instance into Python native datatypes. Deserialization is similar. First we parse a stream into Python native datatypes... - import StringIO + from rest_framework.compat import BytesIO - stream = StringIO.StringIO(content) + stream = BytesIO(content) data = JSONParser().parse(stream) ...then we restore those native datatypes into to a fully populated object instance. From 02ae1682b5585581e88bbd996f7cb7fd22b146f7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Dec 2013 09:45:28 +0000 Subject: [PATCH 074/236] Add note on compat import in tutorial --- docs/tutorial/1-serialization.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 4d4e7258f..e015a545f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -183,6 +183,8 @@ At this point we've translated the model instance into Python native datatypes. Deserialization is similar. First we parse a stream into Python native datatypes... + # This import will use either `StringIO.StringIO` or `io.BytesIO` + # as appropriate, depending on if we're running Python 2 or Python 3. from rest_framework.compat import BytesIO stream = BytesIO(content) From 0e3822d6e0ea71d43ce7108b43184a6b3eba3f3a Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 16:53:06 +0100 Subject: [PATCH 075/236] Updated test class name to be unique --- rest_framework/tests/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9cd..ce7022cdf 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -52,7 +52,7 @@ class ShouldValidateModelSerializer(serializers.ModelSerializer): fields = ('renamed',) -class TestPreSaveValidationExclusions(TestCase): +class TestPreSaveValidationExclusionsSerializer(TestCase): def test_renamed_fields_are_model_validated(self): """ Ensure fields with 'source' applied do get still get model validation. From 71aa5f3c455db9bea5163dca70bfe6f3b3c924be Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 17:16:24 +0100 Subject: [PATCH 076/236] Added missing custom validation method test --- rest_framework/tests/test_validation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9cd..371697f8f 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -47,6 +47,12 @@ class ShouldValidateModel(models.Model): class ShouldValidateModelSerializer(serializers.ModelSerializer): renamed = serializers.CharField(source='should_validate_field', required=False) + def validate_renamed(self, attrs, source): + value = attrs[source] + if len(value) < 3: + raise serializers.ValidationError('Minimum 3 characters.') + return attrs + class Meta: model = ShouldValidateModel fields = ('renamed',) @@ -63,6 +69,13 @@ class TestPreSaveValidationExclusions(TestCase): self.assertEqual(serializer.is_valid(), False) +class TestCustomValidationMethods(TestCase): + def test_custom_validation_method_is_executed(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) + self.assertFalse(serializer.is_valid()) + self.assertIn('renamed', serializer.errors) + + class ValidationSerializer(serializers.Serializer): foo = serializers.CharField() From 973f898a4b8042775a94d3e76188b43f13d493a8 Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 17:45:56 +0100 Subject: [PATCH 077/236] Should it be that way? --- rest_framework/tests/test_validation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ce7022cdf..ab75da252 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -61,6 +61,8 @@ class TestPreSaveValidationExclusionsSerializer(TestCase): # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) self.assertEqual(serializer.is_valid(), False) + self.assertIn('renamed', serializer.errors) + self.assertNotIn('should_validate_field', serializer.errors) class ValidationSerializer(serializers.Serializer): From 22343ee11764aac3686ad500da5c9aae30540e8e Mon Sep 17 00:00:00 2001 From: Vitaly Babiy Date: Sat, 21 Dec 2013 07:05:21 -0500 Subject: [PATCH 078/236] Added links to djangorestframework-camel-case in the third party sections of the docs for both parsers and renderers. --- docs/api-guide/parsers.md | 6 ++++++ docs/api-guide/renderers.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 1030fcb65..db0b666fb 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -186,9 +186,15 @@ The following third party packages are also available. [MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework. +## CamelCase JSON + +[djangorestframework-camel-case] provides a camelCase JSON parser for django REST framework, its maintained by [vbabiy] + [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers [messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack [juanriaza]: https://github.com/juanriaza +[vbabiy]: https://github.com/vbabiy [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack +[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index cf2005691..673b59025 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -419,6 +419,11 @@ Comma-separated values are a plain-text tabular data format, that can be easily [UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. +## CamelCase JSON + +[djangorestframework-camel-case] provides a camelCase JSON render for django REST framework, its maintained by [vbabiy] + + [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -435,8 +440,10 @@ Comma-separated values are a plain-text tabular data format, that can be easily [messagepack]: http://msgpack.org/ [juanriaza]: https://github.com/juanriaza [mjumbewu]: https://github.com/mjumbewu +[vbabiy]: https://github.com/vbabiy [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv [ultrajson]: https://github.com/esnme/ultrajson [hzy]: https://github.com/hzy [drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer +[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file From 1f3ded4559ad18d03ee49b3befd19ddaea7e70b2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 17:18:25 +0000 Subject: [PATCH 079/236] Docs tweaks --- docs/api-guide/parsers.md | 2 +- docs/api-guide/renderers.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index db0b666fb..72a4af643 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -188,7 +188,7 @@ The following third party packages are also available. ## CamelCase JSON -[djangorestframework-camel-case] provides a camelCase JSON parser for django REST framework, its maintained by [vbabiy] +[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy]. [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 673b59025..7798827bc 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -421,7 +421,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## CamelCase JSON -[djangorestframework-camel-case] provides a camelCase JSON render for django REST framework, its maintained by [vbabiy] +[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy]. [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process From bc0e994784f46330b4e849c3326fbd5c500298b3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:10:05 +0000 Subject: [PATCH 080/236] Added example of using APIException class. Closes #1300 --- docs/api-guide/exceptions.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index c46d415e4..221df679d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -88,6 +88,14 @@ 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. +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: + + from rest_framework.exceptions import APIException + + class ServiceUnavailable(APIException): + status_code = 503 + detail = 'Service temporarily unavailable, try again later.' + ## ParseError **Signature:** `ParseError(detail=None)` From a439c80cd856f0661cb66003263c454c5108fe0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:21:53 +0000 Subject: [PATCH 081/236] Less brittle through relationship testing. Closes #1292. --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 28a098805..8351b3df6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -713,7 +713,9 @@ class ModelSerializer(Serializer): is_m2m = isinstance(relation.field, models.fields.related.ManyToManyField) - if is_m2m and not relation.field.rel.through._meta.auto_created: + if (is_m2m and + hasattr(relation.field.rel, 'through') and + not relation.field.rel.through._meta.auto_created): has_through_model = True if nested: From 71ab7cda2a28b0df8df54c1446b4780413909acb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:54:51 +0000 Subject: [PATCH 082/236] Additional test for 'source' behaviour. Refs #1302 --- rest_framework/tests/test_validation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 3fb6ba373..8a8187fa8 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -75,6 +75,10 @@ class TestCustomValidationMethods(TestCase): self.assertFalse(serializer.is_valid()) self.assertIn('renamed', serializer.errors) + def test_custom_validation_method_passing(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) + self.assertTrue(serializer.is_valid()) + class ValidationSerializer(serializers.Serializer): foo = serializers.CharField() From 2d6d725c2f7f2226f9287211e64037816f8f2cac Mon Sep 17 00:00:00 2001 From: amatellanes Date: Sun, 22 Dec 2013 12:39:47 +0100 Subject: [PATCH 083/236] Simplified some functions --- rest_framework/permissions.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index ab6655e7b..f24a51235 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -54,9 +54,7 @@ class IsAuthenticated(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_authenticated(): - return True - return False + return request.user and request.user.is_authenticated() class IsAdminUser(BasePermission): @@ -65,9 +63,7 @@ class IsAdminUser(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_staff: - return True - return False + return request.user and request.user.is_staff class IsAuthenticatedOrReadOnly(BasePermission): @@ -76,11 +72,9 @@ class IsAuthenticatedOrReadOnly(BasePermission): """ def has_permission(self, request, view): - if (request.method in SAFE_METHODS or - request.user and - request.user.is_authenticated()): - return True - return False + return (request.method in SAFE_METHODS or + request.user and + request.user.is_authenticated()) class DjangoModelPermissions(BasePermission): @@ -138,11 +132,9 @@ class DjangoModelPermissions(BasePermission): perms = self.get_required_permissions(request.method, model_cls) - if (request.user and + return (request.user and (request.user.is_authenticated() or not self.authenticated_users_only) and - request.user.has_perms(perms)): - return True - return False + request.user.has_perms(perms)) class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): From e5caf48a12e777df3e5801fa19d98f59b6453aa2 Mon Sep 17 00:00:00 2001 From: bennbollay Date: Sun, 22 Dec 2013 16:53:52 -0800 Subject: [PATCH 084/236] Change the page title to prioritize page content When many tab's are open (which is necessary for DRF's documentation), it becomes impossible to determine which tab contains which pieces of documentation. That they are all related is obvious by the use of a common icon, just not the specific page each tab is loaded to. This change inverts the order of the title to maintain as much useful context as possible on the tabbar. --- mkdocs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.py b/mkdocs.py index d1790168a..09c9dcc67 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -144,7 +144,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): if filename == 'index.md': main_title = 'Django REST framework - APIs made easy' else: - main_title = 'Django REST framework - ' + main_title + main_title = main_title + ' - Django REST framework' if relative_path == 'index.md': canonical_url = base_url From 80e9f0d64b0ace50d413eaccbf28a3b4ded75ed3 Mon Sep 17 00:00:00 2001 From: Yin Jifeng Date: Mon, 23 Dec 2013 11:02:07 +0800 Subject: [PATCH 085/236] fix url double quoted in Django 1.6 get_full_path returns unicode, so we use build_absolute_uri which returns iri_to_uri'ed one --- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/tests/test_templatetags.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/test_templatetags.py diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index e9c1cdd54..5c267ab36 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -144,7 +144,7 @@ def add_query_param(request, key, val): """ Add a query parameter to the current request url, and return the new url. """ - return replace_query_param(request.get_full_path(), key, val) + return replace_query_param(request.build_absolute_uri(), key, val) @register.filter diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py new file mode 100644 index 000000000..cbac768a3 --- /dev/null +++ b/rest_framework/tests/test_templatetags.py @@ -0,0 +1,18 @@ +# encoding: utf-8 +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 + +factory = APIRequestFactory() + + +class TemplateTagTests(TestCase): + + def test_add_query_param_with_non_latin_charactor(self): + request = factory.get("/?q=查询") + json_url = add_query_param(request, "format", "json") + self.assertIn(json_url, [ + "http://testserver/?format=json&q=%E6%9F%A5%E8%AF%A2", + "http://testserver/?q=%E6%9F%A5%E8%AF%A2&format=json", + ]) From d6806340e54408858da4b2dc991be99edd65df76 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 08:50:46 +0100 Subject: [PATCH 086/236] Simplified some examples in tutorial --- docs/tutorial/1-serialization.md | 6 ++---- docs/tutorial/2-requests-and-responses.md | 6 ++---- docs/tutorial/4-authentication-and-permissions.md | 7 ++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e015a545f..2298df59a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,8 +263,7 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -290,8 +289,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 7fa4f3e4a..603edd081 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index b472322a3..986f13ff3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,15 +163,12 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True - # Write permissions are only allowed to the owner of the snippet - return obj.owner == request.user + return request.method in permissions.SAFE_METHODS or obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 74f1cf635536ea99937954a11fa11531a832ebc2 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 08:56:34 +0100 Subject: [PATCH 087/236] Revert "Simplified some examples in tutorial" This reverts commit d6806340e54408858da4b2dc991be99edd65df76. --- docs/tutorial/1-serialization.md | 6 ++++-- docs/tutorial/2-requests-and-responses.md | 6 ++++-- docs/tutorial/4-authentication-and-permissions.md | 7 +++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 2298df59a..e015a545f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,7 +263,8 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - return JSONResponse(serializer.errors, status=400) + else: + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -289,7 +290,8 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - return JSONResponse(serializer.errors, status=400) + else: + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 603edd081..7fa4f3e4a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,7 +59,8 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -84,7 +85,8 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 986f13ff3..b472322a3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,12 +163,15 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + # Write permissions are only allowed to the owner of the snippet - return request.method in permissions.SAFE_METHODS or obj.owner == request.user + return obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 2846ddb5d2ba84b3905d4dc0593afe3a0d4b2749 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 09:06:03 +0100 Subject: [PATCH 088/236] Simplified some examples in tutorial --- docs/tutorial/1-serialization.md | 6 ++---- docs/tutorial/2-requests-and-responses.md | 6 ++---- docs/tutorial/4-authentication-and-permissions.md | 7 ++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e015a545f..2298df59a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,8 +263,7 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -290,8 +289,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 7fa4f3e4a..603edd081 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index b472322a3..986f13ff3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,15 +163,12 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True - # Write permissions are only allowed to the owner of the snippet - return obj.owner == request.user + return request.method in permissions.SAFE_METHODS or obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From d8a95b4b6d4480089d38808b45a7b47f30e81cdd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 09:12:34 +0000 Subject: [PATCH 089/236] Back out permissions example change in favor of easier to follow example --- docs/tutorial/4-authentication-and-permissions.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 986f13ff3..bdc6b5791 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,12 +163,15 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - # Write permissions are only allowed to the owner of the snippet - return request.method in permissions.SAFE_METHODS or obj.owner == request.user + if request.method in permissions.SAFE_METHODS: + return True + + # Write permissions are only allowed to the owner of the snippet. + return obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 3f5e3c28f5a4f8b12f5f3ae6c7b571d08be2bf7e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 11:55:25 +0000 Subject: [PATCH 090/236] Updated tests to pass in python 3 --- rest_framework/templatetags/rest_framework.py | 5 ++++- rest_framework/tests/test_templatetags.py | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 5c267ab36..83c046f99 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import from django import template from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict +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 @@ -144,7 +145,9 @@ def add_query_param(request, key, val): """ Add a query parameter to the current request url, and return the new url. """ - return replace_query_param(request.build_absolute_uri(), key, val) + iri = request.get_full_path() + uri = iri_to_uri(iri) + return replace_query_param(uri, key, val) @register.filter diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index cbac768a3..7ac90ae60 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -10,9 +10,10 @@ factory = APIRequestFactory() class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_charactor(self): - request = factory.get("/?q=查询") + # Ensure we don't double-escape non-latin characters + # that are present in the querystring. + # https://github.com/tomchristie/django-rest-framework/pull/1314 + request = factory.get("/", {'q': '查询'}) json_url = add_query_param(request, "format", "json") - self.assertIn(json_url, [ - "http://testserver/?format=json&q=%E6%9F%A5%E8%AF%A2", - "http://testserver/?q=%E6%9F%A5%E8%AF%A2&format=json", - ]) + self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) + self.assertIn("format=json") From bed2f08c24a13831590ae5fc8cefbb1bca300a96 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 11:57:25 +0000 Subject: [PATCH 091/236] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d1ace1164..b09bd0bea 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -46,6 +46,7 @@ You can determine your currently installed version using `pip freeze`: * Fix compatiblity with newer versions of `django-oauth-plus`. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. * Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. +* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params. ### 2.3.10 From feddd16c54c99977bd5503fd09232828c280fc10 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 12:04:17 +0000 Subject: [PATCH 092/236] Tweak test style --- rest_framework/tests/test_templatetags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 7ac90ae60..609a9e089 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -12,8 +12,8 @@ class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_charactor(self): # Ensure we don't double-escape non-latin characters # that are present in the querystring. - # https://github.com/tomchristie/django-rest-framework/pull/1314 + # See #1314. request = factory.get("/", {'q': '查询'}) json_url = add_query_param(request, "format", "json") self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) - self.assertIn("format=json") + self.assertIn("format=json", json_url) From d24ea39a4e4131486d45492339dcbbfefb6a933b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 14:29:22 +0000 Subject: [PATCH 093/236] Added note on view_name in hyperlinked relationships. Closes #1221 --- docs/api-guide/relations.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 1b089c541..4bee75af7 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -134,7 +134,7 @@ By default this field is read-write, although you can change this behavior using **Arguments**: -* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `-detail`. **required**. * `many` - If applied to a to-many relationship, you should set this argument to `True`. * `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. @@ -202,7 +202,7 @@ This field is always read-only. **Arguments**: -* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `-detail`. **required**. * `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. @@ -454,6 +454,7 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward +[routers]: http://django-rest-framework.org/api-guide/routers#defaultrouter [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers From 75e872473197f9b810c9daf348cb452faadac476 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 14:38:51 +0000 Subject: [PATCH 094/236] Fuller notes on the 'base_name' argument. Closes #1160. --- docs/api-guide/routers.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 895589db9..7efc140a5 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -37,6 +37,18 @@ The example above would generate the following URL patterns: * URL pattern: `^accounts/$` Name: `'account-list'` * URL pattern: `^accounts/{pk}/$` Name: `'account-detail'` +--- + +**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part. + +Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have any `.model` or `.queryset` attribute set. If you try to register that viewset you'll see an error like this: + + 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.model' or '.queryset' attribute. + +This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name. + +--- + ### Extra link and actions Any methods on the viewset decorated with `@link` or `@action` will also be routed. From 25bd6d1d4b7a85279047ab8e35f6faee0bc10a1a Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Mon, 23 Dec 2013 22:27:40 -0600 Subject: [PATCH 095/236] can't save genericrelations via nested serializers in django 1.6 --- rest_framework/tests/test_genericrelations.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index c38bfb9f3..2d3413444 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -69,6 +69,35 @@ class TestGenericRelations(TestCase): } self.assertEqual(serializer.data, expected) + def test_generic_nested_relation(self): + """ + Test saving a GenericRelation field via a nested serializer. + """ + + class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + exclude = ('content_type', 'object_id') + + class BookmarkSerializer(serializers.ModelSerializer): + tags = TagSerializer() + + class Meta: + model = Bookmark + exclude = ('id',) + + data = { + 'url': 'https://docs.djangoproject.com/', + 'tags': [ + {'tag': 'contenttypes'}, + {'tag': 'genericrelations'}, + ] + } + serializer = BookmarkSerializer(data=data) + self.assertTrue(serializer.is_valid()) + serializer.save() + self.assertEqual(serializer.object.tags.count(), 2) + def test_generic_fk(self): """ Test a relationship that spans a GenericForeignKey field. From d30ce2575c6b3901f15eb96eeaf66cc65e1d298b Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Mon, 23 Dec 2013 22:31:31 -0600 Subject: [PATCH 096/236] fix for genericrelation saving --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8351b3df6..c0c810ab9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -894,7 +894,7 @@ class ModelSerializer(Serializer): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many: + for field in meta.many_to_many + meta.virtual_fields: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) From 1f3f2741f519967aa236cd861a79c2c459063197 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Jan 2014 09:28:34 +0000 Subject: [PATCH 097/236] Happy new year --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 684868006..8c6822312 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou # License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/index.md b/docs/index.md index 04804fa79..7688d4281 100644 --- a/docs/index.md +++ b/docs/index.md @@ -219,7 +219,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou ## License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without From 0672d6de6e47ba0269a58ad0da3cc7ff4c82908e Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 2 Jan 2014 16:46:57 -0500 Subject: [PATCH 098/236] Fix bugfix note This fixes a bugfix note that was added because of #1293, which pointed out that a change in a bugfix [1] broke backwards compatibility. The bugfix did not work as expected because a variable was quoted when it should not have been. This removes the quotes around the variable, which brings back the expected functionality. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b09bd0bea..ca966d20e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -98,7 +98,7 @@ You can determine your currently installed version using `pip freeze`: class DisablePaginationMixin(object): def get_paginate_by(self, queryset=None): - if self.request.QUERY_PARAMS['self.paginate_by_param'] == '0': + if self.request.QUERY_PARAMS[self.paginate_by_param] == '0': return None return super(DisablePaginationMixin, self).get_paginate_by(queryset) From e032bad1a7c1f9ad2d9c591617ea92b2e802d7e5 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Thu, 2 Jan 2014 16:54:06 -0500 Subject: [PATCH 099/236] FIx link to tox --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0544a479..a7aa6fc40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ To run the tests, clone the repository, and then: # Run the tests rest_framework/runtests/runtests.py -You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: +You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: tox From e020c51b44b9acecdb01cc90f2d6da977ba5ea0f Mon Sep 17 00:00:00 2001 From: Steven Cummings Date: Thu, 2 Jan 2014 17:18:08 -0600 Subject: [PATCH 100/236] FIX BaseSerializer.from_native has an altered signature * base classes define it with one parameter * BaseSerializer currently defines a second parameter, which we make optional here for method-dispatch passivity --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c0c810ab9..b22ca5783 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -331,7 +331,7 @@ class BaseSerializer(WritableField): return ret - def from_native(self, data, files): + def from_native(self, data, files=None): """ Deserialize primitives -> objects. """ From 3050f0e82a60a12dc35ef7947c2f47de12387919 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Jan 2014 13:06:41 +0000 Subject: [PATCH 101/236] Frontpage tweaks --- README.md | 6 +++--- docs/img/logo.png | Bin 0 -> 45955 bytes docs/index.md | 26 +++++++++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 docs/img/logo.png diff --git a/README.md b/README.md index 8c6822312..403f9c0af 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Django REST framework - -**Awesome web-browseable Web APIs.** + [![build-status-image]][travis] +**Awesome web-browseable Web APIs.** + **Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs]. # Overview diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0f0cf800d316496efbd89f55365bacad00f0ef96 GIT binary patch literal 45955 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 + -# Django REST framework +--- -**Awesome web-browsable Web APIs.** +

    + +

    + + Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. @@ -20,13 +26,16 @@ Some reasons you might want to use REST framework: * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. * [Extensive documentation][index], and [great community support][group]. +* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite]. -There is a live example API for testing purposes, [available here][sandbox]. - -**Below**: *Screenshot from the browsable API* +--- ![Screenshot][image] +**Above**: *Screenshot from the browsable API* + +---- + ## Requirements REST framework requires the following: @@ -140,6 +149,8 @@ The tutorial will walk you through the building blocks that make up REST framewo * [5 - Relationships & hyperlinked APIs][tut-5] * [6 - Viewsets & routers][tut-6] +There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox]. + ## API Guide The API guide is your complete reference manual to all the functionality provided by REST framework. @@ -244,7 +255,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master -[urlobject]: https://github.com/zacharyvoase/urlobject +[mozilla]: http://www.mozilla.org/en-US/about/ +[eventbrite]: https://www.eventbrite.co.uk/about/ [markdown]: http://pypi.python.org/pypi/Markdown/ [yaml]: http://pypi.python.org/pypi/PyYAML [defusedxml]: https://pypi.python.org/pypi/defusedxml From 442916b9649baaf305ff094fe05f026ad04c7818 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Jan 2014 13:24:52 +0000 Subject: [PATCH 102/236] Link to BrightAPI, and remove ad except from frontpage --- docs/template.html | 23 ++--------------------- mkdocs.py | 6 ++++++ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/docs/template.html b/docs/template.html index c065237a5..a397d067b 100644 --- a/docs/template.html +++ b/docs/template.html @@ -170,31 +170,12 @@ + - -

    @@ -7,9 +7,15 @@

    diff --git a/mkdocs.py b/mkdocs.py index 09c9dcc67..92679a21e 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -161,6 +161,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = output.replace('{{ page_id }}', filename[:-3]) output = output.replace('{{ canonical_url }}', canonical_url) + if filename =='index.md': + output = output.replace('{{ ad_block }}', """

    The team behind REST framework is launching a new API service.

    +

    If you want to be first in line when we start issuing invitations, please sign up here.

    """) + else: + output = output.replace('{{ ad_block }}', '') + if prev_url: output = output.replace('{{ prev_url }}', prev_url) output = output.replace('{{ prev_url_disabled }}', '') From e3ae33017d86bed7fdbf6c76e0129f9361cab04d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 6 Jan 2014 15:01:45 +0000 Subject: [PATCH 103/236] Added "nofollow" against docs link. --- rest_framework/templates/rest_framework/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 495163b64..ba45b9bcb 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -33,7 +33,7 @@ @@ -176,9 +176,9 @@ - - - + + + diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 0f980e1cb..a997c7829 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ [python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy -[credits]: http://django-rest-framework.org/topics/credits +[credits]: http://www.django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [marcgibbons]: https://github.com/marcgibbons/ diff --git a/mkdocs.py b/mkdocs.py index 92679a21e..f973096f3 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -18,7 +18,7 @@ if local: suffix = '.html' index = 'index.html' else: - base_url = 'http://django-rest-framework.org' + base_url = 'http://www.django-rest-framework.org' suffix = '' index = '' diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index ba45b9bcb..d19d5a2be 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -33,7 +33,7 @@