From fa470df0a7435b27de4f680423b498afe03f2fd7 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Wed, 11 Jul 2012 07:05:47 -0400 Subject: [PATCH] Update to template tags to include ways to retrieve an object from a model other than Category. --- .../templates/categories/breadcrumbs.html | 8 +- categories/templates/categories/ul_tree.html | 4 +- categories/templatetags/category_tags.py | 225 +++++++------ categories/tests/templatetags.py | 62 +++- doc_src/_static/default.css | 16 +- doc_src/reference/templatetags.rst | 312 +++++++++++++++--- docs/_sources/reference/templatetags.txt | 312 +++++++++++++++--- docs/_static/basic.css | 21 +- docs/_static/default.css | 16 +- docs/_static/doctools.js | 12 +- docs/_static/searchtools.js | 140 +++----- docs/_static/sidebar.js | 5 +- docs/_static/underscore.js | 7 - docs/adding_the_fields.html | 11 +- docs/custom_categories.html | 11 +- docs/genindex.html | 210 ++++-------- docs/getting_started.html | 11 +- docs/index.html | 13 +- docs/installation.html | 11 +- docs/objects.inv | Bin 963 -> 986 bytes docs/reference/index.html | 23 +- docs/reference/management_commands.html | 13 +- docs/reference/models.html | 17 +- docs/reference/settings.html | 23 +- docs/reference/templatetags.html | 269 ++++++++++++--- docs/registering_models.html | 15 +- docs/search.html | 11 +- docs/searchindex.js | 2 +- docs/usage.html | 23 +- 29 files changed, 1132 insertions(+), 671 deletions(-) diff --git a/categories/templates/categories/breadcrumbs.html b/categories/templates/categories/breadcrumbs.html index 6835ffe..d6ec117 100644 --- a/categories/templates/categories/breadcrumbs.html +++ b/categories/templates/categories/breadcrumbs.html @@ -1,5 +1,3 @@ -{% for item in category.get_ancestors %} -{{ item.name }} -{{ separator }} -{% endfor %} -{{ category.name }} \ No newline at end of file +{% spaceless %}{% for item in category.get_ancestors %} +{{ item.name }}{{ separator }}{% endfor %}{{ category.name }} +{% endspaceless %} \ No newline at end of file diff --git a/categories/templates/categories/ul_tree.html b/categories/templates/categories/ul_tree.html index eb07cb2..ef77d11 100644 --- a/categories/templates/categories/ul_tree.html +++ b/categories/templates/categories/ul_tree.html @@ -1,4 +1,4 @@ -{% load category_tags %} +{% load category_tags %}{% spaceless %} \ No newline at end of file +{% endfor %}{% endspaceless %} \ No newline at end of file diff --git a/categories/templatetags/category_tags.py b/categories/templatetags/category_tags.py index 226a3ad..88ed1f1 100644 --- a/categories/templatetags/category_tags.py +++ b/categories/templatetags/category_tags.py @@ -1,35 +1,61 @@ from django import template -from django.template import Library, Node, TemplateSyntaxError, \ - Variable, resolve_variable, VariableDoesNotExist, Context +from django.db.models import get_model +from django.template import (Node, TemplateSyntaxError, Variable, + VariableDoesNotExist) from categories.base import CategoryBase from categories.models import Category from mptt.utils import drilldown_tree_for_node -from mptt.templatetags.mptt_tags import tree_path, tree_info +from mptt.templatetags.mptt_tags import (tree_path, tree_info, recursetree, + full_tree_for_model) register = template.Library() register.filter("category_path", tree_path) register.filter(tree_info) +register.tag(recursetree) +register.tag("full_tree_for_category", full_tree_for_model) -def get_category(category_string): + +def resolve(var, context): + try: + return var.resolve(context) + except VariableDoesNotExist: + try: + return var.var + except AttributeError: + return var + + +def get_cat_model(model): + """ + Return a class from a string or class + """ + try: + if isinstance(model, basestring): + model_class = get_model(*model.split(".")) + elif issubclass(model, CategoryBase): + model_class = model + if model_class is None: + raise TypeError + except TypeError: + raise TemplateSyntaxError("Unknown model submitted: %s" % model) + return model_class + + +def get_category(category_string, model=Category): """ Convert a string, including a path, and return the Category object """ - if category_string.startswith('"') and category_string.endswith('"'): - category = category_string[1:-1] - else: - category = category_string - - if category.startswith('/'): - category = category[1:] - if category.endswith('/'): - category = category[:-1] + model_class = get_cat_model(model) + category = category_string.strip("'\"") + category = category.strip('/') cat_list = category.split('/') if len(cat_list) == 0: return None try: - categories = Category.objects.filter(name = cat_list[-1], level=len(cat_list)-1) + categories = model_class.objects.filter(name=cat_list[-1], + level=len(cat_list) - 1) if len(cat_list) == 1 and len(categories) > 1: return None # If there is only one, use it. If there is more than one, check @@ -40,30 +66,32 @@ def get_category(category_string): for item in categories: if item.parent.name == cat_list[-2]: return item - except Category.DoesNotExist: + except model_class.DoesNotExist: return None class CategoryDrillDownNode(template.Node): - - def __init__(self, category, varname): + def __init__(self, category, varname, model): self.category = template.Variable(category) self.varname = varname + self.model = model def render(self, context): + category = resolve(self.category, context) + if isinstance(category, CategoryBase): + cat = category + else: + cat = get_category(category, self.model) try: - category = self.category.resolve(context) - except template.VariableDoesNotExist: - category = self.category.var - try: - if category is not None: - context[self.varname] = drilldown_tree_for_node(category) + if cat is not None: + context[self.varname] = drilldown_tree_for_node(cat) else: context[self.varname] = [] - except Category.DoesNotExist: + except: context[self.varname] = [] return '' + @register.tag def get_category_drilldown(parser, token): """ @@ -72,11 +100,11 @@ def get_category_drilldown(parser, token): Syntax:: - {% get_category_drilldown "category name" as varname %} + {% get_category_drilldown "category name" [using "app.Model"] as varname %} Example:: - {% get_category_drilldown "/Grandparent/Parent" as family %} + {% get_category_drilldown "/Grandparent/Parent" [using "app.Model"] as family %} or :: @@ -86,30 +114,44 @@ def get_category_drilldown(parser, token): Grandparent, Parent, Child 1, Child 2, Child n """ - bits = token.contents.split() + bits = token.split_contents() error_str = '%(tagname)s tag should be in the format {%% %(tagname)s ' \ - '"category name" as varname %%}.' - if len(bits) != 4 or bits[2] != 'as': - raise template.TemplateSyntaxError, error_str % {'tagname': bits[0]} + '"category name" [using "app.Model"] as varname %%} or ' \ + '{%% %(tagname)s category_obj as varname %%}.' + if len(bits) == 4: + if bits[2] != 'as': + raise template.TemplateSyntaxError, error_str % {'tagname': bits[0]} + if bits[2] == 'as': + varname = bits[3].strip("'\"") + model = "categories.category" + if len(bits) == 6: + if bits[2] not in ('using', 'as') or bits[4] not in ('using', 'as'): + raise template.TemplateSyntaxError, error_str % {'tagname': bits[0]} + if bits[2] == 'as': + varname = bits[3].strip("'\"") + model = bits[5].strip("'\"") + if bits[2] == 'using': + varname = bits[5].strip("'\"") + model = bits[3].strip("'\"") category = bits[1] - varname = bits[3] - return CategoryDrillDownNode(category, varname) + return CategoryDrillDownNode(category, varname, model) + @register.inclusion_tag('categories/breadcrumbs.html') -def breadcrumbs(category,separator="/"): +def breadcrumbs(category_string, separator=' > ', using='categories.category'): """ + {% breadcrumbs category separator="::" using="categories.category" %} + Render breadcrumbs, using the ``categories/breadcrumbs.html`` template, using the optional ``separator`` argument. """ - if isinstance(category, CategoryBase): - cat = category - else: - cat = get_category(category) + cat = get_category(category_string, using) return {'category': cat, 'separator': separator} + @register.inclusion_tag('categories/ul_tree.html') -def display_drilldown_as_ul(category): +def display_drilldown_as_ul(category, using='categories.Category'): """ Render the category with ancestors and children using the ``categories/ul_tree.html`` template. @@ -142,15 +184,12 @@ def display_drilldown_as_ul(category): """ - if isinstance(category, CategoryBase): - cat = category - else: - cat = get_category(category) - + cat = get_category(category, using) return {'category': cat, 'path': drilldown_tree_for_node(cat) or []} + @register.inclusion_tag('categories/ul_tree.html') -def display_path_as_ul(category): +def display_path_as_ul(category, using='categories.Category'): """ Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template. @@ -180,14 +219,18 @@ def display_path_as_ul(category): return {'category': cat, 'path': cat.get_ancestors() or []} + class TopLevelCategoriesNode(template.Node): - def __init__(self, varname): + def __init__(self, varname, model): self.varname = varname + self.model = model def render(self, context): - context[self.varname] = Category.objects.filter(parent=None).order_by('name') + model = get_cat_model(self.model) + context[self.varname] = model.objects.filter(parent=None).order_by('name') return '' + @register.tag def get_top_level_categories(parser, token): """ @@ -195,31 +238,31 @@ def get_top_level_categories(parser, token): Syntax:: - {% get_top_level_categories as categories %} + {% get_top_level_categories [using "app.Model"] as categories %} Returns an list of categories [, , %%}" % bits[0] - ) - if bits[1] != 'as': - raise template.TemplateSyntaxError( - "Usage: {%% %s as %%}" % bits[0] - ) - return TopLevelCategoriesNode(bits[2]) + bits = token.split_contents() + usage = 'Usage: {%% %s [using "app.Model"] as %%}' % bits[0] + if len(bits) == 3: + if bits[1] != 'as': + raise template.TemplateSyntaxError(usage) + varname = bits[2] + model = "categories.category" + elif len(bits) == 5: + if bits[1] not in ('as', 'using') and bits[3] not in ('as', 'using'): + raise template.TemplateSyntaxError(usage) + if bits[1] == 'using': + model = bits[2].strip("'\"") + varname = bits[4].strip("'\"") + else: + model = bits[4].strip("'\"") + varname = bits[2].strip("'\"") -def resolve(var, context): - try: - return var.resolve(context) - except VariableDoesNotExist: - try: - return var.var - except AttributeError: - return var + return TopLevelCategoriesNode(varname, model) -def get_latest_objects_by_category(category, app_label, model_name, set_name, + +def get_latest_objects_by_category(category, app_label, model_name, set_name, date_field='pub_date', num=15): m = get_model(app_label, model_name) if not isinstance(category, CategoryBase): @@ -229,13 +272,16 @@ def get_latest_objects_by_category(category, app_label, model_name, set_name, for cat in list(children) + [category]: if hasattr(cat, '%s_set' % set_name): ids.extend([x.pk for x in getattr(cat, '%s_set' % set_name).all()[:num]]) - + return m.objects.filter(pk__in=ids).order_by('-%s' % date_field)[:num] + class LatestObjectsNode(Node): - def __init__(self, var_name, category, app_label, model_name, set_name, + def __init__(self, var_name, category, app_label, model_name, set_name, date_field='pub_date', num=15): - """Get latest objects of app_label.model_name""" + """ + Get latest objects of app_label.model_name + """ self.category = Variable(category) self.app_label = Variable(app_label) self.model_name = Variable(model_name) @@ -243,43 +289,34 @@ class LatestObjectsNode(Node): self.date_field = Variable(date_field) self.num = Variable(num) self.var_name = var_name - - def get_cache_key(self, category, app_label, model_name, set_name, - date_field, num): - """Get the cache key""" - key = 'latest_objects.%s' % '.'.join([category, app_label, model_name, - set_name, date_field, num]) - + def render(self, context): - """Render this sucker""" + """ + Render this sucker + """ category = resolve(self.category, context) app_label = resolve(self.app_label, context) model_name = resolve(self.model_name, context) set_name = resolve(self.set_name, context) date_field = resolve(self.date_field, context) num = resolve(self.num, context) - - cache_key = self.get_cache_key(category, app_label, model_name, set_name, - date_field, num) - result = cache.get(cache_key) - if not result: - result = get_latest_objects_by_category(category, app_label, model_name, + + result = get_latest_objects_by_category(category, app_label, model_name, set_name, date_field, num) - - cache.set(key, result, 300) context[self.var_name] = result - + return '' + def do_get_latest_objects_by_category(parser, token): """ Get the latest objects by category - + {% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %} """ proper_form = "{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}" bits = token.split_contents() - + if bits[-2] != 'as': raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form)) if len(bits) < 7: @@ -299,9 +336,11 @@ def do_get_latest_objects_by_category(parser, token): num = bits[6] else: num = None - return LatestObjectsNode(var_name, category, app_label, model_name, set_name, + return LatestObjectsNode(var_name, category, app_label, model_name, set_name, date_field, num) +register.tag("get_latest_objects_by_category", do_get_latest_objects_by_category) + @register.filter def tree_queryset(value): @@ -313,7 +352,7 @@ def tree_queryset(value): from copy import deepcopy if not isinstance(value, QuerySet): return value - + qs = value qs2 = deepcopy(qs) # Reaching into the bowels of query sets to find out whether the qs is @@ -331,9 +370,9 @@ def tree_queryset(value): p.id not in include_pages: ancestor_id_list = p.get_ancestors().values_list('id', flat=True) include_pages.update(ancestor_id_list) - + if include_pages: qs = qs | qs.model._default_manager.filter(id__in=include_pages) - + qs = qs.distinct() - return qs \ No newline at end of file + return qs diff --git a/categories/tests/templatetags.py b/categories/tests/templatetags.py index 5568119..bf6bf71 100644 --- a/categories/tests/templatetags.py +++ b/categories/tests/templatetags.py @@ -1,11 +1,13 @@ from django.test import TestCase from django import template +from categories.models import Category + + +class CategoryTagsTest(TestCase): -class GetCategoryTest(TestCase): - fixtures = ['musicgenres.json'] - + def render_template(self, template_string, context={}): """ Return the rendered string or raise an exception. @@ -16,27 +18,51 @@ class GetCategoryTest(TestCase): def testTooFewArguments(self): """ - Ensure that get_cateogry raises an exception if there aren't enough arguments. + Ensure that get_category raises an exception if there aren't enough arguments. """ self.assertRaises(template.TemplateSyntaxError, self.render_template, '{% load category_tags %}{% get_category %}') - def testTooManyArguments(self): - """ - Ensure that get_category raises an exception if there are too many arguments. - """ - self.assertRaises(template.TemplateSyntaxError, self.render_template, '{% load category_tags %}{% get_category "/Rock" as too many arguments %}') - - def testAsIsSecondArgument(self): - """ - Test that the second argument to get_category is 'as'. - """ - self.assertRaises(template.TemplateSyntaxError, self.render_template, '{% load category_tags %}{% get_category "Rock" notas rock %}') - def testBasicUsage(self): """ Test that we can properly retrieve a category. """ - rock_resp = u'\n' + # display_path_as_ul + rock_resp = u'' resp = self.render_template('{% load category_tags %}{% display_path_as_ul "/Rock" %}') self.assertEqual(resp, rock_resp) - \ No newline at end of file + + # display_drilldown_as_ul + expected_resp = u'' + resp = self.render_template('{% load category_tags %}' + '{% display_drilldown_as_ul "/World/Worldbeat" using="categories.category" %}') + self.assertEqual(resp, expected_resp) + + # breadcrumbs + expected_resp = u'World > Worldbeat' + resp = self.render_template('{% load category_tags %}' + '{% breadcrumbs "/World/Worldbeat" using="categories.category" %}') + self.assertEqual(resp, expected_resp) + + # get_top_level_categories + expected_resp = u'Avant-garde|Blues|Country|Easy listening|Electronic|Hip hop/Rap music|Jazz|Latin|Modern folk|Pop|Reggae|Rhythm and blues|Rock|World|' + resp = self.render_template('{% load category_tags %}' + '{% get_top_level_categories using "categories.category" as varname %}' + '{% for item in varname %}{{ item }}|{% endfor %}') + self.assertEqual(resp, expected_resp) + + # get_category_drilldown + expected_resp = u"World|World > Worldbeat|" + resp = self.render_template('{% load category_tags %}' + '{% get_category_drilldown "/World" using "categories.category" as var %}' + '{% for item in var %}{{ item }}|{% endfor %}') + self.assertEqual(resp, expected_resp) + + # recursetree + expected_resp = u'' + ctxt = {'nodes': Category.objects.filter(name__in=("Worldbeat", "Urban cowboy"))} + resp = self.render_template('{% load category_tags %}' + '
    {% recursetree nodes|treeinfo %}
  • {{ node.name }}' + '{% if not node.is_leaf_node %}
      {{ children }}' + '
    {% endif %}
  • {% endrecursetree %}
