From b94da2468cdda6b0ad491574d35097d0e336ea7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Apr 2013 22:40:24 +0100 Subject: [PATCH] Various clean up and lots of docs --- docs/api-guide/filtering.md | 11 +- docs/api-guide/generic-views.md | 194 +++++++++------ docs/api-guide/pagination.md | 3 +- docs/api-guide/renderers.md | 2 +- docs/api-guide/routers.md | 47 +++- docs/api-guide/viewsets.md | 25 +- docs/index.md | 48 ++++ docs/topics/2.3-announcement.md | 149 +++++++++++ docs/tutorial/3-class-based-views.md | 16 +- .../4-authentication-and-permissions.md | 4 +- .../5-relationships-and-hyperlinked-apis.md | 30 +-- docs/tutorial/6-viewsets-and-routers.md | 52 +++- rest_framework/generics.py | 74 ++++-- rest_framework/routers.py | 231 +++++++++++++----- 14 files changed, 655 insertions(+), 231 deletions(-) create mode 100644 docs/topics/2.3-announcement.md diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index ed9463681..805d82f8a 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -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 \ No newline at end of file +[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index c73bc700b..9d09af7fd 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -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 diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 13d4760a3..912ce41bd 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -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' diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 3c8396aa1..e2fc36cae 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -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) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index dbb352fed..7411bd7b7 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -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') + + + + + + + + + + +
URL StyleHTTP MethodActionURL Name
{prefix}/GETlist{basename}-list
POSTcreate
{prefix}/{lookup}/GETretrieve{basename}-detail
PUTupdate
PATCHpartial_update
DELETEdestroy
{prefix}/{lookup}/{methodname}/GET@link decorated method{basename}-{methodname}
POST@action decorated method
+ +## DefaultRouter + + + + + + + + + + + + +
URL StyleHTTP MethodActionURL Name
[.format]GETautomatically generated root viewapi-root
{prefix}/[.format]GETlist{basename}-list
POSTcreate
{prefix}/{lookup}/[.format]GETretrieve{basename}-detail
PUTupdate
PATCHpartial_update
DELETEdestroy
{prefix}/{lookup}/{methodname}/[.format]GET@link decorated method{basename}-{methodname}
POST@action decorated method
+ +# Custom Routers - urlpatterns = router.urlpatterns [cite]: http://guides.rubyonrails.org/routing.html \ No newline at end of file diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index cf6ae33b6..5aa66196e 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -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 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index d51bbe13f..f3abeb6bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md new file mode 100644 index 000000000..e8e8c0916 --- /dev/null +++ b/docs/topics/2.3-announcement.md @@ -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\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. + diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index e05017c50..70cf2c546 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -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. diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 878672bb0..d3ee8e798 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -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. diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 27a108401..c9c375470 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -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 diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 4c1a1abd7..876d89acc 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -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 \ No newline at end of file diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ae03060b9..3440c01dd 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -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): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index afc51f3bb..febb02b34 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -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[^/]+)/$', { - '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[^/]+)/%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