Merge remote-tracking branch 'btimby/description'

This commit is contained in:
Tom Christie 2012-01-25 19:53:04 +00:00
commit f5e54c7c32
6 changed files with 106 additions and 136 deletions

View File

@ -12,10 +12,9 @@ from django.template import RequestContext, loader
from django.utils import simplejson as json 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 import dict2xml, url_resolves
from djangorestframework.utils.breadcrumbs import get_breadcrumbs 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.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from djangorestframework import VERSION from djangorestframework import VERSION
@ -296,6 +295,20 @@ class DocumentingTemplateRenderer(BaseRenderer):
# Okey doke, let's do it # Okey doke, let's do it
return GenericContentForm(view) 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): def render(self, obj=None, media_type=None):
""" """
Renders *obj* using the :attr:`template` set on the class. Renders *obj* using the :attr:`template` set on the class.
@ -316,15 +329,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
login_url = None login_url = None
logout_url = None logout_url = None
name = get_name(self.view) name = self.get_name()
description = get_description(self.view) description = self.get_description()
markeddown = None
if apply_markdown:
try:
markeddown = apply_markdown(description)
except AttributeError:
markeddown = None
breadcrumb_list = get_breadcrumbs(self.view.request.path) breadcrumb_list = get_breadcrumbs(self.view.request.path)
@ -337,7 +343,6 @@ class DocumentingTemplateRenderer(BaseRenderer):
'description': description, 'description': description,
'name': name, 'name': name,
'version': VERSION, 'version': VERSION,
'markeddown': markeddown,
'breadcrumblist': breadcrumb_list, 'breadcrumblist': breadcrumb_list,
'available_formats': self.view._rendered_formats, 'available_formats': self.view._rendered_formats,
'put_form': put_form_instance, 'put_form': put_form_instance,

View File

@ -18,7 +18,7 @@
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/> <link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/> <link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/>
{% endif %} {% endif %}
<title>Django REST framework - {{ name }}</title> <title>Django REST framework - {{ name }}</title>
</head> </head>
<body> <body>
<div id="container"> <div id="container">
@ -50,7 +50,7 @@
<div class='content-main'> <div class='content-main'>
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p> <p>{% autoescape off %}{{ description }}{% endautoescape %}</p>
<div class='module'> <div class='module'>
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %} <pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }} {% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}

View File

@ -1,7 +1,6 @@
from django.test import TestCase from django.test import TestCase
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.compat import apply_markdown from djangorestframework.compat import apply_markdown
from djangorestframework.utils.description import get_name, get_description
# We check that docstrings get nicely un-indented. # We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring DESCRIPTION = """an example docstring
@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase):
"""Ensure Resource names are based on the classname by default.""" """Ensure Resource names are based on the classname by default."""
class MockView(View): class MockView(View):
pass 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):
#def test_resource_name_can_be_set_explicitly(self): """Ensure Resource names can be set using the 'get_name' method."""
# """Ensure Resource names can be set using the 'name' class attribute.""" example = 'Some Other Name'
# example = 'Some Other Name' class MockView(View):
# class MockView(View): def get_name(self):
# name = example return example
# self.assertEquals(get_name(MockView()), example) self.assertEquals(MockView().get_name(), example)
def test_resource_description_uses_docstring_by_default(self): def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default.""" """Ensure Resource names are based on the docstring by default."""
@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #""" # 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):
#def test_resource_description_can_be_set_explicitly(self): """Ensure Resource descriptions can be set using the 'get_description' method."""
# """Ensure Resource descriptions can be set using the 'description' class attribute.""" example = 'Some other description'
# example = 'Some other description' class MockView(View):
# class MockView(View): """docstring"""
# """docstring""" def get_description(self):
# description = example return example
# self.assertEquals(get_description(MockView()), example) self.assertEquals(MockView().get_description(), example)
#def test_resource_description_does_not_require_docstring(self): 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.""" """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' example = 'Some other description'
# class MockView(View): class MockView(View):
# description = example def get_description(self):
# self.assertEquals(get_description(MockView()), example) return example
self.assertEquals(MockView().get_description(), example)
def test_resource_description_can_be_empty(self): 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): class MockView(View):
pass pass
self.assertEquals(get_description(MockView()), '') self.assertEquals(MockView().get_description(), '')
def test_markdown(self): def test_markdown(self):
"""Ensure markdown to HTML works as expected""" """Ensure markdown to HTML works as expected"""

View File

@ -1,6 +1,4 @@
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from djangorestframework.utils.description import get_name
def get_breadcrumbs(url): def get_breadcrumbs(url):
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, 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: else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs # Check if this is a REST framework view, and if so add it to the breadcrumbs
if isinstance(getattr(view, 'cls_instance', None), View): 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 == '': if url == '':
# All done # All done

View File

@ -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

View File

@ -5,15 +5,17 @@ be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour. 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.core.urlresolvers import set_script_prefix, get_script_prefix
from django.http import HttpResponse 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 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.response import Response, ErrorResponse
from djangorestframework.mixins import * from djangorestframework.mixins import *
from djangorestframework import resources, renderers, parsers, authentication, permissions, status from djangorestframework import resources, renderers, parsers, authentication, permissions, status
from djangorestframework.utils.description import get_name, get_description
__all__ = ( __all__ = (
@ -48,7 +50,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
authentication = (authentication.UserLoggedInAuthentication, authentication = (authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication) authentication.BasicAuthentication)
""" """
List of all authenticating methods to attempt. List of all authenticating methods to attempt.
""" """
@ -76,6 +78,59 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
return [method.upper() for method in self.http_method_names if hasattr(self, method)] 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', '<br />'))
return doc
def http_method_not_allowed(self, request, *args, **kwargs): 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. Return an HTTP 405 error if an operation is called which does not have a handler method.
@ -161,8 +216,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
def options(self, request, *args, **kwargs): def options(self, request, *args, **kwargs):
response_obj = { response_obj = {
'name': get_name(self), 'name': self.get_name(),
'description': get_description(self), 'description': self.get_description(),
'renders': self._rendered_media_types, 'renders': self._rendered_media_types,
'parses': self._parsed_media_types, 'parses': self._parsed_media_types,
} }