', ctxt) + self.assertEqual(resp, expected_resp) + diff --git a/doc_src/_static/default.css b/doc_src/_static/default.css index 48cf53f..c719235 100644 --- a/doc_src/_static/default.css +++ b/doc_src/_static/default.css @@ -62,7 +62,7 @@ div.clearer { .headerButton a:hover { color: white; background-color: #787878; - + } li#toc_button { @@ -121,7 +121,7 @@ right:0; top: 84px; bottom: 19px; left: 0px; - width: 229px; + width: 229px; background-color: #E4EBF7; border-right: 1px solid #ACACAC; border-top: 1px solid #2B334F; @@ -362,7 +362,7 @@ p.topic-title { border:1px solid #111111; margin:30px; } -.admonition p { +.admonition p { font: 12px 'Lucida Grande', Geneva, Helvetica, Arial, sans-serif; margin-top: 7px; margin-bottom: 0px; @@ -411,7 +411,7 @@ table.docutils td, table.docutils th { table.docutils th { font-weight: bold; } -/* This alternates colors in up to six table rows (light blue for odd, white for even)*/ +/* This alternates colors in up to six table rows (light blue for odd, white for even)*/ .docutils tr { background: #F0F5F9; } @@ -453,6 +453,7 @@ th { dl { margin-bottom: 15px; + font-size: 12px; } dd p { @@ -544,7 +545,7 @@ td.linenos pre { } td.code { - + } table.highlighttable { @@ -562,7 +563,6 @@ table.highlighttable td.linenos { } tt { font-family:"Bitstream Vera Sans Mono",Monaco,"Lucida Console",Courier,Consolas,monospace; - } tt.descname { @@ -643,7 +643,7 @@ dl.class dd dl.method dt { padding: 3px; background-color: #e9e9e9; border-top: none; - + } dl.function dt { @@ -659,7 +659,7 @@ margin:0 0 0 30px; padding:0 0 12px 6px; } #docstitle { - height: 36px; + height: 36px; background-image: url(header_sm_mid.png); left: 0; top: 0; diff --git a/doc_src/reference/templatetags.rst b/doc_src/reference/templatetags.rst index b5463f4..9f846fd 100644 --- a/doc_src/reference/templatetags.rst +++ b/doc_src/reference/templatetags.rst @@ -1,33 +1,146 @@ -============= -Template Tags -============= +========================= +Template tags and filters +========================= -get_top_level_categories -======================== +.. contents:: + :depth: 2 + :local: + :backlinks: top -Retrieves an alphabetical list of all the categories that have no parents. -Syntax: +Filters +======= + + +``category_path`` +----------------- + +**Optional Parameter:** separator string. *Default:* ``" :: "`` + +Creates a path represented by a categories by joining the items with a separator. + +Each path item will be coerced to unicode, so you can pass a list of category instances, if required. + +**Example using a list of categories:** .. code-block:: django - {% get_top_level_categories as categories %} + {{ some_list|category_path }} -Returns an list of categories ``[, , , , ]`` the result will be:: + + Country :: Country pop :: Urban Cowboy + +**Example using a category node and optional separator parameter:** + +.. code-block:: django + + {{ some_node.get_ancestors|category_path:" > " }} + +If ``some_node`` was category "Urban Cowboy", the result will be:: + + Country > Country pop > Urban Cowboy + +.. _tree_info: + +``tree_info`` +------------- + +**Optional Parameter:** ``"ancestors"`` + +Given a list of categories, it iterates over the list, generating a tuple of the current category and a dict containing information about the tree structure around it, with the following keys: + +``'new_level'`` + ``True`` if the current item is the start of a new level in the tree, ``False`` otherwise. + +``'closed_levels'`` + A list of levels which end after the current item. This will be an empty list if the next category's level is the same as or greater than the level of the current item. + +Provide the optional argument, ``"ancestors"``, to add a list of unicode representations of the ancestors of the current category, in descending order (root node first, immediate parent last), under the key 'ancestors'. + +For example: given the sample tree below, the contents of the list which would be available under the 'ancestors' key are given on the right:: + + Country -> [] + Country pop -> [u'Country pop'] + Urban Cowboy -> [u'Country', u'Country pop'] + +Using this filter with unpacking in a {% for %} tag, you should have enough information about the tree structure to create a hierarchical representation of the tree. + +.. code-block:: django + + {% for node,structure in objects|tree_info %} + {% if structure.new_level %}
  • {% else %}
  • {% endif %} + {{ node.name }} + {% for level in structure.closed_levels %}
{% endfor %} + {% endfor %} + +``tree_queryset`` +----------------- + +Convert a regular category :py:class:`QuerySet` into a new, ordered :py:class:`QuerySet` that includes the categories selected and their ancestors. + +This is especially helpful when you have a subset of categories and want to show the hierarchy for all the items. + +For example, if we add it to the example for :ref:`tree_info`: + +.. code-block:: django + + {% for node,structure in objects|tree_queryset|tree_info %} + {% if structure.new_level %}
  • {% else %}
  • {% endif %} + {{ node.name }} + {% for level in structure.closed_levels %}
{% endfor %} + {% endfor %} + +A list of unrelated categories such as ``[, ]``, the above template example will output the two categories and their ancestors: + +.. code-block:: html + +
  • + Country +
    • + Country pop +
      • + Urban cowboy +
