Various clean up and lots of docs

This commit is contained in:
Tom Christie 2013-04-24 22:40:24 +01:00
parent 835d3f89d3
commit b94da2468c
14 changed files with 655 additions and 231 deletions

View File

@ -8,7 +8,7 @@
The default behavior of REST framework's generic list views is to return the entire queryset for a model manager. Often you will want your API to restrict the items that are returned by the queryset.
The simplest way to filter the queryset of any view that subclasses `MultipleObjectAPIView` is to override the `.get_queryset()` method.
The simplest way to filter the queryset of any view that subclasses `GenericAPIView` is to override the `.get_queryset()` method.
Overriding this method allows you to customize the queryset returned by the view in a number of different ways.
@ -21,7 +21,6 @@ You can do so by filtering based on the value of `request.user`.
For example:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
@ -44,7 +43,6 @@ For example if your URL config contained an entry like this:
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
@ -62,7 +60,6 @@ A final example of filtering the initial queryset would be to determine the init
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
@ -100,7 +97,7 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, listing the set of fields you wish to filter against.
class ProductList(generics.ListAPIView):
model = Product
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_fields = ('category', 'in_stock')
@ -120,7 +117,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
fields = ['category', 'in_stock', 'min_price', 'max_price']
class ProductList(generics.ListAPIView):
model = Product
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_class = ProductFilter
@ -183,4 +180,4 @@ For example:
[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
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py

View File

@ -18,7 +18,7 @@ If the generic views don't suit the needs of your API, you can drop down to usin
Typically when using the generic views, you'll override the view, and set several class attributes.
class UserList(generics.ListCreateAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAdminUser,)
paginate_by = 100
@ -26,17 +26,16 @@ Typically when using the generic views, you'll override the view, and set severa
For more complex cases you might also want to override various methods on the view class. For example.
class UserList(generics.ListCreateAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAdminUser,)
def get_paginate_by(self, queryset):
def get_paginate_by(self):
"""
Use smaller pagination for HTML representations.
"""
page_size_param = self.request.QUERY_PARAMS.get('page_size')
if page_size_param:
return int(page_size_param)
self.request.accepted_renderer.format == 'html':
return 20
return 100
For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
@ -47,6 +46,114 @@ For very simple cases you might want to pass through any class attributes using
# API Reference
## GenericAPIView
This class extends REST framework's `APIView` class, adding commonly required behavior for standard list and detail views.
Each of the concrete generic views provided is built by combining `GenericAPIView`, with one or more mixin classes.
### Attributes
**Basic settings**:
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.
* `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.
**Pagination**:
The following attibutes are used to control pagination when used with list views.
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
* `pagination_serializer_class` - The pagination serializer class to use when determining the style of paginated responses. Defaults to the same value as the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting.
* `page_kwarg` - The name of a URL kwarg or URL query parameter which can be used by the client to control which page is requested. Defaults to `'page'`.
**Other**:
* `filter_backend` - The filter backend class that should be used for filtering the queryset. Defaults to the same value as the `FILTER_BACKEND` setting.
* `allow_empty` - Determines if an empty list should successfully display zero results, or return a 404 response. Defaults to `True`, meaning empty lists will return sucessful `200 OK` responses, with zero results.
* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class.
### Methods
**Base methods**:
#### `get_queryset(self)`
Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute, or the default queryset for the model if the `model` shortcut is being used.
May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request.
For example:
def get_queryset(self):
return self.user.accounts.all()
#### `get_object(self)`
Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset.
May be overridden to provide more complex behavior such as object lookups based on more than one URL kwarg.
For example:
def get_object(self):
queryset = self.get_queryset()
filter = {}
for field in self.multiple_lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
#### `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.
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.
For example:
def get_serializer_class(self):
if self.request.user.is_staff:
return FullAccountSerializer
return BasicAccountSerializer
#### `get_paginate_by(self)`
Returna the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set.
You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response.
For example:
def get_paginate_by(self):
self.request.accepted_renderer.format == 'html':
return 20
return 100
**Save 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.
**Other methods**:
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
* `paginate_queryset(self, queryset, page_size)` - Paginate a queryset.
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backend is in use.
---
# Concrete View Classes
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
## CreateAPIView
@ -63,7 +170,7 @@ Used for **read-only** endpoints to represent a **collection of model instances*
Provides a `get` method handler.
Extends: [MultipleObjectAPIView], [ListModelMixin]
Extends: [GenericAPIView], [ListModelMixin]
## RetrieveAPIView
@ -71,7 +178,7 @@ Used for **read-only** endpoints to represent a **single model instance**.
Provides a `get` method handler.
Extends: [SingleObjectAPIView], [RetrieveModelMixin]
Extends: [GenericAPIView], [RetrieveModelMixin]
## DestroyAPIView
@ -79,7 +186,7 @@ Used for **delete-only** endpoints for a **single model instance**.
Provides a `delete` method handler.
Extends: [SingleObjectAPIView], [DestroyModelMixin]
Extends: [GenericAPIView], [DestroyModelMixin]
## UpdateAPIView
@ -87,7 +194,7 @@ Used for **update-only** endpoints for a **single model instance**.
Provides `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [UpdateModelMixin]
Extends: [GenericAPIView], [UpdateModelMixin]
## ListCreateAPIView
@ -95,7 +202,7 @@ Used for **read-write** endpoints to represent a **collection of model instances
Provides `get` and `post` method handlers.
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin]
Extends: [GenericAPIView], [ListModelMixin], [CreateModelMixin]
## RetrieveUpdateAPIView
@ -103,7 +210,7 @@ Used for **read or update** endpoints to represent a **single model instance**.
Provides `get`, `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin]
Extends: [GenericAPIView], [RetrieveModelMixin], [UpdateModelMixin]
## RetrieveDestroyAPIView
@ -111,7 +218,7 @@ Used for **read or delete** endpoints to represent a **single model instance**.
Provides `get` and `delete` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]
Extends: [GenericAPIView], [RetrieveModelMixin], [DestroyModelMixin]
## RetrieveUpdateDestroyAPIView
@ -119,62 +226,13 @@ Used for **read-write-delete** endpoints to represent a **single model instance*
Provides `get`, `put`, `patch` and `delete` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
---
# Base views
Each of the generic views provided is built by combining one of the base views below, with one or more mixin classes.
## GenericAPIView
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
**Methods**:
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
* `get_serializer_class(self)` - Returns the class that should be used for the serializer.
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
* `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.
**Attributes**:
* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required.
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
## MultipleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
**Attributes**:
* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`.
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
## SingleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
**Attributes**:
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
* `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
Extends: [GenericAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
---
# Mixins
The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour.
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior.
## ListModelMixin
@ -182,7 +240,7 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
Should be mixed in with [MultipleObjectAPIView].
@ -227,14 +285,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w
Should be mixed in with [SingleObjectAPIView].
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
[MultipleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/
[SingleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-single-object/
[multiple-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.list/MultipleObjectMixin/
[single-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.detail/SingleObjectMixin/
[GenericAPIView]: #genericapiview
[SingleObjectAPIView]: #singleobjectapiview
[MultipleObjectAPIView]: #multipleobjectapiview
[ListModelMixin]: #listmodelmixin
[CreateModelMixin]: #createmodelmixin
[RetrieveModelMixin]: #retrievemodelmixin

View File

@ -93,7 +93,8 @@ The default pagination style may be set globally, using the `DEFAULT_PAGINATION_
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
class PaginatedListView(ListAPIView):
model = ExampleModel
queryset = ExampleModel.objects.all()
serializer_class = ExampleModelSerializer
paginate_by = 10
paginate_by_param = 'page_size'

