mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-18 20:41:04 +03:00
Merge branch 'master' into basic-nested-serialization
This commit is contained in:
commit
1aedf57f4a
|
@ -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 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-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-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 [[ ${DJANGO::11} == 'django==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.6a1 --use-mirrors; fi"
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -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.
|
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
|
# Custom permissions
|
||||||
|
@ -173,5 +188,7 @@ Also note that the generic views will only check the object-level permissions fo
|
||||||
[throttling]: throttling.md
|
[throttling]: throttling.md
|
||||||
[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
|
[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
|
||||||
[guardian]: https://github.com/lukaszb/django-guardian
|
[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
|
[2.2-announcement]: ../topics/2.2-announcement.md
|
||||||
[filtering]: filtering.md
|
[filtering]: filtering.md
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
|
|
||||||
# Django REST framework
|
# 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.
|
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
|
## 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
|
## Tutorial
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Django REST framework</title>
|
<title>{{ title }}</title>
|
||||||
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
|
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="{{ description }}">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="Tom Christie">
|
||||||
|
|
||||||
<!-- Le styles -->
|
<!-- Le styles -->
|
||||||
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
|
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
|
||||||
|
|
|
@ -19,6 +19,21 @@ For example, given the following form:
|
||||||
|
|
||||||
`request.method` would return `"DELETE"`.
|
`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 non-form content
|
||||||
|
|
||||||
Browser-based submission of content types other than form are supported by
|
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
|
[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
|
[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
||||||
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
||||||
|
[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724
|
||||||
|
|
|
@ -111,6 +111,7 @@ The following people have helped make REST framework great.
|
||||||
* Ian Dash - [bitmonkey]
|
* Ian Dash - [bitmonkey]
|
||||||
* Bouke Haarsma - [bouke]
|
* Bouke Haarsma - [bouke]
|
||||||
* Pierre Dulac - [dulaccc]
|
* Pierre Dulac - [dulaccc]
|
||||||
|
* Dave Kuhn - [kuhnza]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
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
|
[bitmonkey]: https://github.com/bitmonkey
|
||||||
[bouke]: https://github.com/bouke
|
[bouke]: https://github.com/bouke
|
||||||
[dulaccc]: https://github.com/dulaccc
|
[dulaccc]: https://github.com/dulaccc
|
||||||
|
[kuhnza]: https://github.com/kuhnza
|
||||||
|
|
|
@ -42,11 +42,19 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
### Master
|
### 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 2 support.
|
||||||
* OAuth 1.0a 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)
|
* 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.
|
* Deal with error data nicely when deserializing lists of objects.
|
||||||
* Extra override hook to configure `DjangoModelPermissions` for unauthenticated users.
|
* 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: 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.
|
* Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed.
|
||||||
|
|
||||||
|
|
12
mkdocs.py
12
mkdocs.py
|
@ -57,24 +57,36 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
|
|
||||||
toc = ''
|
toc = ''
|
||||||
text = open(path, 'r').read().decode('utf-8')
|
text = open(path, 'r').read().decode('utf-8')
|
||||||
|
main_title = None
|
||||||
|
description = 'Django, API, REST'
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
if line.startswith('# '):
|
if line.startswith('# '):
|
||||||
title = line[2:].strip()
|
title = line[2:].strip()
|
||||||
template = main_header
|
template = main_header
|
||||||
|
description = description + ', ' + title
|
||||||
elif line.startswith('## '):
|
elif line.startswith('## '):
|
||||||
title = line[3:].strip()
|
title = line[3:].strip()
|
||||||
template = sub_header
|
template = sub_header
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not main_title:
|
||||||
|
main_title = title
|
||||||
anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '')
|
anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '')
|
||||||
template = template.replace('{{ title }}', title)
|
template = template.replace('{{ title }}', title)
|
||||||
template = template.replace('{{ anchor }}', anchor)
|
template = template.replace('{{ anchor }}', anchor)
|
||||||
toc += template + '\n'
|
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'])
|
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 = 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 = output.replace('{{ page_id }}', filename[:-3])
|
||||||
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
|
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
|
||||||
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
|
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '2.2.3'
|
__version__ = '2.2.4'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,16 @@ def is_simple_callable(obj):
|
||||||
"""
|
"""
|
||||||
True if the object is a callable that takes no arguments.
|
True if the object is a callable that takes no arguments.
|
||||||
"""
|
"""
|
||||||
try:
|
function = inspect.isfunction(obj)
|
||||||
args, _, _, defaults = inspect.getargspec(obj)
|
method = inspect.ismethod(obj)
|
||||||
except TypeError:
|
|
||||||
|
if not (function or method):
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
len_args = len(args) if inspect.isfunction(obj) else len(args) - 1
|
args, _, _, defaults = inspect.getargspec(obj)
|
||||||
len_defaults = len(defaults) if defaults else 0
|
len_args = len(args) if function else len(args) - 1
|
||||||
return len_args <= len_defaults
|
len_defaults = len(defaults) if defaults else 0
|
||||||
|
return len_args <= len_defaults
|
||||||
|
|
||||||
|
|
||||||
def get_component(obj, attr_name):
|
def get_component(obj, attr_name):
|
||||||
|
|
|
@ -44,7 +44,7 @@ class CreateModelMixin(object):
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
self.object = serializer.save()
|
self.object = serializer.save(force_insert=True)
|
||||||
self.post_save(self.object, created=True)
|
self.post_save(self.object, created=True)
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
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.
|
# we have relevant permissions, as if this was a POST request.
|
||||||
self.check_permissions(clone_request(request, 'POST'))
|
self.check_permissions(clone_request(request, 'POST'))
|
||||||
created = True
|
created = True
|
||||||
|
save_kwargs = {'force_insert': True}
|
||||||
success_status_code = status.HTTP_201_CREATED
|
success_status_code = status.HTTP_201_CREATED
|
||||||
else:
|
else:
|
||||||
created = False
|
created = False
|
||||||
|
save_kwargs = {'force_update': True}
|
||||||
success_status_code = status.HTTP_200_OK
|
success_status_code = status.HTTP_200_OK
|
||||||
|
|
||||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||||
|
@ -129,7 +131,7 @@ class UpdateModelMixin(object):
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
self.object = serializer.save()
|
self.object = serializer.save(**save_kwargs)
|
||||||
self.post_save(self.object, created=created)
|
self.post_save(self.object, created=created)
|
||||||
return Response(serializer.data, status=success_status_code)
|
return Response(serializer.data, status=success_status_code)
|
||||||
|
|
||||||
|
|
|
@ -231,11 +231,17 @@ class Request(object):
|
||||||
"""
|
"""
|
||||||
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
|
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
|
||||||
self.META.get('CONTENT_TYPE', ''))
|
self.META.get('CONTENT_TYPE', ''))
|
||||||
|
|
||||||
self._perform_form_overloading()
|
self._perform_form_overloading()
|
||||||
# if the HTTP method was not overloaded, we take the raw HTTP method
|
|
||||||
if not _hasattr(self, '_method'):
|
if not _hasattr(self, '_method'):
|
||||||
self._method = 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):
|
def _load_stream(self):
|
||||||
"""
|
"""
|
||||||
Return the content body of the request, as a stream.
|
Return the content body of the request, as a stream.
|
||||||
|
|
|
@ -437,17 +437,17 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
def save_object(self, obj):
|
def save_object(self, obj, **kwargs):
|
||||||
obj.save()
|
obj.save(**kwargs)
|
||||||
|
|
||||||
def save(self):
|
def save(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object and return it.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.object, list):
|
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:
|
else:
|
||||||
self.save_object(self.object)
|
self.save_object(self.object, **kwargs)
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
|
|
||||||
|
@ -502,8 +502,11 @@ class ModelSerializer(Serializer):
|
||||||
"Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
|
"Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
|
||||||
opts = get_concrete_model(cls)._meta
|
opts = get_concrete_model(cls)._meta
|
||||||
pk_field = opts.pk
|
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 = [pk_field]
|
||||||
fields += [field for field in opts.fields if field.serialize]
|
fields += [field for field in opts.fields if field.serialize]
|
||||||
fields += [field for field in opts.many_to_many if field.serialize]
|
fields += [field for field in opts.many_to_many if field.serialize]
|
||||||
|
@ -664,11 +667,11 @@ class ModelSerializer(Serializer):
|
||||||
if instance:
|
if instance:
|
||||||
return self.full_clean(instance)
|
return self.full_clean(instance)
|
||||||
|
|
||||||
def save_object(self, obj):
|
def save_object(self, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object and return it.
|
||||||
"""
|
"""
|
||||||
obj.save()
|
obj.save(**kwargs)
|
||||||
|
|
||||||
if getattr(self, 'm2m_data', None):
|
if getattr(self, 'm2m_data', None):
|
||||||
for accessor_name, object_list in self.m2m_data.items():
|
for accessor_name, object_list in self.m2m_data.items():
|
||||||
|
|
|
@ -60,7 +60,8 @@ class TestRootView(TestCase):
|
||||||
GET requests to ListCreateAPIView should return list of objects.
|
GET requests to ListCreateAPIView should return list of objects.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, self.data)
|
self.assertEqual(response.data, self.data)
|
||||||
|
|
||||||
|
@ -71,7 +72,8 @@ class TestRootView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.post('/', json.dumps(content),
|
request = factory.post('/', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data, {'id': 4, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 4, 'text': 'foobar'})
|
||||||
created = self.objects.get(id=4)
|
created = self.objects.get(id=4)
|
||||||
|
@ -84,7 +86,8 @@ class TestRootView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.put('/', json.dumps(content),
|
request = factory.put('/', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
self.assertEqual(response.data, {"detail": "Method 'PUT' 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
|
DELETE requests to ListCreateAPIView should not be allowed
|
||||||
"""
|
"""
|
||||||
request = factory.delete('/')
|
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.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
self.assertEqual(response.data, {"detail": "Method 'DELETE' 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
|
OPTIONS requests to ListCreateAPIView should return metadata
|
||||||
"""
|
"""
|
||||||
request = factory.options('/')
|
request = factory.options('/')
|
||||||
response = self.view(request).render()
|
with self.assertNumQueries(0):
|
||||||
|
response = self.view(request).render()
|
||||||
expected = {
|
expected = {
|
||||||
'parses': [
|
'parses': [
|
||||||
'application/json',
|
'application/json',
|
||||||
|
@ -126,7 +131,8 @@ class TestRootView(TestCase):
|
||||||
content = {'id': 999, 'text': 'foobar'}
|
content = {'id': 999, 'text': 'foobar'}
|
||||||
request = factory.post('/', json.dumps(content),
|
request = factory.post('/', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data, {'id': 4, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 4, 'text': 'foobar'})
|
||||||
created = self.objects.get(id=4)
|
created = self.objects.get(id=4)
|
||||||
|
@ -154,7 +160,8 @@ class TestInstanceView(TestCase):
|
||||||
GET requests to RetrieveUpdateDestroyAPIView should return a single object.
|
GET requests to RetrieveUpdateDestroyAPIView should return a single object.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/1')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, self.data[0])
|
self.assertEqual(response.data, self.data[0])
|
||||||
|
|
||||||
|
@ -165,7 +172,8 @@ class TestInstanceView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.post('/', json.dumps(content),
|
request = factory.post('/', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."})
|
self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."})
|
||||||
|
|
||||||
|
@ -176,7 +184,8 @@ class TestInstanceView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.put('/1', json.dumps(content),
|
request = factory.put('/1', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
|
@ -190,7 +199,8 @@ class TestInstanceView(TestCase):
|
||||||
request = factory.patch('/1', json.dumps(content),
|
request = factory.patch('/1', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
|
@ -201,7 +211,8 @@ class TestInstanceView(TestCase):
|
||||||
DELETE requests to RetrieveUpdateDestroyAPIView should delete an object.
|
DELETE requests to RetrieveUpdateDestroyAPIView should delete an object.
|
||||||
"""
|
"""
|
||||||
request = factory.delete('/1')
|
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.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(response.content, six.b(''))
|
self.assertEqual(response.content, six.b(''))
|
||||||
ids = [obj.id for obj in self.objects.all()]
|
ids = [obj.id for obj in self.objects.all()]
|
||||||
|
@ -212,7 +223,8 @@ class TestInstanceView(TestCase):
|
||||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||||
"""
|
"""
|
||||||
request = factory.options('/')
|
request = factory.options('/')
|
||||||
response = self.view(request).render()
|
with self.assertNumQueries(0):
|
||||||
|
response = self.view(request).render()
|
||||||
expected = {
|
expected = {
|
||||||
'parses': [
|
'parses': [
|
||||||
'application/json',
|
'application/json',
|
||||||
|
@ -236,7 +248,8 @@ class TestInstanceView(TestCase):
|
||||||
content = {'id': 999, 'text': 'foobar'}
|
content = {'id': 999, 'text': 'foobar'}
|
||||||
request = factory.put('/1', json.dumps(content),
|
request = factory.put('/1', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
|
@ -251,7 +264,8 @@ class TestInstanceView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.put('/1', json.dumps(content),
|
request = factory.put('/1', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
|
@ -263,10 +277,11 @@ class TestInstanceView(TestCase):
|
||||||
at the requested url if it doesn't exist.
|
at the requested url if it doesn't exist.
|
||||||
"""
|
"""
|
||||||
content = {'text': 'foobar'}
|
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),
|
request = factory.put('/5', json.dumps(content),
|
||||||
content_type='application/json')
|
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)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
new_obj = self.objects.get(pk=5)
|
new_obj = self.objects.get(pk=5)
|
||||||
self.assertEqual(new_obj.text, 'foobar')
|
self.assertEqual(new_obj.text, 'foobar')
|
||||||
|
@ -279,7 +294,8 @@ class TestInstanceView(TestCase):
|
||||||
content = {'text': 'foobar'}
|
content = {'text': 'foobar'}
|
||||||
request = factory.put('/test_slug', json.dumps(content),
|
request = factory.put('/test_slug', json.dumps(content),
|
||||||
content_type='application/json')
|
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.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||||
|
|
67
rest_framework/tests/multitable_inheritance.py
Normal file
67
rest_framework/tests/multitable_inheritance.py
Normal file
|
@ -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)
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import django
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -20,21 +21,6 @@ class RootView(generics.ListCreateAPIView):
|
||||||
paginate_by = 10
|
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):
|
class DefaultPageSizeKwargView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
View for testing default paginate_by_param usage
|
View for testing default paginate_by_param usage
|
||||||
|
@ -73,7 +59,9 @@ class IntegrationTestPagination(TestCase):
|
||||||
GET requests to paginated ListCreateAPIView should return paginated results.
|
GET requests to paginated ListCreateAPIView should return paginated results.
|
||||||
"""
|
"""
|
||||||
request = factory.get('/')
|
request = factory.get('/')
|
||||||
response = self.view(request).render()
|
# Note: Database queries are a `SELECT COUNT`, and `SELECT <fields>`
|
||||||
|
with self.assertNumQueries(2):
|
||||||
|
response = self.view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 26)
|
self.assertEqual(response.data['count'], 26)
|
||||||
self.assertEqual(response.data['results'], self.data[:10])
|
self.assertEqual(response.data['results'], self.data[:10])
|
||||||
|
@ -81,7 +69,8 @@ class IntegrationTestPagination(TestCase):
|
||||||
self.assertEqual(response.data['previous'], None)
|
self.assertEqual(response.data['previous'], None)
|
||||||
|
|
||||||
request = factory.get(response.data['next'])
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 26)
|
self.assertEqual(response.data['count'], 26)
|
||||||
self.assertEqual(response.data['results'], self.data[10:20])
|
self.assertEqual(response.data['results'], self.data[10:20])
|
||||||
|
@ -89,7 +78,8 @@ class IntegrationTestPagination(TestCase):
|
||||||
self.assertNotEqual(response.data['previous'], None)
|
self.assertNotEqual(response.data['previous'], None)
|
||||||
|
|
||||||
request = factory.get(response.data['next'])
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 26)
|
self.assertEqual(response.data['count'], 26)
|
||||||
self.assertEqual(response.data['results'], self.data[20:])
|
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()}
|
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date.isoformat()}
|
||||||
for obj in self.objects.all()
|
for obj in self.objects.all()
|
||||||
]
|
]
|
||||||
self.view = FilterFieldsRootView.as_view()
|
|
||||||
|
|
||||||
@unittest.skipUnless(django_filters, 'django-filters not installed')
|
@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
|
GET requests to paginated filtered ListCreateAPIView should return
|
||||||
paginated results. The next and previous links should preserve the
|
paginated results. The next and previous links should preserve the
|
||||||
filtered parameters.
|
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')
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 15)
|
self.assertEqual(response.data['count'], 15)
|
||||||
self.assertEqual(response.data['results'], self.data[:10])
|
self.assertEqual(response.data['results'], self.data[:10])
|
||||||
|
@ -133,7 +150,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
self.assertEqual(response.data['previous'], None)
|
self.assertEqual(response.data['previous'], None)
|
||||||
|
|
||||||
request = factory.get(response.data['next'])
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 15)
|
self.assertEqual(response.data['count'], 15)
|
||||||
self.assertEqual(response.data['results'], self.data[10:15])
|
self.assertEqual(response.data['results'], self.data[10:15])
|
||||||
|
@ -141,7 +159,53 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
self.assertNotEqual(response.data['previous'], None)
|
self.assertNotEqual(response.data['previous'], None)
|
||||||
|
|
||||||
request = factory.get(response.data['previous'])
|
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.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 15)
|
self.assertEqual(response.data['count'], 15)
|
||||||
self.assertEqual(response.data['results'], self.data[:10])
|
self.assertEqual(response.data['results'], self.data[:10])
|
||||||
|
|
|
@ -58,6 +58,14 @@ class TestMethodOverloading(TestCase):
|
||||||
request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
|
request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
|
||||||
self.assertEqual(request.method, '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):
|
class TestContentParsing(TestCase):
|
||||||
def test_standard_behaviour_determines_no_content_GET(self):
|
def test_standard_behaviour_determines_no_content_GET(self):
|
||||||
|
|
12
tox.ini
12
tox.ini
|
@ -8,19 +8,19 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
||||||
[testenv:py3.3-django1.5]
|
[testenv:py3.3-django1.5]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = django==1.5
|
deps = django==1.5
|
||||||
-egit+git://github.com/alex/django-filter.git#egg=django_filter
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
|
|
||||||
[testenv:py3.2-django1.5]
|
[testenv:py3.2-django1.5]
|
||||||
basepython = python3.2
|
basepython = python3.2
|
||||||
deps = django==1.5
|
deps = django==1.5
|
||||||
-egit+git://github.com/alex/django-filter.git#egg=django_filter
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
|
|
||||||
[testenv:py2.7-django1.5]
|
[testenv:py2.7-django1.5]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.5
|
deps = django==1.5
|
||||||
django-filter==0.5.4
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
|
@ -29,7 +29,7 @@ deps = django==1.5
|
||||||
[testenv:py2.6-django1.5]
|
[testenv:py2.6-django1.5]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.5
|
deps = django==1.5
|
||||||
django-filter==0.5.4
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
|
@ -38,7 +38,7 @@ deps = django==1.5
|
||||||
[testenv:py2.7-django1.4]
|
[testenv:py2.7-django1.4]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.4.3
|
deps = django==1.4.3
|
||||||
django-filter==0.5.4
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
|
@ -47,7 +47,7 @@ deps = django==1.4.3
|
||||||
[testenv:py2.6-django1.4]
|
[testenv:py2.6-django1.4]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.4.3
|
deps = django==1.4.3
|
||||||
django-filter==0.5.4
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
|
|
Loading…
Reference in New Issue
Block a user