From 0959d38b27c03863ec376839dcc6d896d04e36ea Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Wed, 18 May 2011 17:15:31 -0400 Subject: [PATCH] Added directives and autodocumenters for template tags and filters in a custom extension. Switched previous template tag/filter docs to use the new directives. Renamed NavigationManager.get_queryset method to the correct get_query_set. Documented shipherd. --- docs/_ext/djangodocs.py | 20 +++---- docs/_ext/philodocs.py | 56 +++++++++++++++++++ docs/conf.py | 3 +- docs/contrib/penfield.rst | 4 ++ docs/contrib/shipherd.rst | 25 +++++++++ docs/index.rst | 2 + docs/templatetags.rst | 13 ++++- .../contrib/penfield/templatetags/penfield.py | 25 +++------ philo/contrib/shipherd/models.py | 54 ++++++++++++------ .../contrib/shipherd/templatetags/shipherd.py | 38 +++++++------ philo/templatetags/collections.py | 25 +++------ philo/templatetags/containers.py | 23 +++----- philo/templatetags/embed.py | 42 ++++++-------- philo/templatetags/include_string.py | 30 ++++------ philo/templatetags/nodes.py | 30 ++++------ 15 files changed, 229 insertions(+), 161 deletions(-) create mode 100644 docs/_ext/philodocs.py diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index 7710786..0d433de 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -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 index 0000000..6c1ecf7 --- /dev/null +++ b/docs/_ext/philodocs.py @@ -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 diff --git a/docs/conf.py b/docs/conf.py index cad16d6..b4b1e16 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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', ) diff --git a/docs/contrib/penfield.rst b/docs/contrib/penfield.rst index 2c277ee..d774dcb 100644 --- a/docs/contrib/penfield.rst +++ b/docs/contrib/penfield.rst @@ -43,3 +43,7 @@ Template filters ++++++++++++++++ .. automodule:: philo.contrib.penfield.templatetags.penfield + +.. autotemplatefilter:: monthname + +.. autotemplatefilter:: apmonthname diff --git a/docs/contrib/shipherd.rst b/docs/contrib/shipherd.rst index 5a1848d..0f3b59d 100644 --- a/docs/contrib/shipherd.rst +++ b/docs/contrib/shipherd.rst @@ -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 diff --git a/docs/index.rst b/docs/index.rst index 83d1118..d387fa8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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! ================================= diff --git a/docs/templatetags.rst b/docs/templatetags.rst index 97f0c56..41d30d5 100644 --- a/docs/templatetags.rst +++ b/docs/templatetags.rst @@ -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 diff --git a/philo/contrib/penfield/templatetags/penfield.py b/philo/contrib/penfield/templatetags/penfield.py index 7b9d946..b263a2b 100644 --- a/philo/contrib/penfield/templatetags/penfield.py +++ b/philo/contrib/penfield/templatetags/penfield.py @@ -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 diff --git a/philo/contrib/shipherd/models.py b/philo/contrib/shipherd/models.py index 7554595..f35be3c 100644 --- a/philo/contrib/shipherd/models.py +++ b/philo/contrib/shipherd/models.py @@ -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 diff --git a/philo/contrib/shipherd/templatetags/shipherd.py b/philo/contrib/shipherd/templatetags/shipherd.py index c8ba4fd..508eace 100644 --- a/philo/contrib/shipherd/templatetags/shipherd.py +++ b/philo/contrib/shipherd/templatetags/shipherd.py @@ -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:: + """ bits = token.contents.split() diff --git a/philo/templatetags/collections.py b/philo/templatetags/collections.py index 62d6138..414a742 100644 --- a/philo/templatetags/collections.py +++ b/philo/templatetags/collections.py @@ -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 with . as %} - """ 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 with . as %} + 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 with . as %} """ 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 diff --git a/philo/templatetags/containers.py b/philo/templatetags/containers.py index 722f2d8..e280e60 100644 --- a/philo/templatetags/containers.py +++ b/philo/templatetags/containers.py @@ -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 [[references .] as ] %} - """ 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 [[references .] as ] %} + 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 [[references .] as ] %} """ 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) diff --git a/philo/templatetags/embed.py b/philo/templatetags/embed.py index 20e04a4..9599240 100644 --- a/philo/templatetags/embed.py +++ b/philo/templatetags/embed.py @@ -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 . with