View File

@ -127,7 +127,7 @@ An example of a view that uses `TemplateHTMLRenderer`:
"""
A view that returns a templated HTML representations of a given user.
"""
model = Users
queryset = User.objects.all()
renderer_classes = (TemplateHTMLRenderer,)
def get(self, request, *args, **kwargs)

View File

@ -8,20 +8,49 @@
Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests.
Conversely, Django stops short of automatically generating URLs, and requires you to explicitly manage your URL configuration.
REST framework adds support for automatic URL routing to Django, and provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs.
REST framework adds support for automatic URL routing, which provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs.
## Usage
Here's an example of a simple URL conf, that uses `DefaultRouter`.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, 'user')
router.register(r'accounts', AccountViewSet, 'account')
urlpatterns = router.urls
# API Guide
Routers provide a convenient and simple shortcut for wiring up your application's URLs.
## SimpleRouter
router = routers.DefaultRouter()
router.register('^/', APIRoot, 'api-root')
router.register('^users/', UserViewSet, 'user')
router.register('^groups/', GroupViewSet, 'group')
router.register('^accounts/', AccountViewSet, 'account')
<table border=1>
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr>
<tr><td rowspan=2>{prefix}/{lookup}/{methodname}/</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr>
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
## DefaultRouter
<table border=1>
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr>
<tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr>
<tr><td rowspan=2>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr>
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
# Custom Routers
urlpatterns = router.urlpatterns
[cite]: http://guides.rubyonrails.org/routing.html

