name and description

This commit is contained in:
Tom Christie 2011-05-23 17:07:31 +01:00
parent e7f8c06dbb
commit c531759147
6 changed files with 126 additions and 48 deletions

View File

@ -2,6 +2,7 @@ syntax: glob
*.pyc
*.db
assetplatform.egg-info/*
*~
coverage.xml
env

View File

@ -467,12 +467,13 @@ class InstanceMixin(object):
Store the callable object on the resource class that has been associated with this view.
"""
view = super(InstanceMixin, cls).as_view(**initkwargs)
if 'resource' in initkwargs:
resource = getattr(cls(**initkwargs), 'resource', None)
if resource:
# We do a little dance when we store the view callable...
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
# as a function when we later look it up (rather than turning it into a method).
# This makes sure our URL reversing works ok.
initkwargs['resource'].view_callable = (view,)
resource.view_callable = (view,)
return view

View File

@ -37,14 +37,15 @@ class TestViewNamesAndDescriptions(TestCase):
"""Ensure Resource names are based on the classname by default."""
class MockView(BaseView):
pass
self.assertEquals(get_name(MockView()), 'Mock View')
self.assertEquals(get_name(MockView()), 'Mock')
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(BaseView):
name = example
self.assertEquals(get_name(MockView()), example)
# 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(BaseView):
# name = example
# self.assertEquals(get_name(MockView()), example)
def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default."""
@ -66,20 +67,21 @@ class TestViewNamesAndDescriptions(TestCase):
self.assertEquals(get_description(MockView()), DESCRIPTION)
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(BaseView):
"""docstring"""
description = example
self.assertEquals(get_description(MockView()), example)
# 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(BaseView):
# """docstring"""
# 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 'description' class attribute."""
example = 'Some other description'
class MockView(BaseView):
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 'description' class attribute."""
# example = 'Some other description'
# class MockView(BaseView):
# description = example
# self.assertEquals(get_description(MockView()), 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"""

View File

@ -1,37 +1,91 @@
"""Get a descriptive name and description for a view,
based on class name and docstring, and override-able by 'name' and 'description' attributes"""
"""
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.
"""
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 getattr(view, 'name', None) is not None:
return view.name
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
"""
if getattr(view, '__name__', None) is not None:
# 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__
elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
# 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.
"""
Provide a description for the view.
By default this is the view's docstring with nice unindention applied."""
if getattr(view, 'description', None) is not None:
return getattr(view, 'description')
By default this is the view's docstring with nice unindention applied.
"""
if getattr(view, '__doc__', None) is not None:
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
# 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 whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
return view.__doc__
# 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
return ''

View File

@ -51,6 +51,18 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View):
name = None
description = None
@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.
"""
view = super(BaseView, cls).as_view(**initkwargs)
view.cls_instance = cls(**initkwargs)
return view
@property
def allowed_methods(self):
"""
@ -122,12 +134,12 @@ class ModelView(BaseView):
class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
"""A view which provides default operations for read/update/delete against a model instance."""
pass
_suffix = 'Instance'
class ListModelView(ListModelMixin, ModelView):
"""A view which provides default operations for list, against a model in the database."""
pass
"""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."""
pass
"""A view which provides default operations for list and create, against a model in the database."""
_suffix = 'List'

View File

@ -6,7 +6,11 @@ from djangorestframework.resources import ModelResource
from blogpost.models import BlogPost, Comment
class BlogPostResource(ModelResource):
"""
A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
"""
model = BlogPost
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
ordering = ('-created',)
@ -14,7 +18,11 @@ class BlogPostResource(ModelResource):
def comments(self, instance):
return reverse('comments', kwargs={'blogpost': instance.key})
class CommentResource(ModelResource):
"""
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
"""
model = Comment
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
ordering = ('-created',)
@ -25,4 +33,4 @@ urlpatterns = patterns('',
url(r'^(?P<key>[^/]+)/$', InstanceModelView.as_view(resource=BlogPostResource)),
url(r'^(?P<blogpost>[^/]+)/comments/$', ListOrCreateModelView.as_view(resource=CommentResource), name='comments'),
url(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', InstanceModelView.as_view(resource=CommentResource)),
)
)