Factor view names/descriptions out of View class

This commit is contained in:
Tom Christie 2013-04-04 21:42:26 +01:00
parent 9e24db022c
commit f68721ade8
6 changed files with 117 additions and 104 deletions

View File

@ -24,6 +24,7 @@ from rest_framework.settings import api_settings
from rest_framework.request import clone_request
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.utils.formatting import get_view_name, get_view_description
from rest_framework import exceptions, parsers, status, VERSION
@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer):
return GenericContentForm()
def get_name(self, view):
try:
return view.get_name()
except AttributeError:
return smart_text(view.__class__.__name__)
return get_view_name(view.__class__)
def get_description(self, view):
try:
return view.get_description(html=True)
except AttributeError:
return smart_text(view.__doc__ or '')
return get_view_description(view.__class__, html=True)
def render(self, data, accepted_media_type=None, renderer_context=None):
"""

View File

@ -14,23 +14,31 @@ class BaseRouter(object):
@property
def urlpatterns(self):
if not hasattr(self, '_urlpatterns'):
print self.get_urlpatterns()
self._urlpatterns = patterns('', *self.get_urlpatterns())
return self._urlpatterns
class DefaultRouter(BaseRouter):
route_list = [
(r'$', {'get': 'list', 'post': 'create'}, '%s-list'),
(r'(?P<pk>[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'),
(r'$', {'get': 'list', 'post': 'create'}, 'list'),
(r'(?P<pk>[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'),
]
extra_routes = (r'(?P<pk>[^/]+)/%s/$', '%s-%s')
extra_routes = r'(?P<pk>[^/]+)/%s/$'
name_format = '%s-%s'
def get_urlpatterns(self):
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
# Bind standard CRUD routes
for suffix, action_mapping, name_format in self.route_list:
for suffix, action_mapping, action_name in self.route_list:
# Only actions which actually exist on the viewset will be bound
bound_actions = {}
@ -40,25 +48,25 @@ class DefaultRouter(BaseRouter):
# Build the url pattern
regex = prefix + suffix
view = viewset.as_view(bound_actions)
name = name_format % base_name
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))
# Bind any extra `@action` or `@link` routes
for attr in dir(viewset):
func = getattr(viewset, attr)
for action_name in dir(viewset):
func = getattr(viewset, action_name)
http_method = getattr(func, 'bind_to_method', None)
# Skip if this is not an @action or @link method
if not http_method:
continue
regex_format, name_format = self.extra_routes
suffix = self.extra_routes % action_name
# Build the url pattern
regex = regex_format % attr
view = viewset.as_view({http_method: attr}, **func.kwargs)
name = name_format % (base_name, attr)
regex = prefix + suffix
view = viewset.as_view({http_method: action_name}, **func.kwargs)
name = self.name_format % (base_name, action_name)
ret.append(url(regex, view, name=name))
# Return a list of url patterns

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
from django.core.urlresolvers import resolve, get_script_prefix
from rest_framework.utils.formatting import get_view_name
def get_breadcrumbs(url):
@ -16,11 +17,11 @@ def get_breadcrumbs(url):
pass
else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs
if isinstance(getattr(view, 'cls_instance', None), APIView):
if issubclass(getattr(view, 'cls', None), APIView):
# Don't list the same view twice in a row.
# Probably an optional trailing slash.
if not seen or seen[-1] != view:
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
breadcrumbs_list.insert(0, (get_view_name(view.cls), prefix + url))
seen.append(view)
if url == '':

View File

@ -0,0 +1,77 @@
"""
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
from django.utils.html import escape
from django.utils.safestring import mark_safe
from rest_framework.compat import apply_markdown
import re
def _remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view classes.
"""
if content.endswith(trailing) and content != trailing:
return content[:-len(trailing)]
return content
def _remove_leading_indent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
"""
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
content = content.strip('\n')
return content
def _camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes.
"""
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
content = re.sub(camelcase_boundry, ' \\1', content).strip()
return ' '.join(content.split('_')).title()
def get_view_name(cls):
"""
Return a formatted name for an `APIView` class or `@api_view` function.
"""
name = cls.__name__
name = _remove_trailing_string(name, 'View')
name = _remove_trailing_string(name, 'ViewSet')
return _camelcase_to_spaces(name)
def get_view_description(cls, html=False):
"""
Return a description for an `APIView` class or `@api_view` function.
"""
description = cls.__doc__ or ''
description = _remove_leading_indent(description)
if html:
return markup_description(description)
return description
def markup_description(description):
"""
Apply HTML markup to the given description.
"""
if apply_markdown:
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '<br />')
return mark_safe(description)

View File

@ -4,51 +4,13 @@ Provides an APIView class that is used as the base of all class-based views.
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
from rest_framework.compat import View, apply_markdown
from rest_framework.compat import View
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.settings import api_settings
import re
def _remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view classes.
"""
if content.endswith(trailing) and content != trailing:
return content[:-len(trailing)]
return content
def _remove_leading_indent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
"""
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
content = content.strip('\n')
return content
def _camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes.
"""
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
content = re.sub(camelcase_boundry, ' \\1', content).strip()
return ' '.join(content.split('_')).title()
from rest_framework.utils.formatting import get_view_name, get_view_description
class APIView(View):
@ -64,13 +26,13 @@ class APIView(View):
@classmethod
def as_view(cls, **initkwargs):
"""
Override the default :meth:`as_view` to store an instance of the view
as an attribute on the callable function. This allows us to discover
information about the view when we do URL reverse lookups.
Store the original class on the view function.
This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
# TODO: deprecate?
view = super(APIView, cls).as_view(**initkwargs)
view.cls_instance = cls(**initkwargs)
view.cls = cls
return view
@property
@ -90,43 +52,10 @@ class APIView(View):
'Vary': 'Accept'
}
def get_name(self):
"""
Return the resource or view class name for use as this view's name.
Override to customize.
"""
# TODO: deprecate?
name = self.__class__.__name__
name = _remove_trailing_string(name, 'View')
return _camelcase_to_spaces(name)
def get_description(self, html=False):
"""
Return the resource or view docstring for use as this view's description.
Override to customize.
"""
# TODO: deprecate?
description = self.__doc__ or ''
description = _remove_leading_indent(description)
if html:
return self.markup_description(description)
return description
def markup_description(self, description):
"""
Apply HTML markup to the description of this view.
"""
# TODO: deprecate?
if apply_markdown:
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '<br />')
return mark_safe(description)
def metadata(self, request):
return {
'name': self.get_name(),
'description': self.get_description(),
'name': get_view_name(self.__class__),
'description': get_view_description(self.__class__),
'renders': [renderer.media_type for renderer in self.renderer_classes],
'parses': [parser.media_type for parser in self.parser_classes],
}

View File

@ -15,9 +15,10 @@ class ViewSetMixin(object):
view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
"""
_is_viewset = True
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
def as_view(cls, actions=None, name_suffix=None, **initkwargs):
"""
Main entry point for a request-response process.
@ -57,6 +58,8 @@ class ViewSetMixin(object):
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
view.cls = cls
return view