From 9dccbcbb3800f83149edf08330f6926659bc5d73 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Sun, 3 Mar 2013 00:00:58 +1100 Subject: Support for X-HTTP-Method-Override header --- rest_framework/request.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/request.py b/rest_framework/request.py index 3e2fbd88..4cdc8b73 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): -- cgit v1.2.3 From 104614c600a391b2d416074f3929e543b86a8492 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Mon, 4 Mar 2013 07:14:38 +1100 Subject: Modified to allow form overloading to take precedence over header. --- rest_framework/request.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/request.py b/rest_framework/request.py index 4cdc8b73..f26d934d 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): """ -- cgit v1.2.3 From a91dca178dc9681a0411a343b960a6f9a9dd8011 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Fri, 8 Mar 2013 17:01:43 +0000 Subject: Correcy typo. --- rest_framework/tests/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f7093401..5f206769 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() -- cgit v1.2.3 From 332c99748f2e0ebc6490b8e7379d8a4b48ba8ee2 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Fri, 8 Mar 2013 17:36:43 +0000 Subject: Add some simple numQueries tests. --- rest_framework/tests/generics.py | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 5f206769..1837898b 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') -- cgit v1.2.3 From e7e470739fc4d2694d1c0e2dbf3f6465b44ad069 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 03:23:44 -0400 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2ae7c215..1b044c5f 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 f2117538..fcfe5a0c 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 beb372c2..93909b65 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.""" -- cgit v1.2.3 From bdcecf48e3c0bd592d50b6b6fac6353a41c3cbfd Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 16:01:14 -0400 Subject: Simplify inherited child check to not use isinstance --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1b044c5f..cd2bb8f1 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] -- cgit v1.2.3 From 354fbc64ba5046ce49d58c8243a4f81caddf3823 Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 17:28:44 -0400 Subject: Group the model-inheritance-related tests together --- rest_framework/tests/models.py | 14 ------ rest_framework/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 (limited to 'rest_framework') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index fcfe5a0c..f2117538 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 00000000..1cca7823 --- /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 93909b65..c4a8a899 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) -- cgit v1.2.3 From bd3fe75e1a41e45b0c9ff1e39707ee059ad0e06a Mon Sep 17 00:00:00 2001 From: Mjumbe Wawatu Ukweli Date: Mon, 11 Mar 2013 17:32:32 -0400 Subject: Further group model inheritance tests --- rest_framework/tests/multitable_inheritance.py | 51 ++++++++++++++++++++++++++ rest_framework/tests/serializer.py | 48 ------------------------ 2 files changed, 51 insertions(+), 48 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/multitable_inheritance.py b/rest_framework/tests/multitable_inheritance.py index 1cca7823..00c15327 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 c4a8a899..beb372c2 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) -- cgit v1.2.3 From 2e481f3318609fff5b884a09cbc9d2c5782deae4 Mon Sep 17 00:00:00 2001 From: Dave Kuhn Date: Tue, 12 Mar 2013 12:00:41 +1100 Subject: Added test for X-HTTP-Method-Override header --- rest_framework/tests/request.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 4892f7a6..97e5af20 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): -- cgit v1.2.3 From 377dc2cda2c3a7aa02f5d761631f73c58745ed9d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 20:49:20 +0000 Subject: Only honor X-HTTP-Method-Override for POST requests. --- rest_framework/request.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/request.py b/rest_framework/request.py index f26d934d..ffbbab33 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): """ -- cgit v1.2.3 From a798a5350a6aa3100695d41d4d37ec7e2e073bdd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 11:42:12 +0000 Subject: Fix duplicated database queries for paginated lists. Closes #713. --- rest_framework/fields.py | 16 +++++++++------- rest_framework/tests/pagination.py | 10 +++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0a199f10..4b6931ad 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 472ffcdd..3c76ca7d 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:]) -- cgit v1.2.3 From 73ab7dc3f18c43d7bfb0c6f7581784d398cb36cf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 12:45:54 +0000 Subject: Use django-filter 0.6a1 and add database query count tests for paginated, filtered lists. --- rest_framework/tests/pagination.py | 100 +++++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 20 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3c76ca7d..1a2d68a6 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]) -- cgit v1.2.3 From 4b68089d44d3ede878eff58f6e3cdad86f5c832e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 13:07:46 +0000 Subject: Version 2.2.4 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 1f5d6e62..cf005636 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 -- cgit v1.2.3 From acc8c1faa4f85dda00723d755e56bb3c980dbc75 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Mar 2013 20:40:39 +0000 Subject: force_insert, force_update arguments. Closes #484. Confirmed by `assertNumQueries(…)` in tests. --- rest_framework/mixins.py | 6 ++++-- rest_framework/serializers.py | 14 +++++++------- rest_framework/tests/generics.py | 10 +++++----- 3 files changed, 16 insertions(+), 14 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8e401204..7d9a6e65 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 cd2bb8f1..4fe857a6 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 1837898b..f564890c 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) -- cgit v1.2.3