Commits

Anonymous committed 38e8713

Release 0.4

Comments (0)

Files changed (15)

 
     python setup.py install
 
-Note that this application requires Python 2.3 or later, and Django
-0.96 or later; you can obtain Python from http://www.python.org/ and
-Django from http://www.djangoproject.com/.
+Note that this application requires Python 2.3 or later, and a recent
+Subversion checkout of Django; you can obtain Python from
+http://www.python.org/ and Django from http://www.djangoproject.com/.
 Alternatively, you can download a packaged version of the entire
 application and use Python's ``distutils`` to install it::
 
-    wget http://django-template-utils.googlecode.com/files/template_utils-0.3.tar.gz
-    tar zxvf template_utils-0.3.tar.gz
-    cd template_utils-0.3
+    wget http://django-template-utils.googlecode.com/files/template_utils-0.4.tar.gz
+    tar zxvf template_utils-0.4.tar.gz
+    cd template_utils-0.4
     python setup.py install
 
 
 
 Currently, five main components are bundled into ``template_utils``:
 
-    * Template tags for `generic content retrieval`_.
+* Template tags for `generic content retrieval`_.
 
-    * Template tags for `robust comparison operations`_.
+* Template tags for `robust comparison operations`_.
 
-    * Template tags for `retrieving public comments`_ (for when a
-      comment-moderation system is in use).
+* Template tags for `retrieving public comments`_ (for when a
+  comment-moderation system is in use).
 
-    * A `generic text-to-HTML conversion system`_ with template filter
-      support.
+* Template tags for `retrieving and parsing RSS and Atom feeds`_
+  and displaying the results in template.
 
-    * A system for generating `template context processors`_ which can
-      add arbitrary settings to template contexts.
+* A `generic text-to-HTML conversion system`_ with template filter
+  support.
+
+* A system for generating `template context processors`_ which can
+  add arbitrary settings to template contexts.
+  
+* `Node classes`_ for simplifying some common types of custom
+  template tags.
+    
 
 .. _generic content retrieval: docs/generic_content.html
 .. _robust comparison operations: docs/comparison.html
 .. _retrieving public comments: docs/public_comments.html
+.. _retrieving and parsing RSS and Atom feeds: docs/feeds.html
 .. _generic text-to-HTML conversion system: docs/markup.html
-.. _template context processors: docs/context_processors.html
+.. _template context processors: docs/context_processors.html
+.. _Node classes: docs/nodes.html
+============================
+Parsing and displaying feeds
+============================
+
+
+Retrieving content from an RSS or Atom feed and displaying it in a
+page is a fairly common need; many sites, for example, syndicate
+content from affiliates or partners, and simply parse a feed to obtain
+data which is then displayed in a sidebar as "latest headlines" or
+similar.
+
+To facilitate this, ``template_utils`` includes a template tag library
+for parsing and displaying RSS and Atom feeds. To use these tags,
+you'll need to have ``template_utils`` in your ``INSTALLED_APPS``
+list, and you'll need to have ``{% load feeds %}`` in your template.
+
+
+``include_feed``
+================
+
+Parse an RSS or Atom feed and render a given number of its items
+into HTML.
+
+It is **highly** recommended that you use `Django's template
+fragment caching`_ to cache the output of this tag for a
+reasonable amount of time (e.g., one hour); polling a feed too
+often is impolite, wastes bandwidth and may lead to the feed
+provider banning your IP address.
+
+.. _Django's template fragment caching: http://www.djangoproject.com/documentation/cache/#template-fragment-caching
+
+Arguments should be:
+
+1. The URL of the feed to parse.
+
+2. The number of items to render (if not supplied, renders all
+   items in the feed).
+   
+3. The name of a template to use for rendering the results into HTML.
+
+The template used to render the results will receive two variables:
+
+``items``
+    A list of dictionaries representing feed items, each with 'title',
+    'summary', 'link' and 'date' members.
+
+``feed``
+    The feed itself, for pulling out arbitrary attributes.
+
+Requires the Universal Feed Parser, which can be obtained at
+http://feedparser.org/. See `its documentation`_ for details of the
+parsed feed object.
+
+.. _its documentation: http://feedparser.org/docs/
+
+Syntax::
+
+    {% include_feed [feed_url] [num_items] [template_name] %}
+
+Example::
+
+    {% include_feed "http://www2.ljworld.com/rss/headlines/" 10 feed_includes/ljworld_headlines.html %}
+
+
+``parse_feed``
+==============
+
+Parses a given feed and returns the result in a given context
+variable.
+
+It is **highly** recommended that you use `Django's template
+fragment caching`_ to cache the output of this tag for a
+reasonable amount of time (e.g., one hour); polling a feed too
+often is impolite, wastes bandwidth and may lead to the feed
+provider banning your IP address.
+
+.. _Django's template fragment caching: http://www.djangoproject.com/documentation/cache/#template-fragment-caching
+
+Arguments should be:
+
+1. The URL of the feed to parse.
+
+2. The name of a context variable in which to return the result.
+
+Requires the Universal Feed Parser, which can be obtained at
+http://feedparser.org/. See `its documentation`_ for details of the
+parsed feed object.
+
+.. _its documentation: http://feedparser.org/docs/
+
+Syntax::
+
+    {% parse_feed [feed_url] as [varname] %}
+
+Example::
+
+    {% parse_feed "http://www2.ljworld.com/rss/headlines/" as ljworld_feed %}
+
+

