mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-29 13:04:03 +03:00
Deal with List/Instance suffixes for viewsets
This commit is contained in:
parent
e301e2d974
commit
8fa79a7fd3
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 == '':
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user