From 9dccbcbb3800f83149edf08330f6926659bc5d73 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Sun, 3 Mar 2013 00:00:58 +1100 Subject: [PATCH 01/21] Support for X-HTTP-Method-Override header --- rest_framework/request.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 3e2fbd88e..4cdc8b738 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -231,9 +231,15 @@ class Request(object): """ self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', '')) + + # Look for method override in header + self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', None) + if self._method: + return + self._perform_form_overloading() # if the HTTP method was not overloaded, we take the raw HTTP method - if not _hasattr(self, '_method'): + if self._method: self._method = self._request.method def _load_stream(self): From 104614c600a391b2d416074f3929e543b86a8492 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Mon, 4 Mar 2013 07:14:38 +1100 Subject: [PATCH 02/21] Modified to allow form overloading to take precedence over header. --- rest_framework/request.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 4cdc8b738..f26d934d7 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -232,15 +232,12 @@ class Request(object): self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', '')) - # Look for method override in header - self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', None) - if self._method: - return - self._perform_form_overloading() - # if the HTTP method was not overloaded, we take the raw HTTP method - if self._method: - self._method = self._request.method + if not _hasattr(self, '_method'): + # Method wasn't overloaded by hidden form element, so look for + # method override in header. If not present default to raw HTTP method + self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', + self._request.method) def _load_stream(self): """ From a91dca178dc9681a0411a343b960a6f9a9dd8011 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Fri, 8 Mar 2013 17:01:43 +0000 Subject: [PATCH 03/21] Correcy typo. --- rest_framework/tests/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f70934016..5f206769c 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -263,7 +263,7 @@ class TestInstanceView(TestCase): at the requested url if it doesn't exist. """ content = {'text': 'foobar'} - # pk fields can not be created on demand, only the database can set th pk for a new object + # pk fields can not be created on demand, only the database can set the pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') response = self.view(request, pk=5).render() From 332c99748f2e0ebc6490b8e7379d8a4b48ba8ee2 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Fri, 8 Mar 2013 17:36:43 +0000 Subject: [PATCH 04/21] Add some simple numQueries tests. --- rest_framework/tests/generics.py | 48 +++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 5f206769c..1837898b5 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -60,7 +60,8 @@ class TestRootView(TestCase): GET requests to ListCreateAPIView should return list of objects. """ request = factory.get('/') - response = self.view(request).render() + with self.assertNumQueries(1): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data) @@ -71,7 +72,8 @@ class TestRootView(TestCase): content = {'text': 'foobar'} request = factory.post('/', json.dumps(content), content_type='application/json') - response = self.view(request).render() + with self.assertNumQueries(1): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) created = self.objects.get(id=4) @@ -84,7 +86,8 @@ class TestRootView(TestCase): content = {'text': 'foobar'} request = factory.put('/', json.dumps(content), content_type='application/json') - response = self.view(request).render() + with self.assertNumQueries(0): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEqual(response.data, {"detail": "Method 'PUT' not allowed."}) @@ -93,7 +96,8 @@ class TestRootView(TestCase): DELETE requests to ListCreateAPIView should not be allowed """ request = factory.delete('/') - response = self.view(request).render() + with self.assertNumQueries(0): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."}) @@ -102,7 +106,8 @@ class TestRootView(TestCase): OPTIONS requests to ListCreateAPIView should return metadata """ request = factory.options('/') - response = self.view(request).render() + with self.assertNumQueries(0): + response = self.view(request).render() expected = { 'parses': [ 'application/json', @@ -126,7 +131,8 @@ class TestRootView(TestCase): content = {'id': 999, 'text': 'foobar'} request = factory.post('/', json.dumps(content), content_type='application/json') - response = self.view(request).render() + with self.assertNumQueries(1): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) created = self.objects.get(id=4) @@ -154,7 +160,8 @@ class TestInstanceView(TestCase): GET requests to RetrieveUpdateDestroyAPIView should return a single object. """ request = factory.get('/1') - response = self.view(request, pk=1).render() + with self.assertNumQueries(1): + response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data[0]) @@ -165,7 +172,8 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.post('/', json.dumps(content), content_type='application/json') - response = self.view(request).render() + with self.assertNumQueries(0): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."}) @@ -176,7 +184,8 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - response = self.view(request, pk='1').render() + with self.assertNumQueries(3): + response = self.view(request, pk='1').render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) @@ -190,7 +199,8 @@ class TestInstanceView(TestCase): request = factory.patch('/1', json.dumps(content), content_type='application/json') - response = self.view(request, pk=1).render() + with self.assertNumQueries(3): + response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) @@ -201,7 +211,8 @@ class TestInstanceView(TestCase): DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. """ request = factory.delete('/1') - response = self.view(request, pk=1).render() + with self.assertNumQueries(2): + response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.content, six.b('')) ids = [obj.id for obj in self.objects.all()] @@ -212,7 +223,8 @@ class TestInstanceView(TestCase): OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata """ request = factory.options('/') - response = self.view(request).render() + with self.assertNumQueries(0): + response = self.view(request).render() expected = { 'parses': [ 'application/json', @@ -236,7 +248,8 @@ class TestInstanceView(TestCase): content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - response = self.view(request, pk=1).render() + with self.assertNumQueries(3): + response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) @@ -251,7 +264,8 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - response = self.view(request, pk=1).render() + with self.assertNumQueries(4): + response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) @@ -266,7 +280,8 @@ class TestInstanceView(TestCase): # pk fields can not be created on demand, only the database can set the pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') - response = self.view(request, pk=5).render() + with self.assertNumQueries(4): + response = self.view(request, pk=5).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) new_obj = self.objects.get(pk=5) self.assertEqual(new_obj.text, 'foobar') @@ -279,7 +294,8 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/test_slug', json.dumps(content), content_type='application/json') - response = self.slug_based_view(request, slug='test_slug').render() + with self.assertNumQueries(2): + response = self.slug_based_view(request, slug='test_slug').render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'}) new_obj = SlugBasedModel.objects.get(slug='test_slug') From e7e470739fc4d2694d1c0e2dbf3f6465b44ad069 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 03:23:44 -0400 Subject: [PATCH 05/21] Use parent's primary key when model is derived via multitable inheritance --- rest_framework/serializers.py | 7 +++-- rest_framework/tests/models.py | 14 ++++++++++ rest_framework/tests/serializer.py | 42 +++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2ae7c215f..1b044c5fa 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -456,8 +456,11 @@ class ModelSerializer(Serializer): "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ opts = get_concrete_model(cls)._meta pk_field = opts.pk - # while pk_field.rel: - # pk_field = pk_field.rel.to._meta.pk + + # If model is a child via multitable inheritance, use parent's pk + while isinstance(pk_field, models.OneToOneField) and pk_field.rel.parent_link: + pk_field = pk_field.rel.to._meta.pk + fields = [pk_field] fields += [field for field in opts.fields if field.serialize] fields += [field for field in opts.many_to_many if field.serialize] diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index f2117538c..fcfe5a0c7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -166,3 +166,17 @@ class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') + + +# Inherited +class ParentModel(RESTFrameworkModel): + name1 = models.CharField(max_length=100) + + +class ChildModel(ParentModel): + name2 = models.CharField(max_length=100) + + +class AssociatedModel(RESTFrameworkModel): + ref = models.OneToOneField(ParentModel, primary_key=True) + name = models.CharField(max_length=100) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index beb372c2b..93909b653 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,7 +4,8 @@ from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, ParentModel, ChildModel, + AssociatedModel) import datetime import pickle @@ -96,6 +97,16 @@ class BrokenModelSerializer(serializers.ModelSerializer): fields = ['some_field'] +class DerivedModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChildModel + + +class AssociatedModelSerializer(serializers.ModelSerializer): + class Meta: + model = AssociatedModel + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -170,6 +181,27 @@ class BasicTests(TestCase): self.assertEqual(set(serializer.data.keys()), set(['name', 'age', 'info'])) + def test_multitable_inherited_model_fields_as_expected(self): + """ + Assert that the parent pointer field is not included in the fields + serialized fields + """ + child = ChildModel(name1='parent name', name2='child name') + serializer = DerivedModelSerializer(child) + self.assertEqual(set(serializer.data.keys()), + set(['name1', 'name2', 'id'])) + + def test_onetoone_primary_key_model_fields_as_expected(self): + """ + Assert that a model with a onetoone field that is the primary key is + not treated like a derived model + """ + parent = ParentModel(name1='parent name') + associate = AssociatedModel(name='hello', ref=parent) + serializer = AssociatedModelSerializer(associate) + self.assertEqual(set(serializer.data.keys()), + set(['name', 'ref'])) + def test_field_with_dictionary(self): """ Make sure that dictionaries from fields are left intact @@ -250,6 +282,14 @@ class ValidationTests(TestCase): self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.errors, {'email': ['This field is required.']}) + def test_multitable_inherited_model(self): + data = { + 'name1': 'parent name', + 'name2': 'child name', + } + serializer = DerivedModelSerializer(data=data) + self.assertEqual(serializer.is_valid(), True) + def test_missing_bool_with_default(self): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" From bdcecf48e3c0bd592d50b6b6fac6353a41c3cbfd Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 16:01:14 -0400 Subject: [PATCH 06/21] Simplify inherited child check to not use isinstance --- 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 1b044c5fa..cd2bb8f1f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -458,7 +458,7 @@ class ModelSerializer(Serializer): pk_field = opts.pk # If model is a child via multitable inheritance, use parent's pk - while isinstance(pk_field, models.OneToOneField) and pk_field.rel.parent_link: + while pk_field.rel and pk_field.rel.parent_link: pk_field = pk_field.rel.to._meta.pk fields = [pk_field] From 354fbc64ba5046ce49d58c8243a4f81caddf3823 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 17:28:44 -0400 Subject: [PATCH 07/21] Group the model-inheritance-related tests together --- rest_framework/tests/models.py | 14 ---- .../tests/multitable_inheritance.py | 16 +++++ rest_framework/tests/serializer.py | 70 +++++++++++-------- 3 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 rest_framework/tests/multitable_inheritance.py diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index fcfe5a0c7..f2117538c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -166,17 +166,3 @@ class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') - - -# Inherited -class ParentModel(RESTFrameworkModel): - name1 = models.CharField(max_length=100) - - -class ChildModel(ParentModel): - name2 = models.CharField(max_length=100) - - -class AssociatedModel(RESTFrameworkModel): - ref = models.OneToOneField(ParentModel, primary_key=True) - name = models.CharField(max_length=100) diff --git a/rest_framework/tests/multitable_inheritance.py b/rest_framework/tests/multitable_inheritance.py new file mode 100644 index 000000000..1cca7823e --- /dev/null +++ b/rest_framework/tests/multitable_inheritance.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.db import models +from rest_framework.tests.models import RESTFrameworkModel + + +class ParentModel(RESTFrameworkModel): + name1 = models.CharField(max_length=100) + + +class ChildModel(ParentModel): + name2 = models.CharField(max_length=100) + + +class AssociatedModel(RESTFrameworkModel): + ref = models.OneToOneField(ParentModel, primary_key=True) + name = models.CharField(max_length=100) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 93909b653..c4a8a8994 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,8 +4,9 @@ from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, ParentModel, ChildModel, - AssociatedModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) +from rest_framework.tests.multitable_inheritance import (ParentModel, + ChildModel, AssociatedModel) import datetime import pickle @@ -181,27 +182,6 @@ class BasicTests(TestCase): self.assertEqual(set(serializer.data.keys()), set(['name', 'age', 'info'])) - def test_multitable_inherited_model_fields_as_expected(self): - """ - Assert that the parent pointer field is not included in the fields - serialized fields - """ - child = ChildModel(name1='parent name', name2='child name') - serializer = DerivedModelSerializer(child) - self.assertEqual(set(serializer.data.keys()), - set(['name1', 'name2', 'id'])) - - def test_onetoone_primary_key_model_fields_as_expected(self): - """ - Assert that a model with a onetoone field that is the primary key is - not treated like a derived model - """ - parent = ParentModel(name1='parent name') - associate = AssociatedModel(name='hello', ref=parent) - serializer = AssociatedModelSerializer(associate) - self.assertEqual(set(serializer.data.keys()), - set(['name', 'ref'])) - def test_field_with_dictionary(self): """ Make sure that dictionaries from fields are left intact @@ -282,14 +262,6 @@ class ValidationTests(TestCase): self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.errors, {'email': ['This field is required.']}) - def test_multitable_inherited_model(self): - data = { - 'name1': 'parent name', - 'name2': 'child name', - } - serializer = DerivedModelSerializer(data=data) - self.assertEqual(serializer.is_valid(), True) - def test_missing_bool_with_default(self): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" @@ -1150,3 +1122,39 @@ class DeserializeListTestCase(TestCase): self.assertFalse(serializer.is_valid()) expected = [{}, {'email': ['This field is required.']}, {}] self.assertEqual(serializer.errors, expected) + + +class IneritedModelSerializationTests(TestCase): + + def test_multitable_inherited_model_fields_as_expected(self): + """ + Assert that the parent pointer field is not included in the fields + serialized fields + """ + child = ChildModel(name1='parent name', name2='child name') + serializer = DerivedModelSerializer(child) + self.assertEqual(set(serializer.data.keys()), + set(['name1', 'name2', 'id'])) + + def test_onetoone_primary_key_model_fields_as_expected(self): + """ + Assert that a model with a onetoone field that is the primary key is + not treated like a derived model + """ + parent = ParentModel(name1='parent name') + associate = AssociatedModel(name='hello', ref=parent) + serializer = AssociatedModelSerializer(associate) + self.assertEqual(set(serializer.data.keys()), + set(['name', 'ref'])) + + def test_data_is_valid_without_parent_ptr(self): + """ + Assert that the pointer to the parent table is not a required field + for input data + """ + data = { + 'name1': 'parent name', + 'name2': 'child name', + } + serializer = DerivedModelSerializer(data=data) + self.assertEqual(serializer.is_valid(), True) From bd3fe75e1a41e45b0c9ff1e39707ee059ad0e06a Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 17:32:32 -0400 Subject: [PATCH 08/21] Further group model inheritance tests --- .../tests/multitable_inheritance.py | 51 +++++++++++++++++++ rest_framework/tests/serializer.py | 48 ----------------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/rest_framework/tests/multitable_inheritance.py b/rest_framework/tests/multitable_inheritance.py index 1cca7823e..00c153276 100644 --- a/rest_framework/tests/multitable_inheritance.py +++ b/rest_framework/tests/multitable_inheritance.py @@ -1,8 +1,11 @@ from __future__ import unicode_literals from django.db import models +from django.test import TestCase +from rest_framework import serializers from rest_framework.tests.models import RESTFrameworkModel +# Models class ParentModel(RESTFrameworkModel): name1 = models.CharField(max_length=100) @@ -14,3 +17,51 @@ class ChildModel(ParentModel): class AssociatedModel(RESTFrameworkModel): ref = models.OneToOneField(ParentModel, primary_key=True) name = models.CharField(max_length=100) + + +# Serializers +class DerivedModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChildModel + + +class AssociatedModelSerializer(serializers.ModelSerializer): + class Meta: + model = AssociatedModel + + +# Tests +class IneritedModelSerializationTests(TestCase): + + def test_multitable_inherited_model_fields_as_expected(self): + """ + Assert that the parent pointer field is not included in the fields + serialized fields + """ + child = ChildModel(name1='parent name', name2='child name') + serializer = DerivedModelSerializer(child) + self.assertEqual(set(serializer.data.keys()), + set(['name1', 'name2', 'id'])) + + def test_onetoone_primary_key_model_fields_as_expected(self): + """ + Assert that a model with a onetoone field that is the primary key is + not treated like a derived model + """ + parent = ParentModel(name1='parent name') + associate = AssociatedModel(name='hello', ref=parent) + serializer = AssociatedModelSerializer(associate) + self.assertEqual(set(serializer.data.keys()), + set(['name', 'ref'])) + + def test_data_is_valid_without_parent_ptr(self): + """ + Assert that the pointer to the parent table is not a required field + for input data + """ + data = { + 'name1': 'parent name', + 'name2': 'child name', + } + serializer = DerivedModelSerializer(data=data) + self.assertEqual(serializer.is_valid(), True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index c4a8a8994..beb372c2b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -5,8 +5,6 @@ from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) -from rest_framework.tests.multitable_inheritance import (ParentModel, - ChildModel, AssociatedModel) import datetime import pickle @@ -98,16 +96,6 @@ class BrokenModelSerializer(serializers.ModelSerializer): fields = ['some_field'] -class DerivedModelSerializer(serializers.ModelSerializer): - class Meta: - model = ChildModel - - -class AssociatedModelSerializer(serializers.ModelSerializer): - class Meta: - model = AssociatedModel - - class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -1122,39 +1110,3 @@ class DeserializeListTestCase(TestCase): self.assertFalse(serializer.is_valid()) expected = [{}, {'email': ['This field is required.']}, {}] self.assertEqual(serializer.errors, expected) - - -class IneritedModelSerializationTests(TestCase): - - def test_multitable_inherited_model_fields_as_expected(self): - """ - Assert that the parent pointer field is not included in the fields - serialized fields - """ - child = ChildModel(name1='parent name', name2='child name') - serializer = DerivedModelSerializer(child) - self.assertEqual(set(serializer.data.keys()), - set(['name1', 'name2', 'id'])) - - def test_onetoone_primary_key_model_fields_as_expected(self): - """ - Assert that a model with a onetoone field that is the primary key is - not treated like a derived model - """ - parent = ParentModel(name1='parent name') - associate = AssociatedModel(name='hello', ref=parent) - serializer = AssociatedModelSerializer(associate) - self.assertEqual(set(serializer.data.keys()), - set(['name', 'ref'])) - - def test_data_is_valid_without_parent_ptr(self): - """ - Assert that the pointer to the parent table is not a required field - for input data - """ - data = { - 'name1': 'parent name', - 'name2': 'child name', - } - serializer = DerivedModelSerializer(data=data) - self.assertEqual(serializer.is_valid(), True) From 2e481f3318609fff5b884a09cbc9d2c5782deae4 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Tue, 12 Mar 2013 12:00:41 +1100 Subject: [PATCH 09/21] Added test for X-HTTP-Method-Override header --- rest_framework/tests/request.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 4892f7a63..97e5af207 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -58,6 +58,14 @@ class TestMethodOverloading(TestCase): request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'})) self.assertEqual(request.method, 'DELETE') + def test_x_http_method_override_header(self): + """ + POST requests can also be overloaded to another method by setting + the X-HTTP-Method-Override header. + """ + request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE')) + self.assertEqual(request.method, 'DELETE') + class TestContentParsing(TestCase): def test_standard_behaviour_determines_no_content_GET(self): From 2bcb8ff12c967e71fb4871a9ac9e72395394d291 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Tue, 12 Mar 2013 13:48:40 +1100 Subject: [PATCH 10/21] Documentation for X-HTTP-Method-Override --- docs/topics/browser-enhancements.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index 6a11f0fac..8b1914231 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -19,6 +19,23 @@ For example, given the following form: `request.method` would return `"DELETE"`. +## HTTP header based method overriding + +REST framework also supports method overriding via the `X-HTTP-Method-Override` +header. This is useful if you are working with non-form content such as +JSON and are working with an older web server and/or hosting provider +(e.g. [Amazon Web Services ELB][aws_elb]) that doesn't recognise particular +HTTP methods such as `PATCH`. + +For example, making a `PATCH` request via `POST` in jQuery: + + $.ajax({ + url: '/myresource/', + method: 'POST', + headers: {'X-HTTP-Method-Override': 'PATCH'}, + ... + }); + ## Browser based submission of non-form content Browser-based submission of content types other than form are supported by @@ -62,3 +79,4 @@ as well as how to support content types other than form-encoded data. [rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work [html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24 [put_delete]: http://amundsen.com/examples/put-delete-forms/ +[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724 From 377dc2cda2c3a7aa02f5d761631f73c58745ed9d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 20:49:20 +0000 Subject: [PATCH 11/21] Only honor X-HTTP-Method-Override for POST requests. --- docs/topics/browser-enhancements.md | 8 +++----- rest_framework/request.py | 11 +++++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index 8b1914231..ce07fe95b 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -21,11 +21,9 @@ For example, given the following form: ## HTTP header based method overriding -REST framework also supports method overriding via the `X-HTTP-Method-Override` -header. This is useful if you are working with non-form content such as -JSON and are working with an older web server and/or hosting provider -(e.g. [Amazon Web Services ELB][aws_elb]) that doesn't recognise particular -HTTP methods such as `PATCH`. +REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb]. + +To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header. For example, making a `PATCH` request via `POST` in jQuery: diff --git a/rest_framework/request.py b/rest_framework/request.py index f26d934d7..ffbbab338 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -233,11 +233,14 @@ class Request(object): self.META.get('CONTENT_TYPE', '')) self._perform_form_overloading() + if not _hasattr(self, '_method'): - # Method wasn't overloaded by hidden form element, so look for - # method override in header. If not present default to raw HTTP method - self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', - self._request.method) + self._method = self._request.method + + if self._method == 'POST': + # Allow X-HTTP-METHOD-OVERRIDE header + self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', + self._method) def _load_stream(self): """ From 208407d569b4c794f7ea6ec114b662b6faf56845 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 20:49:44 +0000 Subject: [PATCH 12/21] 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 ac201e20b..d0b46c36e 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`: * OAuth 2 support. * OAuth 1.0a support. +* Support X-HTTP-Method-Override header. * Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) * Deal with error data nicely when deserializing lists of objects. * Extra override hook to configure `DjangoModelPermissions` for unauthenticated users. From 1aecd71eb49111009f2c55fe8bd3901b3ea35dd5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 20:52:04 +0000 Subject: [PATCH 13/21] Added @kuhnza for work on #695. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b0f0cfa28..b533daa99 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -111,6 +111,7 @@ The following people have helped make REST framework great. * Ian Dash - [bitmonkey] * Bouke Haarsma - [bouke] * Pierre Dulac - [dulaccc] +* Dave Kuhn - [kuhnza] Many thanks to everyone who's contributed to the project. @@ -256,3 +257,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [bitmonkey]: https://github.com/bitmonkey [bouke]: https://github.com/bouke [dulaccc]: https://github.com/dulaccc +[kuhnza]: https://github.com/kuhnza From a798a5350a6aa3100695d41d4d37ec7e2e073bdd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 11:42:12 +0000 Subject: [PATCH 14/21] Fix duplicated database queries for paginated lists. Closes #713. --- docs/topics/release-notes.md | 1 + rest_framework/fields.py | 16 +++++++++------- rest_framework/tests/pagination.py | 10 +++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d0b46c36e..4eaa42ba6 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -48,6 +48,7 @@ You can determine your currently installed version using `pip freeze`: * Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) * Deal with error data nicely when deserializing lists of objects. * Extra override hook to configure `DjangoModelPermissions` for unauthenticated users. +* Bugfix: Fix regression which caused extra database query on paginated list views. * Bugfix: Fix pk relationship bug for some types of 1-to-1 relations. * Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0a199f102..4b6931ad4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -26,14 +26,16 @@ def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. """ - try: - args, _, _, defaults = inspect.getargspec(obj) - except TypeError: + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): return False - else: - len_args = len(args) if inspect.isfunction(obj) else len(args) - 1 - len_defaults = len(defaults) if defaults else 0 - return len_args <= len_defaults + + args, _, _, defaults = inspect.getargspec(obj) + len_args = len(args) if function else len(args) - 1 + len_defaults = len(defaults) if defaults else 0 + return len_args <= len_defaults def get_component(obj, attr_name): diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 472ffcddd..3c76ca7d1 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -73,7 +73,9 @@ class IntegrationTestPagination(TestCase): GET requests to paginated ListCreateAPIView should return paginated results. """ request = factory.get('/') - response = self.view(request).render() + # Note: Database queries are a `SELECT COUNT`, and `SELECT ` + with self.assertNumQueries(2): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 26) self.assertEqual(response.data['results'], self.data[:10]) @@ -81,7 +83,8 @@ class IntegrationTestPagination(TestCase): self.assertEqual(response.data['previous'], None) request = factory.get(response.data['next']) - response = self.view(request).render() + with self.assertNumQueries(2): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 26) self.assertEqual(response.data['results'], self.data[10:20]) @@ -89,7 +92,8 @@ class IntegrationTestPagination(TestCase): self.assertNotEqual(response.data['previous'], None) request = factory.get(response.data['next']) - response = self.view(request).render() + with self.assertNumQueries(2): + response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 26) self.assertEqual(response.data['results'], self.data[20:]) From 73ab7dc3f18c43d7bfb0c6f7581784d398cb36cf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 12:45:54 +0000 Subject: [PATCH 15/21] Use django-filter 0.6a1 and add database query count tests for paginated, filtered lists. --- .travis.yml | 4 +- rest_framework/tests/pagination.py | 100 +++++++++++++++++++++++------ tox.ini | 12 ++-- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f2fe9ad0..205feef92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,8 @@ install: - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" + - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" + - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi" - export PYTHONPATH=. script: diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3c76ca7d1..1a2d68a68 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import datetime from decimal import Decimal +import django from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory @@ -20,21 +21,6 @@ class RootView(generics.ListCreateAPIView): paginate_by = 10 -if django_filters: - class DecimalFilter(django_filters.FilterSet): - decimal = django_filters.NumberFilter(lookup_type='lt') - - class Meta: - model = FilterableItem - fields = ['text', 'decimal', 'date'] - - class FilterFieldsRootView(generics.ListCreateAPIView): - model = FilterableItem - paginate_by = 10 - filter_class = DecimalFilter - filter_backend = filters.DjangoFilterBackend - - class DefaultPageSizeKwargView(generics.ListAPIView): """ View for testing default paginate_by_param usage @@ -119,17 +105,44 @@ class IntegrationTestPaginationAndFiltering(TestCase): {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()} for obj in self.objects.all() ] - self.view = FilterFieldsRootView.as_view() @unittest.skipUnless(django_filters, 'django-filters not installed') - def test_get_paginated_filtered_root_view(self): + def test_get_django_filter_paginated_filtered_root_view(self): """ GET requests to paginated filtered ListCreateAPIView should return paginated results. The next and previous links should preserve the filtered parameters. """ + class DecimalFilter(django_filters.FilterSet): + decimal = django_filters.NumberFilter(lookup_type='lt') + + class Meta: + model = FilterableItem + fields = ['text', 'decimal', 'date'] + + class FilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + paginate_by = 10 + filter_class = DecimalFilter + filter_backend = filters.DjangoFilterBackend + + view = FilterFieldsRootView.as_view() + + EXPECTED_NUM_QUERIES = 2 + if django.VERSION < (1, 4): + # On Django 1.3 we need to use django-filter 0.5.4 + # + # The filter objects there don't expose a `.count()` method, + # which means we only make a single query *but* it's a single + # query across *all* of the queryset, instead of a COUNT and then + # a SELECT with a LIMIT. + # + # Although this is fewer queries, it's actually a regression. + EXPECTED_NUM_QUERIES = 1 + request = factory.get('/?decimal=15.20') - response = self.view(request).render() + with self.assertNumQueries(EXPECTED_NUM_QUERIES): + response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 15) self.assertEqual(response.data['results'], self.data[:10]) @@ -137,7 +150,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertEqual(response.data['previous'], None) request = factory.get(response.data['next']) - response = self.view(request).render() + with self.assertNumQueries(EXPECTED_NUM_QUERIES): + response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 15) self.assertEqual(response.data['results'], self.data[10:15]) @@ -145,7 +159,53 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertNotEqual(response.data['previous'], None) request = factory.get(response.data['previous']) - response = self.view(request).render() + with self.assertNumQueries(EXPECTED_NUM_QUERIES): + response = view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 15) + self.assertEqual(response.data['results'], self.data[:10]) + self.assertNotEqual(response.data['next'], None) + self.assertEqual(response.data['previous'], None) + + def test_get_basic_paginated_filtered_root_view(self): + """ + Same as `test_get_django_filter_paginated_filtered_root_view`, + except using a custom filter backend instead of the django-filter + backend, + """ + + class DecimalFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + return queryset.filter(decimal__lt=Decimal(request.GET['decimal'])) + + class BasicFilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + paginate_by = 10 + filter_backend = DecimalFilterBackend + + view = BasicFilterFieldsRootView.as_view() + + request = factory.get('/?decimal=15.20') + with self.assertNumQueries(2): + response = view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 15) + self.assertEqual(response.data['results'], self.data[:10]) + self.assertNotEqual(response.data['next'], None) + self.assertEqual(response.data['previous'], None) + + request = factory.get(response.data['next']) + with self.assertNumQueries(2): + response = view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 15) + self.assertEqual(response.data['results'], self.data[10:15]) + self.assertEqual(response.data['next'], None) + self.assertNotEqual(response.data['previous'], None) + + request = factory.get(response.data['previous']) + with self.assertNumQueries(2): + response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 15) self.assertEqual(response.data['results'], self.data[:10]) diff --git a/tox.ini b/tox.ini index 677c5d42d..d62359a54 100644 --- a/tox.ini +++ b/tox.ini @@ -8,19 +8,19 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5 - -egit+git://github.com/alex/django-filter.git#egg=django_filter + django-filter==0.6a1 defusedxml==0.3 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5 - -egit+git://github.com/alex/django-filter.git#egg=django_filter + django-filter==0.6a1 defusedxml==0.3 [testenv:py2.7-django1.5] basepython = python2.7 deps = django==1.5 - django-filter==0.5.4 + django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 oauth2==1.5.211 @@ -29,7 +29,7 @@ deps = django==1.5 [testenv:py2.6-django1.5] basepython = python2.6 deps = django==1.5 - django-filter==0.5.4 + django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 oauth2==1.5.211 @@ -38,7 +38,7 @@ deps = django==1.5 [testenv:py2.7-django1.4] basepython = python2.7 deps = django==1.4.3 - django-filter==0.5.4 + django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 oauth2==1.5.211 @@ -47,7 +47,7 @@ deps = django==1.4.3 [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.3 - django-filter==0.5.4 + django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 oauth2==1.5.211 From 4b68089d44d3ede878eff58f6e3cdad86f5c832e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 13:07:46 +0000 Subject: [PATCH 16/21] Version 2.2.4 --- docs/topics/release-notes.md | 4 +++- rest_framework/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4eaa42ba6..5a96c09cc 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,7 +40,9 @@ You can determine your currently installed version using `pip freeze`: ## 2.2.x series -### Master +### 2.2.4 + +**Date**: 13th March 2013 * OAuth 2 support. * OAuth 1.0a support. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 1f5d6e623..cf0056360 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.2.3' +__version__ = '2.2.4' VERSION = __version__ # synonym From a53596ce28359e24313a5fb9bd8f3564eb12678e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 13:13:30 +0000 Subject: [PATCH 17/21] Docs for TokenHasReadWriteScope --- docs/api-guide/permissions.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 719ac1eff..4772c5e0d 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -105,6 +105,21 @@ The default behaviour can also be overridden to support custom model permissions To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. +## TokenHasReadWriteScope + +This permission class is intended for use with either of the `OAuthAuthentication` and `OAuth2Authentication` classes, and ties into the scoping that their backends provide. + +Requests with a safe methods of `GET`, `OPTIONS` or `HEAD` will be allowed if the authenticated token has read permission. + +Requests for `POST`, `PUT`, `PATCH` and `DELETE` will be allowed if the authenticated token has write permission. + +This permission class relies on the implementations of the [django-oauth-plus][django-oauth-plus] and [django-oauth2-provider][django-oauth2-provider] libraries, which both provide limited support for controlling the scope of access tokens: + +* `django-oauth-plus`: Tokens are associated with a `Resource` class which has a `name`, `url` and `is_readonly` properties. +* `django-oauth2-provider`: Tokens are associated with a bitwise `scope` attribute, that defaults to providing bitwise values for `read` and/or `write`. + +If you require more advanced scoping for your API, such as restricting tokens to accessing a subset of functionality of your API then you will need to provide a custom permission class. See the source of the `django-oauth-plus` or `django-oauth2-provider` package for more details on scoping token access. + --- # Custom permissions @@ -173,5 +188,7 @@ Also note that the generic views will only check the object-level permissions fo [throttling]: throttling.md [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions [guardian]: https://github.com/lukaszb/django-guardian +[django-oauth-plus]: http://code.larlet.fr/django-oauth-plus +[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [2.2-announcement]: ../topics/2.2-announcement.md [filtering]: filtering.md From acc8c1faa4f85dda00723d755e56bb3c980dbc75 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 20:40:39 +0000 Subject: [PATCH 18/21] force_insert, force_update arguments. Closes #484. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed by `assertNumQueries(…)` in tests. --- docs/topics/release-notes.md | 4 ++++ rest_framework/mixins.py | 6 ++++-- rest_framework/serializers.py | 14 +++++++------- rest_framework/tests/generics.py | 10 +++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 5a96c09cc..c45fff880 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.2.x series +### Master + +* `Serializer.save()` now supports arbitrary keyword args which are passed through to the object `.save()` method. Mixins use `force_insert` and `force_update` where appropriate, resulting in one less database query. + ### 2.2.4 **Date**: 13th March 2013 diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8e4012049..7d9a6e654 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -44,7 +44,7 @@ class CreateModelMixin(object): if serializer.is_valid(): self.pre_save(serializer.object) - self.object = serializer.save() + self.object = serializer.save(force_insert=True) self.post_save(self.object, created=True) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, @@ -119,9 +119,11 @@ class UpdateModelMixin(object): # we have relevant permissions, as if this was a POST request. self.check_permissions(clone_request(request, 'POST')) created = True + save_kwargs = {'force_insert': True} success_status_code = status.HTTP_201_CREATED else: created = False + save_kwargs = {'force_update': True} success_status_code = status.HTTP_200_OK serializer = self.get_serializer(self.object, data=request.DATA, @@ -129,7 +131,7 @@ class UpdateModelMixin(object): if serializer.is_valid(): self.pre_save(serializer.object) - self.object = serializer.save() + self.object = serializer.save(**save_kwargs) self.post_save(self.object, created=created) return Response(serializer.data, status=success_status_code) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cd2bb8f1f..4fe857a61 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -391,17 +391,17 @@ class BaseSerializer(Field): return self._data - def save_object(self, obj): - obj.save() + def save_object(self, obj, **kwargs): + obj.save(**kwargs) - def save(self): + def save(self, **kwargs): """ Save the deserialized object and return it. """ if isinstance(self.object, list): - [self.save_object(item) for item in self.object] + [self.save_object(item, **kwargs) for item in self.object] else: - self.save_object(self.object) + self.save_object(self.object, **kwargs) return self.object @@ -621,11 +621,11 @@ class ModelSerializer(Serializer): if instance: return self.full_clean(instance) - def save_object(self, obj): + def save_object(self, obj, **kwargs): """ Save the deserialized object and return it. """ - obj.save() + obj.save(**kwargs) if getattr(self, 'm2m_data', None): for accessor_name, object_list in self.m2m_data.items(): diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 1837898b5..f564890cc 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -184,7 +184,7 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - with self.assertNumQueries(3): + with self.assertNumQueries(2): response = self.view(request, pk='1').render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -199,7 +199,7 @@ class TestInstanceView(TestCase): request = factory.patch('/1', json.dumps(content), content_type='application/json') - with self.assertNumQueries(3): + with self.assertNumQueries(2): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -248,7 +248,7 @@ class TestInstanceView(TestCase): content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - with self.assertNumQueries(3): + with self.assertNumQueries(2): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -264,7 +264,7 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - with self.assertNumQueries(4): + with self.assertNumQueries(3): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -280,7 +280,7 @@ class TestInstanceView(TestCase): # pk fields can not be created on demand, only the database can set the pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') - with self.assertNumQueries(4): + with self.assertNumQueries(3): response = self.view(request, pk=5).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) new_obj = self.objects.get(pk=5) From 08bc203f905f838fdcc2f7cc09b91eab4e595bd1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 20:53:39 +0000 Subject: [PATCH 19/21] Docs tweaks. --- docs/index.md | 6 +++--- docs/template.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8e5097b37..5f9d15532 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,9 +9,9 @@ # Django REST framework -**A toolkit for building well-connected, self-describing Web APIs.** +**Web APIs for Django, made easy.** -Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. +Django REST framework is a flexible, powerful library that makes it incredibly easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. @@ -75,7 +75,7 @@ Note that the URL path can be whatever you want, but you must include `'rest_fra ## Quickstart -Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework. +Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework. ## Tutorial diff --git a/docs/template.html b/docs/template.html index e0f88daf5..08620882d 100644 --- a/docs/template.html +++ b/docs/template.html @@ -2,7 +2,7 @@ - Django REST framework + Django REST framework - APIs made easy. From da76bd704726830b0b76aabe7aef91b2deb72b02 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 20:56:16 +0000 Subject: [PATCH 20/21] Tweak text. --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 5f9d15532..5357536d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ Django REST framework is a flexible, powerful library that makes it incredibly easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. -Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. +APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. From 22a389d0ba4dd5ac7b4fa3839491ec2708bbe7df Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Mar 2013 13:41:22 +0000 Subject: [PATCH 21/21] Better titles & descriptions --- docs/template.html | 6 +++--- mkdocs.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/template.html b/docs/template.html index 08620882d..3e0f29aa0 100644 --- a/docs/template.html +++ b/docs/template.html @@ -2,11 +2,11 @@ - Django REST framework - APIs made easy. + {{ title }} - - + + diff --git a/mkdocs.py b/mkdocs.py index 2918f7d3b..f6c89e04b 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -57,24 +57,36 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): toc = '' text = open(path, 'r').read().decode('utf-8') + main_title = None + description = 'Django, API, REST' for line in text.splitlines(): if line.startswith('# '): title = line[2:].strip() template = main_header + description = description + ', ' + title elif line.startswith('## '): title = line[3:].strip() template = sub_header else: continue + if not main_title: + main_title = title anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '') template = template.replace('{{ title }}', title) template = template.replace('{{ anchor }}', anchor) toc += template + '\n' + if filename == 'index.md': + main_title = 'Django REST framework - APIs made easy' + else: + main_title = 'Django REST framework - ' + main_title + content = markdown.markdown(text, ['headerid']) output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index) + output = output.replace('{{ title }}', main_title) + output = output.replace('{{ description }}', description) output = output.replace('{{ page_id }}', filename[:-3]) output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output) output = re.sub(r'
:::bash', r'
', output)