From 8fa79a7fd38dda015afa658084361c6da2856e46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Apr 2013 14:59:21 +0100 Subject: [PATCH] Deal with List/Instance suffixes for viewsets --- docs/api-guide/routers.md | 8 +-- docs/api-guide/viewsets.md | 2 +- docs/tutorial/6-viewsets-and-routers.md | 6 +-- rest_framework/renderers.py | 2 +- rest_framework/routers.py | 72 +++++++++++++------------ rest_framework/utils/breadcrumbs.py | 3 +- rest_framework/utils/formatting.py | 7 ++- rest_framework/viewsets.py | 10 +++- 8 files changed, 64 insertions(+), 46 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 2fda53734..7b211bfd6 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -15,15 +15,15 @@ REST framework adds support for automatic URL routing to Django, and provides yo Here's an example of a simple URL conf, that uses `DefaultRouter`. router = routers.SimpleRouter() - router.register(r'users', UserViewSet, 'user') - router.register(r'accounts', AccountViewSet, 'account') + router.register(r'users', UserViewSet, name='user') + router.register(r'accounts', AccountViewSet, name='account') urlpatterns = router.urls There are three arguments to the `register()` method: * `prefix` - The URL prefix to use for this set of routes. * `viewset` - The viewset class. -* `basename` - The base to use for the URL names that are created. +* `name` - The base to use for the URL names that are created. The example above would generate the following URL patterns: @@ -119,4 +119,4 @@ The following example will only route to the `list` and `retrieve` actions, and If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls()` method. The method should insect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute. -[cite]: http://guides.rubyonrails.org/routing.html \ No newline at end of file +[cite]: http://guides.rubyonrails.org/routing.html diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 36a4dbd5c..8af35bb8d 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -42,7 +42,7 @@ If we need to, we can bind this viewset into two seperate views, like so: Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated. router = DefaultRouter() - router.register(r'users', UserViewSet, 'user') + router.register(r'users', UserViewSet, name='user') urlpatterns = router.urls Rather than writing your own viewsets, you'll often want to use the existing base classes that provide a default set of behavior. For example: diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 876d89acc..25a974a1f 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -105,8 +105,8 @@ Here's our re-wired `urls.py` file. # Create a router and register our viewsets with it. router = DefaultRouter() - router.register(r'snippets', views.SnippetViewSet, 'snippet') - router.register(r'users', views.UserViewSet, 'user') + router.register(r'snippets', views.SnippetViewSet, name='snippet') + router.register(r'users', views.UserViewSet, name='user') # The API URLs are now determined automatically by the router. # Additionally, we include the login URLs for the browseable API. @@ -148,4 +148,4 @@ We've reached the end of our tutorial. If you want to get more involved in the [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 +[twitter]: https://twitter.com/_tomchristie diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 752306add..a0829c8fc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -439,7 +439,7 @@ class BrowsableAPIRenderer(BaseRenderer): return GenericContentForm() def get_name(self, view): - return get_view_name(view.__class__) + return get_view_name(view.__class__, getattr(view, 'suffix', None)) def get_description(self, view): return get_view_description(view.__class__, html=True) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index b70522181..3a8c45085 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -13,6 +13,7 @@ For example, you might have a `urls.py` that looks something like this: urlpatterns = router.urls """ +from collections import namedtuple from django.conf.urls import url, patterns from django.db import models from rest_framework.decorators import api_view @@ -22,6 +23,9 @@ from rest_framework.viewsets import ModelViewSet from rest_framework.urlpatterns import format_suffix_patterns +Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) + + def replace_methodname(format_string, methodname): """ Partially format a format_string, swapping out any @@ -38,8 +42,8 @@ class BaseRouter(object): def __init__(self): self.registry = [] - def register(self, prefix, viewset, basename): - self.registry.append((prefix, viewset, basename)) + def register(self, prefix, viewset, name): + self.registry.append((prefix, viewset, name)) def get_urls(self): raise NotImplemented('get_urls must be overridden') @@ -54,33 +58,36 @@ class BaseRouter(object): class SimpleRouter(BaseRouter): routes = [ # List route. - ( - r'^{prefix}/$', - { + Route( + url=r'^{prefix}/$', + mapping={ 'get': 'list', 'post': 'create' }, - '{basename}-list' + name='{basename}-list', + initkwargs={'suffix': 'List'} ), # Detail route. - ( - r'^{prefix}/{lookup}/$', - { + Route( + url=r'^{prefix}/{lookup}/$', + mapping={ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy' }, - '{basename}-detail' + name='{basename}-detail', + initkwargs={'suffix': 'Instance'} ), # Dynamically generated routes. # Generated using @action or @link decorators on methods of the viewset. - ( - r'^{prefix}/{lookup}/{methodname}/$', - { + Route( + url=r'^{prefix}/{lookup}/{methodname}/$', + mapping={ '{httpmethod}': '{methodname}', }, - '{basename}-{methodnamehyphen}' + name='{basename}-{methodnamehyphen}', + initkwargs={} ), ] @@ -88,8 +95,7 @@ class SimpleRouter(BaseRouter): """ 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)` + Returns a list of the Route namedtuple. """ # Determine any `@action` or `@link` decorated methods on the viewset @@ -101,21 +107,21 @@ class SimpleRouter(BaseRouter): dynamic_routes[httpmethod] = methodname ret = [] - for url_format, method_map, name_format in self.routes: - if method_map == {'{httpmethod}': '{methodname}'}: + for route in self.routes: + if route.mapping == {'{httpmethod}': '{methodname}'}: # Dynamic routes (@link or @action decorator) for httpmethod, methodname in dynamic_routes.items(): - extra_kwargs = getattr(viewset, methodname).kwargs - ret.append(( - replace_methodname(url_format, methodname), - {httpmethod: methodname}, - replace_methodname(name_format, methodname), - extra_kwargs + initkwargs = route.initkwargs.copy() + initkwargs.update(getattr(viewset, methodname).kwargs) + ret.append(Route( + url=replace_methodname(route.url, methodname), + mapping={httpmethod: methodname}, + name=replace_methodname(route.name, methodname), + initkwargs=initkwargs, )) else: # Standard route - extra_kwargs = {} - ret.append((url_format, method_map, name_format, extra_kwargs)) + ret.append(route) return ret @@ -150,17 +156,17 @@ class SimpleRouter(BaseRouter): lookup = self.get_lookup_regex(viewset) routes = self.get_routes(viewset) - for url_format, method_map, name_format, extra_kwargs in routes: + for route in routes: # Only actions which actually exist on the viewset will be bound - method_map = self.get_method_map(viewset, method_map) - if not method_map: + mapping = self.get_method_map(viewset, route.mapping) + if not mapping: continue # Build the url pattern - regex = url_format.format(prefix=prefix, lookup=lookup) - view = viewset.as_view(method_map, **extra_kwargs) - name = name_format.format(basename=basename) + regex = route.url.format(prefix=prefix, lookup=lookup) + view = viewset.as_view(mapping, **route.initkwargs) + name = route.name.format(basename=basename) ret.append(url(regex, view, name=name)) return ret @@ -179,7 +185,7 @@ class DefaultRouter(SimpleRouter): Return a view to use as the API root. """ api_root_dict = {} - list_name = self.routes[0][-1] + list_name = self.routes[0].name for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 18b3b2076..8f8e5710d 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -21,7 +21,8 @@ def get_breadcrumbs(url): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - breadcrumbs_list.insert(0, (get_view_name(view.cls), prefix + url)) + suffix = getattr(view, 'suffix', None) + breadcrumbs_list.insert(0, (get_view_name(view.cls, suffix), prefix + url)) seen.append(view) if url == '': diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 79566db13..ebadb3a67 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -45,14 +45,17 @@ def _camelcase_to_spaces(content): return ' '.join(content.split('_')).title() -def get_view_name(cls): +def get_view_name(cls, suffix=None): """ Return a formatted name for an `APIView` class or `@api_view` function. """ name = cls.__name__ name = _remove_trailing_string(name, 'View') name = _remove_trailing_string(name, 'ViewSet') - return _camelcase_to_spaces(name) + name = _camelcase_to_spaces(name) + if suffix: + name += ' ' + suffix + return name def get_view_description(cls, html=False): diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 9133fd442..bd25df778 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -35,12 +35,16 @@ class ViewSetMixin(object): """ @classonlymethod - def as_view(cls, actions=None, name_suffix=None, **initkwargs): + def as_view(cls, actions=None, **initkwargs): """ Because of the way class based views create a closure around the instantiated view, we need to totally reimplement `.as_view`, and slightly modify the view function that is created and returned. """ + # The suffix initkwarg is reserved for identifing the viewset type + # eg. 'List' or 'Instance'. + cls.suffix = None + # sanitize keyword arguments for key in initkwargs: if key in cls.http_method_names: @@ -74,7 +78,11 @@ class ViewSetMixin(object): # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) + # We need to set these on the view function, so that breadcrumb + # generation can pick out these bits of information from a + # resolved URL. view.cls = cls + view.suffix = initkwargs.get('suffix', None) return view