From 7fa3a214fbd33dac61d3f8836e2c4aeda3149647 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Mon, 23 Jan 2012 13:32:37 -0500 Subject: [PATCH 1/3] Refactored get_name()/get_description() --- djangorestframework/renderers.py | 47 ++++++----- djangorestframework/templates/renderer.html | 11 +-- djangorestframework/tests/description.py | 52 ++++++------ djangorestframework/utils/breadcrumbs.py | 4 +- djangorestframework/utils/description.py | 88 --------------------- djangorestframework/views.py | 73 ++++++++++++++--- 6 files changed, 119 insertions(+), 156 deletions(-) delete mode 100644 djangorestframework/utils/description.py diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index bb186b0a9..5338c3835 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -12,10 +12,9 @@ from django.template import RequestContext, loader from django.utils import simplejson as json -from djangorestframework.compat import apply_markdown, yaml +from djangorestframework.compat import yaml from djangorestframework.utils import dict2xml, url_resolves from djangorestframework.utils.breadcrumbs import get_breadcrumbs -from djangorestframework.utils.description import get_name, get_description from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from djangorestframework import VERSION @@ -226,6 +225,7 @@ class DocumentingTemplateRenderer(BaseRenderer): return content + def _get_form_instance(self, view, method): """ Get a form, possibly bound to either the input or output data. @@ -261,6 +261,7 @@ class DocumentingTemplateRenderer(BaseRenderer): return form_instance + def _get_generic_content_form(self, view): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms @@ -296,6 +297,20 @@ class DocumentingTemplateRenderer(BaseRenderer): # Okey doke, let's do it return GenericContentForm(view) + def get_name(self): + try: + return self.view.get_name() + except AttributeError: + return self.view.__doc__ + + def get_description(self, html=None): + if html is None: + html = bool('html' in self.format) + try: + return self.view.get_description(html) + except AttributeError: + return self.view.__doc__ + def render(self, obj=None, media_type=None): """ Renders *obj* using the :attr:`template` set on the class. @@ -316,15 +331,8 @@ class DocumentingTemplateRenderer(BaseRenderer): login_url = None logout_url = None - name = get_name(self.view) - description = get_description(self.view) - - markeddown = None - if apply_markdown: - try: - markeddown = apply_markdown(description) - except AttributeError: - markeddown = None + name = self.get_name() + description = self.get_description() breadcrumb_list = get_breadcrumbs(self.view.request.path) @@ -332,12 +340,11 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, # TODO: remove 'response': self.view.response, 'description': description, 'name': name, 'version': VERSION, - 'markeddown': markeddown, 'breadcrumblist': breadcrumb_list, 'available_formats': self.view._rendered_formats, 'put_form': put_form_instance, @@ -395,14 +402,12 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): template = 'renderer.txt' -DEFAULT_RENDERERS = ( - JSONRenderer, - JSONPRenderer, - DocumentingHTMLRenderer, - DocumentingXHTMLRenderer, - DocumentingPlainTextRenderer, - XMLRenderer -) +DEFAULT_RENDERERS = ( JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer ) if YAMLRenderer: DEFAULT_RENDERERS += (YAMLRenderer,) diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 0532d515e..3b33a6ff3 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -11,13 +11,8 @@ /* Custom styles */ .version{font-size:8px;} - {% if ADMIN_MEDIA_PREFIX %} - - - {% else %} - - - {% endif %} + + Django REST framework - {{ name }} @@ -50,7 +45,7 @@

{{ name }}

-

{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}

+

{% autoescape off %}{{ description }}{% endautoescape %}

