mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
0dec36eb41
* Start test case * Added 'requests' test client * Address typos * Graceful fallback if requests is not installed. * Add cookie support * Tests for auth and CSRF * Py3 compat * py3 compat * py3 compat * Add get_requests_client * Added SchemaGenerator.should_include_link * add settings for html cutoff on related fields * Router doesn't work if prefix is blank, though project urls.py handles prefix * Fix Django 1.10 to-many deprecation * Add django.core.urlresolvers compatibility * Update django-filter & django-guardian * Check for empty router prefix; adjust URL accordingly It's easiest to fix this issue after we have made the regex. To try to fix it before would require doing something different for List vs Detail, which means we'd have to know which type of url we're constructing before acting accordingly. * Fix misc django deprecations * Use TOC extension instead of header * Fix deprecations for py3k * Add py3k compatibility to is_simple_callable * Add is_simple_callable tests * Drop python 3.2 support (EOL, Dropped by Django) * schema_renderers= should *set* the renderers, not append to them. * API client (#4424) * Fix release notes * Add note about 'User account is disabled.' vs 'Unable to log in' * Clean up schema generation (#4527) * Handle multiple methods on custom action (#4529) * RequestsClient, CoreAPIClient * exclude_from_schema * Added 'get_schema_view()' shortcut * Added schema descriptions * Better descriptions for schemas * Add type annotation to schema generation * Coerce schema 'pk' in path to actual field name * Deprecations move into assertion errors * Use get_schema_view in tests * Updte CoreJSON media type * Handle schema structure correctly when path prefixs exist. Closes #4401 * Add PendingDeprecation to Router schema generation. * Added SCHEMA_COERCE_PATH_PK and SCHEMA_COERCE_METHOD_NAMES * Renamed and documented 'get_schema_fields' interface.
387 lines
14 KiB
Python
387 lines
14 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 __future__ import unicode_literals
|
|
|
|
import itertools
|
|
import warnings
|
|
from collections import OrderedDict, namedtuple
|
|
|
|
from django.conf.urls import url
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
|
|
from rest_framework import exceptions, renderers, views
|
|
from rest_framework.compat import NoReverseMatch
|
|
from rest_framework.renderers import BrowsableAPIRenderer
|
|
from rest_framework.response import Response
|
|
from rest_framework.reverse import reverse
|
|
from rest_framework.schemas import SchemaGenerator
|
|
from rest_framework.settings import api_settings
|
|
from rest_framework.urlpatterns import format_suffix_patterns
|
|
|
|
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
|
DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs'])
|
|
DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
|
|
|
|
|
|
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
|
|
|
|
|
|
def flatten(list_of_lists):
|
|
"""
|
|
Takes an iterable of iterables, returns a single iterable containing all items
|
|
"""
|
|
return itertools.chain(*list_of_lists)
|
|
|
|
|
|
class BaseRouter(object):
|
|
def __init__(self):
|
|
self.registry = []
|
|
|
|
def register(self, prefix, viewset, base_name=None):
|
|
if base_name is None:
|
|
base_name = self.get_default_base_name(viewset)
|
|
self.registry.append((prefix, viewset, base_name))
|
|
|
|
def get_default_base_name(self, viewset):
|
|
"""
|
|
If `base_name` is not specified, attempt to automatically determine
|
|
it from the viewset.
|
|
"""
|
|
raise NotImplementedError('get_default_base_name must be overridden')
|
|
|
|
def get_urls(self):
|
|
"""
|
|
Return a list of URL patterns, given the registered viewsets.
|
|
"""
|
|
raise NotImplementedError('get_urls must be overridden')
|
|
|
|
@property
|
|
def urls(self):
|
|
if not hasattr(self, '_urls'):
|
|
self._urls = self.get_urls()
|
|
return self._urls
|
|
|
|
|
|
class SimpleRouter(BaseRouter):
|
|
|
|
routes = [
|
|
# List route.
|
|
Route(
|
|
url=r'^{prefix}{trailing_slash}$',
|
|
mapping={
|
|
'get': 'list',
|
|
'post': 'create'
|
|
},
|
|
name='{basename}-list',
|
|
initkwargs={'suffix': 'List'}
|
|
),
|
|
# Dynamically generated list routes.
|
|
# Generated using @list_route decorator
|
|
# on methods of the viewset.
|
|
DynamicListRoute(
|
|
url=r'^{prefix}/{methodname}{trailing_slash}$',
|
|
name='{basename}-{methodnamehyphen}',
|
|
initkwargs={}
|
|
),
|
|
# Detail route.
|
|
Route(
|
|
url=r'^{prefix}/{lookup}{trailing_slash}$',
|
|
mapping={
|
|
'get': 'retrieve',
|
|
'put': 'update',
|
|
'patch': 'partial_update',
|
|
'delete': 'destroy'
|
|
},
|
|
name='{basename}-detail',
|
|
initkwargs={'suffix': 'Instance'}
|
|
),
|
|
# Dynamically generated detail routes.
|
|
# Generated using @detail_route decorator on methods of the viewset.
|
|
DynamicDetailRoute(
|
|
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
|
|
name='{basename}-{methodnamehyphen}',
|
|
initkwargs={}
|
|
),
|
|
]
|
|
|
|
def __init__(self, trailing_slash=True):
|
|
self.trailing_slash = trailing_slash and '/' or ''
|
|
super(SimpleRouter, self).__init__()
|
|
|
|
def get_default_base_name(self, viewset):
|
|
"""
|
|
If `base_name` is not specified, attempt to automatically determine
|
|
it from the viewset.
|
|
"""
|
|
queryset = getattr(viewset, 'queryset', None)
|
|
|
|
assert queryset is not None, '`base_name` argument not specified, and could ' \
|
|
'not automatically determine the name from the viewset, as ' \
|
|
'it does not have a `.queryset` attribute.'
|
|
|
|
return queryset.model._meta.object_name.lower()
|
|
|
|
def get_routes(self, viewset):
|
|
"""
|
|
Augment `self.routes` with any dynamically generated routes.
|
|
|
|
Returns a list of the Route namedtuple.
|
|
"""
|
|
# converting to list as iterables are good for one pass, known host needs to be checked again and again for
|
|
# different functions.
|
|
known_actions = list(flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]))
|
|
|
|
# Determine any `@detail_route` or `@list_route` decorated methods on the viewset
|
|
detail_routes = []
|
|
list_routes = []
|
|
for methodname in dir(viewset):
|
|
attr = getattr(viewset, methodname)
|
|
httpmethods = getattr(attr, 'bind_to_methods', None)
|
|
detail = getattr(attr, 'detail', True)
|
|
if httpmethods:
|
|
# checking method names against the known actions list
|
|
if methodname in known_actions:
|
|
raise ImproperlyConfigured('Cannot use @detail_route or @list_route '
|
|
'decorators on method "%s" '
|
|
'as it is an existing route' % methodname)
|
|
httpmethods = [method.lower() for method in httpmethods]
|
|
if detail:
|
|
detail_routes.append((httpmethods, methodname))
|
|
else:
|
|
list_routes.append((httpmethods, methodname))
|
|
|
|
def _get_dynamic_routes(route, dynamic_routes):
|
|
ret = []
|
|
for httpmethods, methodname in dynamic_routes:
|
|
method_kwargs = getattr(viewset, methodname).kwargs
|
|
initkwargs = route.initkwargs.copy()
|
|
initkwargs.update(method_kwargs)
|
|
url_path = initkwargs.pop("url_path", None) or methodname
|
|
ret.append(Route(
|
|
url=replace_methodname(route.url, url_path),
|
|
mapping={httpmethod: methodname for httpmethod in httpmethods},
|
|
name=replace_methodname(route.name, url_path),
|
|
initkwargs=initkwargs,
|
|
))
|
|
|
|
return ret
|
|
|
|
ret = []
|
|
for route in self.routes:
|
|
if isinstance(route, DynamicDetailRoute):
|
|
# Dynamic detail routes (@detail_route decorator)
|
|
ret += _get_dynamic_routes(route, detail_routes)
|
|
elif isinstance(route, DynamicListRoute):
|
|
# Dynamic list routes (@list_route decorator)
|
|
ret += _get_dynamic_routes(route, list_routes)
|
|
else:
|
|
# Standard route
|
|
ret.append(route)
|
|
|
|
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, lookup_prefix=''):
|
|
"""
|
|
Given a viewset, return the portion of URL regex that is used
|
|
to match against a single instance.
|
|
|
|
Note that lookup_prefix is not used directly inside REST rest_framework
|
|
itself, but is required in order to nicely support nested router
|
|
implementations, such as drf-nested-routers.
|
|
|
|
https://github.com/alanjds/drf-nested-routers
|
|
"""
|
|
base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})'
|
|
# Use `pk` as default field, unset set. Default regex should not
|
|
# consume `.json` style suffixes and should break at '/' boundaries.
|
|
lookup_field = getattr(viewset, 'lookup_field', 'pk')
|
|
lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
|
|
lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
|
|
return base_regex.format(
|
|
lookup_prefix=lookup_prefix,
|
|
lookup_url_kwarg=lookup_url_kwarg,
|
|
lookup_value=lookup_value
|
|
)
|
|
|
|
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 route in routes:
|
|
|
|
# Only actions which actually exist on the viewset will be bound
|
|
mapping = self.get_method_map(viewset, route.mapping)
|
|
if not mapping:
|
|
continue
|
|
|
|
# Build the url pattern
|
|
regex = route.url.format(
|
|
prefix=prefix,
|
|
lookup=lookup,
|
|
trailing_slash=self.trailing_slash
|
|
)
|
|
|
|
# If there is no prefix, the first part of the url is probably
|
|
# controlled by project's urls.py and the router is in an app,
|
|
# so a slash in the beginning will (A) cause Django to give
|
|
# warnings and (B) generate URLS that will require using '//'.
|
|
if not prefix and regex[:2] == '^/':
|
|
regex = '^' + regex[2:]
|
|
|
|
view = viewset.as_view(mapping, **route.initkwargs)
|
|
name = route.name.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
|
|
root_view_name = 'api-root'
|
|
default_schema_renderers = [renderers.CoreJSONRenderer, BrowsableAPIRenderer]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
if 'schema_title' in kwargs:
|
|
warnings.warn(
|
|
"Including a schema directly via a router is now pending "
|
|
"deprecation. Use `get_schema_view()` instead.",
|
|
PendingDeprecationWarning
|
|
)
|
|
if 'schema_renderers' in kwargs:
|
|
assert 'schema_title' in kwargs, 'Missing "schema_title" argument.'
|
|
if 'schema_url' in kwargs:
|
|
assert 'schema_title' in kwargs, 'Missing "schema_title" argument.'
|
|
self.schema_title = kwargs.pop('schema_title', None)
|
|
self.schema_url = kwargs.pop('schema_url', None)
|
|
self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers)
|
|
if 'root_renderers' in kwargs:
|
|
self.root_renderers = kwargs.pop('root_renderers')
|
|
else:
|
|
self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)
|
|
super(DefaultRouter, self).__init__(*args, **kwargs)
|
|
|
|
def get_schema_root_view(self, api_urls=None):
|
|
"""
|
|
Return a schema root view.
|
|
"""
|
|
schema_renderers = self.schema_renderers
|
|
schema_generator = SchemaGenerator(
|
|
title=self.schema_title,
|
|
url=self.schema_url,
|
|
patterns=api_urls
|
|
)
|
|
|
|
class APISchemaView(views.APIView):
|
|
_ignore_model_permissions = True
|
|
exclude_from_schema = True
|
|
renderer_classes = schema_renderers
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
schema = schema_generator.get_schema(request)
|
|
if schema is None:
|
|
raise exceptions.PermissionDenied()
|
|
return Response(schema)
|
|
|
|
return APISchemaView.as_view()
|
|
|
|
def get_api_root_view(self, api_urls=None):
|
|
"""
|
|
Return a basic root view.
|
|
"""
|
|
api_root_dict = OrderedDict()
|
|
list_name = self.routes[0].name
|
|
for prefix, viewset, basename in self.registry:
|
|
api_root_dict[prefix] = list_name.format(basename=basename)
|
|
|
|
class APIRootView(views.APIView):
|
|
_ignore_model_permissions = True
|
|
exclude_from_schema = True
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
# Return a plain {"name": "hyperlink"} response.
|
|
ret = OrderedDict()
|
|
namespace = request.resolver_match.namespace
|
|
for key, url_name in api_root_dict.items():
|
|
if namespace:
|
|
url_name = namespace + ':' + url_name
|
|
try:
|
|
ret[key] = reverse(
|
|
url_name,
|
|
args=args,
|
|
kwargs=kwargs,
|
|
request=request,
|
|
format=kwargs.get('format', None)
|
|
)
|
|
except NoReverseMatch:
|
|
# Don't bail out if eg. no list routes exist, only detail routes.
|
|
continue
|
|
|
|
return Response(ret)
|
|
|
|
return APIRootView.as_view()
|
|
|
|
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 = super(DefaultRouter, self).get_urls()
|
|
|
|
if self.include_root_view:
|
|
if self.schema_title:
|
|
view = self.get_schema_root_view(api_urls=urls)
|
|
else:
|
|
view = self.get_api_root_view(api_urls=urls)
|
|
root_url = url(r'^$', view, name=self.root_view_name)
|
|
urls.append(root_url)
|
|
|
|
if self.include_format_suffixes:
|
|
urls = format_suffix_patterns(urls)
|
|
|
|
return urls
|