Deal with List/Instance suffixes for viewsets

This commit is contained in:
Tom Christie 2013-04-26 14:59:21 +01:00
parent e301e2d974
commit 8fa79a7fd3
8 changed files with 64 additions and 46 deletions

View File

@ -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`. Here's an example of a simple URL conf, that uses `DefaultRouter`.
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'users', UserViewSet, 'user') router.register(r'users', UserViewSet, name='user')
router.register(r'accounts', AccountViewSet, 'account') router.register(r'accounts', AccountViewSet, name='account')
urlpatterns = router.urls urlpatterns = router.urls
There are three arguments to the `register()` method: There are three arguments to the `register()` method:
* `prefix` - The URL prefix to use for this set of routes. * `prefix` - The URL prefix to use for this set of routes.
* `viewset` - The viewset class. * `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: The example above would generate the following URL patterns:

View File

@ -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. 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 = DefaultRouter()
router.register(r'users', UserViewSet, 'user') router.register(r'users', UserViewSet, name='user')
urlpatterns = router.urls 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: 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:

View File

@ -105,8 +105,8 @@ Here's our re-wired `urls.py` file.
# Create a router and register our viewsets with it. # Create a router and register our viewsets with it.
router = DefaultRouter() router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, 'snippet') router.register(r'snippets', views.SnippetViewSet, name='snippet')
router.register(r'users', views.UserViewSet, 'user') router.register(r'users', views.UserViewSet, name='user')
# The API URLs are now determined automatically by the router. # The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browseable API. # Additionally, we include the login URLs for the browseable API.

View File

@ -439,7 +439,7 @@ class BrowsableAPIRenderer(BaseRenderer):
return GenericContentForm() return GenericContentForm()
def get_name(self, view): 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): def get_description(self, view):
return get_view_description(view.__class__, html=True) return get_view_description(view.__class__, html=True)

View File

