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/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 diff --git a/docs/index.md b/docs/index.md index 8e5097b37..5357536d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,11 +9,11 @@ # 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. +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. @@ -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..3e0f29aa0 100644 --- a/docs/template.html +++ b/docs/template.html @@ -2,11 +2,11 @@ - Django REST framework + {{ title }} - - + + diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index 6a11f0fac..ce07fe95b 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -19,6 +19,21 @@ For example, given the following form: `request.method` would return `"DELETE"`. +## HTTP header based method overriding + +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: + + $.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 +77,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 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 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index ac201e20b..c45fff880 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,11 +42,19 @@ You can determine your currently installed version using `pip freeze`: ### 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 + * 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. +* 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/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)
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
 
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/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/request.py b/rest_framework/request.py
index 3e2fbd88e..ffbbab338 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -231,11 +231,17 @@ class Request(object):
         """
         self._content_type = self.META.get('HTTP_CONTENT_TYPE',
                                            self.META.get('CONTENT_TYPE', ''))
+
         self._perform_form_overloading()
-        # if the HTTP method was not overloaded, we take the raw HTTP method
+
         if not _hasattr(self, '_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):
         """
         Return the content body of the request, as a stream.
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index f83451d37..b4026852c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -437,17 +437,17 @@ class BaseSerializer(WritableField):
 
         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
 
 
@@ -502,8 +502,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 pk_field.rel 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]
@@ -664,11 +667,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 f70934016..f564890cc 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(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'})
         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(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'})
         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(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'})
         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(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'})
         updated = self.objects.get(id=1)
@@ -263,10 +277,11 @@ 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()
+        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)
         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')
diff --git a/rest_framework/tests/multitable_inheritance.py b/rest_framework/tests/multitable_inheritance.py
new file mode 100644
index 000000000..00c153276
--- /dev/null
+++ b/rest_framework/tests/multitable_inheritance.py
@@ -0,0 +1,67 @@
+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)
+
+
+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)
+
+
+# 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/pagination.py b/rest_framework/tests/pagination.py
index 472ffcddd..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
@@ -73,7 +59,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 +69,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 +78,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:])
@@ -115,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])
@@ -133,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])
@@ -141,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/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):
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