View File

@ -111,26 +111,25 @@ Again, as with `ModelViewSet`, you can use any of the standard attributes and me
Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes.
For example, the definition of `ModelViewSet` looks like this:
## Example
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.ViewSetMixin,
generics.GenericAPIView):
For example, we can create a base viewset class that provides `retrieve`, `update` and `list` operations:
class RetrieveUpdateListViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.ViewSetMixin,
generics.GenericAPIView):
"""
A viewset that provides actions for `create`, `retrieve`,
`update`, `destroy` and `list` actions.
A viewset that provides `retrieve`, `update`, and `list` actions.
To use it, override the class and set the `.queryset`
and `.serializer_class` attributes.
To use it, override the class and set the `.queryset` and
`.serializer_class` attributes.
"""
pass
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API.
For advanced usage, it's worth noting the that `ViewSetMixin` class can also be applied to the standard Django `View` class. Doing so allows you to use REST framework's automatic routing, but don't want to use it's permissions, authentication and other API policies.
For advanced usage, it's worth noting the that `ViewSetMixin` class can also be applied to the standard Django `View` class. Doing so allows you to use REST framework's automatic routing with regular Django views.
[cite]: http://guides.rubyonrails.org/routing.html

View File

@ -73,6 +73,54 @@ If you're intending to use the browseable API you'll probably also want to add R
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
## Example
Let's take a look at a quick example of using REST framework to build a simple model-backed API.
We'll create a read-write API for accessing users and groups.
from django.conf.urls.defaults import url, patterns, include
from django.contrib.auth.models import User, Group
from rest_framework import serializers, viewsets, routers
# Serializers control the representations your API exposes.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'email', 'is_staff', 'groups')
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ('url', 'name')
# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
# Routers provide a convienient way of automatically managing your URLs.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, 'user')
router.register(r'groups', GroupViewSet, 'group')
# Wire up our API URLs, letting the router do the hard work.
# 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'))
)
## Quickstart
Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework.

View File