@ -13,6 +13,7 @@ For example, you might have a `urls.py` that looks something like this:
urlpatterns = router.urls urlpatterns = router.urls
""" """
from collections import namedtuple
from django.conf.urls import url, patterns from django.conf.urls import url, patterns
from django.db import models from django.db import models
from rest_framework.decorators import api_view 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 from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
def replace_methodname(format_string, methodname): def replace_methodname(format_string, methodname):
""" """
Partially format a format_string, swapping out any Partially format a format_string, swapping out any
@ -38,8 +42,8 @@ class BaseRouter(object):
def __init__(self): def __init__(self):
self.registry = [] self.registry = []
def register(self, prefix, viewset, basename): def register(self, prefix, viewset, name):
self.registry.append((prefix, viewset, basename)) self.registry.append((prefix, viewset, name))
def get_urls(self): def get_urls(self):
raise NotImplemented('get_urls must be overridden') raise NotImplemented('get_urls must be overridden')
@ -54,33 +58,36 @@ class BaseRouter(object):
class SimpleRouter(BaseRouter): class SimpleRouter(BaseRouter):
routes = [ routes = [
# List route. # List route.
( Route(
r'^{prefix}/$', url=r'^{prefix}/$',
{ mapping={
'get': 'list', 'get': 'list',
'post': 'create' 'post': 'create'
}, },
'{basename}-list' name='{basename}-list',
initkwargs={'suffix': 'List'}
), ),
# Detail route. # Detail route.
( Route(
r'^{prefix}/{lookup}/$', url=r'^{prefix}/{lookup}/$',
{ mapping={
'get': 'retrieve', 'get': 'retrieve',
'put': 'update', 'put': 'update',
'patch': 'partial_update', 'patch': 'partial_update',
'delete': 'destroy' 'delete': 'destroy'
}, },
'{basename}-detail' name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
), ),
# Dynamically generated routes. # Dynamically generated routes.
# Generated using @action or @link decorators on methods of the viewset. # Generated using @action or @link decorators on methods of the viewset.
( Route(
r'^{prefix}/{lookup}/{methodname}/$', url=r'^{prefix}/{lookup}/{methodname}/$',
{ mapping={
'{httpmethod}': '{methodname}', '{httpmethod}': '{methodname}',
}, },
'{basename}-{methodnamehyphen}' name='{basename}-{methodnamehyphen}',
initkwargs={}
), ),
] ]
@ -88,8 +95,7 @@ class SimpleRouter(BaseRouter):
""" """
Augment `self.routes` with any dynamically generated routes. Augment `self.routes` with any dynamically generated routes.
Returns a list of 4-tuples, of the form: Returns a list of the Route namedtuple.
`(url_format, method_map, name_format, extra_kwargs)`
""" """
# Determine any `@action` or `@link` decorated methods on the viewset # Determine any `@action` or `@link` decorated methods on the viewset
@ -101,21 +107,21 @@ class SimpleRouter(BaseRouter):
dynamic_routes[httpmethod] = methodname dynamic_routes[httpmethod] = methodname
ret = [] ret = []
for url_format, method_map, name_format in self.routes: for route in self.routes:
if method_map == {'{httpmethod}': '{methodname}'}: if route.mapping == {'{httpmethod}': '{methodname}'}:
# Dynamic routes (@link or @action decorator) # Dynamic routes (@link or @action decorator)
for httpmethod, methodname in dynamic_routes.items(): for httpmethod, methodname in dynamic_routes.items():
extra_kwargs = getattr(viewset, methodname).kwargs initkwargs = route.initkwargs.copy()
ret.append(( initkwargs.update(getattr(viewset, methodname).kwargs)
replace_methodname(url_format, methodname), ret.append(Route(
{httpmethod: methodname}, url=replace_methodname(route.url, methodname),
replace_methodname(name_format, methodname), mapping={httpmethod: methodname},
extra_kwargs name=replace_methodname(route.name, methodname),
initkwargs=initkwargs,
)) ))
else: else:
# Standard route # Standard route
extra_kwargs = {} ret.append(route)
ret.append((url_format, method_map, name_format, extra_kwargs))
return ret return ret
@ -150,17 +156,17 @@ class SimpleRouter(BaseRouter):
lookup = self.get_lookup_regex(viewset) lookup = self.get_lookup_regex(viewset)
routes = self.get_routes(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 # Only actions which actually exist on the viewset will be bound
method_map = self.get_method_map(viewset, method_map) mapping = self.get_method_map(viewset, route.mapping)
if not method_map: if not mapping:
continue continue
# Build the url pattern # Build the url pattern
regex = url_format.format(prefix=prefix, lookup=lookup) regex = route.url.format(prefix=prefix, lookup=lookup)
view = viewset.as_view(method_map, **extra_kwargs) view = viewset.as_view(mapping, **route.initkwargs)
name = name_format.format(basename=basename) name = route.name.format(basename=basename)
ret.append(url(regex, view, name=name)) ret.append(url(regex, view, name=name))
return ret return ret
@ -179,7 +185,7 @@ class DefaultRouter(SimpleRouter):
Return a view to use as the API root. Return a view to use as the API root.
""" """
api_root_dict = {} api_root_dict = {}
list_name = self.routes[0][-1] list_name = self.routes[0].name
for prefix, viewset, basename in self.registry: for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename) api_root_dict[prefix] = list_name.format(basename=basename)

View File

@ -21,7 +21,8 @@ def get_breadcrumbs(url):
# Don't list the same view twice in a row. # Don't list the same view twice in a row.
# Probably an optional trailing slash. # Probably an optional trailing slash.
if not seen or seen[-1] != view: 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) seen.append(view)
if url == '': if url == '':

View File

@ -45,14 +45,17 @@ def _camelcase_to_spaces(content):
return ' '.join(content.split('_')).title() 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. Return a formatted name for an `APIView` class or `@api_view` function.
""" """
name = cls.__name__ name = cls.__name__
name = _remove_trailing_string(name, 'View') name = _remove_trailing_string(name, 'View')
name = _remove_trailing_string(name, 'ViewSet') 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): def get_view_description(cls, html=False):

View File

@ -35,12 +35,16 @@ class ViewSetMixin(object):
""" """
@classonlymethod @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 Because of the way class based views create a closure around the
instantiated view, we need to totally reimplement `.as_view`, instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned. 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 # sanitize keyword arguments
for key in initkwargs: for key in initkwargs:
if key in cls.http_method_names: if key in cls.http_method_names:
@ -74,7 +78,11 @@ class ViewSetMixin(object):
# like csrf_exempt from dispatch # like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=()) 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.cls = cls
view.suffix = initkwargs.get('suffix', None)
return view return view