Added directives and autodocumenters for template tags and filters in a custom extens...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 18 May 2011 21:15:31 +0000 (17:15 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 18 May 2011 21:15:31 +0000 (17:15 -0400)
15 files changed:
docs/_ext/djangodocs.py
docs/_ext/philodocs.py [new file with mode: 0644]
docs/conf.py
docs/contrib/penfield.rst
docs/contrib/shipherd.rst
docs/index.rst
docs/templatetags.rst
philo/contrib/penfield/templatetags/penfield.py
philo/contrib/shipherd/models.py
philo/contrib/shipherd/templatetags/shipherd.py
philo/templatetags/collections.py
philo/templatetags/containers.py
philo/templatetags/embed.py
philo/templatetags/include_string.py
philo/templatetags/nodes.py

index 7710786..0d433de 100644 (file)
@@ -32,16 +32,16 @@ def setup(app):
         rolename      = "setting",
         indextemplate = "pair: %s; setting",
     )
-    app.add_crossref_type(
-        directivename = "templatetag",
-        rolename      = "ttag",
-        indextemplate = "pair: %s; template tag"
-    )
-    app.add_crossref_type(
-        directivename = "templatefilter",
-        rolename      = "tfilter",
-        indextemplate = "pair: %s; template filter"
-    )
+    #app.add_crossref_type(
+    #    directivename = "templatetag",
+    #    rolename      = "ttag",
+    #    indextemplate = "pair: %s; template tag"
+    #)
+    #app.add_crossref_type(
+    #    directivename = "templatefilter",
+    #    rolename      = "tfilter",
+    #    indextemplate = "pair: %s; template filter"
+    #)
     app.add_crossref_type(
         directivename = "fieldlookup",
         rolename      = "lookup",
diff --git a/docs/_ext/philodocs.py b/docs/_ext/philodocs.py
new file mode 100644 (file)
index 0000000..6c1ecf7
--- /dev/null
@@ -0,0 +1,56 @@
+import inspect
+
+from sphinx.addnodes import desc_addname
+from sphinx.domains.python import PyModulelevel, PyXRefRole
+from sphinx.ext import autodoc
+
+
+DOMAIN = 'py'
+
+
+class TemplateTag(PyModulelevel):
+       indextemplate = "pair: %s; template tag"
+       
+       def get_signature_prefix(self, sig):
+               return self.objtype + ' '
+       
+       def handle_signature(self, sig, signode):
+               fullname, name_prefix = PyModulelevel.handle_signature(self, sig, signode)
+               
+               for i, node in enumerate(signode):
+                       if isinstance(node, desc_addname):
+                               lib = '.'.join(node[0].split('.')[-2:])
+                               new_node = desc_addname(lib, lib)
+                               signode[i] = new_node
+               
+               return fullname, name_prefix
+
+
+class TemplateTagDocumenter(autodoc.FunctionDocumenter):
+       objtype = 'templatetag'
+       domain = DOMAIN
+       
+       @classmethod
+       def can_document_member(cls, member, membername, isattr, parent):
+               # Only document explicitly.
+               return False
+       
+       def format_args(self):
+               return None
+
+class TemplateFilterDocumenter(autodoc.FunctionDocumenter):
+       objtype = 'templatefilter'
+       domain = DOMAIN
+       
+       @classmethod
+       def can_document_member(cls, member, membername, isattr, parent):
+               # Only document explicitly.
+               return False
+
+def setup(app):
+       app.add_directive_to_domain(DOMAIN, 'templatetag', TemplateTag)
+       app.add_role_to_domain(DOMAIN, 'ttag', PyXRefRole())
+       app.add_directive_to_domain(DOMAIN, 'templatefilter', TemplateTag)
+       app.add_role_to_domain(DOMAIN, 'tfilter', PyXRefRole())
+       app.add_autodocumenter(TemplateTagDocumenter)
+       app.add_autodocumenter(TemplateFilterDocumenter)
\ No newline at end of file
index cad16d6..b4b1e16 100644 (file)
@@ -31,7 +31,7 @@ needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['djangodocs', 'sphinx.ext.autodoc']
+extensions = ['djangodocs', 'sphinx.ext.autodoc', 'philodocs']
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -233,3 +233,4 @@ def skip_attribute_attrs(app, what, name, obj, skip, options):
 
 def setup(app):
        app.connect('autodoc-skip-member', skip_attribute_attrs)
+       #app.connect('autodoc-process-signature', )
index 2c277ee..d774dcb 100644 (file)
@@ -43,3 +43,7 @@ Template filters
 ++++++++++++++++
 
 .. automodule:: philo.contrib.penfield.templatetags.penfield
+
+.. autotemplatefilter:: monthname
+
+.. autotemplatefilter:: apmonthname
index 5a1848d..0f3b59d 100644 (file)
@@ -3,3 +3,28 @@ Shipherd
 
 .. automodule:: philo.contrib.shipherd
        :members:
+
+Models
+++++++
+
+.. automodule:: philo.contrib.shipherd.models
+       :members: Navigation, NavigationItem, NavigationMapper
+
+Navigation caching
+------------------
+
+.. autoclass:: NavigationManager
+       :members:
+
+.. autoclass:: NavigationItemManager
+       :members:
+
+.. autoclass:: NavigationCacheQuerySet
+       :members:
+
+Template tags
++++++++++++++
+
+.. automodule:: philo.contrib.shipherd.templatetags.shipherd
+
+.. autotemplatetag:: recursenavigation
index 83d1118..d387fa8 100644 (file)
@@ -3,6 +3,8 @@
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.
 
+.. module:: philo
+
 Welcome to Philo's documentation!
 =================================
 
index 97f0c56..41d30d5 100644 (file)
@@ -7,7 +7,8 @@ Collections
 +++++++++++
 
 .. automodule:: philo.templatetags.collections
-
+       
+.. autotemplatetag:: membersof
 
 Containers
 ++++++++++
@@ -15,17 +16,27 @@ Containers
 .. automodule:: philo.templatetags.containers
 
 
+.. autotemplatetag:: container
+
+
 Embedding
 +++++++++
 
 .. automodule:: philo.templatetags.embed
 
+.. autotemplatetag:: embed
+
+
 Nodes
 +++++
 
 .. automodule:: philo.templatetags.nodes
 
+.. autotemplatetag:: node_url
+
 String inclusion
 ++++++++++++++++
 
 .. automodule:: philo.templatetags.include_string
+
+.. autotemplatetag:: include_string
index 7b9d946..b263a2b 100644 (file)
@@ -1,25 +1,17 @@
 """
-Penfield supplies two template filters:
-
-.. templatefilter:: monthname
-
-monthname
----------
-Returns the name of a month with the supplied numeric value.
-
-.. templatefilter:: apmonthname
-
-apmonthname
------------
-Returns the Associated Press abbreviated month name for the supplied numeric value.
+Penfield supplies two template filters to handle common use cases for blogs and newsletters.
 
 """
 from django import template
 from django.utils.dates import MONTHS, MONTHS_AP
 
+
 register = template.Library()
 
+
+@register.filter
 def monthname(value):
+       """Returns the name of a month with the supplied numeric value."""
        try:
                value = int(value)
        except:
@@ -30,9 +22,10 @@ def monthname(value):
        except KeyError:
                return value
 
-register.filter('monthname', monthname)
 
+@register.filter
 def apmonthname(value):
+       """Returns the Associated Press abbreviated month name for the supplied numeric value."""
        try:
                value = int(value)
        except:
@@ -41,6 +34,4 @@ def apmonthname(value):
        try:
                return MONTHS_AP[value]
        except KeyError:
-               return value
-
-register.filter('apmonthname', apmonthname)
+               return value
\ No newline at end of file
index 7554595..f35be3c 100644 (file)
@@ -14,8 +14,11 @@ from philo.models.nodes import Node, TargetURLModel
 DEFAULT_NAVIGATION_DEPTH = 3
 
 
-class NavigationQuerySetMapper(object, DictMixin):
-       """This class exists to prevent setting of items in the navigation cache through node.navigation."""
+class NavigationMapper(object, DictMixin):
+       """
+       The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. The fetching goes through the :class:`NavigationManager` and can thus take advantage of the navigation cache. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS`
+       
+       """
        def __init__(self, node):
                self.node = node
        
@@ -28,7 +31,7 @@ class NavigationQuerySetMapper(object, DictMixin):
 
 def navigation(self):
        if not hasattr(self, '_navigation'):
-               self._navigation = NavigationQuerySetMapper(self)
+               self._navigation = NavigationMapper(self)
        return self._navigation
 
 
@@ -40,6 +43,7 @@ class NavigationCacheQuerySet(models.query.QuerySet):
        This subclass will trigger general cache clearing for Navigation.objects when a mass
        update or deletion is performed. As there is no convenient way to iterate over the
        changed or deleted instances, there's no way to be more precise about what gets cleared.
+       
        """
        def update(self, *args, **kwargs):
                super(NavigationCacheQuerySet, self).update(*args, **kwargs)
@@ -51,15 +55,22 @@ class NavigationCacheQuerySet(models.query.QuerySet):
 
 
 class NavigationManager(models.Manager):
-       # Since navigation is going to be hit frequently and changed
-       # relatively infrequently, cache it. Analogous to contenttypes.
+       """
+       Since navigation on a site will be hit frequently, is relatively costly to compute, and is changed relatively infrequently, the NavigationManager maintains a cache which maps nodes to navigations.
+       
+       """
        use_for_related = True
        _cache = {}
        
-       def get_queryset(self):
+       def get_query_set(self):
+               """
+               Returns a :class:`NavigationCacheQuerySet` instance.
+               
+               """
                return NavigationCacheQuerySet(self.model, using=self._db)
        
        def get_cache_for(self, node, update_targets=True):
+               """Returns the navigation cache for a given :class:`.Node`. If update_targets is ``True``, then :meth:`update_targets_for` will be run with the :class:`.Node`."""
                created = False
                if not self.has_cache_for(node):
                        self.create_cache_for(node)
@@ -71,10 +82,11 @@ class NavigationManager(models.Manager):
                return self.__class__._cache[self.db][node]
        
        def has_cache_for(self, node):
+               """Returns ``True`` if a cache exists for the :class:`.Node` and ``False`` otherwise."""
                return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
        
        def create_cache_for(self, node):
-               "This method loops through the nodes ancestors and caches all unique navigation keys."
+               """This method loops through the :class:`.Node`\ s ancestors and caches all unique navigation keys."""
                ancestors = node.get_ancestors(ascending=True, include_self=True)
                
                nodes_to_cache = []
@@ -130,10 +142,7 @@ class NavigationManager(models.Manager):
                return cache
        
        def clear_cache_for(self, node):
-               # Clear the cache for this node and all its descendants. The
-               # navigation for this node has probably changed, and for now,
-               # it isn't worth it to only clear the descendants actually
-               # affected by this.
+               """Clear the cache for the :class:`.Node` and all its descendants. The navigation for this node has probably changed, and it isn't worth it to figure out which descendants were actually affected by this."""
                if not self.has_cache_for(node):
                        # Already cleared.
                        return
@@ -144,10 +153,7 @@ class NavigationManager(models.Manager):
                        cache.pop(node, None)
        
        def update_targets_for(self, node):
-               # Manually update a cache's target nodes in case something's changed there.
-               # This should be a less complex operation than reloading the models each
-               # time. Not as good as selective updates... but not much to be done
-               # about that. TODO: Benchmark it.
+               """Manually updates the target nodes for the :class:`.Node`'s cache in case something's changed there. This is a less complex operation than rebuilding the :class:`.Node`'s cache."""
                caches = self.__class__._cache[self.db][node].values()
                
                target_pks = set()
@@ -165,14 +171,23 @@ class NavigationManager(models.Manager):
                                        item.target_node = targets[targets.index(item.target_node)]
        
        def clear_cache(self):
+               """Clears the manager's entire navigation cache."""
                self.__class__._cache.pop(self.db, None)
 
 
 class Navigation(Entity):
+       """
+       :class:`Navigation` represents a group of :class:`NavigationItem`\ s that have an intrinsic relationship in terms of navigating a website. For example, a ``main`` navigation versus a ``side`` navigation, or a ``authenticated`` navigation versus an ``anonymous`` navigation.
+       
+       """
+       #: A :class:`NavigationManager` instance.
        objects = NavigationManager()
        
+       #: The :class:`.Node` which the :class:`Navigation` is attached to. The :class:`Navigation` will also be available to all the :class:`.Node`'s descendants and will override any :class:`Navigation` with the same key on any of the :class:`.Node`'s ancestors.
        node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
+       #: Each :class:`Navigation` has a ``key`` which consists of one or more word characters so that it can easily be accessed in a template as ``{{ node.navigation.this_key }}``.
        key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
+       #: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed.
        depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
        
        def __init__(self, *args, **kwargs):
@@ -203,16 +218,21 @@ class Navigation(Entity):
 class NavigationItemManager(TreeManager):
        use_for_related = True
        
-       def get_queryset(self):
+       def get_query_set(self):
+               """Returns a :class:`NavigationCacheQuerySet` instance."""
                return NavigationCacheQuerySet(self.model, using=self._db)
 
 
 class NavigationItem(TreeEntity, TargetURLModel):
+       #: A :class:`NavigationItemManager` instance
        objects = NavigationItemManager()
        
+       #: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance.
        navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
+       #: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50.
        text = models.CharField(max_length=50)
        
+       #: The order in which the :class:`NavigationItem` will be displayed.
        order = models.PositiveSmallIntegerField(default=0)
        
        def __init__(self, *args, **kwargs):
@@ -229,6 +249,7 @@ class NavigationItem(TreeEntity, TargetURLModel):
                        raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
        
        def is_active(self, request):
+               """Returns ``True`` if the :class:`NavigationItem` is considered active for a given request and ``False`` otherwise."""
                if self.target_url == request.path:
                        # Handle the `default` case where the target_url and requested path
                        # are identical.
@@ -255,6 +276,7 @@ class NavigationItem(TreeEntity, TargetURLModel):
                return False
        
        def has_active_descendants(self, request):
+               """Returns ``True`` if the :class:`NavigationItem` has active descendants and ``False`` otherwise."""
                for child in self.get_children():
                        if child.is_active(request) or child.has_active_descendants(request):
                                return True
index c8ba4fd..508eace 100644 (file)
@@ -101,12 +101,13 @@ class RecurseNavigationNode(template.Node):
 @register.tag
 def recursenavigation(parser, token):
        """
-       The recursenavigation templatetag takes two arguments:
-       - the node for which the navigation should be found
-       - the navigation's key.
+       The :ttag:`recursenavigation` templatetag takes two arguments:
        
-       It will then recursively loop over each item in the navigation and render the template
-       chunk within the block. recursenavigation sets the following variables in the context:
+       * the :class:`.Node` for which the :class:`.Navigation` should be found
+       * the :class:`.Navigation`'s :attr:`~.Navigation.key`.
+       
+       It will then recursively loop over each :class:`.NavigationItem` in the :class:`.Navigation` and render the template
+       chunk within the block. :ttag:`recursenavigation` sets the following variables in the context:
        
                ==============================  ================================================
                Variable                        Description
@@ -118,25 +119,26 @@ def recursenavigation(parser, token):
                ``navloop.first``               True if this is the first time through the current level
                ``navloop.last``                True if this is the last time through the current level
                ``navloop.parentloop``          This is the loop one level "above" the current one
-               ==============================  ================================================
-               ``item``                        The current item in the loop (a NavigationItem instance)
+               
+               ``item``                        The current item in the loop (a :class:`.NavigationItem` instance)
                ``children``                    If accessed, performs the next level of recursion.
                ``navloop.active``              True if the item is active for this request
                ``navloop.active_descendants``  True if the item has active descendants for this request
                ==============================  ================================================
        
-       Example:
+       Example::
+       
                <ul>
-                       {% recursenavigation node "main" %}
-                               <li{% if navloop.active %} class='active'{% endif %}>
-                                       {{ navloop.item.text }}
-                                       {% if item.get_children %}
-                                               <ul>
-                                                       {{ children }}
-                                               </ul>
-                                       {% endif %}
-                               </li>
-                       {% endrecursenavigation %}
+                   {% recursenavigation node "main" %}
+                       <li{% if navloop.active %} class='active'{% endif %}>
+                           {{ navloop.item.text }}
+                           {% if item.get_children %}
+                               <ul>
+                                   {{ children }}
+                               </ul>
+                           {% endif %}
+                       </li>
+                   {% endrecursenavigation %}
                </ul>
        """
        bits = token.contents.split()
index 62d6138..414a742 100644 (file)
@@ -1,17 +1,6 @@
 """
 The collection template tags are automatically included as builtins if :mod:`philo` is an installed app.
 
-.. templatetag:: membersof
-
-membersof
----------
-
-Given a collection and a content type, sets the results of :meth:`collection.members.with_model <.CollectionMemberManager.with_model>` as a variable in the context.
-
-Usage::
-
-       {% membersof <collection> with <app_label>.<model_name> as <var> %}
-
 """
 
 from django import template
@@ -37,9 +26,14 @@ class MembersofNode(template.Node):
                return ''
 
 
-def do_membersof(parser, token):
+@register.tag
+def membersof(parser, token):
        """
-       {% membersof <collection> with <app_label>.<model_name> as <var> %}
+       Given a collection and a content type, sets the results of :meth:`collection.members.with_model <.CollectionMemberManager.with_model>` as a variable in the context.
+       
+       Usage::
+       
+               {% membersof <collection> with <app_label>.<model_name> as <var> %}
        
        """
        params=token.split_contents()
@@ -62,7 +56,4 @@ def do_membersof(parser, token):
        if params[4] != 'as':
                raise template.TemplateSyntaxError('"%s" template tag requires the fifth parameter to be "as"' % tag)
        
-       return MembersofNode(collection=params[1], model=ct.model_class(), as_var=params[5])
-
-
-register.tag('membersof', do_membersof)
\ No newline at end of file
+       return MembersofNode(collection=params[1], model=ct.model_class(), as_var=params[5])
\ No newline at end of file
index 722f2d8..e280e60 100644 (file)
@@ -1,17 +1,6 @@
 """
 The container template tags are automatically included as builtins if :mod:`philo` is an installed app.
 
-.. templatetag:: container
-
-container
----------
-
-If a template using this tag is used to render a :class:`.Page`, that :class:`.Page` will have associated content which can be set in the admin interface. If a content type is referenced, then a :class:`.ContentReference` object will be created; otherwise, a :class:`.Contentlet` object will be created.
-
-Usage::
-
-       {% container <name> [[references <app_label>.<model_name>] as <variable>] %}
-
 """
 
 from django import template
@@ -75,9 +64,14 @@ class ContainerNode(template.Node):
                return content
 
 
-def do_container(parser, token):
+@register.tag
+def container(parser, token):
        """
-       {% container <name> [[references <app_label>.<model_name>] as <variable>] %}
+       If a template using this tag is used to render a :class:`.Page`, that :class:`.Page` will have associated content which can be set in the admin interface. If a content type is referenced, then a :class:`.ContentReference` object will be created; otherwise, a :class:`.Contentlet` object will be created.
+       
+       Usage::
+       
+               {% container <name> [[references <app_label>.<model_name>] as <variable>] %}
        
        """
        params = token.split_contents()
@@ -111,6 +105,3 @@ def do_container(parser, token):
                
        else: # error
                raise template.TemplateSyntaxError('"%s" template tag provided without arguments (at least one required)' % tag)
-
-
-register.tag('container', do_container)
index 20e04a4..9599240 100644 (file)
@@ -1,25 +1,6 @@
 """
 The embed template tags are automatically included as builtins if :mod:`philo` is an installed app.
 
-.. templatetag:: embed
-
-embed
------
-
-The {% embed %} tag can be used in two ways.
-
-First, to set which template will be used to render a particular model. This declaration can be placed in a base template and will propagate into all templates that extend that template.
-
-Syntax::
-
-       {% embed <app_label>.<model_name> with <template> %}
-
-Second, to embed a specific model instance in the document with a template specified earlier in the template or in a parent template using the first syntax. The instance can be specified as a content type and pk or as a context variable. Any kwargs provided will be passed into the context of the template.
-
-Syntax::
-
-       {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %}
-
 """
 from django import template
 from django.conf import settings
@@ -310,10 +291,22 @@ def parse_content_type(bit, tagname):
        return ct
 
 
-def do_embed(parser, token):
+@register.tag
+def embed(parser, token):
        """
-       {% embed <app_label>.<model_name> with <template> %}
-       {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %}
+       The {% embed %} tag can be used in two ways.
+       
+       First, to set which template will be used to render a particular model. This declaration can be placed in a base template and will propagate into all templates that extend that template.
+       
+       Syntax::
+       
+               {% embed <app_label>.<model_name> with <template> %}
+       
+       Second, to embed a specific model instance in the document with a template specified earlier in the template or in a parent template using the first syntax. The instance can be specified as a content type and pk or as a context variable. Any kwargs provided will be passed into the context of the template.
+       
+       Syntax::
+       
+               {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %}
        
        """
        bits = token.split_contents()
@@ -354,7 +347,4 @@ def do_embed(parser, token):
        except ValueError:
                return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
        else:
-               return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
-
-
-register.tag('embed', do_embed)
\ No newline at end of file
+               return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
\ No newline at end of file
index ecf9b45..cb0a8b5 100644 (file)
@@ -1,17 +1,3 @@
-"""
-.. templatetag:: include_string
-
-include_string
---------------
-
-Include a flat string by interpreting it as a template. The compiled template will be rendered with the current context.
-
-Usage::
-
-       {% include_string <template_code> %}
-
-"""
-
 from django import template
 from django.conf import settings
 
@@ -35,12 +21,18 @@ class IncludeStringNode(template.Node):
                        return settings.TEMPLATE_STRING_IF_INVALID
 
 
-def do_include_string(parser, token):
+@register.tag
+def include_string(parser, token):
+       """
+       Include a flat string by interpreting it as a template. The compiled template will be rendered with the current context.
+       
+       Usage::
+       
+               {% include_string <template_code> %}
+       
+       """
        bits = token.split_contents()
        if len(bits) != 2:
                raise TemplateSyntaxError("%r tag takes one argument: the template string to be included" % bits[0])
        string = parser.compile_filter(bits[1])
-       return IncludeStringNode(string)
-
-
-register.tag('include_string', do_include_string)
\ No newline at end of file
+       return IncludeStringNode(string)
\ No newline at end of file
index 1d095bc..189fdd5 100644 (file)
@@ -1,20 +1,6 @@
 """
 The node template tags are automatically included as builtins if :mod:`philo` is an installed app.
 
-.. templatetag:: node_url
-
-node_url
---------
-
-The :ttag:`node_url` tag allows access to :meth:`.View.reverse` from a template for a :class:`.Node`. By default, the :class:`.Node` that is used for the call is pulled from the context variable ``node``; however, this can be overridden with the ``[for <node>]`` option.
-
-Usage::
-
-       {% node_url [for <node>] [as <var>] %}
-       {% node_url with <obj> [for <node>] [as <var>] %}
-       {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
-       {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>] %}
-
 """
 
 from django import template
@@ -84,13 +70,17 @@ class NodeURLNode(template.Node):
                        return url
 
 
-@register.tag(name='node_url')
-def do_node_url(parser, token):
+@register.tag
+def node_url(parser, token):
        """
-       {% node_url [for <node>] [as <var>] %}
-       {% node_url with <obj> [for <node>] [as <var>] %}
-       {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
-       {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>] %}
+       The :ttag:`node_url` tag allows access to :meth:`.View.reverse` from a template for a :class:`.Node`. By default, the :class:`.Node` that is used for the call is pulled from the context variable ``node``; however, this can be overridden with the ``[for <node>]`` option.
+       
+       Usage::
+       
+               {% node_url [for <node>] [as <var>] %}
+               {% node_url with <obj> [for <node>] [as <var>] %}
+               {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
+               {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>] %}
        
        """
        params = token.split_contents()