@ -0,0 +1,149 @@
# REST framework 2.3 announcement
REST framework 2.3 is geared towards making it easier and quicker to build your Web APIs.
## ViewSets & Routers
We've introduced
## Easier Serializers
REST framework lets you be totally explict regarding how you want to represent relationships, allowing you to choose between styles such as hyperlinking or primary key relationships.
The ability to specify exactly how you want to represent relationships is powerful, but it also introduces complexity. In order to keep things more simple, REST framework now allows you to include reverse relationships simply by including the field name in the `fields` metadata of the serializer class.
For example, in REST framework 2.2, reverse relationships needed to be included explicitly on a serializer class.
class BlogSerializer(serializers.ModelSerializer):
comments = serializers.PrimaryKeyRelatedField(many=True)
class Meta:
model = Blog
fields = ('id', 'title', 'created', 'comments')
As of 2.3, you can simply include the field name, and the appropriate serializer field will automatically be used for the relationship.
class BlogSerializer(serializers.ModelSerializer):
"""
Don't need to specify the 'comments' field explicitly anymore.
"""
class Meta:
model = Blog
fields = ('id', 'title', 'created', 'comments')
Similarly, you can now easily include the primary key in hyperlinked relationships, simply by adding the field name to the metadata.
class BlogSerializer(serializers.HyperlinkedModelSerializer):
"""
This is a hyperlinked serializer, which default to using
a field named 'url' as the primary identifier.
Note that we can now easily also add in the 'id' field.
"""
class Meta:
model = Blog
fields = ('url', 'id', 'title', 'created', 'comments')
## Less complex views
This release rationalises the API and implementation of the Generic views, dropping the dependancy on Django's `SingleObjectMixin` and `MultipleObjectMixin` classes, removing a number of unneeded attributes, and generally making the implementation more obvious and easy to work with.
This improvement is reflected in improved documentation for the `GenericAPIView` base class, and should make it easier to determine how to override methods on the base class if you need to write customized subclasses.
---
## API Changes
### Simplified generic view classes
The functionality provided by `SingleObjectAPIView` and `MultipleObjectAPIView` base classes has now been moved into the base class `GenericAPIView`. The implementation of this base class is simple enough that providing subclasses for the base classes of detail and list views is somewhat unnecessary.
Additionally the base generic view no longer inherits from Django's `SingleObjectMixin` or `MultipleObjectMixin` classes, simplifying the implementation, and meaning you don't need to cross-reference across to Django's codebase.
Using the `SingleObjectAPIView` and `MultipleObjectAPIView` base classes continues to be supported, but will raise a `PendingDeprecationWarning`. You should instead simply use `GenericAPIView` as the base for any generic view subclasses.
### Removed attributes
The following attributes and methods, were previously present as part of Django's generic view implementations, but were unneeded and unusedand have now been entirely removed.
* context_object_name
* get_context_data()
* get_context_object_name()
The following attributes and methods, which were previously present as part of Django's generic view implementations have also been entirely removed.
* paginator_class
* get_paginator()
* get_allow_empty()
* get_slug_field()
There may be cases when removing these bits of API might mean you need to write a little more code if your view has highly customized behavior, but generally we believe that providing a coarser-grained API will make the views easier to work with, and is the right trade-off to make for the vast majority of cases.
Note that the listed attributes and methods have never been a documented part of the REST framework API, and as such are not covered by the deprecation policy.
### Simplified methods
The `get_object` and `get_paginate_by` methods no longer take an optional queryset argument. This makes overridden these methods more obvious, and a little more simple.
Using an optional queryset with these methods continues to be supported, but will raise a `PendingDeprecationWarning`.
### Deprecated attributes
The following attributes are used to control queryset lookup, and have all been moved into a pending deprecation state.
* pk_url_kwarg = 'pk'
* slug_url_kwarg = 'slug'
* slug_field = 'slug'
Their usage is replaced with a single attribute:
* lookup_field = 'pk'
This attribute is used both as the regex keyword argument in the URL conf, and as the model field to filter against when looking up a model instance. To use non-pk based lookup, simply set the `lookup_field` argument to an alternative field, and ensure that the keyword argument in the url conf matches the field name.
For example, a view with 'username' based lookup might look like this:
class UserDetail(generics.RetrieveAPIView):
lookup_field = 'username'
queryset = User.objects.all()
serializer_class = UserSerializer
And would have the following entry in the urlconf:
url(r'^users/(?P<username>\w+)/$', UserDetail.as_view()),
Usage of the old-style attributes continues to be supported, but will raise a `PendingDeprecationWarning`.
## Other notes
### Explict view attributes
The usage of `model` attribute in generic Views is still supported, but it's usage is being discouraged in favour of using explict `queryset` and `serializer_class` attributes.
For example, the following is now the recommended style for using generic views:
class AccountListView(generics.RetrieveAPIView):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
Using explict `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
It also makes it the usage of overridden `get_queryset()` or `get_serializer_class()` methods more obvious.
class AccountListView(generics.RetrieveAPIView):
serializer_class = MyModelSerializer
def get_queryset(self):
"""
Determine the queryset dynamically, depending on the
user making the request.
Note that overriding this method follows on more obviously now
that an explicit `queryset` attribute is the usual view style.
"""
return self.user.accounts
### Django 1.3 support
The 2.3 release series will be the last series to provide compatiblity with Django 1.3.

View File

@ -92,8 +92,8 @@ Let's take a look at how we can compose our views by using the mixin classes.
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.MultipleObjectAPIView):
model = Snippet
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
@ -102,15 +102,15 @@ Let's take a look at how we can compose our views by using the mixin classes.
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
We'll take a moment to examine exactly what's happening here. We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
We'll take a moment to examine exactly what's happening here. We're building our view using `GenericAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
class SnippetDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.SingleObjectAPIView):
model = Snippet
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
@ -122,7 +122,7 @@ The base class provides the core functionality, and the mixin classes provide th
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
Pretty similar. This time we're using the `SingleObjectAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
Pretty similar. Again we're using the `GenericAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
## Using generic class based views
@ -134,12 +134,12 @@ Using the mixin classes we've rewritten the views to use slightly less code than
class SnippetList(generics.ListCreateAPIView):
model = Snippet
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
model = Snippet
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django.

View File