+
  • + Rhythm and blues +
    • + Urban contemporary +
+ +.. note:: + Categories that have similar ancestors are grouped accordingly. There is no duplication of the ancestor tree. -display_path_as_ul -================== +Inclusion tags +============== -Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template. +``display_path_as_ul`` +---------------------- -Example: +**Template Rendered:** ``categories/ul_tree.html`` + +**Syntax 1:** ``{% display_path_as_ul %}`` + +**Syntax 2:** ``{% display_path_as_ul [ using="app.Model"] %}`` + +Render the category with ancestors, but no children. + +Pass either an object that subclasses :py:class:`CategoryBase` or a path string for the category. Add ``using="app.Model"`` to specify which model when using a path string. The default model used is :py:class:`Category`. + +**Example, using Category model:** .. code-block:: django {% display_path_as_ul "/Grandparent/Parent" %} -or +**Example, using custom model:** + +.. code-block:: django + + {% display_path_as_ul "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example, using an object:** .. code-block:: django @@ -37,51 +150,39 @@ Returns: .. code-block:: html - + -get_category_drilldown -====================== +``display_drilldown_as_ul`` +--------------------------- -Retrieves the specified category, its ancestors and its immediate children -as an iterable. +**Template rendered:** ``categories/ul_tree.html`` -Example: +**Syntax 1:** ``{% display_drilldown_as_ul category_obj %}`` -.. code-block:: django +**Syntax 2:** ``{% display_drilldown_as_ul "/Grandparent/Parent" [using="app.Model"] %}`` - {% get_category_drilldown "/Grandparent/Parent" as family %} +Render the category with ancestors and children. -or - -.. code-block:: django - - {% get_category_drilldown category_obj as family %} - -Sets ``family`` to:: - - [Grandparent, Parent, Child 1, Child 2, Child n] - - -display_drilldown_as_ul -======================= - -Render the category with ancestors and children using the -``categories/ul_tree.html`` template. - -Example: +**Example, using Category model:** .. code-block:: django {% display_drilldown_as_ul "/Grandparent/Parent" %} -or: +**Example, using custom model:** + +.. code-block:: django + + {% display_drilldown_as_ul "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example, using an object:** .. code-block:: django @@ -110,18 +211,30 @@ Returns: -breadcrumbs tag -=============== +``breadcrumbs tag`` +------------------- -Render breadcrumbs, using the ``categories/breadcrumbs.html`` template, using the optional ``separator`` argument. +**Template rendered:** ``categories/breadcrumbs.html`` -Example: +**Syntax 1:** ``{% breadcrumbs category_obj [separator=" :: "] %}`` + +**Syntax 2:** ``{% breadcrumbs "/Grandparent/Parent" [separator=" :: "] [using="app.Model"] %}`` + +Render breadcrumbs for the given path using ``::`` or the given separator. + +**Example using Category model:** .. code-block:: django {% breadcrumbs "/Grandparent/Parent" %} -or: +**Example using a custom model:** + +.. code-block:: django + + {% breadcrumbs "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example using an object:** .. code-block:: django @@ -133,14 +246,103 @@ Returns: Grandparent / Parent -You can alter the separator used in the template by adding a string argument to be the separator: +You can alter the separator used in the template by adding a separator argument: .. code-block:: django - {% breadcrumbs category_obj "::" %} + {% breadcrumbs category_obj separator=" > " %} Returns: .. code-block:: html - Grandparent :: Parent + Grandparent > Parent + + +Template Tags +============= + +``get_top_level_categories`` +---------------------------- + +Retrieves an alphabetical list of all the categories that have no parents. + +Syntax: + +.. code-block:: django + + {% get_top_level_categories [using "app.Model"] as categories %} + +Returns an list of categories ``[, , [using "app.Model"] as %}`` + +**Syntax 2:** ``{% get_category_drilldown as %}`` + +Retrieves the specified category, its ancestors and its immediate children as an iterable. Syntax 1 allows for the retrieval of the category object via a slash-delimited path. The optional ``using "app.Model"`` allows you to specify from which model to retrieve the object. + +Example: + +.. code-block:: django + + {% get_category_drilldown "/Grandparent/Parent" using "family.Member" as family %} + +The second syntax uses an instance of any object that subclasses :py:class:`CategoryBase` + +.. code-block:: django + + {% get_category_drilldown category_obj as family %} + +Both examples sets ``family`` to:: + + [Grandparent, Parent, Child 1, Child 2, Child n] + +``recursetree`` +--------------- + +This tag renders a section of your template recursively for each node in your +tree. + +For example: + +.. code-block:: django + +
    + {% recursetree nodes %} +
  • + {{ node.name }} + {% if not node.is_leaf_node %} +
      + {{ children }} +
    + {% endif %} +
  • + {% endrecursetree %} +
+ +Note the special variables ``node`` and ``children``. +These are magically inserted into your context while you're inside the +``recursetree`` tag. + + ``node`` is an instance of your MPTT model. + + ``children`` : This variable holds the rendered HTML for the children of + ``node``. + +.. note:: + If you already have variables called ``node`` or ``children`` in your + template, and you need to access them inside the ``recursetree`` block, + you'll need to alias them to some other name first: + + .. code-block:: django + + {% with node as friendly_node %} + {% recursetree nodes %} + {{ node.name }} is friends with {{ friendly_node.name }} + {{ children }} + {% endrecursetree %} + {% endwith %} diff --git a/docs/_sources/reference/templatetags.txt b/docs/_sources/reference/templatetags.txt index b5463f4..9f846fd 100644 --- a/docs/_sources/reference/templatetags.txt +++ b/docs/_sources/reference/templatetags.txt @@ -1,33 +1,146 @@ -============= -Template Tags -============= +========================= +Template tags and filters +========================= -get_top_level_categories -======================== +.. contents:: + :depth: 2 + :local: + :backlinks: top -Retrieves an alphabetical list of all the categories that have no parents. -Syntax: +Filters +======= + + +``category_path`` +----------------- + +**Optional Parameter:** separator string. *Default:* ``" :: "`` + +Creates a path represented by a categories by joining the items with a separator. + +Each path item will be coerced to unicode, so you can pass a list of category instances, if required. + +**Example using a list of categories:** .. code-block:: django - {% get_top_level_categories as categories %} + {{ some_list|category_path }} -Returns an list of categories ``[, , , , ]`` the result will be:: + + Country :: Country pop :: Urban Cowboy + +**Example using a category node and optional separator parameter:** + +.. code-block:: django + + {{ some_node.get_ancestors|category_path:" > " }} + +If ``some_node`` was category "Urban Cowboy", the result will be:: + + Country > Country pop > Urban Cowboy + +.. _tree_info: + +``tree_info`` +------------- + +**Optional Parameter:** ``"ancestors"`` + +Given a list of categories, it iterates over the list, generating a tuple of the current category and a dict containing information about the tree structure around it, with the following keys: + +``'new_level'`` + ``True`` if the current item is the start of a new level in the tree, ``False`` otherwise. + +``'closed_levels'`` + A list of levels which end after the current item. This will be an empty list if the next category's level is the same as or greater than the level of the current item. + +Provide the optional argument, ``"ancestors"``, to add a list of unicode representations of the ancestors of the current category, in descending order (root node first, immediate parent last), under the key 'ancestors'. + +For example: given the sample tree below, the contents of the list which would be available under the 'ancestors' key are given on the right:: + + Country -> [] + Country pop -> [u'Country pop'] + Urban Cowboy -> [u'Country', u'Country pop'] + +Using this filter with unpacking in a {% for %} tag, you should have enough information about the tree structure to create a hierarchical representation of the tree. + +.. code-block:: django + + {% for node,structure in objects|tree_info %} + {% if structure.new_level %}
  • {% else %}
  • {% endif %} + {{ node.name }} + {% for level in structure.closed_levels %}
{% endfor %} + {% endfor %} + +``tree_queryset`` +----------------- + +Convert a regular category :py:class:`QuerySet` into a new, ordered :py:class:`QuerySet` that includes the categories selected and their ancestors. + +This is especially helpful when you have a subset of categories and want to show the hierarchy for all the items. + +For example, if we add it to the example for :ref:`tree_info`: + +.. code-block:: django + + {% for node,structure in objects|tree_queryset|tree_info %} + {% if structure.new_level %}
  • {% else %}
  • {% endif %} + {{ node.name }} + {% for level in structure.closed_levels %}
{% endfor %} + {% endfor %} + +A list of unrelated categories such as ``[, ]``, the above template example will output the two categories and their ancestors: + +.. code-block:: html + +
  • + Country +
    • + Country pop +
      • + Urban cowboy +
+
  • + Rhythm and blues +
    • + Urban contemporary +
+ +.. note:: + Categories that have similar ancestors are grouped accordingly. There is no duplication of the ancestor tree. -display_path_as_ul -================== +Inclusion tags +============== -Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template. +``display_path_as_ul`` +---------------------- -Example: +**Template Rendered:** ``categories/ul_tree.html`` + +**Syntax 1:** ``{% display_path_as_ul %}`` + +**Syntax 2:** ``{% display_path_as_ul [ using="app.Model"] %}`` + +Render the category with ancestors, but no children. + +Pass either an object that subclasses :py:class:`CategoryBase` or a path string for the category. Add ``using="app.Model"`` to specify which model when using a path string. The default model used is :py:class:`Category`. + +**Example, using Category model:** .. code-block:: django {% display_path_as_ul "/Grandparent/Parent" %} -or +**Example, using custom model:** + +.. code-block:: django + + {% display_path_as_ul "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example, using an object:** .. code-block:: django @@ -37,51 +150,39 @@ Returns: .. code-block:: html - + -get_category_drilldown -====================== +``display_drilldown_as_ul`` +--------------------------- -Retrieves the specified category, its ancestors and its immediate children -as an iterable. +**Template rendered:** ``categories/ul_tree.html`` -Example: +**Syntax 1:** ``{% display_drilldown_as_ul category_obj %}`` -.. code-block:: django +**Syntax 2:** ``{% display_drilldown_as_ul "/Grandparent/Parent" [using="app.Model"] %}`` - {% get_category_drilldown "/Grandparent/Parent" as family %} +Render the category with ancestors and children. -or - -.. code-block:: django - - {% get_category_drilldown category_obj as family %} - -Sets ``family`` to:: - - [Grandparent, Parent, Child 1, Child 2, Child n] - - -display_drilldown_as_ul -======================= - -Render the category with ancestors and children using the -``categories/ul_tree.html`` template. - -Example: +**Example, using Category model:** .. code-block:: django {% display_drilldown_as_ul "/Grandparent/Parent" %} -or: +**Example, using custom model:** + +.. code-block:: django + + {% display_drilldown_as_ul "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example, using an object:** .. code-block:: django @@ -110,18 +211,30 @@ Returns: -breadcrumbs tag -=============== +``breadcrumbs tag`` +------------------- -Render breadcrumbs, using the ``categories/breadcrumbs.html`` template, using the optional ``separator`` argument. +**Template rendered:** ``categories/breadcrumbs.html`` -Example: +**Syntax 1:** ``{% breadcrumbs category_obj [separator=" :: "] %}`` + +**Syntax 2:** ``{% breadcrumbs "/Grandparent/Parent" [separator=" :: "] [using="app.Model"] %}`` + +Render breadcrumbs for the given path using ``::`` or the given separator. + +**Example using Category model:** .. code-block:: django {% breadcrumbs "/Grandparent/Parent" %} -or: +**Example using a custom model:** + +.. code-block:: django + + {% breadcrumbs "/Grandparent/Parent" using="coolapp.MusicGenre" %} + +**Example using an object:** .. code-block:: django @@ -133,14 +246,103 @@ Returns: Grandparent / Parent -You can alter the separator used in the template by adding a string argument to be the separator: +You can alter the separator used in the template by adding a separator argument: .. code-block:: django - {% breadcrumbs category_obj "::" %} + {% breadcrumbs category_obj separator=" > " %} Returns: .. code-block:: html - Grandparent :: Parent + Grandparent > Parent + + +Template Tags +============= + +``get_top_level_categories`` +---------------------------- + +Retrieves an alphabetical list of all the categories that have no parents. + +Syntax: + +.. code-block:: django + + {% get_top_level_categories [using "app.Model"] as categories %} + +Returns an list of categories ``[, , [using "app.Model"] as %}`` + +**Syntax 2:** ``{% get_category_drilldown as %}`` + +Retrieves the specified category, its ancestors and its immediate children as an iterable. Syntax 1 allows for the retrieval of the category object via a slash-delimited path. The optional ``using "app.Model"`` allows you to specify from which model to retrieve the object. + +Example: + +.. code-block:: django + + {% get_category_drilldown "/Grandparent/Parent" using "family.Member" as family %} + +The second syntax uses an instance of any object that subclasses :py:class:`CategoryBase` + +.. code-block:: django + + {% get_category_drilldown category_obj as family %} + +Both examples sets ``family`` to:: + + [Grandparent, Parent, Child 1, Child 2, Child n] + +``recursetree`` +--------------- + +This tag renders a section of your template recursively for each node in your +tree. + +For example: + +.. code-block:: django + +
    + {% recursetree nodes %} +
  • + {{ node.name }} + {% if not node.is_leaf_node %} +
      + {{ children }} +
    + {% endif %} +
  • + {% endrecursetree %} +
+ +Note the special variables ``node`` and ``children``. +These are magically inserted into your context while you're inside the +``recursetree`` tag. + + ``node`` is an instance of your MPTT model. + + ``children`` : This variable holds the rendered HTML for the children of + ``node``. + +.. note:: + If you already have variables called ``node`` or ``children`` in your + template, and you need to access them inside the ``recursetree`` block, + you'll need to alias them to some other name first: + + .. code-block:: django + + {% with node as friendly_node %} + {% recursetree nodes %} + {{ node.name }} is friends with {{ friendly_node.name }} + {{ children }} + {% endrecursetree %} + {% endwith %} diff --git a/docs/_static/basic.css b/docs/_static/basic.css index f0379f3..925f7a5 100644 --- a/docs/_static/basic.css +++ b/docs/_static/basic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -79,14 +79,6 @@ div.sphinxsidebar input { font-size: 1em; } -div.sphinxsidebar input[type="text"] { - width: 170px; -} - -div.sphinxsidebar input[type="submit"] { - width: 30px; -} - img { border: 0; } @@ -244,6 +236,7 @@ img.align-center, .figure.align-center, object.align-center { } .align-center { + clear: both; text-align: center; } @@ -420,7 +413,7 @@ dl.glossary dt { } .footnote:target { - background-color: #ffa; + background-color: #ffa } .line-block { @@ -447,16 +440,10 @@ dl.glossary dt { font-style: oblique; } -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - /* -- code displays --------------------------------------------------------- */ pre { overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ } td.linenos pre { @@ -537,4 +524,4 @@ span.eqno { #top-link { display: none; } -} \ No newline at end of file +} diff --git a/docs/_static/default.css b/docs/_static/default.css index 48cf53f..c719235 100644 --- a/docs/_static/default.css +++ b/docs/_static/default.css @@ -62,7 +62,7 @@ div.clearer { .headerButton a:hover { color: white; background-color: #787878; - + } li#toc_button { @@ -121,7 +121,7 @@ right:0; top: 84px; bottom: 19px; left: 0px; - width: 229px; + width: 229px; background-color: #E4EBF7; border-right: 1px solid #ACACAC; border-top: 1px solid #2B334F; @@ -362,7 +362,7 @@ p.topic-title { border:1px solid #111111; margin:30px; } -.admonition p { +.admonition p { font: 12px 'Lucida Grande', Geneva, Helvetica, Arial, sans-serif; margin-top: 7px; margin-bottom: 0px; @@ -411,7 +411,7 @@ table.docutils td, table.docutils th { table.docutils th { font-weight: bold; } -/* This alternates colors in up to six table rows (light blue for odd, white for even)*/ +/* This alternates colors in up to six table rows (light blue for odd, white for even)*/ .docutils tr { background: #F0F5F9; } @@ -453,6 +453,7 @@ th { dl { margin-bottom: 15px; + font-size: 12px; } dd p { @@ -544,7 +545,7 @@ td.linenos pre { } td.code { - + } table.highlighttable { @@ -562,7 +563,6 @@ table.highlighttable td.linenos { } tt { font-family:"Bitstream Vera Sans Mono",Monaco,"Lucida Console",Courier,Consolas,monospace; - } tt.descname { @@ -643,7 +643,7 @@ dl.class dd dl.method dt { padding: 3px; background-color: #e9e9e9; border-top: none; - + } dl.function dt { @@ -659,7 +659,7 @@ margin:0 0 0 30px; padding:0 0 12px 6px; } #docstitle { - height: 36px; + height: 36px; background-image: url(header_sm_mid.png); left: 0; top: 0; diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index d4619fd..eeea95e 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -2,9 +2,9 @@ * doctools.js * ~~~~~~~~~~~ * - * Sphinx JavaScript utilities for all documentation. + * Sphinx JavaScript utilties for all documentation. * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -185,9 +185,9 @@ var Documentation = { body.highlightText(this.toLowerCase(), 'highlighted'); }); }, 10); - $('') - .appendTo($('#searchbox')); + $('') + .appendTo($('.sidebar .this-page-menu')); } }, @@ -213,7 +213,7 @@ var Documentation = { * helper function to hide the search marks again */ hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); + $('.sidebar .this-page-menu li.highlight-link').fadeOut(300); $('span.highlighted').removeClass('highlighted'); }, diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 663be4c..5cbfe00 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -1,10 +1,10 @@ /* - * searchtools.js_t - * ~~~~~~~~~~~~~~~~ + * searchtools.js + * ~~~~~~~~~~~~~~ * * Sphinx JavaScript utilties for the full-text search. * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -36,11 +36,10 @@ jQuery.makeSearchSummary = function(text, keywords, hlwords) { return rv; } - /** * Porter Stemmer */ -var Stemmer = function() { +var PorterStemmer = function() { var step2list = { ational: 'ate', @@ -301,20 +300,20 @@ var Search = { }, query : function(query) { - var stopwords = ["and","then","into","it","as","are","in","if","for","no","there","their","was","is","be","to","that","but","they","not","such","with","by","a","on","these","of","will","this","near","the","or","at"]; + var stopwords = ['and', 'then', 'into', 'it', 'as', 'are', 'in', + 'if', 'for', 'no', 'there', 'their', 'was', 'is', + 'be', 'to', 'that', 'but', 'they', 'not', 'such', + 'with', 'by', 'a', 'on', 'these', 'of', 'will', + 'this', 'near', 'the', 'or', 'at']; - // Stem the searchterms and add them to the correct list - var stemmer = new Stemmer(); + // stem the searchterms and add them to the correct list + var stemmer = new PorterStemmer(); var searchterms = []; var excluded = []; var hlterms = []; var tmp = query.split(/\s+/); - var objectterms = []; + var object = (tmp.length == 1) ? tmp[0].toLowerCase() : null; for (var i = 0; i < tmp.length; i++) { - if (tmp[i] != "") { - objectterms.push(tmp[i].toLowerCase()); - } - if ($u.indexOf(stopwords, tmp[i]) != -1 || tmp[i].match(/^\d+$/) || tmp[i] == "") { // skip this "word" @@ -345,6 +344,9 @@ var Search = { var filenames = this._index.filenames; var titles = this._index.titles; var terms = this._index.terms; + var objects = this._index.objects; + var objtypes = this._index.objtypes; + var objnames = this._index.objnames; var fileMap = {}; var files = null; // different result priorities @@ -355,19 +357,40 @@ var Search = { $('#search-progress').empty(); // lookup as object - for (var i = 0; i < objectterms.length; i++) { - var others = [].concat(objectterms.slice(0,i), - objectterms.slice(i+1, objectterms.length)) - var results = this.performObjectSearch(objectterms[i], others); - // Assume first word is most likely to be the object, - // other words more likely to be in description. - // Therefore put matches for earlier words first. - // (Results are eventually used in reverse order). - objectResults = results[0].concat(objectResults); - importantResults = results[1].concat(importantResults); - unimportantResults = results[2].concat(unimportantResults); + if (object != null) { + for (var prefix in objects) { + for (var name in objects[prefix]) { + var fullname = (prefix ? prefix + '.' : '') + name; + if (fullname.toLowerCase().indexOf(object) > -1) { + match = objects[prefix][name]; + descr = objnames[match[1]] + _(', in ') + titles[match[0]]; + // XXX the generated anchors are not generally correct + // XXX there may be custom prefixes + result = [filenames[match[0]], fullname, '#'+fullname, descr]; + switch (match[2]) { + case 1: objectResults.push(result); break; + case 0: importantResults.push(result); break; + case 2: unimportantResults.push(result); break; + } + } + } + } } + // sort results descending + objectResults.sort(function(a, b) { + return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); + }); + + importantResults.sort(function(a, b) { + return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); + }); + + unimportantResults.sort(function(a, b) { + return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); + }); + + // perform the search on the required terms for (var i = 0; i < searchterms.length; i++) { var word = searchterms[i]; @@ -466,7 +489,7 @@ var Search = { listItem.slideDown(5, function() { displayNextItem(); }); - }, "text"); + }); } else { // no source available, just display title Search.output.append(listItem); @@ -487,74 +510,9 @@ var Search = { } } displayNextItem(); - }, - - performObjectSearch : function(object, otherterms) { - var filenames = this._index.filenames; - var objects = this._index.objects; - var objnames = this._index.objnames; - var titles = this._index.titles; - - var importantResults = []; - var objectResults = []; - var unimportantResults = []; - - for (var prefix in objects) { - for (var name in objects[prefix]) { - var fullname = (prefix ? prefix + '.' : '') + name; - if (fullname.toLowerCase().indexOf(object) > -1) { - var match = objects[prefix][name]; - var objname = objnames[match[1]][2]; - var title = titles[match[0]]; - // If more than one term searched for, we require other words to be - // found in the name/title/description - if (otherterms.length > 0) { - var haystack = (prefix + ' ' + name + ' ' + - objname + ' ' + title).toLowerCase(); - var allfound = true; - for (var i = 0; i < otherterms.length; i++) { - if (haystack.indexOf(otherterms[i]) == -1) { - allfound = false; - break; - } - } - if (!allfound) { - continue; - } - } - var descr = objname + _(', in ') + title; - anchor = match[3]; - if (anchor == '') - anchor = fullname; - else if (anchor == '-') - anchor = objnames[match[1]][1] + '-' + fullname; - result = [filenames[match[0]], fullname, '#'+anchor, descr]; - switch (match[2]) { - case 1: objectResults.push(result); break; - case 0: importantResults.push(result); break; - case 2: unimportantResults.push(result); break; - } - } - } - } - - // sort results descending - objectResults.sort(function(a, b) { - return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); - }); - - importantResults.sort(function(a, b) { - return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); - }); - - unimportantResults.sort(function(a, b) { - return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); - }); - - return [importantResults, objectResults, unimportantResults] } } $(document).ready(function() { Search.init(); -}); \ No newline at end of file +}); diff --git a/docs/_static/sidebar.js b/docs/_static/sidebar.js index a45e192..7318517 100644 --- a/docs/_static/sidebar.js +++ b/docs/_static/sidebar.js @@ -16,7 +16,7 @@ * Once the browser is closed the cookie is deleted and the position * reset to the default (expanded). * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -29,9 +29,6 @@ $(function() { var sidebar = $('.sphinxsidebar'); var sidebarwrapper = $('.sphinxsidebarwrapper'); - // for some reason, the document has no sidebar; do not run into errors - if (!sidebar.length) return; - // original margin-left of the bodywrapper and width of the sidebar // with the sidebar expanded var bw_margin_expanded = bodywrapper.css('margin-left'); diff --git a/docs/_static/underscore.js b/docs/_static/underscore.js index 5d89914..9146e08 100644 --- a/docs/_static/underscore.js +++ b/docs/_static/underscore.js @@ -1,10 +1,3 @@ -// Underscore.js 0.5.5 -// (c) 2009 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore is freely distributable under the terms of the MIT license. -// Portions of Underscore are inspired by or borrowed from Prototype.js, -// Oliver Steele's Functional, and John Resig's Micro-Templating. -// For all details and documentation: -// http://documentcloud.github.com/underscore/ (function(){var j=this,n=j._,i=function(a){this._wrapped=a},m=typeof StopIteration!=="undefined"?StopIteration:"__break__",b=j._=function(a){return new i(a)};if(typeof exports!=="undefined")exports._=b;var k=Array.prototype.slice,o=Array.prototype.unshift,p=Object.prototype.toString,q=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;b.VERSION="0.5.5";b.each=function(a,c,d){try{if(a.forEach)a.forEach(c,d);else if(b.isArray(a)||b.isArguments(a))for(var e=0,f=a.length;e - - Adding the fields to the database — Django Categories 1.0.5 documentation - + Adding the fields to the database — Django Categories v1.0.5 documentation - - +
-

Django Categories 1.0.5 documentation

+

Django Categories v1.0.5 documentation