django-rest-framework/rest_framework/routers.py

237 lines
7.5 KiB
Python

"""
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 django.db import models
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.viewsets import ModelViewSet
from rest_framework.urlpatterns import format_suffix_patterns
def replace_methodname(format_string, methodname):
"""
Partially format a format_string, swapping out any
'{methodname}'' or '{methodnamehyphen}'' components.
"""
methodnamehyphen = methodname.replace('_', '-')
ret = format_string
ret = ret.replace('{methodname}', methodname)
ret = ret.replace('{methodnamehyphen}', methodnamehyphen)
return ret
class BaseRouter(object):
def __init__(self):
self.registry = []
def register(self, prefix, viewset, basename):
self.registry.append((prefix, viewset, basename))
def get_urls(self):
raise NotImplemented('get_urls must be overridden')
@property
def urls(self):
if not hasattr(self, '_urls'):
self._urls = patterns('', *self.get_urls())
return self._urls
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}-{methodnamehyphen}'
),
]
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 url_format, method_map, name_format in self.routes:
if method_map == {'{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
))
else:
# Standard route
extra_kwargs = {}
ret.append((url_format, method_map, name_format, extra_kwargs))
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
method_map = self.get_method_map(viewset, method_map)
if not method_map:
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)
ret.append(url(regex, view, name=name))
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
class AutoRouter(DefaultRouter):
"""
A router class that doesn't require you to register any viewsets,
but instead automatically creates routes for all installed models.
Useful for quick and dirty prototyping.
"""
def __init__(self):
super(AutoRouter, self).__init__()
for model in models.get_models():
prefix = model._meta.verbose_name_plural.replace(' ', '_')
basename = model._meta.object_name.lower()
classname = model.__name__
DynamicViewSet = type(
classname,
(ModelViewSet,),
{}
)
DynamicViewSet.model = model
self.register(prefix, DynamicViewSet, basename)