aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md65
-rw-r--r--docs/404.html201
-rwxr-xr-xdocs/api-guide/authentication.md4
-rw-r--r--docs/api-guide/fields.md4
-rw-r--r--docs/api-guide/filtering.md9
-rwxr-xr-xdocs/api-guide/generic-views.md4
-rw-r--r--docs/api-guide/permissions.md5
-rw-r--r--docs/api-guide/relations.md13
-rw-r--r--docs/api-guide/renderers.md15
-rw-r--r--docs/api-guide/routers.md22
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/api-guide/status-codes.md21
-rw-r--r--docs/api-guide/views.md4
-rw-r--r--docs/api-guide/viewsets.md2
-rw-r--r--docs/img/travis-status.pngbin0 -> 10023 bytes
-rw-r--r--docs/index.md15
-rw-r--r--docs/template.html4
-rw-r--r--docs/topics/2.2-announcement.md2
-rw-r--r--docs/topics/contributing.md99
-rw-r--r--docs/topics/credits.md4
-rw-r--r--docs/topics/release-notes.md28
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md3
-rw-r--r--docs/tutorial/quickstart.md5
-rwxr-xr-xmkdocs.py12
-rw-r--r--rest_framework/__init__.py18
-rw-r--r--rest_framework/compat.py7
-rw-r--r--rest_framework/fields.py9
-rw-r--r--rest_framework/generics.py12
-rw-r--r--rest_framework/mixins.py10
-rw-r--r--rest_framework/renderers.py9
-rw-r--r--rest_framework/request.py11
-rw-r--r--rest_framework/response.py4
-rw-r--r--rest_framework/routers.py14
-rw-r--r--rest_framework/serializers.py22
-rw-r--r--rest_framework/status.py17
-rw-r--r--rest_framework/tests/test_fields.py26
-rw-r--r--rest_framework/tests/test_generics.py60
-rw-r--r--rest_framework/tests/test_renderers.py72
-rw-r--r--rest_framework/tests/test_serializer.py62
-rw-r--r--rest_framework/tests/test_serializer_nested.py35
-rw-r--r--rest_framework/tests/test_status.py33
-rw-r--r--rest_framework/urlpatterns.py2
-rw-r--r--rest_framework/utils/encoders.py5
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py3
45 files changed, 849 insertions, 127 deletions
diff --git a/README.md b/README.md
index 62883e32..68486800 100644
--- a/README.md
+++ b/README.md
@@ -48,48 +48,51 @@ 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:
- from django.conf.urls.defaults import url, patterns, include
- from django.contrib.auth.models import User, Group
- from rest_framework import viewsets, routers
+```python
+from django.conf.urls.defaults import url, patterns, include
+from django.contrib.auth.models import User, Group
+from rest_framework import viewsets, routers
- # ViewSets define the view behavior.
- class UserViewSet(viewsets.ModelViewSet):
- model = User
+# ViewSets define the view behavior.
+class UserViewSet(viewsets.ModelViewSet):
+ model = User
- class GroupViewSet(viewsets.ModelViewSet):
- model = Group
+class GroupViewSet(viewsets.ModelViewSet):
+ model = Group
- # Routers provide an easy way of automatically determining the URL conf
- router = routers.DefaultRouter()
- router.register(r'users', UserViewSet)
- router.register(r'groups', GroupViewSet)
+# Routers provide an easy way of automatically determining the URL conf
+router = routers.DefaultRouter()
+router.register(r'users', UserViewSet)
+router.register(r'groups', GroupViewSet)
- # Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
- url(r'^', include(router.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+# Wire up our API using automatic URL routing.
+# Additionally, we include login URLs for the browseable API.
+urlpatterns = patterns('',
+ url(r'^', include(router.urls)),
+ url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+)
+```
We'd also like to configure a couple of settings for our API.
Add the following to your `settings.py` module:
- REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
-
- # Use Django's standard `django.contrib.auth` permissions,
- # or allow read-only access for unauthenticated users.
- 'DEFAULT_PERMISSION_CLASSES': [
- 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
- ]
- }
-
+```python
+REST_FRAMEWORK = {
+ # Use hyperlinked styles by default.
+ # Only used if the `serializer_class` attribute is not set on a view.
+ 'DEFAULT_MODEL_SERIALIZER_CLASS':
+ 'rest_framework.serializers.HyperlinkedModelSerializer',
+
+ # Use Django's standard `django.contrib.auth` permissions,
+ # or allow read-only access for unauthenticated users.
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
+ ]
+}
+```
Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
That's it, we're done!
diff --git a/docs/404.html b/docs/404.html
new file mode 100644
index 00000000..4938da6e
--- /dev/null
+++ b/docs/404.html
@@ -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">&times;</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>
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 1a1c68b8..53efc49a 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -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.
+ from django.contrib.auth import get_user_model
+ from django.db.models.signals import post_save
from django.dispatch import receiver
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):
if created:
Token.objects.create(user=instance)
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 4272c9a7..e05c0306 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -41,7 +41,7 @@ Defaults to `True`.
### `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.
@@ -286,7 +286,7 @@ An image representation.
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`.
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index a0132ffc..0e02a2a7 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -360,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.
+# 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
[django-filter]: https://github.com/alex/django-filter
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
@@ -368,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
[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
+[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index b9242724..83c3e45f 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -163,12 +163,14 @@ For example:
return 20
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.
* `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.
+* `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.
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 871de84e..6a0f48f4 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -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.
+## 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
[authentication]: authentication.md
[throttling]: throttling.md
@@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t
[filtering]: filtering.md
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
+[rest-condition]: https://github.com/caxap/rest_condition
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index b9d96b5e..1b089c54 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -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.
class AlbumSerializer(serializers.ModelSerializer):
- tracks = RelatedField(many=True)
+ tracks = serializers.RelatedField(many=True)
class Meta:
model = Album
@@ -442,7 +442,18 @@ In the 2.4 release, these parts of the API will be removed entirely.
For more details see the [2.2 release announcement][2.2-announcement].
+---
+
+# 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://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 1f286ef1..cf200569 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -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'`.
-**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`
@@ -167,14 +173,14 @@ The template name is determined by (in order of preference):
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.
"""
queryset = User.objects.all()
renderer_classes = (TemplateHTMLRenderer,)
- def get(self, request, *args, **kwargs)
+ def get(self, request, *args, **kwargs):
self.object = self.get_object()
return Response({'user': self.object}, template_name='user_detail.html')
@@ -411,7 +417,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
## UltraJSON
-[UltraJSON][ultrajson] is a blazing-fast C JSON encoder which can give 2-10x performance increases on typical workloads. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package.
+[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
[conneg]: content-negotiation.md
@@ -419,6 +425,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[cors]: http://www.w3.org/TR/cors/
[cors-docs]: ../topics/ajax-csrf-cors.md
+[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
[testing]: testing.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index fb48197e..895589db 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo
## 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
@@ -150,4 +150,24 @@ 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.
+# 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.
+
+## 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
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
+[wq.db]: http://wq.io/wq.db
+[wq.db-router]: http://wq.io/docs/app.py
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 4c3fb9d3..6fc25f57 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -425,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_
fields = ('url', 'account_name', 'users', 'created')
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:
diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md
index 409f659b..64c46434 100644
--- a/docs/api-guide/status-codes.md
+++ b/docs/api-guide/status-codes.md
@@ -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 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]
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_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
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md
index 15581e09..194a7a6b 100644
--- a/docs/api-guide/views.md
+++ b/docs/api-guide/views.md
@@ -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
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
-[settings]: api-guide/settings.md
-[throttling]: api-guide/throttling.md
+[settings]: settings.md
+[throttling]: throttling.md
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 1062cb32..4fdd9364 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
#### 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):
"""
diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png
new file mode 100644
index 00000000..fec98cf9
--- /dev/null
+++ b/docs/img/travis-status.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index bb2129f6..04804fa7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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.
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 rest_framework import viewsets, routers
@@ -112,7 +112,7 @@ Here's our project's root `urls.py` module:
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.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
@@ -177,6 +177,7 @@ General guides to using REST framework.
* [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
+* [Contributing to REST framework][contributing]
* [2.0 Announcement][rest-framework-2-announcement]
* [2.2 Announcement][2.2-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
[image]: img/quickstart.png
[index]: .
-[oauth1-section]: api-guide/authentication.html#oauthauthentication
-[oauth2-section]: api-guide/authentication.html#oauth2authentication
-[serializer-section]: api-guide/serializers.html#serializers
-[modelserializer-section]: api-guide/serializers.html#modelserializer
-[functionview-section]: api-guide/views.html#function-based-views
+[oauth1-section]: api-guide/authentication#oauthauthentication
+[oauth2-section]: api-guide/authentication#oauth2authentication
+[serializer-section]: api-guide/serializers#serializers
+[modelserializer-section]: api-guide/serializers#modelserializer
+[functionview-section]: api-guide/views#function-based-views
[sandbox]: http://restframework.herokuapp.com/
[quickstart]: tutorial/quickstart.md
diff --git a/docs/template.html b/docs/template.html
index 749d0afe..c065237a 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -4,6 +4,7 @@
<meta charset="utf-8">
<title>{{ title }}</title>
<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="description" content="{{ description }}">
<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/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/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/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
@@ -170,7 +172,7 @@
<div>
<hr>
-<p><strong>The team behind REST framework are launching a new API service.</strong></p>
+<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>
diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md
index 7d276049..0f980e1c 100644
--- a/docs/topics/2.2-announcement.md
+++ b/docs/topics/2.2-announcement.md
@@ -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/
[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
-[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
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[marcgibbons]: https://github.com/marcgibbons/
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 123e4a8a..906950bb 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -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.
-# 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.
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
-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:
* 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.
* 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
+To start developing on Django REST framework, clone the repo:
+
+ git clone git@github.com:tomchristie/django-rest-framework.git
+
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
+
+## Testing
+
+To run the tests, clone the repository, and then:
+
+ # Setup the virtual environment
+ virtualenv env
+ env/bin/activate
+ pip install -r requirements.txt
+ pip install -r optionals.txt
+
+ # Run the tests
+ rest_framework/runtests/runtests.py
-* git clone & PYTHONPATH
-* Pep8
-* Recommend editor that runs pep8
+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:
-### Pull requests
+ tox
-* Make pull requests early
-* Describe branching
+## Pull requests
-### Managing compatibility issues
+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.
-* Describe compat module
+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.
-# Testing
+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.
-* Running the tests
-* tox
+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
@@ -77,7 +119,7 @@ Some other tips:
* Keep paragraphs reasonably short.
* 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
@@ -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
-* 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.
+
+## Getting started
+
+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.
+
+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.
-# Core committers
+## Linking to your package
-* Still use pull reqs
-* Credits
+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
-[github]: https://github.com/tomchristie/django-rest-framework
-[django-packages]: https://www.djangopackages.com/grids/g/api/
+[code-of-conduct]: https://www.djangoproject.com/conduct/
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
[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
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
+[django-reusable-app]: https://github.com/dabapps/django-reusable-app
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index e6c9c034..1a838421 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -179,6 +179,8 @@ The following people have helped make REST framework great.
* 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.
@@ -394,3 +396,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 3df8869a..54865053 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -42,8 +42,36 @@ You can determine your currently installed version using `pip freeze`:
### 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.
+* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied.
+
+### 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.
* Added JSON renderer support for numpy scalars.
+* Added `transform_<fieldname>` hooks on serializers for easily modifying field output.
* 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
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index ecf92a7b..b472322a 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -67,6 +67,9 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not
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):
queryset = User.objects.all()
serializer_class = UserSerializer
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 80bb9abb..8bf8c7f5 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior
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
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].
+[readme-example-api]: ../#example
[image]: ../img/quickstart.png
[tutorial]: 1-serialization.md
[guide]: ../#api-guide
diff --git a/mkdocs.py b/mkdocs.py
index 13228a0c..d1790168 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -19,7 +19,7 @@ if local:
index = 'index.html'
else:
base_url = 'http://django-rest-framework.org'
- suffix = '.html'
+ suffix = ''
index = ''
@@ -90,7 +90,10 @@ for idx in range(len(path_list)):
path = path_list[idx]
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
if idx < len(path_list) - 1:
@@ -143,6 +146,10 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
else:
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)
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('{{ description }}', description)
output = output.replace('{{ page_id }}', filename[:-3])
+ output = output.replace('{{ canonical_url }}', canonical_url)
if prev_url:
output = output.replace('{{ prev_url }}', prev_url)
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 2bd2991b..f5483b9d 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -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)
HTTP_HEADER_ENCODING = 'iso-8859-1'
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 581e29fc..05bd99e0 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -69,6 +69,13 @@ try:
except ImportError:
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:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 6c07dbb3..f1de447c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -428,7 +428,7 @@ class BooleanField(WritableField):
def field_from_native(self, data, files, field_name, into):
# HTML checkboxes do not explicitly represent unchecked as `False`
# we deal with that here...
- if isinstance(data, QueryDict):
+ if isinstance(data, QueryDict) and self.default is None:
self.default = False
return super(BooleanField, self).field_from_native(
@@ -514,6 +514,11 @@ class ChoiceField(WritableField):
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):
"""
Validates that the input is in self.choices.
@@ -966,7 +971,7 @@ class ImageField(FileField):
return None
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
# have to read the data into memory.
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7cb80a84..fd411ad3 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -344,6 +344,18 @@ class GenericAPIView(views.APIView):
"""
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):
"""
Return a dictionary of metadata about the view.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 4606c78b..43950c4b 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
+from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
@@ -127,7 +128,12 @@ class UpdateModelMixin(object):
files=request.FILES, partial=partial)
if serializer.is_valid():
- self.pre_save(serializer.object)
+ try:
+ 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.post_save(self.object, created=created)
return Response(serializer.data, status=success_status_code)
@@ -186,5 +192,7 @@ class DestroyModelMixin(object):
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
+ self.pre_delete(obj)
obj.delete()
+ self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index fe4f43d4..2fdd3337 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -20,6 +20,7 @@ from rest_framework.compat import StringIO
from rest_framework.compat import six
from rest_framework.compat import smart_text
from rest_framework.compat import yaml
+from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
@@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer):
In the absence of the View having an associated form then return None.
"""
if request.method == method:
- data = request.DATA
- files = request.FILES
+ try:
+ data = request.DATA
+ files = request.FILES
+ except ParseError:
+ data = None
+ files = None
else:
data = None
files = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index b883d0d4..fcea2508 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -356,7 +356,16 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
- parsed = parser.parse(stream, media_type, self.parser_context)
+ try:
+ 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
# DataAndFiles object. Unpack the result as required.
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 5877c8a3..1dc6abcf 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse):
assert charset, 'renderer returned unicode, and did not specify ' \
'a charset value.'
return bytes(ret.encode(charset))
+
+ if not ret:
+ del self['Content-Type']
+
return ret
@property
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 3fee1e49..97b35c10 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -184,18 +184,24 @@ class SimpleRouter(BaseRouter):
bound_methods[method] = action
return bound_methods
- def get_lookup_regex(self, viewset):
+ def get_lookup_regex(self, viewset, lookup_prefix=''):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
+
+ Note that lookup_prefix is not used directly inside REST rest_framework
+ itself, but is required in order to nicely support nested router
+ implementations, such as drf-nested-routers.
+
+ https://github.com/alanjds/drf-nested-routers
"""
if self.trailing_slash:
- base_regex = '(?P<{lookup_field}>[^/]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)'
else:
# Don't consume `.json` style suffixes
- base_regex = '(?P<{lookup_field}>[^/.]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)'
lookup_field = getattr(viewset, 'lookup_field', 'pk')
- return base_regex.format(lookup_field=lookup_field)
+ return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix)
def get_urls(self):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 163abf4f..40caa1f3 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -412,7 +412,13 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists
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 value:
@@ -872,7 +878,7 @@ class ModelSerializer(Serializer):
# Reverse fk or one-to-one relations
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:
related_data[field_name] = attrs.pop(field_name)
@@ -896,7 +902,10 @@ class ModelSerializer(Serializer):
# Update an existing instance...
if instance is not None:
for key, val in attrs.items():
- setattr(instance, key, val)
+ try:
+ setattr(instance, key, val)
+ except ValueError:
+ self._errors[key] = self.error_messages['required']
# ...or create a new instance
else:
@@ -940,11 +949,16 @@ class ModelSerializer(Serializer):
del(obj._m2m_data)
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():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
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)
self.save_object(related_item)
diff --git a/rest_framework/status.py b/rest_framework/status.py
index b9f249f9..76435371 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
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_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
index ab2cceac..5c96bce9 100644
--- a/rest_framework/tests/test_fields.py
+++ b/rest_framework/tests/test_fields.py
@@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase):
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
def test_invalid_choice_model(self):
- s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'})
+ 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.
+ Test that the 'empty' value is correctly passed and used depending on
+ the 'null' property on the model field.
"""
- s = ChoiceFieldModelSerializer(data={'choice' : ''})
+ s = ChoiceFieldModelSerializer(data={'choice': ''})
self.assertTrue(s.is_valid())
self.assertEqual(s.data['choice'], '')
- s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''})
+ s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
self.assertTrue(s.is_valid())
self.assertEqual(s.data['choice'], None)
@@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase):
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):
"""
diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py
index 79cd99ac..996bd5b0 100644
--- a/rest_framework/tests/test_generics.py
+++ b/rest_framework/tests/test_generics.py
@@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
model = BasicModel
+ def get_queryset(self):
+ queryset = super(InstanceView, self).get_queryset()
+ return queryset.exclude(text='filtered out')
+
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field() # read only
@@ -160,10 +164,10 @@ class TestInstanceView(TestCase):
"""
Create 3 BasicModel intances.
"""
- items = ['foo', 'bar', 'baz']
+ items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
BasicModel(text=item).save()
- self.objects = BasicModel.objects
+ self.objects = BasicModel.objects.exclude(text='filtered out')
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
@@ -352,6 +356,17 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1)
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):
"""
PUT requests to RetrieveUpdateDestroyAPIView should create an object
@@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object):
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):
def setUp(self):
@@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
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):
"""
GET requests to ListCreateAPIView should return filtered list.
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py
index 76299a89..2ae8ae18 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/rest_framework/tests/test_renderers.py
@@ -15,7 +15,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
+from collections import MutableMapping
import datetime
+import json
import pickle
import re
@@ -64,11 +66,23 @@ class MockView(APIView):
class MockGETView(APIView):
-
def get(self, request, **kwargs):
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):
renderer_classes = (BrowsableAPIRenderer, )
@@ -88,8 +102,10 @@ urlpatterns = patterns('',
url(r'^cache$', MockGETView.as_view()),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, 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'^html1$', HTMLView1.as_view()),
+ url(r'^empty$', EmptyGETView.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -219,6 +235,22 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
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"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -244,6 +276,44 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('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):
"""
Test basic JSON rendering.
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
index 1f85a474..14d1c664 100644
--- a/rest_framework/tests/test_serializer.py
+++ b/rest_framework/tests/test_serializer.py
@@ -558,6 +558,29 @@ class ModelValidationTests(TestCase):
self.assertFalse(second_serializer.is_valid())
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):
"""
Test ModelSerializer validation with partial=True
@@ -1720,3 +1743,42 @@ class TestSerializerTransformMethods(TestCase):
'b_renamed': None,
}
)
+
+
+class DefaultTrueBooleanModel(models.Model):
+ cat = models.BooleanField(default=True)
+ dog = models.BooleanField(default=False)
+
+
+class SerializerDefaultTrueBoolean(TestCase):
+
+ def setUp(self):
+ super(SerializerDefaultTrueBoolean, self).setUp()
+
+ class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DefaultTrueBooleanModel
+ fields = ('cat', 'dog')
+
+ self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
+
+ def test_enabled_as_false(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False,
+ 'dog': False})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
+
+ def test_enabled_as_true(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': True,
+ 'dog': True})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], True)
+ self.assertEqual(serializer.data['dog'], True)
+
+ def test_enabled_partial(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False},
+ partial=True)
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
index 029f8bff..7114a060 100644
--- a/rest_framework/tests/test_serializer_nested.py
+++ b/rest_framework/tests/test_serializer_nested.py
@@ -6,6 +6,7 @@ Doesn't cover model serializers.
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import serializers
+from . import models
class WritableNestedSerializerBasicTests(TestCase):
@@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
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)
+
diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py
new file mode 100644
index 00000000..7b1bdae3
--- /dev/null
+++ b/rest_framework/tests/test_status.py
@@ -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)) \ No newline at end of file
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index d9143bb4..0ff137b0 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
allowed_pattern = '(%s)' % '|'.join(allowed)
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
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)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 35ad206b..3ac920c6 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -44,6 +44,11 @@ class JSONEncoder(json.JSONEncoder):
return str(o)
elif hasattr(o, 'tolist'):
return o.tolist()
+ elif hasattr(o, '__getitem__'):
+ try:
+ return dict(o)
+ except:
+ pass
elif hasattr(o, '__iter__'):
return [i for i in o]
return super(JSONEncoder, self).default(o)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..5e409001
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index adf083cb..969b3fc1 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ def get_version(package):
Return package version as listed in `__version__` in `init.py`.
"""
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):
@@ -45,6 +45,7 @@ version = get_version('rest_framework')
if sys.argv[-1] == 'publish':
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(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")