mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-29 13:04:03 +03:00
Merge branch 'master' into 2.4.0
Conflicts: .travis.yml docs/api-guide/routers.md docs/topics/release-notes.md rest_framework/compat.py
This commit is contained in:
commit
9c41c007af
14
.travis.yml
14
.travis.yml
|
@ -7,14 +7,14 @@ python:
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
|
- DJANGO="django==1.6.1"
|
||||||
- DJANGO="django==1.5.1 --use-mirrors"
|
- DJANGO="django==1.5.5"
|
||||||
- DJANGO="django==1.4.5 --use-mirrors"
|
- DJANGO="django==1.4.10"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
- pip install defusedxml==0.3 --use-mirrors
|
- pip install defusedxml==0.3
|
||||||
- pip install django-filter==0.6 --use-mirrors
|
- pip install django-filter==0.6
|
||||||
- "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.4 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
|
||||||
|
@ -27,6 +27,6 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.5 --use-mirrors"
|
env: DJANGO="django==1.4.10"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.5 --use-mirrors"
|
env: DJANGO="django==1.4.10"
|
||||||
|
|
|
@ -48,6 +48,7 @@ Let's take a look at a quick example of using REST framework to build a simple m
|
||||||
|
|
||||||
Here's our project's root `urls.py` module:
|
Here's our project's root `urls.py` module:
|
||||||
|
|
||||||
|
```python
|
||||||
from django.conf.urls.defaults import url, patterns, include
|
from django.conf.urls.defaults import url, patterns, include
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
from rest_framework import viewsets, routers
|
from rest_framework import viewsets, routers
|
||||||
|
@ -72,11 +73,13 @@ Here's our project's root `urls.py` module:
|
||||||
url(r'^', include(router.urls)),
|
url(r'^', include(router.urls)),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
```
|
||||||
|
|
||||||
We'd also like to configure a couple of settings for our API.
|
We'd also like to configure a couple of settings for our API.
|
||||||
|
|
||||||
Add the following to your `settings.py` module:
|
Add the following to your `settings.py` module:
|
||||||
|
|
||||||
|
```python
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
# Use hyperlinked styles by default.
|
# Use hyperlinked styles by default.
|
||||||
# Only used if the `serializer_class` attribute is not set on a view.
|
# Only used if the `serializer_class` attribute is not set on a view.
|
||||||
|
@ -89,7 +92,7 @@ Add the following to your `settings.py` module:
|
||||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
```
|
||||||
Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
|
Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
|
||||||
|
|
||||||
That's it, we're done!
|
That's it, we're done!
|
||||||
|
|
201
docs/404.html
Normal file
201
docs/404.html
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Django REST framework - 404 - Page not found</title>
|
||||||
|
<link href="http://django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||||
|
<link rel="canonical" href="http://django-rest-framework.org/404"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Django, API, REST, 404 - Page not found">
|
||||||
|
<meta name="author" content="Tom Christie">
|
||||||
|
|
||||||
|
<!-- Le styles -->
|
||||||
|
<link href="http://django-rest-framework.org/css/prettify.css" rel="stylesheet">
|
||||||
|
<link href="http://django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
|
||||||
|
<link href="http://django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
|
||||||
|
<link href="http://django-rest-framework.org/css/default.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var _gaq = _gaq || [];
|
||||||
|
_gaq.push(['_setAccount', 'UA-18852272-2']);
|
||||||
|
_gaq.push(['_trackPageview']);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||||
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||||
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||||
|
})();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="prettyPrint()" class="404-page">
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
|
||||||
|
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small disabled" href="#">Next <i class="icon-arrow-right icon-white"></i></a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small disabled" href="#"><i class="icon-arrow-left icon-white"></i> Previous</a>
|
||||||
|
<a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
|
||||||
|
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</a>
|
||||||
|
<a class="brand" href="http://django-rest-framework.org">Django REST framework</a>
|
||||||
|
<div class="nav-collapse collapse">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a href="http://django-rest-framework.org">Home</a></li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/requests">Requests</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/responses">Responses</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/views">Views</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/routers">Routers</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/parsers">Parsers</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/renderers">Renderers</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/serializers">Serializers</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/authentication">Authentication</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/permissions">Permissions</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/throttling">Throttling</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/filtering">Filtering</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/pagination">Pagination</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/testing">Testing</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/api-guide/settings">Settings</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/release-notes">Release Notes</a></li>
|
||||||
|
<li><a href="http://django-rest-framework.org/topics/credits">Credits</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="nav pull-right">
|
||||||
|
<!-- TODO
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="#">Trunk</a></li>
|
||||||
|
<li><a href="#">2.0.0</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
|
</ul>
|
||||||
|
</div><!--/.nav-collapse -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body-content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<!-- Search Modal -->
|
||||||
|
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h3 id="myModalLabel">Documentation search</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Custom google search -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var cx = '015016005043623903336:rxraeohqk6w';
|
||||||
|
var gcse = document.createElement('script');
|
||||||
|
gcse.type = 'text/javascript';
|
||||||
|
gcse.async = true;
|
||||||
|
gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
|
||||||
|
'//www.google.com/cse/cse.js?cx=' + cx;
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(gcse, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<gcse:search></gcse:search>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div id="main-content" class="span12">
|
||||||
|
<h1 id="404-page-not-found" style="text-align: center">404</h1>
|
||||||
|
<p style="text-align: center"><strong>Page not found</strong></p>
|
||||||
|
<p style="text-align: center">Try the <a href="http://django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
|
||||||
|
</div><!--/span-->
|
||||||
|
</div><!--/row-->
|
||||||
|
</div><!--/.fluid-container-->
|
||||||
|
</div><!--/.body content-->
|
||||||
|
|
||||||
|
<div id="push"></div>
|
||||||
|
</div><!--/.wrapper -->
|
||||||
|
|
||||||
|
<footer class="span12">
|
||||||
|
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Le javascript
|
||||||
|
================================================== -->
|
||||||
|
<!-- Placed at the end of the document so the pages load faster -->
|
||||||
|
<script src="http://django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
|
||||||
|
<script src="http://django-rest-framework.org/js/prettify-1.0.js"></script>
|
||||||
|
<script src="http://django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
|
||||||
|
<script>
|
||||||
|
//$('.side-nav').scrollspy()
|
||||||
|
var shiftWindow = function() { scrollBy(0, -50) };
|
||||||
|
if (location.hash) shiftWindow();
|
||||||
|
window.addEventListener("hashchange", shiftWindow);
|
||||||
|
|
||||||
|
$('.dropdown-menu').on('click touchstart', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamically force sidenav to no higher than browser window
|
||||||
|
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
$(window).resize(function(){
|
||||||
|
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body></html>
|
|
@ -162,10 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
|
||||||
|
|
||||||
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=get_user_model())
|
||||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
Token.objects.create(user=instance)
|
Token.objects.create(user=instance)
|
||||||
|
@ -265,6 +267,12 @@ This authentication class depends on the optional [django-oauth2-provider][djang
|
||||||
'provider.oauth2',
|
'provider.oauth2',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting:
|
||||||
|
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework.authentication.OAuth2Authentication',
|
||||||
|
),
|
||||||
|
|
||||||
You must also include the following in your root `urls.py` module:
|
You must also include the following in your root `urls.py` module:
|
||||||
|
|
||||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
||||||
|
|
|
@ -82,7 +82,7 @@ Note that the exception handler will only be called for responses generated by r
|
||||||
|
|
||||||
## APIException
|
## APIException
|
||||||
|
|
||||||
**Signature:** `APIException(detail=None)`
|
**Signature:** `APIException()`
|
||||||
|
|
||||||
The **base class** for all exceptions raised inside REST framework.
|
The **base class** for all exceptions raised inside REST framework.
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ Defaults to `True`.
|
||||||
|
|
||||||
### `default`
|
### `default`
|
||||||
|
|
||||||
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
|
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||||
|
|
||||||
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
|
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ An image representation.
|
||||||
|
|
||||||
Corresponds to `django.forms.fields.ImageField`.
|
Corresponds to `django.forms.fields.ImageField`.
|
||||||
|
|
||||||
Requires the `PIL` package.
|
Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained.
|
||||||
|
|
||||||
Signature and validation is the same as with `FileField`.
|
Signature and validation is the same as with `FileField`.
|
||||||
|
|
||||||
|
@ -299,9 +299,9 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||||
|
|
||||||
# Custom fields
|
# Custom fields
|
||||||
|
|
||||||
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
|
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
|
||||||
|
|
||||||
The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
|
The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|
|
@ -165,8 +165,8 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
min_price = django_filters.NumberFilter(lookup_type='gte')
|
min_price = django_filters.NumberFilter(name="price", lookup_type='gte')
|
||||||
max_price = django_filters.NumberFilter(lookup_type='lte')
|
max_price = django_filters.NumberFilter(name="price", lookup_type='lte')
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
||||||
|
@ -176,10 +176,49 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
filter_class = ProductFilter
|
filter_class = ProductFilter
|
||||||
|
|
||||||
|
|
||||||
Which will allow you to make requests such as:
|
Which will allow you to make requests such as:
|
||||||
|
|
||||||
http://example.com/api/products?category=clothing&max_price=10.00
|
http://example.com/api/products?category=clothing&max_price=10.00
|
||||||
|
|
||||||
|
You can also span relationships using `django-filter`, let's assume that each
|
||||||
|
product has foreign key to `Manufacturer` model, so we create filter that
|
||||||
|
filters using `Manufacturer` name. For example:
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
from myapp.models import Product
|
||||||
|
from myapp.serializers import ProductSerializer
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['category', 'in_stock', 'manufacturer__name`]
|
||||||
|
|
||||||
|
This enables us to make queries like:
|
||||||
|
|
||||||
|
http://example.com/api/products?manufacturer__name=foo
|
||||||
|
|
||||||
|
This is nice, but it shows underlying model structure in REST API, which may
|
||||||
|
be undesired, but you can use:
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
from myapp.models import Product
|
||||||
|
from myapp.serializers import ProductSerializer
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
manufacturer = django_filters.CharFilter(name="manufacturer__name")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['category', 'in_stock', 'manufacturer`]
|
||||||
|
|
||||||
|
And now you can execute:
|
||||||
|
|
||||||
|
http://example.com/api/products?manufacturer=foo
|
||||||
|
|
||||||
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
|
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -195,9 +234,9 @@ For more details on using filter sets see the [django-filter documentation][djan
|
||||||
|
|
||||||
## SearchFilter
|
## SearchFilter
|
||||||
|
|
||||||
The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
|
The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
|
||||||
|
|
||||||
The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
|
The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
|
||||||
|
|
||||||
class UserListView(generics.ListAPIView):
|
class UserListView(generics.ListAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
@ -321,6 +360,14 @@ For example, you might need to restrict users to only being able to see objects
|
||||||
|
|
||||||
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
|
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
|
||||||
|
|
||||||
|
# Third party packages
|
||||||
|
|
||||||
|
The following third party packages provide additional filter implementations.
|
||||||
|
|
||||||
|
## Django REST framework chain
|
||||||
|
|
||||||
|
The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||||
[django-filter]: https://github.com/alex/django-filter
|
[django-filter]: https://github.com/alex/django-filter
|
||||||
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
||||||
|
@ -329,3 +376,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views,
|
||||||
[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
||||||
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
||||||
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
||||||
|
[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain
|
||||||
|
|
|
@ -65,7 +65,8 @@ The following attributes control the basic view behavior.
|
||||||
|
|
||||||
* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
|
* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
|
||||||
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
|
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
|
||||||
* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf.
|
* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value.
|
||||||
|
* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`.
|
||||||
|
|
||||||
**Shortcuts**:
|
**Shortcuts**:
|
||||||
|
|
||||||
|
@ -120,11 +121,27 @@ For example:
|
||||||
|
|
||||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
|
Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
|
||||||
|
|
||||||
|
#### `get_filter_backends(self)`
|
||||||
|
|
||||||
|
Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute.
|
||||||
|
|
||||||
|
May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
def get_filter_backends(self):
|
||||||
|
if "geo_route" in self.request.QUERY_PARAMS:
|
||||||
|
return (GeoRouteFilter, CategoryFilter)
|
||||||
|
elif "geo_point" in self.request.QUERY_PARAMS:
|
||||||
|
return (GeoPointFilter, CategoryFilter)
|
||||||
|
|
||||||
|
return (CategoryFilter,)
|
||||||
|
|
||||||
#### `get_serializer_class(self)`
|
#### `get_serializer_class(self)`
|
||||||
|
|
||||||
Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used.
|
Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used.
|
||||||
|
|
||||||
May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of uesr.
|
May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -146,12 +163,14 @@ For example:
|
||||||
return 20
|
return 20
|
||||||
return 100
|
return 100
|
||||||
|
|
||||||
**Save hooks**:
|
**Save / deletion hooks**:
|
||||||
|
|
||||||
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes.
|
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes.
|
||||||
|
|
||||||
* `pre_save(self, obj)` - A hook that is called before saving an object.
|
* `pre_save(self, obj)` - A hook that is called before saving an object.
|
||||||
* `post_save(self, obj, created=False)` - A hook that is called after saving an object.
|
* `post_save(self, obj, created=False)` - A hook that is called after saving an object.
|
||||||
|
* `pre_delete(self, obj)` - A hook that is called before deleting an object.
|
||||||
|
* `post_delete(self, obj)` - A hook that is called after deleting an object.
|
||||||
|
|
||||||
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
|
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per
|
||||||
|
|
||||||
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
|
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
|
||||||
|
|
||||||
|
## REST Condition
|
||||||
|
|
||||||
|
The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
|
||||||
|
|
||||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
[throttling]: throttling.md
|
[throttling]: throttling.md
|
||||||
|
@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t
|
||||||
[filtering]: filtering.md
|
[filtering]: filtering.md
|
||||||
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
|
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
|
||||||
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
|
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
|
||||||
|
[rest-condition]: https://github.com/caxap/rest_condition
|
||||||
|
|
|
@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o
|
||||||
For example, the following serializer.
|
For example, the following serializer.
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
tracks = RelatedField(many=True)
|
tracks = serializers.RelatedField(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Album
|
model = Album
|
||||||
|
@ -54,7 +54,7 @@ Would serialize to the following representation.
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'Things We Lost In The Fire',
|
'album_name': 'Things We Lost In The Fire',
|
||||||
'artist': 'Low'
|
'artist': 'Low',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
'1: Sunflower',
|
'1: Sunflower',
|
||||||
'2: Whitetail',
|
'2: Whitetail',
|
||||||
|
@ -86,7 +86,7 @@ Would serialize to a representation like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'The Roots',
|
'album_name': 'The Roots',
|
||||||
'artist': 'Undun'
|
'artist': 'Undun',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
89,
|
89,
|
||||||
90,
|
90,
|
||||||
|
@ -121,7 +121,7 @@ Would serialize to a representation like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'Graceland',
|
'album_name': 'Graceland',
|
||||||
'artist': 'Paul Simon'
|
'artist': 'Paul Simon',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
'http://www.example.com/api/tracks/45/',
|
'http://www.example.com/api/tracks/45/',
|
||||||
'http://www.example.com/api/tracks/46/',
|
'http://www.example.com/api/tracks/46/',
|
||||||
|
@ -159,7 +159,7 @@ Would serialize to a representation like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'Dear John',
|
'album_name': 'Dear John',
|
||||||
'artist': 'Loney Dear'
|
'artist': 'Loney Dear',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
'Airport Surroundings',
|
'Airport Surroundings',
|
||||||
'Everything Turns to You',
|
'Everything Turns to You',
|
||||||
|
@ -194,7 +194,7 @@ Would serialize to a representation like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'The Eraser',
|
'album_name': 'The Eraser',
|
||||||
'artist': 'Thom Yorke'
|
'artist': 'Thom Yorke',
|
||||||
'track_listing': 'http://www.example.com/api/track_list/12/',
|
'track_listing': 'http://www.example.com/api/track_list/12/',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,7 +234,7 @@ Would serialize to a nested representation like this:
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'The Grey Album',
|
'album_name': 'The Grey Album',
|
||||||
'artist': 'Danger Mouse'
|
'artist': 'Danger Mouse',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
{'order': 1, 'title': 'Public Service Announcement'},
|
{'order': 1, 'title': 'Public Service Announcement'},
|
||||||
{'order': 2, 'title': 'What More Can I Say'},
|
{'order': 2, 'title': 'What More Can I Say'},
|
||||||
|
@ -271,7 +271,7 @@ This custom field would then serialize to the following representation.
|
||||||
|
|
||||||
{
|
{
|
||||||
'album_name': 'Sometimes I Wish We Were an Eagle',
|
'album_name': 'Sometimes I Wish We Were an Eagle',
|
||||||
'artist': 'Bill Callahan'
|
'artist': 'Bill Callahan',
|
||||||
'tracks': [
|
'tracks': [
|
||||||
'Track 1: Jim Cain (04:39)',
|
'Track 1: Jim Cain (04:39)',
|
||||||
'Track 2: Eid Ma Clack Shaw (04:19)',
|
'Track 2: Eid Ma Clack Shaw (04:19)',
|
||||||
|
|
|
@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan
|
||||||
|
|
||||||
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
|
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
|
||||||
|
|
||||||
**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
|
---
|
||||||
|
|
||||||
|
**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
|
||||||
|
|
||||||
|
The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**.media_type**: `application/javascript`
|
**.media_type**: `application/javascript`
|
||||||
|
|
||||||
|
@ -167,14 +173,14 @@ The template name is determined by (in order of preference):
|
||||||
|
|
||||||
An example of a view that uses `TemplateHTMLRenderer`:
|
An example of a view that uses `TemplateHTMLRenderer`:
|
||||||
|
|
||||||
class UserDetail(generics.RetrieveUserAPIView):
|
class UserDetail(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
A view that returns a templated HTML representations of a given user.
|
A view that returns a templated HTML representations of a given user.
|
||||||
"""
|
"""
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
renderer_classes = (TemplateHTMLRenderer,)
|
renderer_classes = (TemplateHTMLRenderer,)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs)
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
return Response({'user': self.object}, template_name='user_detail.html')
|
return Response({'user': self.object}, template_name='user_detail.html')
|
||||||
|
|
||||||
|
@ -409,12 +415,17 @@ The following third party packages are also available.
|
||||||
|
|
||||||
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
|
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
|
||||||
|
|
||||||
|
## UltraJSON
|
||||||
|
|
||||||
|
[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package.
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||||
[conneg]: content-negotiation.md
|
[conneg]: content-negotiation.md
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||||
[cors]: http://www.w3.org/TR/cors/
|
[cors]: http://www.w3.org/TR/cors/
|
||||||
[cors-docs]: ../topics/ajax-csrf-cors.md
|
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||||
|
[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
|
||||||
[testing]: testing.md
|
[testing]: testing.md
|
||||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
|
@ -426,3 +437,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
||||||
[mjumbewu]: https://github.com/mjumbewu
|
[mjumbewu]: https://github.com/mjumbewu
|
||||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||||
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
|
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
|
||||||
|
[ultrajson]: https://github.com/esnme/ultrajson
|
||||||
|
[hzy]: https://github.com/hzy
|
||||||
|
[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
|
||||||
|
|
|
@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Here's an example of a simple URL conf, that uses `DefaultRouter`.
|
Here's an example of a simple URL conf, that uses `SimpleRouter`.
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
|
@ -214,5 +214,27 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an
|
||||||
|
|
||||||
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
|
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
|
||||||
|
|
||||||
|
# Third Party Packages
|
||||||
|
|
||||||
|
The following third party packages are also available.
|
||||||
|
|
||||||
|
## DRF Nested Routers
|
||||||
|
|
||||||
|
The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
|
||||||
|
|
||||||
|
[cite]: http://guides.rubyonrails.org/routing.html
|
||||||
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
|
|
||||||
|
## wq.db
|
||||||
|
|
||||||
|
The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration.
|
||||||
|
|
||||||
|
from wq.db.rest import app
|
||||||
|
from myapp.models import MyModel
|
||||||
|
|
||||||
|
app.router.register_model(MyModel)
|
||||||
|
|
||||||
[cite]: http://guides.rubyonrails.org/routing.html
|
[cite]: http://guides.rubyonrails.org/routing.html
|
||||||
[route-decorators]: viewsets.html#marking-extra-actions-for-routing
|
[route-decorators]: viewsets.html#marking-extra-actions-for-routing
|
||||||
|
[wq.db]: http://wq.io/wq.db
|
||||||
|
[wq.db-router]: http://wq.io/docs/app.py
|
||||||
|
|
|
@ -67,6 +67,21 @@ At this point we've translated the model instance into Python native datatypes.
|
||||||
json
|
json
|
||||||
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
|
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
|
||||||
|
|
||||||
|
### Customizing field representation
|
||||||
|
|
||||||
|
Sometimes when serializing objects, you may not want to represent everything exactly the way it is in your model.
|
||||||
|
|
||||||
|
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
||||||
|
|
||||||
|
description = serializers.TextField()
|
||||||
|
description_html = serializers.TextField(source='description', read_only=True)
|
||||||
|
|
||||||
|
def transform_description_html(self, obj, value):
|
||||||
|
from django.contrib.markup.templatetags.markup import markdown
|
||||||
|
return markdown(value)
|
||||||
|
|
||||||
|
These methods are essentially the reverse of `validate_<fieldname>` (see *Validation* below.)
|
||||||
|
|
||||||
## Deserializing objects
|
## Deserializing objects
|
||||||
|
|
||||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
@ -84,7 +99,6 @@ Deserialization is similar. First we parse a stream into Python native datatype
|
||||||
# True
|
# True
|
||||||
serializer.object
|
serializer.object
|
||||||
# <Comment object at 0x10633b2d0>
|
# <Comment object at 0x10633b2d0>
|
||||||
>>> serializer.deserialize('json', stream)
|
|
||||||
|
|
||||||
When deserializing data, we can either create a new instance, or update an existing instance.
|
When deserializing data, we can either create a new instance, or update an existing instance.
|
||||||
|
|
||||||
|
@ -411,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_
|
||||||
fields = ('url', 'account_name', 'users', 'created')
|
fields = ('url', 'account_name', 'users', 'created')
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
|
||||||
Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
||||||
|
|
||||||
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc
|
||||||
|
|
||||||
The full set of HTTP status codes included in the `status` module is listed below.
|
The full set of HTTP status codes included in the `status` module is listed below.
|
||||||
|
|
||||||
|
The module also includes a set of helper functions for testing if a status code is in a given range.
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
class ExampleTestCase(APITestCase):
|
||||||
|
def test_url_root(self):
|
||||||
|
url = reverse('index')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertTrue(status.is_success(response.status_code))
|
||||||
|
|
||||||
|
|
||||||
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
|
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
|
||||||
and [RFC 6585][rfc6585].
|
and [RFC 6585][rfc6585].
|
||||||
|
|
||||||
|
@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s
|
||||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
|
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
|
||||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
|
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
|
||||||
|
|
||||||
|
## Helper functions
|
||||||
|
|
||||||
|
The following helper functions are available for identifying the category of the response code.
|
||||||
|
|
||||||
|
is_informational() # 1xx
|
||||||
|
is_success() # 2xx
|
||||||
|
is_redirect() # 3xx
|
||||||
|
is_client_error() # 4xx
|
||||||
|
is_server_error() # 5xx
|
||||||
|
|
||||||
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
|
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
|
||||||
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||||
|
|
|
@ -205,10 +205,10 @@ You can use any of REST framework's test case classes as you would for the regul
|
||||||
Ensure we can create a new account object.
|
Ensure we can create a new account object.
|
||||||
"""
|
"""
|
||||||
url = reverse('account-list')
|
url = reverse('account-list')
|
||||||
expected = {'name': 'DabApps'}
|
data = {'name': 'DabApps'}
|
||||||
response = self.client.post(url, data, format='json')
|
response = self.client.post(url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data, expected)
|
self.assertEqual(response.data, data)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ using the `APIView` class based views.
|
||||||
Or, if you're using the `@api_view` decorator with function based views.
|
Or, if you're using the `@api_view` decorator with function based views.
|
||||||
|
|
||||||
@api_view('GET')
|
@api_view('GET')
|
||||||
@throttle_classes(UserRateThrottle)
|
@throttle_classes([UserRateThrottle])
|
||||||
def example_view(request, format=None):
|
def example_view(request, format=None):
|
||||||
content = {
|
content = {
|
||||||
'status': 'request was permitted'
|
'status': 'request was permitted'
|
||||||
|
|
|
@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o
|
||||||
|
|
||||||
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
|
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
|
||||||
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
|
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
|
||||||
[settings]: api-guide/settings.md
|
[settings]: settings.md
|
||||||
[throttling]: api-guide/throttling.md
|
[throttling]: throttling.md
|
||||||
|
|
|
@ -178,7 +178,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
|
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example:
|
||||||
|
|
||||||
class AccountViewSet(viewsets.ModelViewSet):
|
class AccountViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
BIN
docs/img/travis-status.png
Normal file
BIN
docs/img/travis-status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
|
@ -100,7 +100,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
|
||||||
We're ready to create our API now.
|
We're ready to create our API now.
|
||||||
Here's our project's root `urls.py` module:
|
Here's our project's root `urls.py` module:
|
||||||
|
|
||||||
from django.conf.urls.defaults import url, patterns, include
|
from django.conf.urls import url, patterns, include
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
from rest_framework import viewsets, routers
|
from rest_framework import viewsets, routers
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ Here's our project's root `urls.py` module:
|
||||||
model = Group
|
model = Group
|
||||||
|
|
||||||
|
|
||||||
# Routers provide an easy way of automatically determining the URL conf
|
# Routers provide an easy way of automatically determining the URL conf.
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'users', UserViewSet)
|
router.register(r'users', UserViewSet)
|
||||||
router.register(r'groups', GroupViewSet)
|
router.register(r'groups', GroupViewSet)
|
||||||
|
@ -177,6 +177,7 @@ General guides to using REST framework.
|
||||||
* [Browser enhancements][browser-enhancements]
|
* [Browser enhancements][browser-enhancements]
|
||||||
* [The Browsable API][browsableapi]
|
* [The Browsable API][browsableapi]
|
||||||
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
||||||
|
* [Contributing to REST framework][contributing]
|
||||||
* [2.0 Announcement][rest-framework-2-announcement]
|
* [2.0 Announcement][rest-framework-2-announcement]
|
||||||
* [2.2 Announcement][2.2-announcement]
|
* [2.2 Announcement][2.2-announcement]
|
||||||
* [2.3 Announcement][2.3-announcement]
|
* [2.3 Announcement][2.3-announcement]
|
||||||
|
@ -255,11 +256,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||||
[image]: img/quickstart.png
|
[image]: img/quickstart.png
|
||||||
[index]: .
|
[index]: .
|
||||||
[oauth1-section]: api-guide/authentication.html#oauthauthentication
|
[oauth1-section]: api-guide/authentication#oauthauthentication
|
||||||
[oauth2-section]: api-guide/authentication.html#oauth2authentication
|
[oauth2-section]: api-guide/authentication#oauth2authentication
|
||||||
[serializer-section]: api-guide/serializers.html#serializers
|
[serializer-section]: api-guide/serializers#serializers
|
||||||
[modelserializer-section]: api-guide/serializers.html#modelserializer
|
[modelserializer-section]: api-guide/serializers#modelserializer
|
||||||
[functionview-section]: api-guide/views.html#function-based-views
|
[functionview-section]: api-guide/views#function-based-views
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
|
|
||||||
[quickstart]: tutorial/quickstart.md
|
[quickstart]: tutorial/quickstart.md
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ title }}</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">
|
||||||
|
<link rel="canonical" href="{{ canonical_url }}"/>
|
||||||
<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="{{ description }}">
|
<meta name="description" content="{{ description }}">
|
||||||
<meta name="author" content="Tom Christie">
|
<meta name="author" content="Tom Christie">
|
||||||
|
@ -101,6 +102,7 @@
|
||||||
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
|
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
|
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
|
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
|
||||||
|
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
|
||||||
|
@ -167,7 +169,32 @@
|
||||||
<div id="table-of-contents">
|
<div id="table-of-contents">
|
||||||
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
|
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
|
||||||
{{ toc }}
|
{{ toc }}
|
||||||
|
<div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p><strong>The team behind REST framework is launching a new API service.</strong></p>
|
||||||
|
|
||||||
|
<p>If you want to be first in line when we start issuing invitations, please sign up here:</p>
|
||||||
|
|
||||||
|
<!-- Begin MailChimp Signup Form -->
|
||||||
|
<link href="//cdn-images.mailchimp.com/embedcode/slim-081711.css" rel="stylesheet" type="text/css">
|
||||||
|
<style type="text/css">
|
||||||
|
#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
|
||||||
|
/* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
|
||||||
|
We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
|
||||||
|
</style>
|
||||||
|
<div id="mc_embed_signup" style="background: rgb(245, 245, 245)">
|
||||||
|
<form action="http://dabapps.us1.list-manage1.com/subscribe/post?u=cf73a9994eb5b8d8d461b5dfb&id=cb6af8e8bd" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
|
||||||
|
<!-- <label for="mce-EMAIL">Keep me posted!</label>
|
||||||
|
--> <input style="width: 90%" type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
|
||||||
|
<div class="clear"><input class="btn btn-success" type="submit" value="Yes, keep me posted!" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</style></div>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<!--End mc_embed_signup-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re
|
||||||
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
|
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
|
||||||
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
|
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
|
||||||
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
|
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
|
||||||
[credits]: http://django-rest-framework.org/topics/credits.html
|
[credits]: http://django-rest-framework.org/topics/credits
|
||||||
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||||
[marcgibbons]: https://github.com/marcgibbons/
|
[marcgibbons]: https://github.com/marcgibbons/
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
## Javascript clients
|
## Javascript clients
|
||||||
|
|
||||||
If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
|
If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
|
||||||
|
|
||||||
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
|
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
|
||||||
|
|
||||||
|
|
|
@ -6,50 +6,92 @@
|
||||||
|
|
||||||
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
|
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
|
||||||
|
|
||||||
# Community
|
## Community
|
||||||
|
|
||||||
If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users.
|
The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
|
||||||
|
|
||||||
You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter.
|
If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
|
||||||
|
|
||||||
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
|
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
|
||||||
|
|
||||||
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
|
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
|
||||||
|
|
||||||
|
## Code of conduct
|
||||||
|
|
||||||
|
Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
|
||||||
|
|
||||||
|
Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
|
||||||
|
|
||||||
|
The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
|
||||||
|
|
||||||
# Issues
|
# Issues
|
||||||
|
|
||||||
It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
||||||
|
|
||||||
Some tips on good issue reporting:
|
Some tips on good issue reporting:
|
||||||
|
|
||||||
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
||||||
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
||||||
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
|
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
|
||||||
|
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
|
||||||
|
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
|
||||||
|
|
||||||
|
## Triaging issues
|
||||||
|
|
||||||
|
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
|
||||||
|
|
||||||
* TODO: Triage
|
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
|
||||||
|
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
|
||||||
|
* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
|
||||||
|
* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
|
||||||
|
* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
To start developing on Django REST framework, clone the repo:
|
||||||
|
|
||||||
* git clone & PYTHONPATH
|
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||||
* Pep8
|
|
||||||
* Recommend editor that runs pep8
|
|
||||||
|
|
||||||
### Pull requests
|
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
|
||||||
|
|
||||||
* Make pull requests early
|
## Testing
|
||||||
* Describe branching
|
|
||||||
|
|
||||||
### Managing compatibility issues
|
To run the tests, clone the repository, and then:
|
||||||
|
|
||||||
* Describe compat module
|
# Setup the virtual environment
|
||||||
|
virtualenv env
|
||||||
|
env/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r optionals.txt
|
||||||
|
|
||||||
# Testing
|
# Run the tests
|
||||||
|
rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
* Running the tests
|
You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||||
* tox
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
|
||||||
|
|
||||||
|
It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests.
|
||||||
|
|
||||||
|
It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
|
||||||
|
|
||||||
|
GitHub's documentation for working on pull requests is [available here][pull-requests].
|
||||||
|
|
||||||
|
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
|
||||||
|
|
||||||
|
Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect.
|
||||||
|
|
||||||
|
![Travis status][travis-status]
|
||||||
|
|
||||||
|
*Above: Travis build notifications*
|
||||||
|
|
||||||
|
## Managing compatibility issues
|
||||||
|
|
||||||
|
Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
|
@ -77,7 +119,7 @@ Some other tips:
|
||||||
|
|
||||||
* Keep paragraphs reasonably short.
|
* Keep paragraphs reasonably short.
|
||||||
* Use double spacing after the end of sentences.
|
* Use double spacing after the end of sentences.
|
||||||
* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'.
|
* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
|
||||||
|
|
||||||
## Markdown style
|
## Markdown style
|
||||||
|
|
||||||
|
@ -118,25 +160,34 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note:** Make sure you do this thing.
|
**Note:** A useful documentation note.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Third party packages
|
# Third party packages
|
||||||
|
|
||||||
* Django reusable app
|
New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
|
||||||
|
|
||||||
# Core committers
|
## Getting started
|
||||||
|
|
||||||
* Still use pull reqs
|
If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
|
||||||
* Credits
|
|
||||||
|
We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
|
||||||
|
|
||||||
|
## Linking to your package
|
||||||
|
|
||||||
|
Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
|
||||||
|
|
||||||
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
|
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
|
||||||
[github]: https://github.com/tomchristie/django-rest-framework
|
[code-of-conduct]: https://www.djangoproject.com/conduct/
|
||||||
[django-packages]: https://www.djangopackages.com/grids/g/api/
|
|
||||||
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
||||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
||||||
|
[pep-8]: http://www.python.org/dev/peps/pep-0008/
|
||||||
|
[travis-status]: ../img/travis-status.png
|
||||||
|
[pull-requests]: https://help.github.com/articles/using-pull-requests
|
||||||
|
[tox]: http://tox.readthedocs.org/en/latest/
|
||||||
[markdown]: http://daringfireball.net/projects/markdown/basics
|
[markdown]: http://daringfireball.net/projects/markdown/basics
|
||||||
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
||||||
[mou]: http://mouapp.com/
|
[mou]: http://mouapp.com/
|
||||||
|
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
|
||||||
|
|
|
@ -169,6 +169,18 @@ The following people have helped make REST framework great.
|
||||||
* Edmond Wong - [edmondwong]
|
* Edmond Wong - [edmondwong]
|
||||||
* Ben Reilly - [bwreilly]
|
* Ben Reilly - [bwreilly]
|
||||||
* Tai Lee - [mrmachine]
|
* Tai Lee - [mrmachine]
|
||||||
|
* Markus Kaiserswerth - [mkai]
|
||||||
|
* Henry Clifford - [hcliff]
|
||||||
|
* Thomas Badaud - [badale]
|
||||||
|
* Colin Huang - [tamakisquare]
|
||||||
|
* Ross McFarland - [ross]
|
||||||
|
* Jacek Bzdak - [jbzdak]
|
||||||
|
* Alexander Lukanin - [alexanderlukanin13]
|
||||||
|
* Yamila Moreno - [yamila-moreno]
|
||||||
|
* Rob Hudson - [robhudson]
|
||||||
|
* Alex Good - [alexjg]
|
||||||
|
* Ian Foote - [ian-foote]
|
||||||
|
* Chuck Harmston - [chuckharmston]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -374,3 +386,15 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[edmondwong]: https://github.com/edmondwong
|
[edmondwong]: https://github.com/edmondwong
|
||||||
[bwreilly]: https://github.com/bwreilly
|
[bwreilly]: https://github.com/bwreilly
|
||||||
[mrmachine]: https://github.com/mrmachine
|
[mrmachine]: https://github.com/mrmachine
|
||||||
|
[mkai]: https://github.com/mkai
|
||||||
|
[hcliff]: https://github.com/hcliff
|
||||||
|
[badale]: https://github.com/badale
|
||||||
|
[tamakisquare]: https://github.com/tamakisquare
|
||||||
|
[ross]: https://github.com/ross
|
||||||
|
[jbzdak]: https://github.com/jbzdak
|
||||||
|
[alexanderlukanin13]: https://github.com/alexanderlukanin13
|
||||||
|
[yamila-moreno]: https://github.com/yamila-moreno
|
||||||
|
[robhudson]: https://github.com/robhudson
|
||||||
|
[alexjg]: https://github.com/alexjg
|
||||||
|
[ian-foote]: https://github.com/ian-foote
|
||||||
|
[chuckharmston]: https://github.com/chuckharmston
|
||||||
|
|
|
@ -45,11 +45,43 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
|
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
|
||||||
* `six` no longer bundled. For Django <= 1.4.1, install `six` package.
|
* `six` no longer bundled. For Django <= 1.4.1, install `six` package.
|
||||||
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
|
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
|
||||||
|
* Added `NUM_PROXIES` setting for smarter client IP identification.
|
||||||
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
|
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
|
||||||
* Added `cache` attribute to throttles to allow overriding of default cache.
|
* Added `cache` attribute to throttles to allow overriding of default cache.
|
||||||
* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off.
|
* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off.
|
||||||
|
|
||||||
|
### Master
|
||||||
|
|
||||||
|
* JSON renderer now deals with objects that implement a dict-like interface.
|
||||||
|
* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations.
|
||||||
|
|
||||||
|
### 2.3.10
|
||||||
|
|
||||||
|
**Date**: 6th December 2013
|
||||||
|
|
||||||
|
* Add in choices information for ChoiceFields in response to `OPTIONS` requests.
|
||||||
|
* Added `pre_delete()` and `post_delete()` method hooks.
|
||||||
|
* Added status code category helper functions.
|
||||||
|
* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception.
|
||||||
|
* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header.
|
||||||
|
* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400.
|
||||||
|
|
||||||
|
### 2.3.9
|
||||||
|
|
||||||
|
**Date**: 15th November 2013
|
||||||
|
|
||||||
|
* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`.
|
||||||
|
* Include errors in HTML forms in browsable API.
|
||||||
|
>>>>>>> master
|
||||||
* Added JSON renderer support for numpy scalars.
|
* Added JSON renderer support for numpy scalars.
|
||||||
|
* Added `transform_<fieldname>` hooks on serializers for easily modifying field output.
|
||||||
* Added `get_context` hook in `BrowsableAPIRenderer`.
|
* Added `get_context` hook in `BrowsableAPIRenderer`.
|
||||||
|
* Allow serializers to be passed `files` but no `data`.
|
||||||
|
* `HTMLFormRenderer` now renders serializers directly to HTML without needing to create an intermediate form object.
|
||||||
|
* Added `get_filter_backends` hook.
|
||||||
|
* Added queryset aggregates to allowed fields in `OrderingFilter`.
|
||||||
|
* Bugfix: Fix decimal suppoprt with `YAMLRenderer`.
|
||||||
|
* Bugfix: Fix submission of unicode in browsable API through raw data form.
|
||||||
|
|
||||||
### 2.3.8
|
### 2.3.8
|
||||||
|
|
||||||
|
@ -64,7 +96,7 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views.
|
* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views.
|
||||||
* Bugfix: `required=True` argument fixed for boolean serializer fields.
|
* Bugfix: `required=True` argument fixed for boolean serializer fields.
|
||||||
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
||||||
* Bugfix: Client sending emptry string instead of file now clears `FileField`.
|
* Bugfix: Client sending empty string instead of file now clears `FileField`.
|
||||||
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
|
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
|
||||||
|
|
||||||
### 2.3.7
|
### 2.3.7
|
||||||
|
|
|
@ -225,7 +225,7 @@ For the moment we won't use any of REST framework's other features, we'll just w
|
||||||
|
|
||||||
We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
|
We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
|
||||||
|
|
||||||
Edit the `snippet/views.py` file, and add the following.
|
Edit the `snippets/views.py` file, and add the following.
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
|
@ -35,7 +35,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r
|
||||||
|
|
||||||
Okay, let's go ahead and start using these new components to write a few views.
|
Okay, let's go ahead and start using these new components to write a few views.
|
||||||
|
|
||||||
We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
|
We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
|
@ -64,7 +64,7 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
||||||
|
|
||||||
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
|
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
|
||||||
|
|
||||||
Here is the view for an individual snippet.
|
Here is the view for an individual snippet, in the `views.py` module.
|
||||||
|
|
||||||
@api_view(['GET', 'PUT', 'DELETE'])
|
@api_view(['GET', 'PUT', 'DELETE'])
|
||||||
def snippet_detail(request, pk):
|
def snippet_detail(request, pk):
|
||||||
|
@ -147,7 +147,7 @@ Similarly, we can control the format of the request that we send, using the `Con
|
||||||
# POST using form data
|
# POST using form data
|
||||||
curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
|
curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
|
||||||
|
|
||||||
{"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"}
|
{"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"}
|
||||||
|
|
||||||
# POST using JSON
|
# POST using JSON
|
||||||
curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"
|
curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"
|
||||||
|
|
|
@ -4,7 +4,7 @@ We can also write our API views using class based views, rather than function ba
|
||||||
|
|
||||||
## Rewriting our API using class based views
|
## Rewriting our API using class based views
|
||||||
|
|
||||||
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
|
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring of `views.py`.
|
||||||
|
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
@ -30,7 +30,7 @@ We'll start by rewriting the root view as a class based view. All this involves
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view.
|
So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`.
|
||||||
|
|
||||||
class SnippetDetail(APIView):
|
class SnippetDetail(APIView):
|
||||||
"""
|
"""
|
||||||
|
@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
|
||||||
|
|
||||||
That's looking good. Again, it's still pretty similar to the function based view right now.
|
That's looking good. Again, it's still pretty similar to the function based view right now.
|
||||||
|
|
||||||
We'll also need to refactor our URLconf slightly now we're using class based views.
|
We'll also need to refactor our `urls.py` slightly now we're using class based views.
|
||||||
|
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
@ -83,7 +83,7 @@ One of the big wins of using class based views is that it allows us to easily co
|
||||||
|
|
||||||
The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
|
The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
|
||||||
|
|
||||||
Let's take a look at how we can compose our views by using the mixin classes.
|
Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again.
|
||||||
|
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor
|
||||||
|
|
||||||
## Using generic class based views
|
## Using generic class based views
|
||||||
|
|
||||||
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
|
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down our `views.py` module even more.
|
||||||
|
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
|
|
@ -12,7 +12,7 @@ Currently our API doesn't have any restrictions on who can edit or delete code s
|
||||||
We're going to make a couple of changes to our `Snippet` model class.
|
We're going to make a couple of changes to our `Snippet` model class.
|
||||||
First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code.
|
First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code.
|
||||||
|
|
||||||
Add the following two fields to the model.
|
Add the following two fields to the `Snippet` model in `models.py`.
|
||||||
|
|
||||||
owner = models.ForeignKey('auth.User', related_name='snippets')
|
owner = models.ForeignKey('auth.User', related_name='snippets')
|
||||||
highlighted = models.TextField()
|
highlighted = models.TextField()
|
||||||
|
@ -52,7 +52,7 @@ You might also want to create a few different users, to use for testing the API.
|
||||||
|
|
||||||
## Adding endpoints for our User models
|
## Adding endpoints for our User models
|
||||||
|
|
||||||
Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy:
|
Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add:
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
@ -65,7 +65,10 @@ Now that we've got some users to work with, we'd better add representations of t
|
||||||
|
|
||||||
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
|
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
|
||||||
|
|
||||||
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
|
We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class UserList(generics.ListAPIView):
|
class UserList(generics.ListAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
@ -76,7 +79,11 @@ We'll also add a couple of views. We'd like to just use read-only views for the
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
Finally we need to add those views into the API, by referencing them from the URL conf.
|
Make sure to also import the `UserSerializer` class
|
||||||
|
|
||||||
|
from snippets.serializers import UserSerializer
|
||||||
|
|
||||||
|
Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`.
|
||||||
|
|
||||||
url(r'^users/$', views.UserList.as_view()),
|
url(r'^users/$', views.UserList.as_view()),
|
||||||
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
|
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
|
||||||
|
@ -94,7 +101,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin
|
||||||
|
|
||||||
## Updating our serializer
|
## Updating our serializer
|
||||||
|
|
||||||
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
|
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`:
|
||||||
|
|
||||||
owner = serializers.Field(source='owner.username')
|
owner = serializers.Field(source='owner.username')
|
||||||
|
|
||||||
|
|
|
@ -85,10 +85,14 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all()
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
|
|
||||||
Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`.
|
Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`.
|
||||||
|
|
||||||
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.
|
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.
|
||||||
|
|
||||||
|
Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute.
|
||||||
|
|
||||||
|
For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications.
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||||
|
@ -169,6 +173,7 @@ Great, that was easy!
|
||||||
|
|
||||||
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
|
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
|
||||||
|
|
||||||
|
[readme-example-api]: ../#example
|
||||||
[image]: ../img/quickstart.png
|
[image]: ../img/quickstart.png
|
||||||
[tutorial]: 1-serialization.md
|
[tutorial]: 1-serialization.md
|
||||||
[guide]: ../#api-guide
|
[guide]: ../#api-guide
|
||||||
|
|
12
mkdocs.py
12
mkdocs.py
|
@ -19,7 +19,7 @@ if local:
|
||||||
index = 'index.html'
|
index = 'index.html'
|
||||||
else:
|
else:
|
||||||
base_url = 'http://django-rest-framework.org'
|
base_url = 'http://django-rest-framework.org'
|
||||||
suffix = '.html'
|
suffix = ''
|
||||||
index = ''
|
index = ''
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,7 +90,10 @@ for idx in range(len(path_list)):
|
||||||
path = path_list[idx]
|
path = path_list[idx]
|
||||||
rel = '../' * path.count('/')
|
rel = '../' * path.count('/')
|
||||||
|
|
||||||
if idx > 0:
|
if idx == 1 and not local:
|
||||||
|
# Link back to '/', not '/index'
|
||||||
|
prev_url_map[path] = '/'
|
||||||
|
elif idx > 0:
|
||||||
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
|
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
|
||||||
|
|
||||||
if idx < len(path_list) - 1:
|
if idx < len(path_list) - 1:
|
||||||
|
@ -143,6 +146,10 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
else:
|
else:
|
||||||
main_title = 'Django REST framework - ' + main_title
|
main_title = 'Django REST framework - ' + main_title
|
||||||
|
|
||||||
|
if relative_path == 'index.md':
|
||||||
|
canonical_url = base_url
|
||||||
|
else:
|
||||||
|
canonical_url = base_url + '/' + relative_path[:-3] + suffix
|
||||||
prev_url = prev_url_map.get(relative_path)
|
prev_url = prev_url_map.get(relative_path)
|
||||||
next_url = next_url_map.get(relative_path)
|
next_url = next_url_map.get(relative_path)
|
||||||
|
|
||||||
|
@ -152,6 +159,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
output = output.replace('{{ title }}', main_title)
|
output = output.replace('{{ title }}', main_title)
|
||||||
output = output.replace('{{ description }}', description)
|
output = output.replace('{{ description }}', description)
|
||||||
output = output.replace('{{ page_id }}', filename[:-3])
|
output = output.replace('{{ page_id }}', filename[:-3])
|
||||||
|
output = output.replace('{{ canonical_url }}', canonical_url)
|
||||||
|
|
||||||
if prev_url:
|
if prev_url:
|
||||||
output = output.replace('{{ prev_url }}', prev_url)
|
output = output.replace('{{ prev_url }}', prev_url)
|
||||||
|
|
|
@ -1,6 +1,20 @@
|
||||||
__version__ = '2.3.8'
|
"""
|
||||||
|
______ _____ _____ _____ __ _
|
||||||
|
| ___ \ ___/ ___|_ _| / _| | |
|
||||||
|
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
|
||||||
|
| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
|
||||||
|
| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
|
||||||
|
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
||||||
|
"""
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
__title__ = 'Django REST framework'
|
||||||
|
__version__ = '2.3.10'
|
||||||
|
__author__ = 'Tom Christie'
|
||||||
|
__license__ = 'BSD 2-Clause'
|
||||||
|
__copyright__ = 'Copyright 2011-2013 Tom Christie'
|
||||||
|
|
||||||
|
# Version synonym
|
||||||
|
VERSION = __version__
|
||||||
|
|
||||||
# Header encoding (see RFC5987)
|
# Header encoding (see RFC5987)
|
||||||
HTTP_HEADER_ENCODING = 'iso-8859-1'
|
HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||||
|
|
|
@ -65,6 +65,13 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
# UserDict moves in Python 3
|
||||||
|
try:
|
||||||
|
from UserDict import UserDict
|
||||||
|
from UserDict import DictMixin
|
||||||
|
except ImportError:
|
||||||
|
from collections import UserDict
|
||||||
|
from collections import MutableMapping as DictMixin
|
||||||
|
|
||||||
# Try to import PIL in either of the two ways it can end up installed.
|
# Try to import PIL in either of the two ways it can end up installed.
|
||||||
try:
|
try:
|
||||||
|
@ -76,6 +83,22 @@ except ImportError:
|
||||||
Image = None
|
Image = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_name(model_cls):
|
||||||
|
try:
|
||||||
|
return model_cls._meta.model_name
|
||||||
|
except AttributeError:
|
||||||
|
# < 1.6 used module_name instead of model_name
|
||||||
|
return model_cls._meta.module_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_concrete_model(model_cls):
|
||||||
|
try:
|
||||||
|
return model_cls._meta.concrete_model
|
||||||
|
except AttributeError:
|
||||||
|
# 1.3 does not include concrete model
|
||||||
|
return model_cls
|
||||||
|
|
||||||
|
|
||||||
# Django 1.5 add support for custom auth user model
|
# Django 1.5 add support for custom auth user model
|
||||||
if django.VERSION >= (1, 5):
|
if django.VERSION >= (1, 5):
|
||||||
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
|
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
|
||||||
|
|
|
@ -125,6 +125,7 @@ class Field(object):
|
||||||
use_files = False
|
use_files = False
|
||||||
form_field_class = forms.CharField
|
form_field_class = forms.CharField
|
||||||
type_label = 'field'
|
type_label = 'field'
|
||||||
|
widget = None
|
||||||
|
|
||||||
def __init__(self, source=None, label=None, help_text=None):
|
def __init__(self, source=None, label=None, help_text=None):
|
||||||
self.parent = None
|
self.parent = None
|
||||||
|
@ -136,9 +137,29 @@ class Field(object):
|
||||||
|
|
||||||
if label is not None:
|
if label is not None:
|
||||||
self.label = smart_text(label)
|
self.label = smart_text(label)
|
||||||
|
else:
|
||||||
|
self.label = None
|
||||||
|
|
||||||
if help_text is not None:
|
if help_text is not None:
|
||||||
self.help_text = strip_multiple_choice_msg(smart_text(help_text))
|
self.help_text = strip_multiple_choice_msg(smart_text(help_text))
|
||||||
|
else:
|
||||||
|
self.help_text = None
|
||||||
|
|
||||||
|
self._errors = []
|
||||||
|
self._value = None
|
||||||
|
self._name = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self):
|
||||||
|
return self._errors
|
||||||
|
|
||||||
|
def widget_html(self):
|
||||||
|
if not self.widget:
|
||||||
|
return ''
|
||||||
|
return self.widget.render(self._name, self._value)
|
||||||
|
|
||||||
|
def label_tag(self):
|
||||||
|
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
||||||
|
|
||||||
def initialize(self, parent, field_name):
|
def initialize(self, parent, field_name):
|
||||||
"""
|
"""
|
||||||
|
@ -301,6 +322,7 @@ class WritableField(Field):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
data = data or {}
|
||||||
if self.use_files:
|
if self.use_files:
|
||||||
files = files or {}
|
files = files or {}
|
||||||
try:
|
try:
|
||||||
|
@ -470,6 +492,7 @@ class ChoiceField(WritableField):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), *args, **kwargs):
|
||||||
|
self.empty = kwargs.pop('empty', '')
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
if not self.required:
|
if not self.required:
|
||||||
|
@ -486,6 +509,11 @@ class ChoiceField(WritableField):
|
||||||
|
|
||||||
choices = property(_get_choices, _set_choices)
|
choices = property(_get_choices, _set_choices)
|
||||||
|
|
||||||
|
def metadata(self):
|
||||||
|
data = super(ChoiceField, self).metadata()
|
||||||
|
data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
|
||||||
|
return data
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
"""
|
"""
|
||||||
Validates that the input is in self.choices.
|
Validates that the input is in self.choices.
|
||||||
|
@ -510,9 +538,10 @@ class ChoiceField(WritableField):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
if value in validators.EMPTY_VALUES:
|
value = super(ChoiceField, self).from_native(value)
|
||||||
return None
|
if value == self.empty or value in validators.EMPTY_VALUES:
|
||||||
return super(ChoiceField, self).from_native(value)
|
return self.empty
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class EmailField(CharField):
|
class EmailField(CharField):
|
||||||
|
@ -751,6 +780,7 @@ class IntegerField(WritableField):
|
||||||
type_name = 'IntegerField'
|
type_name = 'IntegerField'
|
||||||
type_label = 'integer'
|
type_label = 'integer'
|
||||||
form_field_class = forms.IntegerField
|
form_field_class = forms.IntegerField
|
||||||
|
empty = 0
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _('Enter a whole number.'),
|
'invalid': _('Enter a whole number.'),
|
||||||
|
@ -782,6 +812,7 @@ class FloatField(WritableField):
|
||||||
type_name = 'FloatField'
|
type_name = 'FloatField'
|
||||||
type_label = 'float'
|
type_label = 'float'
|
||||||
form_field_class = forms.FloatField
|
form_field_class = forms.FloatField
|
||||||
|
empty = 0
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _("'%s' value must be a float."),
|
'invalid': _("'%s' value must be a float."),
|
||||||
|
@ -802,6 +833,7 @@ class DecimalField(WritableField):
|
||||||
type_name = 'DecimalField'
|
type_name = 'DecimalField'
|
||||||
type_label = 'decimal'
|
type_label = 'decimal'
|
||||||
form_field_class = forms.DecimalField
|
form_field_class = forms.DecimalField
|
||||||
|
empty = Decimal('0')
|
||||||
|
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _('Enter a number.'),
|
'invalid': _('Enter a number.'),
|
||||||
|
@ -934,7 +966,7 @@ class ImageField(FileField):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from rest_framework.compat import Image
|
from rest_framework.compat import Image
|
||||||
assert Image is not None, 'PIL must be installed for ImageField support'
|
assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.'
|
||||||
|
|
||||||
# We need to get a file object for PIL. We might have a path or we might
|
# We need to get a file object for PIL. We might have a path or we might
|
||||||
# have to read the data into memory.
|
# have to read the data into memory.
|
||||||
|
|
|
@ -4,7 +4,7 @@ returned by list views.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from rest_framework.compat import django_filters, six, guardian
|
from rest_framework.compat import django_filters, six, guardian, get_model_name
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ class OrderingFilter(BaseFilterBackend):
|
||||||
|
|
||||||
def remove_invalid_fields(self, queryset, ordering):
|
def remove_invalid_fields(self, queryset, ordering):
|
||||||
field_names = [field.name for field in queryset.model._meta.fields]
|
field_names = [field.name for field in queryset.model._meta.fields]
|
||||||
|
field_names += queryset.query.aggregates.keys()
|
||||||
return [term for term in ordering if term.lstrip('-') in field_names]
|
return [term for term in ordering if term.lstrip('-') in field_names]
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
@ -158,7 +159,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||||
model_cls = queryset.model
|
model_cls = queryset.model
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'app_label': model_cls._meta.app_label,
|
'app_label': model_cls._meta.app_label,
|
||||||
'model_name': model_cls._meta.module_name
|
'model_name': get_model_name(model_cls)
|
||||||
}
|
}
|
||||||
permission = self.perm_format % kwargs
|
permission = self.perm_format % kwargs
|
||||||
return guardian.shortcuts.get_objects_for_user(user, permission, queryset)
|
return guardian.shortcuts.get_objects_for_user(user, permission, queryset)
|
||||||
|
|
|
@ -25,13 +25,13 @@ def strict_positive_int(integer_string, cutoff=None):
|
||||||
ret = min(ret, cutoff)
|
ret = min(ret, cutoff)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_object_or_404(queryset, **filter_kwargs):
|
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||||
"""
|
"""
|
||||||
Same as Django's standard shortcut, but make sure to raise 404
|
Same as Django's standard shortcut, but make sure to raise 404
|
||||||
if the filter_kwargs don't match the required types.
|
if the filter_kwargs don't match the required types.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return _get_object_or_404(queryset, **filter_kwargs)
|
return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ class GenericAPIView(views.APIView):
|
||||||
# If you want to use object lookups other than pk, set this attribute.
|
# If you want to use object lookups other than pk, set this attribute.
|
||||||
# For more complex lookup requirements override `get_object()`.
|
# For more complex lookup requirements override `get_object()`.
|
||||||
lookup_field = 'pk'
|
lookup_field = 'pk'
|
||||||
|
lookup_url_kwarg = None
|
||||||
|
|
||||||
# Pagination settings
|
# Pagination settings
|
||||||
paginate_by = api_settings.PAGINATE_BY
|
paginate_by = api_settings.PAGINATE_BY
|
||||||
|
@ -147,8 +148,8 @@ class GenericAPIView(views.APIView):
|
||||||
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
|
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
|
||||||
page = page_kwarg or page_query_param or 1
|
page = page_kwarg or page_query_param or 1
|
||||||
try:
|
try:
|
||||||
page_number = strict_positive_int(page)
|
page_number = paginator.validate_number(page)
|
||||||
except ValueError:
|
except InvalidPage:
|
||||||
if page == 'last':
|
if page == 'last':
|
||||||
page_number = paginator.num_pages
|
page_number = paginator.num_pages
|
||||||
else:
|
else:
|
||||||
|
@ -174,6 +175,14 @@ class GenericAPIView(views.APIView):
|
||||||
method if you want to apply the configured filtering backend to the
|
method if you want to apply the configured filtering backend to the
|
||||||
default queryset.
|
default queryset.
|
||||||
"""
|
"""
|
||||||
|
for backend in self.get_filter_backends():
|
||||||
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_filter_backends(self):
|
||||||
|
"""
|
||||||
|
Returns the list of filter backends that this view requires.
|
||||||
|
"""
|
||||||
filter_backends = self.filter_backends or []
|
filter_backends = self.filter_backends or []
|
||||||
if not filter_backends and self.filter_backend:
|
if not filter_backends and self.filter_backend:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -184,10 +193,8 @@ class GenericAPIView(views.APIView):
|
||||||
DeprecationWarning, stacklevel=2
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
filter_backends = [self.filter_backend]
|
filter_backends = [self.filter_backend]
|
||||||
|
return filter_backends
|
||||||
|
|
||||||
for backend in filter_backends:
|
|
||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
########################
|
########################
|
||||||
### The following methods provide default implementations
|
### The following methods provide default implementations
|
||||||
|
@ -278,9 +285,11 @@ class GenericAPIView(views.APIView):
|
||||||
pass # Deprecation warning
|
pass # Deprecation warning
|
||||||
|
|
||||||
# Perform the lookup filtering.
|
# Perform the lookup filtering.
|
||||||
|
# Note that `pk` and `slug` are deprecated styles of lookup filtering.
|
||||||
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||||
|
lookup = self.kwargs.get(lookup_url_kwarg, None)
|
||||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||||
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
||||||
lookup = self.kwargs.get(self.lookup_field, None)
|
|
||||||
|
|
||||||
if lookup is not None:
|
if lookup is not None:
|
||||||
filter_kwargs = {self.lookup_field: lookup}
|
filter_kwargs = {self.lookup_field: lookup}
|
||||||
|
@ -335,6 +344,18 @@ class GenericAPIView(views.APIView):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def pre_delete(self, obj):
|
||||||
|
"""
|
||||||
|
Placeholder method for calling before deleting an object.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_delete(self, obj):
|
||||||
|
"""
|
||||||
|
Placeholder method for calling after saving an object.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def metadata(self, request):
|
def metadata(self, request):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of metadata about the view.
|
Return a dictionary of metadata about the view.
|
||||||
|
|
|
@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -127,7 +128,12 @@ class UpdateModelMixin(object):
|
||||||
files=request.FILES, partial=partial)
|
files=request.FILES, partial=partial)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
try:
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
|
except ValidationError as err:
|
||||||
|
# full_clean on model instance may be called in pre_save, so we
|
||||||
|
# have to handle eventual errors.
|
||||||
|
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
||||||
self.object = serializer.save(**save_kwargs)
|
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)
|
||||||
|
@ -158,7 +164,8 @@ class UpdateModelMixin(object):
|
||||||
Set any attributes on the object that are implicit in the request.
|
Set any attributes on the object that are implicit in the request.
|
||||||
"""
|
"""
|
||||||
# pk and/or slug attributes are implicit in the URL.
|
# pk and/or slug attributes are implicit in the URL.
|
||||||
lookup = self.kwargs.get(self.lookup_field, None)
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||||
|
lookup = self.kwargs.get(lookup_url_kwarg, None)
|
||||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||||
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
slug = self.kwargs.get(self.slug_url_kwarg, None)
|
||||||
slug_field = slug and self.slug_field or None
|
slug_field = slug and self.slug_field or None
|
||||||
|
@ -185,5 +192,7 @@ class DestroyModelMixin(object):
|
||||||
"""
|
"""
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
self.pre_delete(obj)
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
self.post_delete(obj)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
@ -83,7 +83,7 @@ class YAMLParser(BaseParser):
|
||||||
data = stream.read().decode(encoding)
|
data = stream.read().decode(encoding)
|
||||||
return yaml.safe_load(data)
|
return yaml.safe_load(data)
|
||||||
except (ValueError, yaml.parser.ParserError) as exc:
|
except (ValueError, yaml.parser.ParserError) as exc:
|
||||||
raise ParseError('YAML parse error - %s' % six.u(exc))
|
raise ParseError('YAML parse error - %s' % six.text_type(exc))
|
||||||
|
|
||||||
|
|
||||||
class FormParser(BaseParser):
|
class FormParser(BaseParser):
|
||||||
|
@ -153,7 +153,7 @@ class XMLParser(BaseParser):
|
||||||
try:
|
try:
|
||||||
tree = etree.parse(stream, parser=parser, forbid_dtd=True)
|
tree = etree.parse(stream, parser=parser, forbid_dtd=True)
|
||||||
except (etree.ParseError, ValueError) as exc:
|
except (etree.ParseError, ValueError) as exc:
|
||||||
raise ParseError('XML parse error - %s' % six.u(exc))
|
raise ParseError('XML parse error - %s' % six.text_type(exc))
|
||||||
data = self._xml_convert(tree.getroot())
|
data = self._xml_convert(tree.getroot())
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -3,7 +3,8 @@ Provides a set of pluggable permission policies.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework.compat import oauth2_provider_scope, oauth2_constants
|
from rest_framework.compat import (get_model_name, oauth2_provider_scope,
|
||||||
|
oauth2_constants)
|
||||||
|
|
||||||
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
||||||
|
|
||||||
|
@ -106,7 +107,7 @@ class DjangoModelPermissions(BasePermission):
|
||||||
"""
|
"""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'app_label': model_cls._meta.app_label,
|
'app_label': model_cls._meta.app_label,
|
||||||
'model_name': model_cls._meta.module_name
|
'model_name': get_model_name(model_cls)
|
||||||
}
|
}
|
||||||
return [perm % kwargs for perm in self.perms_map[method]]
|
return [perm % kwargs for perm in self.perms_map[method]]
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ class DjangoObjectPermissions(DjangoModelPermissions):
|
||||||
def get_required_object_permissions(self, method, model_cls):
|
def get_required_object_permissions(self, method, model_cls):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'app_label': model_cls._meta.app_label,
|
'app_label': model_cls._meta.app_label,
|
||||||
'model_name': model_cls._meta.module_name
|
'model_name': get_model_name(model_cls)
|
||||||
}
|
}
|
||||||
return [perm % kwargs for perm in self.perms_map[method]]
|
return [perm % kwargs for perm in self.perms_map[method]]
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ from rest_framework.compat import StringIO
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
from rest_framework.compat import smart_text
|
from rest_framework.compat import smart_text
|
||||||
from rest_framework.compat import yaml
|
from rest_framework.compat import yaml
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.request import is_form_media_type, override_method
|
from rest_framework.request import is_form_media_type, override_method
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
|
@ -272,7 +273,9 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
return [self.template_name]
|
return [self.template_name]
|
||||||
elif hasattr(view, 'get_template_names'):
|
elif hasattr(view, 'get_template_names'):
|
||||||
return view.get_template_names()
|
return view.get_template_names()
|
||||||
raise ImproperlyConfigured('Returned a template response with no template_name')
|
elif hasattr(view, 'template_name'):
|
||||||
|
return [view.template_name]
|
||||||
|
raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response')
|
||||||
|
|
||||||
def get_exception_template(self, response):
|
def get_exception_template(self, response):
|
||||||
template_names = [name % {'status_code': response.status_code}
|
template_names = [name % {'status_code': response.status_code}
|
||||||
|
@ -334,71 +337,15 @@ class HTMLFormRenderer(BaseRenderer):
|
||||||
template = 'rest_framework/form.html'
|
template = 'rest_framework/form.html'
|
||||||
charset = 'utf-8'
|
charset = 'utf-8'
|
||||||
|
|
||||||
def data_to_form_fields(self, data):
|
|
||||||
fields = {}
|
|
||||||
for key, val in data.fields.items():
|
|
||||||
if getattr(val, 'read_only', True):
|
|
||||||
# Don't include read-only fields.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if getattr(val, 'fields', None):
|
|
||||||
# Nested data not supported by HTML forms.
|
|
||||||
continue
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
kwargs['required'] = val.required
|
|
||||||
|
|
||||||
#if getattr(v, 'queryset', None):
|
|
||||||
# kwargs['queryset'] = v.queryset
|
|
||||||
|
|
||||||
if getattr(val, 'choices', None) is not None:
|
|
||||||
kwargs['choices'] = val.choices
|
|
||||||
|
|
||||||
if getattr(val, 'regex', None) is not None:
|
|
||||||
kwargs['regex'] = val.regex
|
|
||||||
|
|
||||||
if getattr(val, 'widget', None):
|
|
||||||
widget = copy.deepcopy(val.widget)
|
|
||||||
kwargs['widget'] = widget
|
|
||||||
|
|
||||||
if getattr(val, 'default', None) is not None:
|
|
||||||
kwargs['initial'] = val.default
|
|
||||||
|
|
||||||
if getattr(val, 'label', None) is not None:
|
|
||||||
kwargs['label'] = val.label
|
|
||||||
|
|
||||||
if getattr(val, 'help_text', None) is not None:
|
|
||||||
kwargs['help_text'] = val.help_text
|
|
||||||
|
|
||||||
fields[key] = val.form_field_class(**kwargs)
|
|
||||||
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
Render serializer data and return an HTML form, as a string.
|
Render serializer data and return an HTML form, as a string.
|
||||||
"""
|
"""
|
||||||
# The HTMLFormRenderer currently uses something of a hack to render
|
renderer_context = renderer_context or {}
|
||||||
# the content, by translating each of the serializer fields into
|
|
||||||
# an html form field, creating a dynamic form using those fields,
|
|
||||||
# and then rendering that form.
|
|
||||||
|
|
||||||
# This isn't strictly neccessary, as we could render the serilizer
|
|
||||||
# fields to HTML directly. The implementation is historical and will
|
|
||||||
# likely change at some point.
|
|
||||||
|
|
||||||
self.renderer_context = renderer_context or {}
|
|
||||||
request = renderer_context['request']
|
request = renderer_context['request']
|
||||||
|
|
||||||
# Creating an on the fly form see:
|
|
||||||
# http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
|
||||||
fields = self.data_to_form_fields(data)
|
|
||||||
DynamicForm = type(str('DynamicForm'), (forms.Form,), fields)
|
|
||||||
data = None if data.empty else data
|
|
||||||
|
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
context = RequestContext(request, {'form': DynamicForm(data)})
|
context = RequestContext(request, {'form': data})
|
||||||
|
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -419,8 +366,13 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
renderers = [renderer for renderer in view.renderer_classes
|
renderers = [renderer for renderer in view.renderer_classes
|
||||||
if not issubclass(renderer, BrowsableAPIRenderer)]
|
if not issubclass(renderer, BrowsableAPIRenderer)]
|
||||||
|
non_template_renderers = [renderer for renderer in renderers
|
||||||
|
if not hasattr(renderer, 'get_template_names')]
|
||||||
|
|
||||||
if not renderers:
|
if not renderers:
|
||||||
return None
|
return None
|
||||||
|
elif non_template_renderers:
|
||||||
|
return non_template_renderers[0]()
|
||||||
return renderers[0]()
|
return renderers[0]()
|
||||||
|
|
||||||
def get_content(self, renderer, data,
|
def get_content(self, renderer, data,
|
||||||
|
@ -468,6 +420,17 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
|
||||||
In the absence of the View having an associated form then return None.
|
In the absence of the View having an associated form then return None.
|
||||||
"""
|
"""
|
||||||
|
if request.method == method:
|
||||||
|
try:
|
||||||
|
data = request.DATA
|
||||||
|
files = request.FILES
|
||||||
|
except ParseError:
|
||||||
|
data = None
|
||||||
|
files = None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
files = None
|
||||||
|
|
||||||
with override_method(view, request, method) as request:
|
with override_method(view, request, method) as request:
|
||||||
obj = getattr(view, 'object', None)
|
obj = getattr(view, 'object', None)
|
||||||
if not self.show_form_for_method(view, method, request, obj):
|
if not self.show_form_for_method(view, method, request, obj):
|
||||||
|
@ -480,9 +443,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
|
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
|
||||||
return
|
return
|
||||||
|
|
||||||
serializer = view.get_serializer(instance=obj)
|
serializer = view.get_serializer(instance=obj, data=data, files=files)
|
||||||
|
serializer.is_valid()
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
|
|
||||||
form_renderer = self.form_renderer_class()
|
form_renderer = self.form_renderer_class()
|
||||||
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
||||||
|
|
||||||
|
@ -574,6 +538,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
|
||||||
renderer = self.get_default_renderer(view)
|
renderer = self.get_default_renderer(view)
|
||||||
|
|
||||||
|
raw_data_post_form = self.get_raw_data_form(view, 'POST', request)
|
||||||
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
|
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
|
||||||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
|
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
|
||||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||||
|
@ -592,12 +557,11 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
|
||||||
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
||||||
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
||||||
'patch_form': self.get_rendered_html_form(view, 'PATCH', request),
|
|
||||||
'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
|
'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
|
||||||
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
|
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
|
||||||
|
|
||||||
'raw_data_put_form': raw_data_put_form,
|
'raw_data_put_form': raw_data_put_form,
|
||||||
'raw_data_post_form': self.get_raw_data_form(view, 'POST', request),
|
'raw_data_post_form': raw_data_post_form,
|
||||||
'raw_data_patch_form': raw_data_patch_form,
|
'raw_data_patch_form': raw_data_patch_form,
|
||||||
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
|
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
|
||||||
|
|
||||||
|
|
|
@ -334,7 +334,7 @@ class Request(object):
|
||||||
self._CONTENT_PARAM in self._data and
|
self._CONTENT_PARAM in self._data and
|
||||||
self._CONTENTTYPE_PARAM in self._data):
|
self._CONTENTTYPE_PARAM in self._data):
|
||||||
self._content_type = self._data[self._CONTENTTYPE_PARAM]
|
self._content_type = self._data[self._CONTENTTYPE_PARAM]
|
||||||
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING))
|
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
|
||||||
self._data, self._files = (Empty, Empty)
|
self._data, self._files = (Empty, Empty)
|
||||||
|
|
||||||
def _parse(self):
|
def _parse(self):
|
||||||
|
@ -356,7 +356,16 @@ class Request(object):
|
||||||
if not parser:
|
if not parser:
|
||||||
raise exceptions.UnsupportedMediaType(media_type)
|
raise exceptions.UnsupportedMediaType(media_type)
|
||||||
|
|
||||||
|
try:
|
||||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||||
|
except:
|
||||||
|
# If we get an exception during parsing, fill in empty data and
|
||||||
|
# re-raise. Ensures we don't simply repeat the error when
|
||||||
|
# attempting to render the browsable renderer response, or when
|
||||||
|
# logging the request or similar.
|
||||||
|
self._data = QueryDict('', self._request._encoding)
|
||||||
|
self._files = MultiValueDict()
|
||||||
|
raise
|
||||||
|
|
||||||
# Parser classes may return the raw data, or a
|
# Parser classes may return the raw data, or a
|
||||||
# DataAndFiles object. Unpack the result as required.
|
# DataAndFiles object. Unpack the result as required.
|
||||||
|
|
|
@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse):
|
||||||
assert charset, 'renderer returned unicode, and did not specify ' \
|
assert charset, 'renderer returned unicode, and did not specify ' \
|
||||||
'a charset value.'
|
'a charset value.'
|
||||||
return bytes(ret.encode(charset))
|
return bytes(ret.encode(charset))
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
del self['Content-Type']
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -6,8 +6,8 @@ form encoded input.
|
||||||
Serialization in REST framework is a two-phase process:
|
Serialization in REST framework is a two-phase process:
|
||||||
|
|
||||||
1. Serializers marshal between complex types like model instances, and
|
1. Serializers marshal between complex types like model instances, and
|
||||||
python primatives.
|
python primitives.
|
||||||
2. The process of marshalling between python primatives and request and
|
2. The process of marshalling between python primitives and request and
|
||||||
response content is handled by parsers and renderers.
|
response content is handled by parsers and renderers.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
@ -31,9 +31,17 @@ from rest_framework.relations import *
|
||||||
from rest_framework.fields import *
|
from rest_framework.fields import *
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_name(name):
|
||||||
|
"""Converts 'first_name' to 'First name'"""
|
||||||
|
if not name:
|
||||||
|
return ''
|
||||||
|
return name.replace('_', ' ').capitalize()
|
||||||
|
|
||||||
|
|
||||||
class RelationsList(list):
|
class RelationsList(list):
|
||||||
_deleted = []
|
_deleted = []
|
||||||
|
|
||||||
|
|
||||||
class NestedValidationError(ValidationError):
|
class NestedValidationError(ValidationError):
|
||||||
"""
|
"""
|
||||||
The default ValidationError behavior is to stringify each item in the list
|
The default ValidationError behavior is to stringify each item in the list
|
||||||
|
@ -48,9 +56,13 @@ class NestedValidationError(ValidationError):
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
if isinstance(message, dict):
|
if isinstance(message, dict):
|
||||||
self.messages = [message]
|
self._messages = [message]
|
||||||
else:
|
else:
|
||||||
self.messages = message
|
self._messages = message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def messages(self):
|
||||||
|
return self._messages
|
||||||
|
|
||||||
|
|
||||||
class DictWithMetadata(dict):
|
class DictWithMetadata(dict):
|
||||||
|
@ -254,10 +266,13 @@ class BaseSerializer(WritableField):
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
if field_name in self._errors:
|
if field_name in self._errors:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
source = field.source or field_name
|
||||||
|
if self.partial and source not in attrs:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
||||||
if validate_method:
|
if validate_method:
|
||||||
source = field.source or field_name
|
|
||||||
attrs = validate_method(attrs, source)
|
attrs = validate_method(attrs, source)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
||||||
|
@ -300,14 +315,19 @@ class BaseSerializer(WritableField):
|
||||||
"""
|
"""
|
||||||
ret = self._dict_class()
|
ret = self._dict_class()
|
||||||
ret.fields = self._dict_class()
|
ret.fields = self._dict_class()
|
||||||
ret.empty = obj is None
|
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
|
if field.read_only and obj is None:
|
||||||
|
continue
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
|
method = getattr(self, 'transform_%s' % field_name, None)
|
||||||
|
if callable(method):
|
||||||
|
value = method(obj, value)
|
||||||
ret[key] = value
|
ret[key] = value
|
||||||
ret.fields[key] = field
|
ret.fields[key] = self.augment_field(field, field_name, key, value)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def from_native(self, data, files):
|
def from_native(self, data, files):
|
||||||
|
@ -315,6 +335,7 @@ class BaseSerializer(WritableField):
|
||||||
Deserialize primitives -> objects.
|
Deserialize primitives -> objects.
|
||||||
"""
|
"""
|
||||||
self._errors = {}
|
self._errors = {}
|
||||||
|
|
||||||
if data is not None or files is not None:
|
if data is not None or files is not None:
|
||||||
attrs = self.restore_fields(data, files)
|
attrs = self.restore_fields(data, files)
|
||||||
if attrs is not None:
|
if attrs is not None:
|
||||||
|
@ -325,6 +346,15 @@ class BaseSerializer(WritableField):
|
||||||
if not self._errors:
|
if not self._errors:
|
||||||
return self.restore_object(attrs, instance=getattr(self, 'object', None))
|
return self.restore_object(attrs, instance=getattr(self, 'object', None))
|
||||||
|
|
||||||
|
def augment_field(self, field, field_name, key, value):
|
||||||
|
# This horrible stuff is to manage serializers rendering to HTML
|
||||||
|
field._errors = self._errors.get(key) if self._errors else None
|
||||||
|
field._name = field_name
|
||||||
|
field._value = self.init_data.get(key) if self._errors and self.init_data else value
|
||||||
|
if not field.label:
|
||||||
|
field.label = pretty_name(key)
|
||||||
|
return field
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
"""
|
"""
|
||||||
Override default so that the serializer can be used as a nested field
|
Override default so that the serializer can be used as a nested field
|
||||||
|
@ -375,8 +405,14 @@ class BaseSerializer(WritableField):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set the serializer object if it exists
|
# Set the serializer object if it exists
|
||||||
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
|
||||||
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
|
|
||||||
|
# If we have a model manager or similar object then we need
|
||||||
|
# to iterate through each instance.
|
||||||
|
if (self.many and
|
||||||
|
not hasattr(obj, '__iter__') and
|
||||||
|
is_simple_callable(getattr(obj, 'all', None))):
|
||||||
|
obj = obj.all()
|
||||||
|
|
||||||
if self.source == '*':
|
if self.source == '*':
|
||||||
if value:
|
if value:
|
||||||
|
@ -503,6 +539,9 @@ class BaseSerializer(WritableField):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object and return it.
|
||||||
"""
|
"""
|
||||||
|
# Clear cached _data, which may be invalidated by `save()`
|
||||||
|
self._data = None
|
||||||
|
|
||||||
if isinstance(self.object, list):
|
if isinstance(self.object, list):
|
||||||
[self.save_object(item, **kwargs) for item in self.object]
|
[self.save_object(item, **kwargs) for item in self.object]
|
||||||
|
|
||||||
|
@ -751,6 +790,8 @@ class ModelSerializer(Serializer):
|
||||||
# TODO: TypedChoiceField?
|
# TODO: TypedChoiceField?
|
||||||
if model_field.flatchoices: # This ModelField contains choices
|
if model_field.flatchoices: # This ModelField contains choices
|
||||||
kwargs['choices'] = model_field.flatchoices
|
kwargs['choices'] = model_field.flatchoices
|
||||||
|
if model_field.null:
|
||||||
|
kwargs['empty'] = None
|
||||||
return ChoiceField(**kwargs)
|
return ChoiceField(**kwargs)
|
||||||
|
|
||||||
# put this below the ChoiceField because min_value isn't a valid initializer
|
# put this below the ChoiceField because min_value isn't a valid initializer
|
||||||
|
@ -822,13 +863,13 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
# Reverse fk or one-to-one relations
|
# Reverse fk or one-to-one relations
|
||||||
for (obj, model) in meta.get_all_related_objects_with_model():
|
for (obj, model) in meta.get_all_related_objects_with_model():
|
||||||
field_name = obj.field.related_query_name()
|
field_name = obj.get_accessor_name()
|
||||||
if field_name in attrs:
|
if field_name in attrs:
|
||||||
related_data[field_name] = attrs.pop(field_name)
|
related_data[field_name] = attrs.pop(field_name)
|
||||||
|
|
||||||
# Reverse m2m relations
|
# Reverse m2m relations
|
||||||
for (obj, model) in meta.get_all_related_m2m_objects_with_model():
|
for (obj, model) in meta.get_all_related_m2m_objects_with_model():
|
||||||
field_name = obj.field.related_query_name()
|
field_name = obj.get_accessor_name()
|
||||||
if field_name in attrs:
|
if field_name in attrs:
|
||||||
m2m_data[field_name] = attrs.pop(field_name)
|
m2m_data[field_name] = attrs.pop(field_name)
|
||||||
|
|
||||||
|
@ -846,7 +887,10 @@ class ModelSerializer(Serializer):
|
||||||
# Update an existing instance...
|
# Update an existing instance...
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
for key, val in attrs.items():
|
for key, val in attrs.items():
|
||||||
|
try:
|
||||||
setattr(instance, key, val)
|
setattr(instance, key, val)
|
||||||
|
except ValueError:
|
||||||
|
self._errors[key] = self.error_messages['required']
|
||||||
|
|
||||||
# ...or create a new instance
|
# ...or create a new instance
|
||||||
else:
|
else:
|
||||||
|
@ -872,7 +916,7 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
def save_object(self, obj, **kwargs):
|
def save_object(self, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object.
|
||||||
"""
|
"""
|
||||||
if getattr(obj, '_nested_forward_relations', None):
|
if getattr(obj, '_nested_forward_relations', None):
|
||||||
# Nested relationships need to be saved before we can save the
|
# Nested relationships need to be saved before we can save the
|
||||||
|
@ -890,11 +934,16 @@ class ModelSerializer(Serializer):
|
||||||
del(obj._m2m_data)
|
del(obj._m2m_data)
|
||||||
|
|
||||||
if getattr(obj, '_related_data', None):
|
if getattr(obj, '_related_data', None):
|
||||||
|
related_fields = dict([
|
||||||
|
(field.get_accessor_name(), field)
|
||||||
|
for field, model
|
||||||
|
in obj._meta.get_all_related_objects_with_model()
|
||||||
|
])
|
||||||
for accessor_name, related in obj._related_data.items():
|
for accessor_name, related in obj._related_data.items():
|
||||||
if isinstance(related, RelationsList):
|
if isinstance(related, RelationsList):
|
||||||
# Nested reverse fk relationship
|
# Nested reverse fk relationship
|
||||||
for related_item in related:
|
for related_item in related:
|
||||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
fk_field = related_fields[accessor_name].field.name
|
||||||
setattr(related_item, fk_field, obj)
|
setattr(related_item, fk_field, obj)
|
||||||
self.save_object(related_item)
|
self.save_object(related_item)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
def is_informational(code):
|
||||||
|
return code >= 100 and code <= 199
|
||||||
|
|
||||||
|
def is_success(code):
|
||||||
|
return code >= 200 and code <= 299
|
||||||
|
|
||||||
|
def is_redirect(code):
|
||||||
|
return code >= 300 and code <= 399
|
||||||
|
|
||||||
|
def is_client_error(code):
|
||||||
|
return code >= 400 and code <= 499
|
||||||
|
|
||||||
|
def is_server_error(code):
|
||||||
|
return code >= 500 and code <= 599
|
||||||
|
|
||||||
|
|
||||||
HTTP_100_CONTINUE = 100
|
HTTP_100_CONTINUE = 100
|
||||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||||
HTTP_200_OK = 200
|
HTTP_200_OK = 200
|
||||||
|
|
|
@ -111,7 +111,9 @@
|
||||||
|
|
||||||
<div class="content-main">
|
<div class="content-main">
|
||||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||||
|
{% block description %}
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
{% endblock %}
|
||||||
<div class="request-info" style="clear: both" >
|
<div class="request-info" style="clear: both" >
|
||||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
@ -152,7 +154,7 @@
|
||||||
{% with form=raw_data_post_form %}
|
{% with form=raw_data_post_form %}
|
||||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% include "rest_framework/form.html" %}
|
{% include "rest_framework/raw_data_form.html" %}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -189,7 +191,7 @@
|
||||||
{% with form=raw_data_put_or_patch_form %}
|
{% with form=raw_data_put_or_patch_form %}
|
||||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% include "rest_framework/form.html" %}
|
{% include "rest_framework/raw_data_form.html" %}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
{% if raw_data_put_form %}
|
{% if raw_data_put_form %}
|
||||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||||
|
@ -220,9 +222,6 @@
|
||||||
</div><!-- ./wrapper -->
|
</div><!-- ./wrapper -->
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<!--<div id="footer">
|
|
||||||
<a class="powered-by" href='http://django-rest-framework.org'>Django REST framework</a>
|
|
||||||
</div>-->
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
{% for field in form %}
|
{% for field in form.fields.values %}
|
||||||
<div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
|
{% if not field.read_only %}
|
||||||
|
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||||
{{ field.label_tag|add_class:"control-label" }}
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{ field }}
|
{{ field.widget_html }}
|
||||||
<span class="help-block">{{ field.help_text }}</span>
|
{% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %}
|
||||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
12
rest_framework/templates/rest_framework/raw_data_form.html
Normal file
12
rest_framework/templates/rest_framework/raw_data_form.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% load rest_framework %}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="control-group">
|
||||||
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
|
<div class="controls">
|
||||||
|
{{ field }}
|
||||||
|
<span class="help-block">{{ field.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer):
|
||||||
model = TimeFieldModel
|
model = TimeFieldModel
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_CHOICES = [
|
||||||
|
('red', 'Red'),
|
||||||
|
('green', 'Green'),
|
||||||
|
('blue', 'Blue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldModel(models.Model):
|
||||||
|
choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ChoiceFieldModel
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldModelWithNull(models.Model):
|
||||||
|
choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ChoiceFieldModelWithNull
|
||||||
|
|
||||||
|
|
||||||
class BasicFieldTests(TestCase):
|
class BasicFieldTests(TestCase):
|
||||||
def test_auto_now_fields_read_only(self):
|
def test_auto_now_fields_read_only(self):
|
||||||
"""
|
"""
|
||||||
|
@ -667,34 +692,71 @@ class ChoiceFieldTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for the ChoiceField options generator
|
Tests for the ChoiceField options generator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SAMPLE_CHOICES = [
|
|
||||||
('red', 'Red'),
|
|
||||||
('green', 'Green'),
|
|
||||||
('blue', 'Blue'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_choices_required(self):
|
def test_choices_required(self):
|
||||||
"""
|
"""
|
||||||
Make sure proper choices are rendered if field is required
|
Make sure proper choices are rendered if field is required
|
||||||
"""
|
"""
|
||||||
f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
|
f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
|
||||||
self.assertEqual(f.choices, self.SAMPLE_CHOICES)
|
self.assertEqual(f.choices, SAMPLE_CHOICES)
|
||||||
|
|
||||||
def test_choices_not_required(self):
|
def test_choices_not_required(self):
|
||||||
"""
|
"""
|
||||||
Make sure proper choices (plus blank) are rendered if the field isn't required
|
Make sure proper choices (plus blank) are rendered if the field isn't required
|
||||||
"""
|
"""
|
||||||
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
|
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_invalid_choice_model(self):
|
||||||
|
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||||
|
self.assertFalse(s.is_valid())
|
||||||
|
self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
|
||||||
|
self.assertEqual(s.data['choice'], '')
|
||||||
|
|
||||||
|
def test_empty_choice_model(self):
|
||||||
|
"""
|
||||||
|
Test that the 'empty' value is correctly passed and used depending on
|
||||||
|
the 'null' property on the model field.
|
||||||
|
"""
|
||||||
|
s = ChoiceFieldModelSerializer(data={'choice': ''})
|
||||||
|
self.assertTrue(s.is_valid())
|
||||||
|
self.assertEqual(s.data['choice'], '')
|
||||||
|
|
||||||
|
s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
|
||||||
|
self.assertTrue(s.is_valid())
|
||||||
|
self.assertEqual(s.data['choice'], None)
|
||||||
|
|
||||||
def test_from_native_empty(self):
|
def test_from_native_empty(self):
|
||||||
"""
|
"""
|
||||||
Make sure from_native() returns None on empty param.
|
Make sure from_native() returns an empty string on empty param by default.
|
||||||
"""
|
"""
|
||||||
f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES)
|
f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
|
||||||
result = f.from_native('')
|
self.assertEqual(f.from_native(''), '')
|
||||||
self.assertEqual(result, None)
|
self.assertEqual(f.from_native(None), '')
|
||||||
|
|
||||||
|
def test_from_native_empty_override(self):
|
||||||
|
"""
|
||||||
|
Make sure you can override from_native() behavior regarding empty values.
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
|
||||||
|
self.assertEqual(f.from_native(''), None)
|
||||||
|
self.assertEqual(f.from_native(None), None)
|
||||||
|
|
||||||
|
def test_metadata_choices(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices are included in the field's metadata.
|
||||||
|
"""
|
||||||
|
choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
|
||||||
|
f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.metadata()['choices'], choices)
|
||||||
|
|
||||||
|
def test_metadata_choices_not_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices are included in the field's metadata.
|
||||||
|
"""
|
||||||
|
choices = [{'value': v, 'display_name': n}
|
||||||
|
for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
|
||||||
|
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.metadata()['choices'], choices)
|
||||||
|
|
||||||
|
|
||||||
class EmailFieldTests(TestCase):
|
class EmailFieldTests(TestCase):
|
||||||
|
|
|
@ -80,3 +80,16 @@ class FileSerializerTests(TestCase):
|
||||||
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
|
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
self.assertEqual(serializer.errors, {'file': [errmsg]})
|
self.assertEqual(serializer.errors, {'file': [errmsg]})
|
||||||
|
|
||||||
|
def test_validation_with_no_data(self):
|
||||||
|
"""
|
||||||
|
Validation should still function when no data dictionary is provided.
|
||||||
|
"""
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
file = BytesIO(six.b('stuff'))
|
||||||
|
file.name = 'stuff.txt'
|
||||||
|
file.size = len(file.getvalue())
|
||||||
|
uploaded_file = UploadedFile(file=file, created=now)
|
||||||
|
|
||||||
|
serializer = UploadedFileSerializer(files={'file': file})
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
|
|
@ -364,6 +364,12 @@ class OrdringFilterModel(models.Model):
|
||||||
text = models.CharField(max_length=100)
|
text = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingFilterRelatedModel(models.Model):
|
||||||
|
related_object = models.ForeignKey(OrdringFilterModel,
|
||||||
|
related_name="relateds")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OrderingFilterTests(TestCase):
|
class OrderingFilterTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Sequence of title/text is:
|
# Sequence of title/text is:
|
||||||
|
@ -473,3 +479,36 @@ class OrderingFilterTests(TestCase):
|
||||||
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_ordering_by_aggregate_field(self):
|
||||||
|
# create some related models to aggregate order by
|
||||||
|
num_objs = [2, 5, 3]
|
||||||
|
for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
|
||||||
|
num_objs):
|
||||||
|
for _ in range(num_relateds):
|
||||||
|
new_related = OrderingFilterRelatedModel(
|
||||||
|
related_object=obj
|
||||||
|
)
|
||||||
|
new_related.save()
|
||||||
|
|
||||||
|
class OrderingListView(generics.ListAPIView):
|
||||||
|
model = OrdringFilterModel
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = 'title'
|
||||||
|
queryset = OrdringFilterModel.objects.all().annotate(
|
||||||
|
models.Count("relateds"))
|
||||||
|
|
||||||
|
view = OrderingListView.as_view()
|
||||||
|
request = factory.get('?ordering=relateds__count')
|
||||||
|
response = view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
[
|
||||||
|
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||||
|
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||||
|
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""
|
"""
|
||||||
model = BasicModel
|
model = BasicModel
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super(InstanceView, self).get_queryset()
|
||||||
|
return queryset.exclude(text='filtered out')
|
||||||
|
|
||||||
|
|
||||||
class SlugSerializer(serializers.ModelSerializer):
|
class SlugSerializer(serializers.ModelSerializer):
|
||||||
slug = serializers.Field() # read only
|
slug = serializers.Field() # read only
|
||||||
|
@ -160,10 +164,10 @@ class TestInstanceView(TestCase):
|
||||||
"""
|
"""
|
||||||
Create 3 BasicModel intances.
|
Create 3 BasicModel intances.
|
||||||
"""
|
"""
|
||||||
items = ['foo', 'bar', 'baz']
|
items = ['foo', 'bar', 'baz', 'filtered out']
|
||||||
for item in items:
|
for item in items:
|
||||||
BasicModel(text=item).save()
|
BasicModel(text=item).save()
|
||||||
self.objects = BasicModel.objects
|
self.objects = BasicModel.objects.exclude(text='filtered out')
|
||||||
self.data = [
|
self.data = [
|
||||||
{'id': obj.id, 'text': obj.text}
|
{'id': obj.id, 'text': obj.text}
|
||||||
for obj in self.objects.all()
|
for obj in self.objects.all()
|
||||||
|
@ -352,6 +356,17 @@ class TestInstanceView(TestCase):
|
||||||
updated = self.objects.get(id=1)
|
updated = self.objects.get(id=1)
|
||||||
self.assertEqual(updated.text, 'foobar')
|
self.assertEqual(updated.text, 'foobar')
|
||||||
|
|
||||||
|
def test_put_to_filtered_out_instance(self):
|
||||||
|
"""
|
||||||
|
PUT requests to an URL of instance which is filtered out should not be
|
||||||
|
able to create new objects.
|
||||||
|
"""
|
||||||
|
data = {'text': 'foo'}
|
||||||
|
filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
|
||||||
|
request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
|
||||||
|
response = self.view(request, pk=filtered_out_pk).render()
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def test_put_as_create_on_id_based_url(self):
|
def test_put_as_create_on_id_based_url(self):
|
||||||
"""
|
"""
|
||||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||||
|
@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object):
|
||||||
return queryset.filter(text='other')
|
return queryset.filter(text='other')
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFieldModel(models.Model):
|
||||||
|
field_a = models.CharField(max_length=100)
|
||||||
|
field_b = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicSerializerView(generics.ListCreateAPIView):
|
||||||
|
model = TwoFieldModel
|
||||||
|
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
class DynamicSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = TwoFieldModel
|
||||||
|
fields = ('field_b',)
|
||||||
|
return DynamicSerializer
|
||||||
|
return super(DynamicSerializerView, self).get_serializer_class()
|
||||||
|
|
||||||
|
|
||||||
class TestFilterBackendAppliedToViews(TestCase):
|
class TestFilterBackendAppliedToViews(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase):
|
||||||
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': 'foo'})
|
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
|
||||||
|
|
||||||
|
|
||||||
class TwoFieldModel(models.Model):
|
|
||||||
field_a = models.CharField(max_length=100)
|
|
||||||
field_b = models.CharField(max_length=100)
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicSerializerView(generics.ListCreateAPIView):
|
|
||||||
model = TwoFieldModel
|
|
||||||
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
if self.request.method == 'POST':
|
|
||||||
class DynamicSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = TwoFieldModel
|
|
||||||
fields = ('field_b',)
|
|
||||||
return DynamicSerializer
|
|
||||||
return super(DynamicSerializerView, self).get_serializer_class()
|
|
||||||
|
|
||||||
|
|
||||||
class TestFilterBackendAppliedToViews(TestCase):
|
|
||||||
|
|
||||||
def test_dynamic_serializer_form_in_browsable_api(self):
|
def test_dynamic_serializer_form_in_browsable_api(self):
|
||||||
"""
|
"""
|
||||||
GET requests to ListCreateAPIView should return filtered list.
|
GET requests to ListCreateAPIView should return filtered list.
|
||||||
|
|
|
@ -430,3 +430,88 @@ class TestCustomPaginationSerializer(TestCase):
|
||||||
'objects': ['john', 'paul']
|
'objects': ['john', 'paul']
|
||||||
}
|
}
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class NonIntegerPage(object):
|
||||||
|
|
||||||
|
def __init__(self, paginator, object_list, prev_token, token, next_token):
|
||||||
|
self.paginator = paginator
|
||||||
|
self.object_list = object_list
|
||||||
|
self.prev_token = prev_token
|
||||||
|
self.token = token
|
||||||
|
self.next_token = next_token
|
||||||
|
|
||||||
|
def has_next(self):
|
||||||
|
return not not self.next_token
|
||||||
|
|
||||||
|
def next_page_number(self):
|
||||||
|
return self.next_token
|
||||||
|
|
||||||
|
def has_previous(self):
|
||||||
|
return not not self.prev_token
|
||||||
|
|
||||||
|
def previous_page_number(self):
|
||||||
|
return self.prev_token
|
||||||
|
|
||||||
|
|
||||||
|
class NonIntegerPaginator(object):
|
||||||
|
|
||||||
|
def __init__(self, object_list, per_page):
|
||||||
|
self.object_list = object_list
|
||||||
|
self.per_page = per_page
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
# pretend like we don't know how many pages we have
|
||||||
|
return None
|
||||||
|
|
||||||
|
def page(self, token=None):
|
||||||
|
if token:
|
||||||
|
try:
|
||||||
|
first = self.object_list.index(token)
|
||||||
|
except ValueError:
|
||||||
|
first = 0
|
||||||
|
else:
|
||||||
|
first = 0
|
||||||
|
n = len(self.object_list)
|
||||||
|
last = min(first + self.per_page, n)
|
||||||
|
prev_token = self.object_list[last - (2 * self.per_page)] if first else None
|
||||||
|
next_token = self.object_list[last] if last < n else None
|
||||||
|
return NonIntegerPage(self, self.object_list[first:last], prev_token, token, next_token)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNonIntegerPagination(TestCase):
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_pagination_serializer(self):
|
||||||
|
objects = ['john', 'paul', 'george', 'ringo']
|
||||||
|
paginator = NonIntegerPaginator(objects, 2)
|
||||||
|
|
||||||
|
request = APIRequestFactory().get('/foobar')
|
||||||
|
serializer = CustomPaginationSerializer(
|
||||||
|
instance=paginator.page(),
|
||||||
|
context={'request': request}
|
||||||
|
)
|
||||||
|
expected = {
|
||||||
|
'links': {
|
||||||
|
'next': 'http://testserver/foobar?page={0}'.format(objects[2]),
|
||||||
|
'prev': None
|
||||||
|
},
|
||||||
|
'total_results': None,
|
||||||
|
'objects': objects[:2]
|
||||||
|
}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
request = APIRequestFactory().get('/foobar')
|
||||||
|
serializer = CustomPaginationSerializer(
|
||||||
|
instance=paginator.page('george'),
|
||||||
|
context={'request': request}
|
||||||
|
)
|
||||||
|
expected = {
|
||||||
|
'links': {
|
||||||
|
'next': None,
|
||||||
|
'prev': 'http://testserver/foobar?page={0}'.format(objects[0]),
|
||||||
|
},
|
||||||
|
'total_results': None,
|
||||||
|
'objects': objects[2:]
|
||||||
|
}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
|
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
|
||||||
from rest_framework.compat import guardian
|
from rest_framework.compat import guardian, get_model_name
|
||||||
from rest_framework.filters import DjangoObjectPermissionsFilter
|
from rest_framework.filters import DjangoObjectPermissionsFilter
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
from rest_framework.tests.models import BasicModel
|
from rest_framework.tests.models import BasicModel
|
||||||
|
@ -202,7 +202,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
|
||||||
|
|
||||||
# give everyone model level permissions, as we are not testing those
|
# give everyone model level permissions, as we are not testing those
|
||||||
everyone = Group.objects.create(name='everyone')
|
everyone = Group.objects.create(name='everyone')
|
||||||
model_name = BasicPermModel._meta.module_name
|
model_name = get_model_name(BasicPermModel)
|
||||||
app_label = BasicPermModel._meta.app_label
|
app_label = BasicPermModel._meta.app_label
|
||||||
f = '{0}_{1}'.format
|
f = '{0}_{1}'.format
|
||||||
perms = {
|
perms = {
|
||||||
|
|
|
@ -16,7 +16,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||||
from rest_framework.parsers import YAMLParser, XMLParser
|
from rest_framework.parsers import YAMLParser, XMLParser
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from collections import MutableMapping
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -65,11 +67,23 @@ class MockView(APIView):
|
||||||
|
|
||||||
|
|
||||||
class MockGETView(APIView):
|
class MockGETView(APIView):
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
return Response({'foo': ['bar', 'baz']})
|
return Response({'foo': ['bar', 'baz']})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MockPOSTView(APIView):
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
return Response({'foo': request.DATA})
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyGETView(APIView):
|
||||||
|
renderer_classes = (JSONRenderer,)
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class HTMLView(APIView):
|
class HTMLView(APIView):
|
||||||
renderer_classes = (BrowsableAPIRenderer, )
|
renderer_classes = (BrowsableAPIRenderer, )
|
||||||
|
|
||||||
|
@ -89,8 +103,10 @@ urlpatterns = patterns('',
|
||||||
url(r'^cache$', MockGETView.as_view()),
|
url(r'^cache$', MockGETView.as_view()),
|
||||||
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
|
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
|
||||||
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
|
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
|
||||||
|
url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
|
||||||
url(r'^html$', HTMLView.as_view()),
|
url(r'^html$', HTMLView.as_view()),
|
||||||
url(r'^html1$', HTMLView1.as_view()),
|
url(r'^html1$', HTMLView1.as_view()),
|
||||||
|
url(r'^empty$', EmptyGETView.as_view()),
|
||||||
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
|
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -220,6 +236,22 @@ class RendererEndToEndTests(TestCase):
|
||||||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_parse_error_renderers_browsable_api(self):
|
||||||
|
"""Invalid data should still render the browsable API correctly."""
|
||||||
|
resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
|
||||||
|
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_204_no_content_responses_have_no_content_type_set(self):
|
||||||
|
"""
|
||||||
|
Regression test for #1196
|
||||||
|
|
||||||
|
https://github.com/tomchristie/django-rest-framework/issues/1196
|
||||||
|
"""
|
||||||
|
resp = self.client.get('/empty')
|
||||||
|
self.assertEqual(resp.get('Content-Type', None), None)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||||
|
@ -245,6 +277,44 @@ class JSONRendererTests(TestCase):
|
||||||
ret = JSONRenderer().render(_('test'))
|
ret = JSONRenderer().render(_('test'))
|
||||||
self.assertEqual(ret, b'"test"')
|
self.assertEqual(ret, b'"test"')
|
||||||
|
|
||||||
|
def test_render_dict_abc_obj(self):
|
||||||
|
class Dict(MutableMapping):
|
||||||
|
def __init__(self):
|
||||||
|
self._dict = dict()
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._dict.__getitem__(key)
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
return self._dict.__setitem__(key, value)
|
||||||
|
def __delitem__(self, key):
|
||||||
|
return self._dict.__delitem__(key)
|
||||||
|
def __iter__(self):
|
||||||
|
return self._dict.__iter__()
|
||||||
|
def __len__(self):
|
||||||
|
return self._dict.__len__()
|
||||||
|
def keys(self):
|
||||||
|
return self._dict.keys()
|
||||||
|
|
||||||
|
x = Dict()
|
||||||
|
x['key'] = 'string value'
|
||||||
|
x[2] = 3
|
||||||
|
ret = JSONRenderer().render(x)
|
||||||
|
data = json.loads(ret.decode('utf-8'))
|
||||||
|
self.assertEquals(data, {'key': 'string value', '2': 3})
|
||||||
|
|
||||||
|
def test_render_obj_with_getitem(self):
|
||||||
|
class DictLike(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._dict = {}
|
||||||
|
def set(self, value):
|
||||||
|
self._dict = dict(value)
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._dict[key]
|
||||||
|
|
||||||
|
x = DictLike()
|
||||||
|
x.set({'a': 1, 'b': 'string'})
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
JSONRenderer().render(x)
|
||||||
|
|
||||||
def test_without_content_type_args(self):
|
def test_without_content_type_args(self):
|
||||||
"""
|
"""
|
||||||
Test basic JSON rendering.
|
Test basic JSON rendering.
|
||||||
|
@ -329,7 +399,7 @@ if yaml:
|
||||||
|
|
||||||
class YAMLRendererTests(TestCase):
|
class YAMLRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the YAML Renderer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
|
@ -355,6 +425,17 @@ if yaml:
|
||||||
data = parser.parse(StringIO(content))
|
data = parser.parse(StringIO(content))
|
||||||
self.assertEqual(obj, data)
|
self.assertEqual(obj, data)
|
||||||
|
|
||||||
|
def test_render_decimal(self):
|
||||||
|
"""
|
||||||
|
Test YAML decimal rendering.
|
||||||
|
"""
|
||||||
|
renderer = YAMLRenderer()
|
||||||
|
content = renderer.render({'field': Decimal('111.2')}, 'application/yaml')
|
||||||
|
self.assertYAMLContains(content, "field: '111.2'")
|
||||||
|
|
||||||
|
def assertYAMLContains(self, content, string):
|
||||||
|
self.assertTrue(string in content, '%r not in %r' % (string, content))
|
||||||
|
|
||||||
|
|
||||||
class XMLRendererTestCase(TestCase):
|
class XMLRendererTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.conf.urls import patterns
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
@ -15,12 +16,13 @@ from rest_framework.parsers import (
|
||||||
MultiPartParser,
|
MultiPartParser,
|
||||||
JSONParser
|
JSONParser
|
||||||
)
|
)
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request, Empty
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory, APIClient
|
from rest_framework.test import APIRequestFactory, APIClient
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
from io import BytesIO
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,6 +148,34 @@ class TestContentParsing(TestCase):
|
||||||
request.parsers = (JSONParser(), )
|
request.parsers = (JSONParser(), )
|
||||||
self.assertEqual(request.DATA, json_data)
|
self.assertEqual(request.DATA, json_data)
|
||||||
|
|
||||||
|
def test_form_POST_unicode(self):
|
||||||
|
"""
|
||||||
|
JSON POST via default web interface with unicode data
|
||||||
|
"""
|
||||||
|
# Note: environ and other variables here have simplified content compared to real Request
|
||||||
|
CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
|
||||||
|
environ = {
|
||||||
|
'REQUEST_METHOD': 'POST',
|
||||||
|
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
|
||||||
|
'CONTENT_LENGTH': len(CONTENT),
|
||||||
|
'wsgi.input': BytesIO(CONTENT),
|
||||||
|
}
|
||||||
|
wsgi_request = WSGIRequest(environ=environ)
|
||||||
|
wsgi_request._load_post_and_files()
|
||||||
|
parsers = (JSONParser(), FormParser(), MultiPartParser())
|
||||||
|
parser_context = {
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
'kwargs': {},
|
||||||
|
'args': (),
|
||||||
|
}
|
||||||
|
request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
|
||||||
|
method = request.method
|
||||||
|
self.assertEqual(method, 'POST')
|
||||||
|
self.assertEqual(request._content_type, 'application/json')
|
||||||
|
self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
|
||||||
|
self.assertEqual(request._data, Empty)
|
||||||
|
self.assertEqual(request._files, Empty)
|
||||||
|
|
||||||
# def test_accessing_post_after_data_form(self):
|
# def test_accessing_post_after_data_form(self):
|
||||||
# """
|
# """
|
||||||
# Ensures request.POST can be accessed after request.DATA in
|
# Ensures request.POST can be accessed after request.DATA in
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
|
@ -136,6 +137,7 @@ class BasicTests(TestCase):
|
||||||
'Happy new year!',
|
'Happy new year!',
|
||||||
datetime.datetime(2012, 1, 1)
|
datetime.datetime(2012, 1, 1)
|
||||||
)
|
)
|
||||||
|
self.actionitem = ActionItem(title='Some to do item',)
|
||||||
self.data = {
|
self.data = {
|
||||||
'email': 'tom@example.com',
|
'email': 'tom@example.com',
|
||||||
'content': 'Happy new year!',
|
'content': 'Happy new year!',
|
||||||
|
@ -157,8 +159,7 @@ class BasicTests(TestCase):
|
||||||
expected = {
|
expected = {
|
||||||
'email': '',
|
'email': '',
|
||||||
'content': '',
|
'content': '',
|
||||||
'created': None,
|
'created': None
|
||||||
'sub_comment': ''
|
|
||||||
}
|
}
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
@ -264,6 +265,20 @@ class BasicTests(TestCase):
|
||||||
"""
|
"""
|
||||||
self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
|
self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
|
||||||
|
|
||||||
|
def test_serializer_data_is_cleared_on_save(self):
|
||||||
|
"""
|
||||||
|
Check _data attribute is cleared on `save()`
|
||||||
|
|
||||||
|
Regression test for #1116
|
||||||
|
— id field is not populated if `data` is accessed prior to `save()`
|
||||||
|
"""
|
||||||
|
serializer = ActionItemSerializer(self.actionitem)
|
||||||
|
self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.')
|
||||||
|
serializer.save()
|
||||||
|
self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DictStyleSerializer(serializers.Serializer):
|
class DictStyleSerializer(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
|
@ -496,6 +511,33 @@ class CustomValidationTests(TestCase):
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
|
self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
|
||||||
|
|
||||||
|
def test_partial_update(self):
|
||||||
|
"""
|
||||||
|
Make sure that validate_email isn't called when partial=True and email
|
||||||
|
isn't found in data.
|
||||||
|
"""
|
||||||
|
initial_data = {
|
||||||
|
'email': 'tom@example.com',
|
||||||
|
'content': 'A test comment',
|
||||||
|
'created': datetime.datetime(2012, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = self.CommentSerializerWithFieldValidator(data=initial_data)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
instance = serializer.object
|
||||||
|
|
||||||
|
new_content = 'An *updated* test comment'
|
||||||
|
partial_data = {
|
||||||
|
'content': new_content
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = self.CommentSerializerWithFieldValidator(instance=instance,
|
||||||
|
data=partial_data,
|
||||||
|
partial=True)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
instance = serializer.object
|
||||||
|
self.assertEqual(instance.content, new_content)
|
||||||
|
|
||||||
|
|
||||||
class PositiveIntegerAsChoiceTests(TestCase):
|
class PositiveIntegerAsChoiceTests(TestCase):
|
||||||
def test_positive_integer_in_json_is_correctly_parsed(self):
|
def test_positive_integer_in_json_is_correctly_parsed(self):
|
||||||
|
@ -516,6 +558,29 @@ class ModelValidationTests(TestCase):
|
||||||
self.assertFalse(second_serializer.is_valid())
|
self.assertFalse(second_serializer.is_valid())
|
||||||
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
|
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
|
||||||
|
|
||||||
|
def test_foreign_key_is_null_with_partial(self):
|
||||||
|
"""
|
||||||
|
Test ModelSerializer validation with partial=True
|
||||||
|
|
||||||
|
Specifically test that a null foreign key does not pass validation
|
||||||
|
"""
|
||||||
|
album = Album(title='test')
|
||||||
|
album.save()
|
||||||
|
|
||||||
|
class PhotoSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Photo
|
||||||
|
|
||||||
|
photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
|
||||||
|
self.assertTrue(photo_serializer.is_valid())
|
||||||
|
photo = photo_serializer.save()
|
||||||
|
|
||||||
|
# Updating only the album (foreign key)
|
||||||
|
photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
|
||||||
|
self.assertFalse(photo_serializer.is_valid())
|
||||||
|
self.assertTrue('album' in photo_serializer.errors)
|
||||||
|
self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required'])
|
||||||
|
|
||||||
def test_foreign_key_with_partial(self):
|
def test_foreign_key_with_partial(self):
|
||||||
"""
|
"""
|
||||||
Test ModelSerializer validation with partial=True
|
Test ModelSerializer validation with partial=True
|
||||||
|
@ -1643,3 +1708,38 @@ class SerializerSupportsManyRelationships(TestCase):
|
||||||
serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
|
serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
|
self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
|
||||||
|
|
||||||
|
|
||||||
|
class TransformMethodsSerializer(serializers.Serializer):
|
||||||
|
a = serializers.CharField()
|
||||||
|
b_renamed = serializers.CharField(source='b')
|
||||||
|
|
||||||
|
def transform_a(self, obj, value):
|
||||||
|
return value.lower()
|
||||||
|
|
||||||
|
def transform_b_renamed(self, obj, value):
|
||||||
|
if value is not None:
|
||||||
|
return 'and ' + value
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerializerTransformMethods(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.s = TransformMethodsSerializer()
|
||||||
|
|
||||||
|
def test_transform_methods(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.s.to_native({'a': 'GREEN EGGS', 'b': 'HAM'}),
|
||||||
|
{
|
||||||
|
'a': 'green eggs',
|
||||||
|
'b_renamed': 'and HAM',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_fields(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.s.to_native({'a': 'GREEN EGGS'}),
|
||||||
|
{
|
||||||
|
'a': 'green eggs',
|
||||||
|
'b_renamed': None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
15
rest_framework/tests/test_serializer_empty.py
Normal file
15
rest_framework/tests/test_serializer_empty.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class EmptySerializerTestCase(TestCase):
|
||||||
|
def test_empty_serializer(self):
|
||||||
|
class FooBarSerializer(serializers.Serializer):
|
||||||
|
foo = serializers.IntegerField()
|
||||||
|
bar = serializers.SerializerMethodField('get_bar')
|
||||||
|
|
||||||
|
def get_bar(self, obj):
|
||||||
|
return 'bar'
|
||||||
|
|
||||||
|
serializer = FooBarSerializer()
|
||||||
|
self.assertEquals(serializer.data, {'foo': 0})
|
|
@ -6,6 +6,7 @@ Doesn't cover model serializers.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class WritableNestedSerializerBasicTests(TestCase):
|
class WritableNestedSerializerBasicTests(TestCase):
|
||||||
|
@ -244,3 +245,104 @@ class WritableNestedSerializerObjectTests(TestCase):
|
||||||
serializer = self.AlbumSerializer(data=data, many=True)
|
serializer = self.AlbumSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), True)
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
self.assertEqual(serializer.object, expected_object)
|
self.assertEqual(serializer.object, expected_object)
|
||||||
|
|
||||||
|
|
||||||
|
class ForeignKeyNestedSerializerUpdateTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class Artist(object):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other.name
|
||||||
|
|
||||||
|
class Album(object):
|
||||||
|
def __init__(self, name, artist):
|
||||||
|
self.name, self.artist = name, artist
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other.name and self.artist == other.artist
|
||||||
|
|
||||||
|
class ArtistSerializer(serializers.Serializer):
|
||||||
|
name = serializers.CharField()
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
if instance:
|
||||||
|
instance.name = attrs['name']
|
||||||
|
else:
|
||||||
|
instance = Artist(attrs['name'])
|
||||||
|
return instance
|
||||||
|
|
||||||
|
class AlbumSerializer(serializers.Serializer):
|
||||||
|
name = serializers.CharField()
|
||||||
|
by = ArtistSerializer(source='artist')
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
if instance:
|
||||||
|
instance.name = attrs['name']
|
||||||
|
instance.artist = attrs['artist']
|
||||||
|
else:
|
||||||
|
instance = Album(attrs['name'], attrs['artist'])
|
||||||
|
return instance
|
||||||
|
|
||||||
|
self.Artist = Artist
|
||||||
|
self.Album = Album
|
||||||
|
self.AlbumSerializer = AlbumSerializer
|
||||||
|
|
||||||
|
def test_create_via_foreign_key_with_source(self):
|
||||||
|
"""
|
||||||
|
Check that we can both *create* and *update* into objects across
|
||||||
|
ForeignKeys that have a `source` specified.
|
||||||
|
Regression test for #1170
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
'name': 'Discovery',
|
||||||
|
'by': {'name': 'Daft Punk'},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = self.Album(artist=self.Artist('Daft Punk'), name='Discovery')
|
||||||
|
|
||||||
|
# create
|
||||||
|
serializer = self.AlbumSerializer(data=data)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, expected)
|
||||||
|
|
||||||
|
# update
|
||||||
|
original = self.Album(artist=self.Artist('The Bats'), name='Free All the Monsters')
|
||||||
|
serializer = self.AlbumSerializer(instance=original, data=data)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedModelSerializerUpdateTests(TestCase):
|
||||||
|
def test_second_nested_level(self):
|
||||||
|
john = models.Person.objects.create(name="john")
|
||||||
|
|
||||||
|
post = john.blogpost_set.create(title="Test blog post")
|
||||||
|
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||||
|
post.blogpostcomment_set.create(text="I love this blog post")
|
||||||
|
|
||||||
|
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.BlogPostComment
|
||||||
|
|
||||||
|
class BlogPostSerializer(serializers.ModelSerializer):
|
||||||
|
comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
|
||||||
|
class Meta:
|
||||||
|
model = models.BlogPost
|
||||||
|
fields = ('id', 'title', 'comments')
|
||||||
|
|
||||||
|
class PersonSerializer(serializers.ModelSerializer):
|
||||||
|
posts = BlogPostSerializer(many=True, source='blogpost_set')
|
||||||
|
class Meta:
|
||||||
|
model = models.Person
|
||||||
|
fields = ('id', 'name', 'age', 'posts')
|
||||||
|
|
||||||
|
serialize = PersonSerializer(instance=john)
|
||||||
|
deserialize = PersonSerializer(data=serialize.data, instance=john)
|
||||||
|
self.assertTrue(deserialize.is_valid())
|
||||||
|
|
||||||
|
result = deserialize.object
|
||||||
|
result.save()
|
||||||
|
self.assertEqual(result.id, john.id)
|
||||||
|
|
||||||
|
|
33
rest_framework/tests/test_status.py
Normal file
33
rest_framework/tests/test_status.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.status import (
|
||||||
|
is_informational, is_success, is_redirect, is_client_error, is_server_error
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatus(TestCase):
|
||||||
|
def test_status_categories(self):
|
||||||
|
self.assertFalse(is_informational(99))
|
||||||
|
self.assertTrue(is_informational(100))
|
||||||
|
self.assertTrue(is_informational(199))
|
||||||
|
self.assertFalse(is_informational(200))
|
||||||
|
|
||||||
|
self.assertFalse(is_success(199))
|
||||||
|
self.assertTrue(is_success(200))
|
||||||
|
self.assertTrue(is_success(299))
|
||||||
|
self.assertFalse(is_success(300))
|
||||||
|
|
||||||
|
self.assertFalse(is_redirect(299))
|
||||||
|
self.assertTrue(is_redirect(300))
|
||||||
|
self.assertTrue(is_redirect(399))
|
||||||
|
self.assertFalse(is_redirect(400))
|
||||||
|
|
||||||
|
self.assertFalse(is_client_error(399))
|
||||||
|
self.assertTrue(is_client_error(400))
|
||||||
|
self.assertTrue(is_client_error(499))
|
||||||
|
self.assertFalse(is_client_error(500))
|
||||||
|
|
||||||
|
self.assertFalse(is_server_error(499))
|
||||||
|
self.assertTrue(is_server_error(500))
|
||||||
|
self.assertTrue(is_server_error(599))
|
||||||
|
self.assertFalse(is_server_error(600))
|
|
@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||||
allowed_pattern = '(%s)' % '|'.join(allowed)
|
allowed_pattern = '(%s)' % '|'.join(allowed)
|
||||||
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
|
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
|
||||||
else:
|
else:
|
||||||
suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
|
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
|
||||||
|
|
||||||
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
|
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
|
||||||
|
|
|
@ -45,6 +45,11 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
return str(o)
|
return str(o)
|
||||||
elif hasattr(o, 'tolist'):
|
elif hasattr(o, 'tolist'):
|
||||||
return o.tolist()
|
return o.tolist()
|
||||||
|
elif hasattr(o, '__getitem__'):
|
||||||
|
try:
|
||||||
|
return dict(o)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
elif hasattr(o, '__iter__'):
|
elif hasattr(o, '__iter__'):
|
||||||
return [i for i in o]
|
return [i for i in o]
|
||||||
return super(JSONEncoder, self).default(o)
|
return super(JSONEncoder, self).default(o)
|
||||||
|
@ -90,6 +95,9 @@ else:
|
||||||
node.flow_style = best_style
|
node.flow_style = best_style
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
SafeDumper.add_representer(decimal.Decimal,
|
||||||
|
SafeDumper.represent_decimal)
|
||||||
|
|
||||||
SafeDumper.add_representer(SortedDict,
|
SafeDumper.add_representer(SortedDict,
|
||||||
yaml.representer.SafeRepresenter.represent_dict)
|
yaml.representer.SafeRepresenter.represent_dict)
|
||||||
SafeDumper.add_representer(DictWithMetadata,
|
SafeDumper.add_representer(DictWithMetadata,
|
||||||
|
|
|
@ -154,8 +154,8 @@ class APIView(View):
|
||||||
Returns a dict that is passed through to Parser.parse(),
|
Returns a dict that is passed through to Parser.parse(),
|
||||||
as the `parser_context` keyword argument.
|
as the `parser_context` keyword argument.
|
||||||
"""
|
"""
|
||||||
# Note: Additionally `request` will also be added to the context
|
# Note: Additionally `request` and `encoding` will also be added
|
||||||
# by the Request object.
|
# to the context by the Request object.
|
||||||
return {
|
return {
|
||||||
'view': self,
|
'view': self,
|
||||||
'args': getattr(self, 'args', ()),
|
'args': getattr(self, 'args', ()),
|
||||||
|
|
|
@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views.
|
||||||
user_detail = UserViewSet.as_view({'get': 'retrieve'})
|
user_detail = UserViewSet.as_view({'get': 'retrieve'})
|
||||||
|
|
||||||
Typically, rather than instantiate views from viewsets directly, you'll
|
Typically, rather than instantiate views from viewsets directly, you'll
|
||||||
regsiter the viewset with a router and let the URL conf be determined
|
register the viewset with a router and let the URL conf be determined
|
||||||
automatically.
|
automatically.
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -12,7 +12,7 @@ def get_version(package):
|
||||||
Return package version as listed in `__version__` in `init.py`.
|
Return package version as listed in `__version__` in `init.py`.
|
||||||
"""
|
"""
|
||||||
init_py = open(os.path.join(package, '__init__.py')).read()
|
init_py = open(os.path.join(package, '__init__.py')).read()
|
||||||
return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
||||||
|
|
||||||
|
|
||||||
def get_packages(package):
|
def get_packages(package):
|
||||||
|
@ -45,6 +45,7 @@ version = get_version('rest_framework')
|
||||||
|
|
||||||
if sys.argv[-1] == 'publish':
|
if sys.argv[-1] == 'publish':
|
||||||
os.system("python setup.py sdist upload")
|
os.system("python setup.py sdist upload")
|
||||||
|
os.system("python setup.py bdist_wheel upload")
|
||||||
print("You probably want to also tag the version now:")
|
print("You probably want to also tag the version now:")
|
||||||
print(" git tag -a %s -m 'version %s'" % (version, version))
|
print(" git tag -a %s -m 'version %s'" % (version, version))
|
||||||
print(" git push --tags")
|
print(" git push --tags")
|
||||||
|
|
20
tox.ini
20
tox.ini
|
@ -7,19 +7,19 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
[testenv:py3.3-django1.6]
|
[testenv:py3.3-django1.6]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
deps = Django==1.6.1
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
|
|
||||||
[testenv:py3.2-django1.6]
|
[testenv:py3.2-django1.6]
|
||||||
basepython = python3.2
|
basepython = python3.2
|
||||||
deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
deps = Django==1.6.1
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
|
|
||||||
[testenv:py2.7-django1.6]
|
[testenv:py2.7-django1.6]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
deps = Django==1.6.1
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
@ -29,7 +29,7 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
||||||
|
|
||||||
[testenv:py2.6-django1.6]
|
[testenv:py2.6-django1.6]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
deps = Django==1.6.1
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
@ -39,19 +39,19 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/
|
||||||
|
|
||||||
[testenv:py3.3-django1.5]
|
[testenv:py3.3-django1.5]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = django==1.5
|
deps = django==1.5.5
|
||||||
django-filter==0.6a1
|
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.5
|
||||||
django-filter==0.6a1
|
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.5
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
@ -61,7 +61,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.5
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
@ -71,7 +71,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.10
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
@ -81,7 +81,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.10
|
||||||
django-filter==0.6a1
|
django-filter==0.6a1
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
django-oauth-plus==2.0
|
django-oauth-plus==2.0
|
||||||
|
|
Loading…
Reference in New Issue
Block a user