Created
June 4, 2009 12:55
-
-
Save zacharyvoase/123605 to your computer and use it in GitHub Desktop.
Django template tag for building URLs.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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