docs/generic_content.txt

 The finer-grained method involves subclassing the ``Node`` class
 common to most of the tags listed above; they're instances of
 ``template_utils.templatetags.generic_content.GenericContentNode``,
-which knows how to look for and apply lookup arguments found in the
-``GENERIC_CONTENT_LOOKUP_KWARGS`` setting. Two methods on
-``GenericContentNode`` are of interest:
-
-* ``__init__`` looks up the ``GENERIC_CONTENT_LOOKUP_KWARGS`` setting
-  and reads any appropriate filtering arguments out of it. Subclassing
-  and overriding ``__init__`` allows you to custmize this behavior.
-
-* ``_get_query_set`` is responsible for supplying the actual
-  ``QuerySet`` which will be used to retrieve objects from the
-  database.
-
-When overriding ``_get_query_set``, take note that at the time it is
-called, the ``GenericContentNode`` will have an instance variable
-``queryset``; this will be a ``QuerySet`` of the model
-specified in the tag call (with filtering applied as described above).
-
-The ``Node`` used for ``get_random_object`` and ``get_random_objects``
-(``RandomObjectNode``) is a useful example of this: it subclasses
-``GenericContentNode`` and overrides ``_get_query_set``, applying
-``order_by('?')`` to the ``queryset`` instance variable to obtain
-random ordering before returning the ``QuerySet``.
+which is documented in the file ``nodes.txt`` in this directory.
 
 The arguments to ``register`` are:
 
-    1. A string to use as the name of the filter.
+1. A string to use as the name of the filter.
 
-    2. The filter function.
+2. The filter function.
 
 Once the filter is registered, it can be used the same as the built-in
 filters::
 ``MARKUP_FILTER``; if you specify this setting, it should be a tuple
 with two elements:
 
-    1. The name of a filter to use.
+1. The name of a filter to use.
 
-    2. A dictionary of keyword arguments to pass to it.
+2. A dictionary of keyword arguments to pass to it.
 
 So to have Markdown with "safe mode" be the default behavior, you
 would add the following to your settings file::
 
 But remember that this will override *all* use of ``MARKUP_FILTER``,
 so any custom keyword arguments you specified there will not be used
