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 *.pyc
*.db *.db
assetplatform.egg-info/*
*~ *~
coverage.xml coverage.xml
env 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. Store the callable object on the resource class that has been associated with this view.
""" """
view = super(InstanceMixin, cls).as_view(**initkwargs) 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 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 # 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). # as a function when we later look it up (rather than turning it into a method).
# This makes sure our URL reversing works ok. # This makes sure our URL reversing works ok.
initkwargs['resource'].view_callable = (view,) resource.view_callable = (view,)
return view return view

View File

@ -37,14 +37,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(BaseView): class MockView(BaseView):
pass pass
self.assertEquals(get_name(MockView()), 'Mock View') self.assertEquals(get_name(MockView()), 'Mock')
def test_resource_name_can_be_set_explicitly(self): # This has been turned off now.
"""Ensure Resource names can be set using the 'name' class attribute.""" #def test_resource_name_can_be_set_explicitly(self):
example = 'Some Other Name' # """Ensure Resource names can be set using the 'name' class attribute."""
class MockView(BaseView): # example = 'Some Other Name'
name = example # class MockView(BaseView):
self.assertEquals(get_name(MockView()), example) # name = example
# self.assertEquals(get_name(MockView()), 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."""
@ -66,20 +67,21 @@ class TestViewNamesAndDescriptions(TestCase):
self.assertEquals(get_description(MockView()), DESCRIPTION) self.assertEquals(get_description(MockView()), DESCRIPTION)
def test_resource_description_can_be_set_explicitly(self): # This has been turned off now
"""Ensure Resource descriptions can be set using the 'description' class attribute.""" #def test_resource_description_can_be_set_explicitly(self):
example = 'Some other description' # """Ensure Resource descriptions can be set using the 'description' class attribute."""
class MockView(BaseView): # example = 'Some other description'
"""docstring""" # class MockView(BaseView):
description = example # """docstring"""
self.assertEquals(get_description(MockView()), example) # description = example
# self.assertEquals(get_description(MockView()), 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 'description' class attribute."""
example = 'Some other description' # example = 'Some other description'
class MockView(BaseView): # class MockView(BaseView):
description = example # description = example
self.assertEquals(get_description(MockView()), example) # self.assertEquals(get_description(MockView()), 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"""

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 import re
from djangorestframework.resources import Resource, FormResource, ModelResource
# These a a bit Grungy, but they do the job.
def get_name(view): 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 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 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__ 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__ 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: else:
return '' return ''
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip() return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
def get_description(view): 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.""" By default this is the view's docstring with nice unindention applied.
if getattr(view, 'description', None) is not None: """
return getattr(view, 'description')
if getattr(view, '__doc__', None) is not None: # If we're looking up the name of a view callable, as found by reverse,
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()] # 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: # If this view has a resource that's been overridden, then use the resource's doctring
whitespace_pattern = '^' + (' ' * min(whitespace_counts)) if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__) doc = view.resource.__doc__
return view.__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 name = None
description = 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 @property
def allowed_methods(self): def allowed_methods(self):
""" """
@ -122,12 +134,12 @@ class ModelView(BaseView):
class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
"""A view which provides default operations for read/update/delete against a model instance.""" """A view which provides default operations for read/update/delete against a model instance."""
pass _suffix = 'Instance'
class ListModelView(ListModelMixin, ModelView): class ListModelView(ListModelMixin, ModelView):
"""A view which provides default operations for list, against a model in the database.""" """A view which provides default operations for list, against a model in the database."""
pass _suffix = 'List'
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
"""A view which provides default operations for list and create, against a model in the database.""" """A view which provides default operations for list and create, against a model in the database."""
pass _suffix = 'List'

View File

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