Skip to content

Instantly share code, notes, and snippets.

@zacharyvoase
Created June 4, 2009 12:55
Show Gist options
  • Save zacharyvoase/123605 to your computer and use it in GitHub Desktop.
Save zacharyvoase/123605 to your computer and use it in GitHub Desktop.
Django template tag for building URLs.
# -*- coding: utf-8 -*-
# Copyright (c) 2009 Zachary Voase
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
"""
buildurl.py - Django template tag for building URLs.
Please note that this requires the URLObject library, also by Zachary Voase,
which can be found at http://github.com/disturbyte/urlobject, or installed via
``easy_install URLObject``. Also, to install/use this template tag, drop this
source file into a ``templatetags`` package inside an installed app on your
Django project, then use ``{% load buildurl %}`` in your templates to load it.
The ``{% buildurl %}`` template tag is used to create and modify URLs from
within your Django templates. At the moment, it’s relatively difficult to do
so from within a template. Let’s say, for example, that you have a view which
represents both a search and sort. Users can click on one of a list of search
terms and they'll be taken to a page with the same ordering but a different
search term, or they can click on one of a list of sorting parameters and see
the same search sorted differently. This is nearly impossible to do inside
templates alone, and any developer trying to solve it will no doubt have to
create a custom filter or template tag which will only solve this problem.
Luckily, however, this sort of thing is easy with BuildURL. Consider a
template which is rendered with ``search_terms`` (a list of possible search
terms as strings), ``sort_params`` (a list of sorting parameters), and
``current_url`` (the URL the user is currently looking at). To achieve the
solution to the problem described above, a template might look something like
this:
{% load buildurl %}
{% for search_term in search_terms %}
<a href="{% buildurl %}
<url from="{{ current_url }}">
<query-append key="search">{{ search_term }}</query-append>
</url>
{% endbuildurl %}" class="search">{{ search_term }}</a>
{% endfor %}
{% for sort_param in sort_params %}
<a href="{% buildurl %}
<url from="{{ current_url }}">
<query-append key="sort">{{ sort_param }}</query-append>
</url>
{% endbuildurl %}" class="sort">{{ sort_param }}</a>
{% endfor %}
The root ``url`` element is needed, and can optionally have a ``from``
attribute with the URL to modify. Alternatively, you can leave out any
attributes from the ``<url>`` tag and build a URL from scratch, or you can
even specify an ``as="foobar"`` attribute which, rather than rendering the URL
into the template, will store the resulting URLObject instance in the context
as ``foobar``.
There are several elements which you can put into the body of the XML. There
are six basic ones which will set an attribute of the URL to whatever text is
inside them; these are ``host``, ``port``, ``path``, ``fragment``, ``scheme``
and ``query``. These are useful when building up a URL from scratch.
There are two other accepted elements: ``path-append`` and ``query-append``.
The first of these is useful; it takes a string and adds it on to the current
path. For example:
<url from="/foo">
<path-append>bar</path-append>
</url>
Will render as ``'/foo/bar'``. If you want a trailing slash, just add one on
to the end of the string (i.e.``<path-append>bar/</path-append>``).
The latter is slightly more complex. ``query-append`` takes a required ``key``
attribute as well as the text inside the tag, and adds a query parameter on to
the current URL. For example:
<url from="/foo">
<query-append key="spam">eggs</query-append>
</url>
Will render as ``'/foo?spam=eggs'``. It also accepts another attribute,
``override``, which should be set to either ``"true"`` or ``"false"``. By
default, ``query-append`` behaves as if it were set to ``"true"``. The
behaviour with each of these can be demonstrated like so:
<url from="/foo">
<query-append key="spam">eggs</query-append>
<query-append key="spam" override="true">ham</query-append>
</url>
This will render as ``'/foo?spam=ham'``. Conversely:
<url from="/foo">
<query-append key="spam">eggs</query-append>
<query-append key="spam" override="false">ham</query-append>
</url>
This will render as ``'/foo?spam=eggs&spam=ham``.
If you choose to specify the ``as`` attribute on the ``<url>`` tag, the
behaviour of ``buildurl`` will change. Whereas normally it would render the
URL in the place where ``{% buildurl %}`` was called, with ``as`` it stores
the ``URLObject`` instance in the context under the specified name. With this,
the example presented at the beginning could be rewritten like so:
{% load buildurl %}
{% for search_term in search_terms %}
{% buildurl %}
<url from="{{ current_url }}" as="search_url">
<query-append key="search">{{ search_term }}</query-append>
</url>
{% endbuildurl %}
<a href="{{ search_url }}" class="search">{{ search_term }}</a>
{% endfor %}
{% for sort_param in sort_params %}
{% buildurl %}
<url from="{{ current_url }}" as="sort_url">
<query-append key="sort">{{ sort_param }}</query-append>
</url>
{% endbuildurl %}
<a href="{{ sort_url }}" class="sort">{{ sort_param }}</a>
{% endfor %}
You can then access all the regular ``URLObject`` methods and attributes from
the template code. For more information, I recommend you consult the
documentation for the URLObject library, which can be found at
http://github.com/disturbyte/urlobject.
"""
from xml.dom import minidom
from django import template
from urlobject import URLObject
register = template.Library()
# These all represent `with_ATTR()` methods on `URLObject` instances.
WITH_ATTRS = ('host', 'port', 'path', 'fragment', 'scheme', 'query')
class BuildURLNode(template.Node):
def __init__(self, template):
self.template = template
def render(self, context):
xml_data = self.template.render(context)
url, var_name = parse_url_from_xml(xml_data)
if var_name is not None:
context[var_name] = url
return u''
else:
return unicode(parse_url_from_xml(xml_data))
@classmethod
def build_url(cls, parser, token):
nodelist = parser.parse(('endbuildurl',))
parser.delete_first_token()
return cls(nodelist)
register.tag('buildurl', BuildURLNode.build_url)
def text(node):
if isinstance(node, minidom.Text):
return node.wholeText
else:
return ''.join(text(child) for child in node.childNodes)
def parse_url_from_xml(data):
""""""
# Parse the document.
url_element = minidom.parseString(data).documentElement
# It is possible to specify a base URL to modify. Otherwise, we start with
# an empty `URLObject` instance.
from_url = url_element.attributes.get('from', None)
if from_url:
url = URLObject.parse(from_url.value)
else:
url = URLObject()
var_name = url_element.attributes.get('as', None)
if var_name:
var_name = var_name.value
else:
var_name = None
for element in url_element.childNodes:
if isinstance(element, minidom.Text):
# Text elements between child nodes of <url> are probably junk,
# and invalid anyway.
pass
elif element.tagName in WITH_ATTRS:
# Examples:
# '<port>3309</port>' => url.with_port('3309')
# '<host>google.com</host> => url.with_host('google.com')
# '<scheme>https</scheme> => url.with_scheme('https')
url = getattr(url, 'with_' + element.tagName)(
text(element).strip())
elif element.tagName == 'query-append':
# Examples:
# '<query-append key="foo">bar</query-append>'
# => url | ('foo', 'bar')
# '<query-append key="foo" overwrite="false">bar</query-append>'
# => url & ('foo', 'bar')
key = element.attributes['key'].value
value = text(element).strip()
overwrite = element.attributes.get('overwrite', None)
if overwrite and (overwrite.value.lower() == 'true'):
url |= (key, value)
else:
url &= (key, value)
elif element.tagName == 'path-append':
# Example:
# '<path-append>foobar</path-append>' => url / 'foobar'
url /= text(element).strip()
return (url, var_name)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment