diff options
Diffstat (limited to 'rest_framework/routers.py')
| -rw-r--r-- | rest_framework/routers.py | 231 |
1 files changed, 174 insertions, 57 deletions
diff --git a/rest_framework/routers.py b/rest_framework/routers.py index afc51f3b..febb02b3 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 - - -class DefaultRouter(BaseRouter): - route_list = [ - (r'$', { - 'get': 'list', - 'post': 'create' - }, 'list'), - (r'(?P<pk>[^/]+)/$', { - 'get': 'retrieve', - 'put': 'update', - 'patch': 'partial_update', - 'delete': 'destroy' - }, 'detail'), + 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}-{methodname}' + ), ] - extra_routes = r'(?P<pk>[^/]+)/%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 - # 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 + 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 = [] - # 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)) + for prefix, viewset, basename in self.registry: + lookup = self.get_lookup_regex(viewset) + routes = self.get_routes(viewset) - # 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) + for url_format, method_map, name_format, extra_kwargs in routes: - # Skip if this is not an @action or @link method - if not http_method: + # 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 - 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 |