{{ response.status }} {{ response.status_text }}{% autoescape off %}
 {% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py
index 56dcdfabb..212b4ca4d 100644
--- a/djangorestframework/tests/description.py
+++ b/djangorestframework/tests/description.py
@@ -1,7 +1,6 @@
 from django.test import TestCase
 from djangorestframework.views import View
 from djangorestframework.compat import apply_markdown
-from djangorestframework.utils.description import get_name, get_description
 
 # We check that docstrings get nicely un-indented.
 DESCRIPTION = """an example docstring
@@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase):
         """Ensure Resource names are based on the classname by default."""
         class MockView(View):
             pass
-        self.assertEquals(get_name(MockView()), 'Mock')
+        self.assertEquals(MockView().get_name(), 'Mock')
 
-    # This has been turned off now.
-    #def test_resource_name_can_be_set_explicitly(self):
-    #    """Ensure Resource names can be set using the 'name' class attribute."""
-    #    example = 'Some Other Name'
-    #    class MockView(View):
-    #        name = example
-    #    self.assertEquals(get_name(MockView()), example)
+    def test_resource_name_can_be_set_explicitly(self):
+        """Ensure Resource names can be set using the 'get_name' method."""
+        example = 'Some Other Name'
+        class MockView(View):
+            def get_name(self):
+                return example
+        self.assertEquals(MockView().get_name(), example)
 
     def test_resource_description_uses_docstring_by_default(self):
         """Ensure Resource names are based on the docstring by default."""
@@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase):
 
             # hash style header #"""
 
-        self.assertEquals(get_description(MockView()), DESCRIPTION)
+        self.assertEquals(MockView().get_description(), DESCRIPTION)
 
-    # This has been turned off now
-    #def test_resource_description_can_be_set_explicitly(self):
-    #    """Ensure Resource descriptions can be set using the 'description' class attribute."""
-    #    example = 'Some other description'
-    #    class MockView(View):
-    #        """docstring"""
-    #        description = example
-    #    self.assertEquals(get_description(MockView()), example)
+    def test_resource_description_can_be_set_explicitly(self):
+        """Ensure Resource descriptions can be set using the 'get_description' method."""
+        example = 'Some other description'
+        class MockView(View):
+            """docstring"""
+            def get_description(self):
+                return example
+        self.assertEquals(MockView().get_description(), example)
 
-    #def test_resource_description_does_not_require_docstring(self):
-    #    """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
-    #    example = 'Some other description'
-    #    class MockView(View):
-    #        description = example
-    #    self.assertEquals(get_description(MockView()), example)
+    def test_resource_description_does_not_require_docstring(self):
+        """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
+        example = 'Some other description'
+        class MockView(View):
+            def get_description(self):
+                return example
+        self.assertEquals(MockView().get_description(), example)
 
     def test_resource_description_can_be_empty(self):
-        """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
+        """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
         class MockView(View):
             pass
-        self.assertEquals(get_description(MockView()), '')
+        self.assertEquals(MockView().get_description(), '')
 
     def test_markdown(self):
         """Ensure markdown to HTML works as expected"""
diff --git a/djangorestframework/utils/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py
index cfc63a479..85e13a5a4 100644
--- a/djangorestframework/utils/breadcrumbs.py
+++ b/djangorestframework/utils/breadcrumbs.py
@@ -1,6 +1,4 @@
 from django.core.urlresolvers import resolve
-from djangorestframework.utils.description import get_name
-
 
 def get_breadcrumbs(url):
     """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
@@ -17,7 +15,7 @@ def get_breadcrumbs(url):
         else:
             # Check if this is a REST framework view, and if so add it to the breadcrumbs
             if isinstance(getattr(view, 'cls_instance', None), View):
-                breadcrumbs_list.insert(0, (get_name(view), url))
+                breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
 
         if url == '':
             # All done
diff --git a/djangorestframework/utils/description.py b/djangorestframework/utils/description.py
deleted file mode 100644
index 096cf57ff..000000000
--- a/djangorestframework/utils/description.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""
-Get a descriptive name and description for a view.
-"""
-import re
-from djangorestframework.resources import Resource, FormResource, ModelResource
-
-
-# These a a bit Grungy, but they do the job.
-
-def get_name(view):
-    """
-    Return a name for the view.
-
-    If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
-    """
-
-    # If we're looking up the name of a view callable, as found by reverse,
-    # grok the class instance that we stored when as_view was called.
-    if getattr(view, 'cls_instance', None):
-        view = view.cls_instance
-
-    # If this view has a resource that's been overridden, then use that resource for the name
-    if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
-        name = view.resource.__name__
-
-        # Chomp of any non-descriptive trailing part of the resource class name
-        if name.endswith('Resource') and name != 'Resource':
-            name = name[:-len('Resource')]
-
-        # If the view has a descriptive suffix, eg '*** List', '*** Instance'
-        if getattr(view, '_suffix', None):
-            name += view._suffix
-
-    # Otherwise if it's a function view use the function's name
-    elif getattr(view, '__name__', None) is not None:
-        name = view.__name__
-
-    # If it's a view class with no resource then grok the name from the class name
-    elif getattr(view, '__class__', None) is not None:
-        name = view.__class__.__name__
-
-        # Chomp of any non-descriptive trailing part of the view class name
-        if name.endswith('View') and name != 'View':
-            name = name[:-len('View')]
-
-    # I ain't got nuthin fo' ya
-    else:
-        return ''
-
-    return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
-
-
-def get_description(view):
-    """
-    Provide a description for the view.
-
-    By default this is the view's docstring with nice unindention applied.
-    """
-
-    # If we're looking up the name of a view callable, as found by reverse,
-    # grok the class instance that we stored when as_view was called.
-    if getattr(view, 'cls_instance', None):
-        view = view.cls_instance
-
-    # If this view has a resource that's been overridden, then use the resource's doctring
-    if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
-        doc = view.resource.__doc__
-
-    # Otherwise use the view doctring
-    elif getattr(view, '__doc__', None):
-        doc = view.__doc__
-
-    # I ain't got nuthin fo' ya
-    else:
-        return ''
-
-    if not doc:
-        return ''
-
-    whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
-
-    # unindent the docstring if needed
-    if whitespace_counts:
-        whitespace_pattern = '^' + (' ' * min(whitespace_counts))
-        return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
-
-    # otherwise return it as-is
-    return doc
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 9f53868b3..9dd84bf2a 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -5,15 +5,17 @@ be subclassing in your implementation.
 By setting or modifying class attributes on your view, you change it's predefined behaviour.
 """
 
+import re
 from django.core.urlresolvers import set_script_prefix, get_script_prefix
 from django.http import HttpResponse
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
 from django.views.decorators.csrf import csrf_exempt
 
-from djangorestframework.compat import View as DjangoView
+from djangorestframework.compat import View as DjangoView, apply_markdown
 from djangorestframework.response import Response, ErrorResponse
 from djangorestframework.mixins import *
 from djangorestframework import resources, renderers, parsers, authentication, permissions, status
-from djangorestframework.utils.description import get_name, get_description
 
 
 __all__ = (
@@ -25,6 +27,7 @@ __all__ = (
 )
 
 
+
 class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
     """
     Handles incoming requests and maps them to REST operations.
@@ -47,13 +50,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
     List of parsers the resource can parse the request with.
     """
 
-    authentication = (authentication.UserLoggedInAuthentication,
-                      authentication.BasicAuthentication)
+    authentication = ( authentication.UserLoggedInAuthentication,
+                       authentication.BasicAuthentication )
     """
     List of all authenticating methods to attempt.
     """
 
-    permissions = (permissions.FullAnonAccess,)
+    permissions = ( permissions.FullAnonAccess, )
     """
     List of all permissions that must be checked.
     """
@@ -76,6 +79,59 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
         """
         return [method.upper() for method in self.http_method_names if hasattr(self, method)]
 
+    def get_name(self):
+        """
+        Return the resource or view class name for use as this view's name.
+        Override to customize.
+        """
+        # If this view has a resource that's been overridden, then use that resource for the name
+        if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource):
+            name = self.resource.__name__
+
+            # Chomp of any non-descriptive trailing part of the resource class name
+            if name.endswith('Resource') and name != 'Resource':
+                name = name[:-len('Resource')]
+
+            # If the view has a descriptive suffix, eg '*** List', '*** Instance'
+            if getattr(self, '_suffix', None):
+                name += self._suffix
+        # If it's a view class with no resource then grok the name from the class name
+        elif getattr(self, '__class__', None) is not None:
+            name = self.__class__.__name__
+
+            # Chomp of any non-descriptive trailing part of the view class name
+            if name.endswith('View') and name != 'View':
+                name = name[:-len('View')]
+        else:
+            name = ''
+        return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
+
+    def get_description(self, html=False):
+        """
+        Return the resource or view docstring for use as this view's description.
+        Override to customize.
+        """
+        # If this view has a resource that's been overridden, then use the resource's doctring
+        if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource):
+            doc = self.resource.__doc__
+        # Otherwise use the view doctring
+        elif getattr(self, '__doc__', None):
+            doc = self.__doc__
+        else:
+            doc = ''
+        whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
+        # unindent the docstring if needed
+        if whitespace_counts:
+            whitespace_pattern = '^' + (' ' * min(whitespace_counts))
+            doc = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
+        if doc and html:
+            if apply_markdown:
+                doc = apply_markdown(doc)
+            else:
+                doc = escape(doc)
+                doc = mark_safe(doc.replace('\n', '
')) + return doc + def http_method_not_allowed(self, request, *args, **kwargs): """ Return an HTTP 405 error if an operation is called which does not have a handler method. @@ -161,8 +217,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def options(self, request, *args, **kwargs): response_obj = { - 'name': get_name(self), - 'description': get_description(self), + 'name': self.get_name(), + 'description': self.get_description(), 'renders': self._rendered_media_types, 'parses': self._parsed_media_types, } @@ -184,21 +240,18 @@ class ModelView(View): """ resource = resources.ModelResource - class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ _suffix = 'Instance' - class ListModelView(ListModelMixin, ModelView): """ A view which provides default operations for list, against a model in the database. """ _suffix = 'List' - class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): """ A view which provides default operations for list and create, against a model in the database. From 049d417ebc2c88b6aeed7946efdb3baeab9a4432 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 24 Jan 2012 14:11:10 -0500 Subject: [PATCH 2/3] Reverted formatting --- djangorestframework/renderers.py | 18 +++++++++--------- djangorestframework/templates/renderer.html | 11 ++++++++--- djangorestframework/views.py | 10 ++++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 5338c3835..bb0f789aa 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -225,7 +225,6 @@ class DocumentingTemplateRenderer(BaseRenderer): return content - def _get_form_instance(self, view, method): """ Get a form, possibly bound to either the input or output data. @@ -261,7 +260,6 @@ class DocumentingTemplateRenderer(BaseRenderer): return form_instance - def _get_generic_content_form(self, view): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms @@ -340,7 +338,7 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, # TODO: remove 'response': self.view.response, 'description': description, 'name': name, @@ -402,12 +400,14 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): template = 'renderer.txt' -DEFAULT_RENDERERS = ( JSONRenderer, - JSONPRenderer, - DocumentingHTMLRenderer, - DocumentingXHTMLRenderer, - DocumentingPlainTextRenderer, - XMLRenderer ) +DEFAULT_RENDERERS = ( + JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer +) if YAMLRenderer: DEFAULT_RENDERERS += (YAMLRenderer,) diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 3b33a6ff3..ff761023f 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -11,9 +11,14 @@ /* Custom styles */ .version{font-size:8px;} - - - Django REST framework - {{ name }} + {% if ADMIN_MEDIA_PREFIX %} + + + {% else %} + + + {% endif %} + Django REST framework - {{ name }}
diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 9dd84bf2a..d89c2afd9 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -27,7 +27,6 @@ __all__ = ( ) - class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Handles incoming requests and maps them to REST operations. @@ -50,13 +49,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): List of parsers the resource can parse the request with. """ - authentication = ( authentication.UserLoggedInAuthentication, - authentication.BasicAuthentication ) + authentication = (authentication.UserLoggedInAuthentication, + authentication.BasicAuthentication) """ List of all authenticating methods to attempt. """ - permissions = ( permissions.FullAnonAccess, ) + permissions = (permissions.FullAnonAccess,) """ List of all permissions that must be checked. """ @@ -240,18 +239,21 @@ class ModelView(View): """ resource = resources.ModelResource + class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ _suffix = 'Instance' + class ListModelView(ListModelMixin, ModelView): """ A view which provides default operations for list, against a model in the database. """ _suffix = 'List' + class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): """ A view which provides default operations for list and create, against a model in the database. From d3ec860dd1fedf27042b3d08bf6c7e4f4adbddc9 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 24 Jan 2012 14:36:34 -0500 Subject: [PATCH 3/3] Modified test case to pass regardless of JSON library --- djangorestframework/tests/renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index adb46f7fa..77b5430fa 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,3 +1,5 @@ +import re + from django.conf.urls.defaults import patterns, url from django.test import TestCase @@ -187,6 +189,8 @@ class JSONRendererTests(TestCase): obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json') + # Fix failing test case which depends on version of JSON library. + content = re.sub(' +\n', '\n', content) self.assertEquals(content, _flat_repr) def test_with_content_type_args(self):