@ -68,12 +68,12 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not
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.
class UserList(generics.ListAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
Finally we need to add those views into the API, by referencing them from the URL conf.

View File

@ -34,8 +34,8 @@ Instead of using a concrete generic view, we'll use the base class for represent
from rest_framework import renderers
from rest_framework.response import Response
class SnippetHighlight(generics.SingleObjectAPIView):
model = Snippet
class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = (renderers.StaticHTMLRenderer,)
def get(self, request, *args, **kwargs):
@ -143,34 +143,16 @@ We can change the default list style to use pagination, by modifying our `settin
'PAGINATE_BY': 10
}
Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well seperated from your other project settings.
Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well separated from your other project settings.
We could also customize the pagination style if we needed too, but in this case we'll just stick with the default.
## Reviewing our work
## Browsing the API
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations.
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
In [part 6][tut-6] of the tutorial we'll look at how we can use ViewSets and Routers to reduce the amount of code we need to build our API.
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
## Onwards and upwards
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
* Follow [the author][twitter] on Twitter and say hi.
**Now go build awesome things.**
[repo]: https://github.com/tomchristie/rest-framework-tutorial
[sandbox]: http://restframework.herokuapp.com/
[github]: https://github.com/tomchristie/django-rest-framework
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[twitter]: https://twitter.com/_tomchristie
[tut-6]: 6-viewsets-and-routers.md

View File

@ -1,6 +1,6 @@
# Tutorial 6 - ViewSets & Routers
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modelling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
`ViewSet` classes are almost the same thing as `View` classes, except that they provide operations such as `read`, or `update`, and not method handlers such as `get` or `put`.
@ -19,7 +19,7 @@ First of all let's refactor our `UserListView` and `UserDetailView` views into a
queryset = User.objects.all()
serializer_class = UserSerializer
Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two seperate classes.
Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
@ -103,21 +103,49 @@ Here's our re-wired `urls.py` file.
from snippets import views
from rest_framework.routers import DefaultRouter
# Create a router and register our views and view sets with it.
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'^/$', views.api_root)
router.register(r'^snippets/', views.SnippetViewSet, 'snippet')
router.register(r'^users/', views.UserViewSet, 'user')
router.register(r'snippets', views.SnippetViewSet, 'snippet')
router.register(r'users', views.UserViewSet, 'user')
# The urlconf is determined automatically by the router.
urlpatterns = router.urlpatterns
# We can still add format suffixes to all our URL patterns.
urlpatterns = format_suffix_patterns(urlpatterns)
# The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browseable API.
urlpatterns = patterns('',
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
)
Registering the viewsets with the router is similar to providing a urlpattern. We include three arguments - the URL prefix for the views, the viewset itself, and the base name that should be used for constructing the URL names, such as `snippet-list`.
The `DefaultRouter` class we're using also automatically creates the API root view for us, so we can now delete the `api_root` method from our `views` module.
## Trade-offs between views vs viewsets.
Using view sets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimises the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using view sets is less explicit than building your views individually.
## Reviewing our work
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
## Onwards and upwards
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
* Follow [the author][twitter] on Twitter and say hi.
**Now go build awesome things.**
[repo]: https://github.com/tomchristie/rest-framework-tutorial
[sandbox]: http://restframework.herokuapp.com/
[github]: https://github.com/tomchristie/django-rest-framework
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[twitter]: https://twitter.com/_tomchristie

View File

@ -18,21 +18,35 @@ class GenericAPIView(views.APIView):
Base class for all other generic views.
"""
# You'll need to either set these attributes,
# or override `get_queryset`/`get_serializer_class`.
queryset = None
serializer_class = None
# Shortcut which may be used in place of `queryset`/`serializer_class`
model = None
# If you want to use object lookups other than pk, set this attribute.
lookup_field = 'pk'
filter_backend = api_settings.FILTER_BACKEND
# Pagination settings
paginate_by = api_settings.PAGINATE_BY
paginate_by_param = api_settings.PAGINATE_BY_PARAM
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
page_kwarg = 'page'
lookup_field = 'pk'
# The filter backend class to use for queryset filtering
filter_backend = api_settings.FILTER_BACKEND
# Determines if the view will return 200 or 404 responses for empty lists.
allow_empty = True
# This shortcut may be used instead of setting either (or both)
# of the `queryset`/`serializer_class` attributes, although using
# the explicit style is generally preferred.
model = None
# If the `model` shortcut is used instead of `serializer_class`, then the
# serializer class will be constructed using this class as the base.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
######################################
# These are pending deprecation...
@ -61,7 +75,7 @@ class GenericAPIView(views.APIView):
return serializer_class(instance, data=data, files=files,
many=many, partial=partial, context=context)
def get_pagination_serializer(self, page=None):
def get_pagination_serializer(self, page):
"""
Return a serializer instance to use with paginated data.
"""
@ -73,32 +87,15 @@ class GenericAPIView(views.APIView):
context = self.get_serializer_context()
return pagination_serializer_class(instance=page, context=context)
def get_paginate_by(self, queryset=None):
"""
Return the size of pages to use with pagination.
If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
from a named query parameter in the url, eg. ?page_size=100
Otherwise defaults to using `self.paginate_by`.
"""
if self.paginate_by_param:
query_params = self.request.QUERY_PARAMS
try:
return int(query_params[self.paginate_by_param])
except (KeyError, ValueError):
pass
return self.paginate_by
def paginate_queryset(self, queryset, page_size, paginator_class=Paginator):
"""
Paginate a queryset.
"""
paginator = paginator_class(queryset, page_size,
allow_empty_first_page=self.allow_empty)
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
page_kwarg = self.kwargs.get(self.page_kwarg)
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
page = page_kwarg or page_query_param or 1
try:
page_number = int(page)
except ValueError:
@ -133,6 +130,27 @@ class GenericAPIView(views.APIView):
### The following methods provide default implementations
### that you may want to override for more complex cases.
def get_paginate_by(self, queryset=None):
"""
Return the size of pages to use with pagination.
If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
from a named query parameter in the url, eg. ?page_size=100
Otherwise defaults to using `self.paginate_by`.
"""
if queryset is not None:
pass # TODO: Deprecation warning
if self.paginate_by_param:
query_params = self.request.QUERY_PARAMS
try:
return int(query_params[self.paginate_by_param])
except (KeyError, ValueError):
pass
return self.paginate_by
def get_serializer_class(self):
"""
Return the class to use for the serializer.
@ -202,6 +220,7 @@ class GenericAPIView(views.APIView):
# TODO: Deprecation warning
filter_kwargs = {self.slug_field: slug}
else:
# TODO: Fix error message
raise AttributeError("Generic detail view %s must be called with "
"either an object pk or a slug."
% self.__class__.__name__)
@ -216,6 +235,9 @@ class GenericAPIView(views.APIView):
########################
### The following are placeholder methods,
### and are intended to be overridden.
###
### The are not called by GenericAPIView directly,
### but are used by the mixin methods.
def pre_save(self, obj):
"""

View File

@ -1,81 +1,198 @@
"""
Routers provide a convenient and consistent way of automatically
determining the URL conf for your API.
They are used by simply instantiating a Router class, and then registering
all the required ViewSets with that router.
For example, you might have a `urls.py` that looks something like this:
router = routers.DefaultRouter()
router.register('users', UserViewSet, 'user')
router.register('accounts', AccountViewSet, 'account')
urlpatterns = router.urls
"""
from django.conf.urls import url, patterns
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
class BaseRouter(object):
def __init__(self):
self.registry = []
def register(self, prefix, viewset, base_name):
self.registry.append((prefix, viewset, base_name))
def register(self, prefix, viewset, basename):
self.registry.append((prefix, viewset, basename))
def get_urlpatterns(self):
raise NotImplemented('get_urlpatterns must be overridden')
def get_urls(self):
raise NotImplemented('get_urls must be overridden')
@property
def urlpatterns(self):
if not hasattr(self, '_urlpatterns'):
self._urlpatterns = patterns('', *self.get_urlpatterns())
return self._urlpatterns
def urls(self):
if not hasattr(self, '_urls'):
self._urls = patterns('', *self.get_urls())
return self._urls
class DefaultRouter(BaseRouter):
route_list = [
(r'$', {
'get': 'list',
'post': 'create'
}, 'list'),
(r'(?P<pk>[^/]+)/$', {
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}, 'detail'),
class SimpleRouter(BaseRouter):
routes = [
# List route.
(
r'^{prefix}/$',
{
'get': 'list',
'post': 'create'
},
'{basename}-list'
),
# Detail route.
(
r'^{prefix}/{lookup}/$',
{
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
'{basename}-detail'
),
# Dynamically generated routes.
# Generated using @action or @link decorators on methods of the viewset.
(
r'^{prefix}/{lookup}/{methodname}/$',
{
'{httpmethod}': '{methodname}',
},
'{basename}-{methodname}'
),
]
extra_routes = r'(?P<pk>[^/]+)/%s/$'
name_format = '%s-%s'
def get_urlpatterns(self):
def get_routes(self, viewset):
"""
Augment `self.routes` with any dynamically generated routes.
Returns a list of 4-tuples, of the form:
`(url_format, method_map, name_format, extra_kwargs)`
"""
# Determine any `@action` or `@link` decorated methods on the viewset
dynamic_routes = {}
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethod = getattr(attr, 'bind_to_method', None)
if httpmethod:
dynamic_routes[httpmethod] = methodname
ret = []
for prefix, viewset, base_name in self.registry:
# Bind regular views
if not getattr(viewset, '_is_viewset', False):
regex = prefix
view = viewset
name = base_name
ret.append(url(regex, view, name=name))
continue
for url_format, method_map, name_format in self.routes:
if method_map == {'{httpmethod}': '{methodname}'}:
# Dynamic routes
for httpmethod, methodname in dynamic_routes.items():
extra_kwargs = getattr(viewset, methodname).kwargs
ret.append((
url_format.replace('{methodname}', methodname),
{httpmethod: methodname},
name_format.replace('{methodname}', methodname),
extra_kwargs
))
else:
# Standard route
extra_kwargs = {}
ret.append((url_format, method_map, name_format, extra_kwargs))
# Bind standard CRUD routes
for suffix, action_mapping, action_name in self.route_list:
return ret
def get_method_map(self, viewset, method_map):
"""
Given a viewset, and a mapping of http methods to actions,
return a new mapping which only includes any mappings that
are actually implemented by the viewset.
"""
bound_methods = {}
for method, action in method_map.items():
if hasattr(viewset, action):
bound_methods[method] = action
return bound_methods
def get_lookup_regex(self, viewset):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
"""
base_regex = '(?P<{lookup_field}>[^/]+)'
lookup_field = getattr(viewset, 'lookup_field', 'pk')
return base_regex.format(lookup_field=lookup_field)
def get_urls(self):
"""
Use the registered viewsets to generate a list of URL patterns.
"""
ret = []
for prefix, viewset, basename in self.registry:
lookup = self.get_lookup_regex(viewset)
routes = self.get_routes(viewset)
for url_format, method_map, name_format, extra_kwargs in routes:
# Only actions which actually exist on the viewset will be bound
bound_actions = {}
for method, action in action_mapping.items():
if hasattr(viewset, action):
bound_actions[method] = action
# Build the url pattern
regex = prefix + suffix
view = viewset.as_view(bound_actions, name_suffix=action_name)
name = self.name_format % (base_name, action_name)
ret.append(url(regex, view, name=name))
# Bind any extra `@action` or `@link` routes
for action_name in dir(viewset):
func = getattr(viewset, action_name)
http_method = getattr(func, 'bind_to_method', None)
# Skip if this is not an @action or @link method
if not http_method:
method_map = self.get_method_map(viewset, method_map)
if not method_map:
continue
suffix = self.extra_routes % action_name
# Build the url pattern
regex = prefix + suffix
view = viewset.as_view({http_method: action_name}, **func.kwargs)
name = self.name_format % (base_name, action_name)
regex = url_format.format(prefix=prefix, lookup=lookup)
view = viewset.as_view(method_map, **extra_kwargs)
name = name_format.format(basename=basename)
ret.append(url(regex, view, name=name))
# Return a list of url patterns
return ret
class DefaultRouter(SimpleRouter):
"""
The default router extends the SimpleRouter, but also adds in a default
API root view, and adds format suffix patterns to the URLs.
"""
include_root_view = True
include_format_suffixes = True
def get_api_root_view(self):
"""
Return a view to use as the API root.
"""
api_root_dict = {}
list_name = self.routes[0][-1]
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@api_view(('GET',))
def api_root(request, format=None):
ret = {}
for key, url_name in api_root_dict.items():
ret[key] = reverse(url_name, request=request, format=format)
return Response(ret)
return api_root
def get_urls(self):
"""
Generate the list of URL patterns, including a default root view
for the API, and appending `.json` style format suffixes.
"""
urls = []
if self.include_root_view:
root_url = url(r'^$', self.get_api_root_view(), name='api-root')
urls.append(root_url)
default_urls = super(DefaultRouter, self).get_urls()
urls.extend(default_urls)
if self.include_format_suffixes:
urls = format_suffix_patterns(urls)
return urls