-when you pass an argument to ``apply_markup``.
+when you pass an argument to ``apply_markup``.
+
+Also, note that on Django trunk templates automatically escape
+variable output by default; the ``apply_markup`` filter will mark its
+output as "safe" in order to avoid escaping of the generated HTML.
+============================
+Custom template node classes
+============================
+
+
+The core of Django's template system is the class
+``django.template.Node``; a Django template is, ultimately, a list of
+``Node`` instances, and it is the output of each ``Node``'s
+``render()`` method which becomes the final template output.
+
+As such, most custom template tags involve subclasses of ``Node``, and
+many classes of custom tags can be greatly simplified by providing an
+intermediate ``Node`` class which implements a generic form of the
+desired behavior. Included in ``template_utils.nodes`` are two
+``Node`` subclasses which follow that pattern.
+
+
+``template_utils.nodes.ContextUpdatingNode``
+============================================
+
+This is a ``Node`` subclass which simplifies the common case of
+writing a custom tag to add some values to the current template
+context.
+
+To use, import and subclass it, and -- rather than defining
+``render()`` as with a standard ``Node`` subclass -- define a method
+named ``get_content()``. This method should accept a ``Context``
+instance as its first positional argument, and should return a
+dictionary; the keys and values in that dictionary will be added to
+the context as new variables and values.
+
+
+``template_utils.nodes.GenericContentNode``
+===========================================
+
+Base Node class for retrieving objects from any model.
+
+By itself, this class will retrieve a number of objects from a
+particular model (specified by an "app_name.model_name" string)
+and store them in a specified context variable (these are the
+``num``, ``model`` and ``varname`` arguments to the constructor,
+respectively), but is also intended to be subclassed for
+customization.
+
+There are two ways to add extra bits to the eventual database
+lookup:
+
+1. Add the setting ``GENERIC_CONTENT_LOOKUP_KWARGS`` to your
+   settings file; this should be a dictionary whose keys are
+   "app_name.model_name" strings corresponding to models, and whose
+   values are dictionaries of keyword arguments which will be
+   passed to ``filter()``.
+
+2. Subclass and override ``_get_query_set``; all that's expected
+   is that it will return a ``QuerySet`` which will be used to
+   retrieve the object(s). The default ``QuerySet`` for the
+   specified model (filtered as described above) will be available
+   as ``self.query_set`` if you want to work with it.
+
+For finer-grained flexibility, override ``__init__()`` to control the
+manner in which lookup arguments are determined.

docs/public_comments.txt

