diff options
| author | Tom Christie | 2013-04-04 21:42:26 +0100 | 
|---|---|---|
| committer | Tom Christie | 2013-04-04 21:42:26 +0100 | 
| commit | f68721ade8d66806296323116ff9a61773ad2be1 (patch) | |
| tree | d9f44f935ad338f7accb9500383c15bf30bc3621 | |
| parent | 9e24db022cd8da1a588dd43e6239e07798881c02 (diff) | |
| download | django-rest-framework-f68721ade8d66806296323116ff9a61773ad2be1.tar.bz2 | |
Factor view names/descriptions out of View class
| -rw-r--r-- | rest_framework/renderers.py | 11 | ||||
| -rw-r--r-- | rest_framework/routers.py | 34 | ||||
| -rw-r--r-- | rest_framework/utils/breadcrumbs.py | 5 | ||||
| -rw-r--r-- | rest_framework/utils/formatting.py | 77 | ||||
| -rw-r--r-- | rest_framework/views.py | 89 | ||||
| -rw-r--r-- | rest_framework/viewsets.py | 5 | 
6 files changed, 117 insertions, 104 deletions
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c15e0db..752306ad 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 283add8d..c37909ff 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<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 diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index af21ac79..18b3b207 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 00000000..79566db1 --- /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', '<br />') +    return mark_safe(description) diff --git a/rest_framework/views.py b/rest_framework/views.py index 81cbdcbb..12298ca5 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', '<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],          } diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 887a9722..0818c0d9 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  | 
