diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c15e0db3..752306add 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 283add8de..c37909ff7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -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[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), + (r'$', {'get': 'list', 'post': 'create'}, 'list'), + (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'), ] - extra_routes = (r'(?P[^/]+)/%s/$', '%s-%s') + extra_routes = r'(?P[^/]+)/%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 diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index af21ac79b..18b3b2076 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -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 == '': diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py new file mode 100644 index 000000000..79566db13 --- /dev/null +++ b/rest_framework/utils/formatting.py @@ -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', '
') + return mark_safe(description) diff --git a/rest_framework/views.py b/rest_framework/views.py index 81cbdcbb2..12298ca52 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -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', '
') - 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], } diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 887a97223..0818c0d9f 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -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