-==========================
-Retrieving public comments
-==========================
-
-
-Given the amount of comment spam on the Web, it's increasingly common
-for sites to employ some form of comment moderation; for example,
-comments posted more than 30 days after a weblog entry's publication
-might be non-public until approved by the entry's author, or comments
-might be submitted to Akismet and marked non-public if Akismet thinks
-they're spam.
-
-Django's bundled comments system provides the ability to have
-mdoerated comments via the ``is_public`` field on both the ``Comment``
-and ``FreeComment`` models, but the built-in tags for retrieving lists
-of comments don't allow filtering on ``is_public`` or any other field;
-as a result, templates which display comment lists often have to
-include extra logic to weed out non-public comments.
-
-This library provides two tags which are nearly identical to Django's
-bundled ``get_comment_list`` and ``get_free_comment_list`` tags; the
-sole difference is that the tags included here will only retrive
-comments with ``is_public=True``.
-
-To use these tags, you'll need to have ``template_utils`` in your
-``INSTALLED_APPS`` list, and you'll need to have ``{% load
-public_comments %}`` in your template.
-
-
-``get_public_comment_list``
-===========================
-
-Like Django's built-in ``get_comment_list``, this tag retrieves
-instances of ``django.contrib.comments.models.Comment`` (which
-requires registration and login to post) associated with a particular
-object, but this tag filters for ``is_public=True``.
-
-Syntax::
-
-    {% get_public_comment_list for [app_name].[model_name] [object_id] as [varname] %}
-
-Example::
-
-    {% get_public_comment_list for weblog.entry entry.id as comment_list %}
-
-As with the ``get_comment_list`` tag, you can optionally pass an extra
-argument -- ``reversed`` -- at the end of the tag to get the list in
-reverse (i.e., newest comments first) order::
-
-    {% get_public_comment_list for weblog.entry entry.id as comment_list reversed %}
-
-
-``get_public_free_comment_list``
-================================
-
-Like Django's built-in ``get_free_comment_list``, this tag retrieves
-instances of ``django.contrib.comments.models.FreeComment`` (which
-allows anonymous commenting) associated with a particular object, but
-this tag filters for ``is_public=True``.
-
-Syntax::
-
-    {% get_public_free_comment_list for [app_name].[model_name] [object_id] as [varname] %}
-
-Example::
-
-    {% get_public_free_comment_list for weblog.entry entry.id as comment_list %}
-
-As with the ``get_free_comment_list`` tag, you can optionally pass an
-extra argument -- ``reversed`` -- at the end of the tag to get the
-list in reverse (i.e., newest comments first) order::
-
-    {% get_public_free_comment_list for weblog.entry entry.id as comment_list reversed %}
-
-
-``get_public_comment_count``
-============================
-
-Like Django's built-in ``get_comment_count``, this tag returns the
-count of (registered) comments attached to an object, but it only
-counts comments which have ``is_public=True``.
-
-Syntax::
-
-    {% get_public_comment_count for [app_name].[model_name] [object_id] as [varname] %}
-
-Example::
-
-    {% get_public_comment_count for weblog.entry entry.id as  comment_list %}
-
-
-``get_public_free_comment_count``
-=================================
-
-Like Django's built-in ``get_free_comment_count``, this tag returns
-the count of (unregistered) comments attached to an object, but it
-only counts comments which have ``is_public=True``.
-
-Syntax::
-
-    {% get_public_free_comment_count for [app_name].[model_name] [object_id] as [varname] %}
-
-Example::
-
-    {% get_public_free_comment_count for weblog.entry entry.id as  comment_list %}
 from distutils.core import setup
 
 setup(name='template_utils',
-      version='0.3',
+      version='0.4',
       description='Template-related utilities for Django applications',
       author='James Bennett',
       author_email='james@b-list.org',

template_utils/markup.py

 
 """
 
+
 def textile(text, **kwargs):
     """
     Applies Textile conversion to a string, and returns the HTML.
     and use the ``register`` method to add it; ``register`` expects
     two arguments:
     
-        1. The name to associate with the filter.
+    1. The name to associate with the filter.
     
-        2. The actual filter function.
+    2. The actual filter function.
     
     So, for example, you might define a new filter function called
     ``my_filter``, and register it like so::
     The filter to use for conversion is determined in either of two
     ways:
     
-        1. If the keyword argument ``filter_name`` is supplied, it
-           will be used as the filter name.
+    1. If the keyword argument ``filter_name`` is supplied, it will be
+       used as the filter name.
     
-        2. Absent an explicit argument, the filter name will be taken
-           from the ``MARKUP_FILTER`` setting in your Django settings
-           file (see below).
+    2. Absent an explicit argument, the filter name will be taken from
+       the ``MARKUP_FILTER`` setting in your Django settings file (see
+       below).
     
     Additionally, arbitrary keyword arguments can be supplied, and
     they will be passed on to the filter function.
     The Django setting ``MARKUP_FILTER`` can be used to specify
     default behavior; if used, its value should be a 2-tuple:
     
-        * The first element should be the name of a filter.
+    * The first element should be the name of a filter.
     
-        * The second element should be a dictionary to use as keyword
-          arguments for that filter.
+    * The second element should be a dictionary to use as keyword
+      arguments for that filter.
     
     So, for example, to have the default behavior apply Markdown with
     safe mode enabled, you would add this to your Django settings
     that, by always supplying ``filter_name`` explicitly, it is
     possible to use this formatter without configuring or even
     installing Django.
+
+
+    Django and template autoescaping
+    ================================
+
+    Django's template system defaults to escaping the output of
+    template variables, which can interfere with functions intended to
+    return HTML. ``MarkupFormatter`` does not in any way tamper with
+    Django's autoescaping, so pasing the results of formatting
+    directly to a Django template will result in that text being
+    escaped.
+
+    If you need to use ``MarkupFormatter`` for items which will be
+    passed to a Django template as variables, use the function
+    ``django.utils.safestring.mark_safe`` to tell Django's template
+    system not to escape that text.
+    
+    For convenience, a Django template filter is included (in
+    ``templatetags/generic_markup.py``) which applies
+    ``MarkupFormatter`` to a string and marks the result as not
+    requiring autoescaping.
     
     
     Examples
     """
     def __init__(self):
         self._filters = {}
-        for filter_name, filter_func in DEFAULT_MARKUP_FILTERS.iteritems():
+        for filter_name, filter_func in DEFAULT_MARKUP_FILTERS.items():
             self.register(filter_name, filter_func)
     
     def register(self, filter_name, filter_func):

template_utils/nodes.py

+"""
+Subclass of ``template.Node`` for easy context updating.
+
+"""
+
+from django.db.models import get_model
+from django.conf import settings
+from django import template
+
+
+class ContextUpdatingNode(template.Node):
+    """
+    Node that updates the context with certain values.
+    
+    Subclasses should define ``get_content()``, which should return a
+    dictionary to be added to the context.
+    
+    """
+    def render(self, context):
+        context.update(self.get_content(context))
+        return ''
+
+    def get_context(self, context):
+        raise NotImplementedError
+
+
+class GenericContentNode(ContextUpdatingNode):
+    """
+    Base Node class for retrieving objects from any model.
+
+    By itself, this class will retrieve a number of objects from a
+    particular model (specified by an "app_name.model_name" string)
+    and store them in a specified context variable (these are the
+    ``num``, ``model`` and ``varname`` arguments to the constructor,
+    respectively), but is also intended to be subclassed for
+    customization.
+
+    There are two ways to add extra bits to the eventual database
+    lookup:
+
+    1. Add the setting ``GENERIC_CONTENT_LOOKUP_KWARGS`` to your
+       settings file; this should be a dictionary whose keys are
+       "app_name.model_name" strings corresponding to models, and whose
+       values are dictionaries of keyword arguments which will be
+       passed to ``filter()``.
+
+    2. Subclass and override ``_get_query_set``; all that's expected
+       is that it will return a ``QuerySet`` which will be used to
+       retrieve the object(s). The default ``QuerySet`` for the
+       specified model (filtered as described above) will be available
+       as ``self.query_set`` if you want to work with it.
+    
+    """
+    def __init__(self, model, num, varname):
+        self.num = num
+        self.varname = varname
+        lookup_dict = getattr(settings, 'GENERIC_CONTENT_LOOKUP_KWARGS', {})
+        self.model = get_model(*model.split('.'))
+        if self.model is None:
+            raise template.TemplateSyntaxError("Generic content tag got invalid model: %s" % model)
+        self.query_set = self.model._default_manager.filter(**lookup_dict.get(model, {}))
+        
+    def _get_query_set(self):
+        return self.query_set
+    
+    def get_content(self, context):
+        query_set = self._get_query_set()
+        if self.num == 1:
+            result = query_set[0]
+        else:
+            result = list(query_set[:self.num])
+        return { self.varname: result }

template_utils/templatetags/comparison.py

 
 class ComparisonNode(template.Node):
     def __init__(self, var1, var2, comparison, nodelist_true, nodelist_false):
-        self.var1, self.var2 = var1, var2
+        self.var1 = template.Variable(var1)
+        self.var2 = template.Variable(var2)
         self.comparison = comparison
         self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
     
     def render(self, context):
-        # The values to compare may have been passed as template
-        # variables or as literal values, so resolve them before
-        # doing the comparison.
-        var1 = resolve_variable_or_literal(self.var1, context)
-        var2 = resolve_variable_or_literal(self.var2, context)
-        result = cmp(var1, var2)
-        if COMPARISON_DICT[self.comparison](result):
-            return self.nodelist_true.render(context)
+        try:
+            result = cmp(self.var1.resolve(context),
+                         self.var2.resolve(context))
+            if COMPARISON_DICT[self.comparison](result):
+                return self.nodelist_true.render(context)
+        # If either variable fails to resolve, return nothing.
+        except template.VariableDoesNotExist:
+            return ''
+        # If the types don't permit comparison, return nothing.
+        except TypeError:
+            return ''
         return self.nodelist_false.render(context)
 
 

template_utils/templatetags/feeds.py

+"""
+Tags which can retrieve and parse RSS and Atom feeds, and return the
+results for use in templates.
+
+"""
+
+import datetime
+import feedparser
+from django import template
+from django.template.loader import render_to_string
+
+from template_utils.nodes import ContextUpdatingNode
+
+
+class FeedIncludeNode(template.Node):
+    def __init__(self, feed_url, template_name, num_items=None):
+        self.feed_url = template.Variable(feed_url)
+        self.num_items = num_items
+        self.template_name = template_name
+
+    def render(self, context):
+        feed_url = self.feed_url.resolve(context)
+        feed = feedparser.parse(feed_url)
+        items = []
+        num_items = int(self.num_items) or len(feed['entries'])
+        for i in range(num_items):
+            pub_date = feed['entries'][i].updated_parsed
+            published = datetime.date(pub_date[0], pub_date[1], pub_date[2])
+            items.append({ 'title': feed['entries'][i].title,
+                           'summary': feed['entries'][i].summary,
+                           'link': feed['entries'][i].link,
+                           'date': published })
+        return render_to_string(self.template_name, { 'items': items,
+                                                      'feed': feed })
+
+
+class FeedParserNode(ContextUpdatingNode):
+    def __init__(self, feed_url, varname):
+        self.feed_url = template.Variable(feed_url)
+        self.varname = varname
+    
+    def get_content(self, context):
+        feed_url = self.feed_url.resolve(context)
+        return { self.varname: feedparser.parse(feed_url) }
+
+
+def do_include_feed(parser, token):
+    """
+    Parse an RSS or Atom feed and render a given number of its items
+    into HTML.
+    
+    It is **highly** recommended that you use `Django's template
+    fragment caching`_ to cache the output of this tag for a
+    reasonable amount of time (e.g., one hour); polling a feed too
+    often is impolite, wastes bandwidth and may lead to the feed
+    provider banning your IP address.
+    
+    .. _Django's template fragment caching: http://www.djangoproject.com/documentation/cache/#template-fragment-caching
+    
+    Arguments should be:
+    
+    1. The URL of the feed to parse.
+    
+    2. The number of items to render (if not supplied, renders all
+       items in the feed).
+       
+    3. The name of a template to use for rendering the results into HTML.
+    
+    The template used to render the results will receive two variables:
+    
+    ``items``
+        A list of dictionaries representing feed items, each with
+        'title', 'summary', 'link' and 'date' members.
+    
+    ``feed``
+        The feed itself, for pulling out arbitrary attributes.
+    
+    Requires the Universal Feed Parser, which can be obtained at
+    http://feedparser.org/. See `its documentation`_ for details of the
+    parsed feed object.
+    
+    .. _its documentation: http://feedparser.org/docs/
+    
+    Syntax::
+    
+        {% include_feed [feed_url] [num_items] [template_name] %}
+    
+    Example::
+    
+        {% include_feed "http://www2.ljworld.com/rss/headlines/" 10 feed_includes/ljworld_headlines.html %}
+    
+    """
+    bits = token.contents.split()
+    if len(bits) == 3:
+        return FeedIncludeNode(feed_url=bits[1], template_name=bits[2])
+    elif len(bits) == 4:
+        return FeedIncludeNode(feed_url=bits[1], num_items=bits[2], template_name=bits[3])
+    else:
+        raise template.TemplateSyntaxError("'%s' tag takes either two or three arguments" % bits[0])
+
+def do_parse_feed(parser, token):
+    """
+    Parses a given feed and returns the result in a given context
+    variable.
+    
+    It is **highly** recommended that you use `Django's template
+    fragment caching`_ to cache the output of this tag for a
+    reasonable amount of time (e.g., one hour); polling a feed too
+    often is impolite, wastes bandwidth and may lead to the feed
+    provider banning your IP address.
+    
+    .. _Django's template fragment caching: http://www.djangoproject.com/documentation/cache/#template-fragment-caching
+    
+    Arguments should be:
+    
+    1. The URL of the feed to parse.
+    
+    2. The name of a context variable in which to return the result.
+    
+    Requires the Universal Feed Parser, which can be obtained at
+    http://feedparser.org/. See `its documentation`_ for details of the
+    parsed feed object.
+    
+    .. _its documentation: http://feedparser.org/docs/
+    
+    Syntax::
+    
+        {% parse_feed [feed_url] as [varname] %}
+    
+    Example::
+    
+        {% parse_feed "http://www2.ljworld.com/rss/headlines/" as ljworld_feed %}
+    
+    """
+    bits = token.contents.split()
+    if len(bits) != 4:
+        raise template.TemplateSyntaxError(u"'%s' tag takes three arguments" % bits[0])
+    return FeedParserNode(bits[1], bits[3])
+
+register = template.Library()
+register.tag('include_feed', do_include_feed)
+register.tag('parse_feed', do_parse_feed)

template_utils/templatetags/generic_content.py

 
 
 from django import template
-from django.conf import settings
 from django.db.models import get_model
 
-
-class GenericContentNode(template.Node):
-    """
-    Base Node class for retrieving objects from any model.
-
-    By itself, this class will retrieve a number of objects from a
-    particular model (specified by an "app_name.model_name" string)
-    and store them in a specified context variable (these are the
-    ``num``, ``model`` and ``varname`` arguments to the constructor,
-    respectively), but is also intended to be subclassed for
-    customization.
-
-    There are two ways to add extra bits to the eventual database lookup:
-
-    1. Add the setting ``GENERIC_CONTENT_LOOKUP_KWARGS`` to your
-       settings file; this should be a dictionary whose keys are
-       "app_name.model_name" strings correponding to models, and whose
-       values are dictionaries of keyword arguments which will be
-       passed to ``filter()``.
-
-    2. Subclass and override ``_get_query_set``; all that's expected
-       is that it will return a ``QuerySet`` which will be used to
-       retrieve the object(s) he default ``QuerySet`` for the
-       specified model (filtered as described above) will be available
-       as ``self.query_set`` if you want to work with it.
-    
-    """
-    def __init__(self, model, num, varname):
-        self.model, self.num, self.varname = model, int(num), varname
-        lookup_dict = getattr(settings, 'GENERIC_CONTENT_LOOKUP_KWARGS', {})
-        model = get_model(*self.model.split('.'))
-        if model is None:
-            raise template.TemplateSyntaxError("Generic content tag got invalid model: %s" % self.model)
-        self.query_set = model._default_manager.filter(**lookup_dict.get(self.model, {}))
-        
-    def _get_query_set(self):
-        return self.query_set
-    
-    def render(self, context):
-        query_set = self._get_query_set()
-        if self.num == 1:
-            context[self.varname] = query_set[0]
-        else:
-            context[self.varname] = list(query_set[:self.num])
-        return ''
+from template_utils.nodes import ContextUpdatingNode, GenericContentNode
 
 
 class RandomObjectsNode(GenericContentNode):
         return self.query_set.order_by('?')
 
 
-class RetrieveObjectNode(template.Node):
+class RetrieveObjectNode(ContextUpdatingNode):
     """
     ``Node`` subclass which retrieves a single object -- by
     primary-key lookup -- from a given model.
     
     """
     def __init__(self, model, pk, varname):
-        self.model, self.pk, self.varname = model, pk, varname
+        self.pk = template.Variable(pk)
+        self.varname = varname
+        self.model = get_model(*model.split('.'))
+        if self.model is None:
+            raise template.TemplateSyntaxError("Generic content tag got invalid model: %s" % model)
     
-    def render(self, context):
-        model = get_model(*self.model.split('.'))
-        if model is None:
-            raise template.TemplateSyntaxError("Generic content tag got invalid model: %s" % self.model)
-        context[self.varname] = model._default_manager.get(pk=self.pk)
-        return ''
+    def get_content(self, context):
+        return { self.varname: self.model._default_manager.get(pk=self.pk.resolve(context))}
 
 
 def do_latest_object(parser, token):

template_utils/templatetags/generic_markup.py

 
 from django.conf import settings
 from django.template import Library
+from django.utils.safestring import mark_safe
 
 from template_utils.markup import formatter
 
     
     """
     if arg is not None:
-        return formatter(value, filter_name=arg)
+        return mark_safe(formatter(value, filter_name=arg))
     return formatter(value)
 
 def smartypants(value):
     except ImportError:
         if settings.DEBUG:
             raise template.TemplateSyntaxError("Error in smartypants filter: the Python smartypants module is not installed or could not be imported")
-        return value
+        return mark_safe(value)
     else:
-        return smartyPants(value)
+        return mark_safe(smartyPants(value))
 
 register = Library()
 register.filter(apply_markup)

template_utils/utils.py

-"""
-Utility functions for working with templates and for writing custom
-template tags.
-
-"""
-
-from django import template
-
-
-def resolve_variable_or_literal(path, context):
-    """
-    Given a string and a template context, tries to return the most
-    appropriate resolution of that string for that context.
-
-    Tries the following steps, in order:
-
-        1. Call ``template.resolve_variable``; if it succeeds, return
-           that value.
-
-        2. Check to see if the string is numeric; if so, return it
-           converted to an ``int``.
-
-        3. If both of the above fail, return the string as-is.
-    
-    """
-    try:
-        result = template.resolve_variable(path, context)
-    except template.VariableDoesNotExist:
-        if path.isdigit():
-            result = int(path)
-        else:
-            result = path
-    return result