41

I have URLs like http://example.com/depict?smiles=CO&width=200&height=200 (and with several other optional arguments)

My urls.py contains:

urlpatterns = patterns('',
    (r'^$', 'cansmi.index'),
    (r'^cansmi$', 'cansmi.cansmi'),
    url(r'^depict$', cyclops.django.depict, name="cyclops-depict"),

I can go to that URL and get the 200x200 PNG that was constructed, so I know that part works.

In my template from the "cansmi.cansmi" response I want to construct a URL for the named template "cyclops-depict" given some query parameters. I thought I could do

{% url cyclops-depict smiles=input_smiles width=200 height=200 %}

where "input_smiles" is an input to the template via a form submission. In this case it's the string "CO" and I thought it would create a URL like the one at top.

This template fails with a TemplateSyntaxError:

Caught an exception while rendering: Reverse for 'cyclops-depict' with arguments '()' and keyword arguments '{'smiles': u'CO', 'height': 200, 'width': 200}' not found.

This is a rather common error message both here on StackOverflow and elsewhere. In every case I found, people were using them with parameters in the URL path regexp, which is not the case I have where the parameters go into the query.

That means I'm doing it wrong. How do I do it right? That is, I want to construct the full URL, including path and query parameters, using something in the template.

For reference,

% python manage.py shell
Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.core.urlresolvers import reverse
>>> reverse("cyclops-depict", kwargs=dict())
'/depict'
>>> reverse("cyclops-depict", kwargs=dict(smiles="CO"))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Library/Python/2.6/site-packages/django/core/urlresolvers.py", line 356, in reverse
    *args, **kwargs)))
  File "/Library/Python/2.6/site-packages/django/core/urlresolvers.py", line 302, in reverse
    "arguments '%s' not found." % (lookup_view_s, args, kwargs))
NoReverseMatch: Reverse for 'cyclops-depict' with arguments '()' and keyword arguments '{'smiles': 'CO'}' not found.
John Carter
  • 53,924
  • 26
  • 111
  • 144
Andrew Dalke
  • 14,889
  • 4
  • 39
  • 54

6 Answers6

57

Building an url with query string by string concatenation as suggested by some answers is as bad idea as building SQL queries by string concatenation. It is complicated, unelegant and especially dangerous with a user provided (untrusted) input. Unfortunately Django does not offer an easy possibility to pass query parameters to the reverse function.

Python standard urllib however provides the desired query string encoding functionality.

In my application I've created a helper function:

def url_with_querystring(path, **kwargs):
    return path + '?' + urllib.urlencode(kwargs) # for Python 3, use urllib.parse.urlencode instead

Then I call it in the view as follows:

quick_add_order_url = url_with_querystring(reverse(order_add),
    responsible=employee.id, scheduled_for=datetime.date.today(),
    subject='hello world!')
# http://localhost/myapp/order/add/?responsible=5&
#     scheduled_for=2011-03-17&subject=hello+world%21

Please note the proper encoding of special characters like space and exclamation mark!

Flimm
  • 136,138
  • 45
  • 251
  • 267
geekQ
  • 29,027
  • 11
  • 62
  • 58
21

Your regular expresion has no place holders (that's why you are getting NoReverseMatch):

url(r'^depict$', cyclops.django.depict, name="cyclops-depict"),

You could do it like this:

{% url cyclops-depict %}?smiles=CO&width=200&height=200

URLconf search does not include GET or POST parameters

Or if you wish to use {% url %} tag you should restructure your url pattern to something like

r'^depict/(?P<width>\d+)/(?P<height>\d+)/(?P<smiles>\w+)$' 

then you could do something like

{% url cyclops-depict 200 200 "CO" %}

Follow-up:

Simple example for custom tag:

from django.core.urlresolvers import reverse
from django import template
register = template.Library()

@register.tag(name="myurl")
def myurl(parser, token):
    tokens = token.split_contents()
    return MyUrlNode(tokens[1:])

class MyUrlNode(template.Node):
    def __init__(self, tokens):
        self.tokens = tokens
    def render(self, context):
        url = reverse('cyclops-depict')
        qs = '&'.join([t for t in self.tokens])
        return '?'.join((url,qs))

You could use this tag in your templates like so:

{% myurl width=200 height=200 name=SomeName %}

and hopefully it should output something like

/depict?width=200&height=200&name=SomeName
Davor Lucic
  • 28,970
  • 8
  • 66
  • 76
  • 3
    That's rather inelegant, which is why I thought there must be a better solution. Some of the parameters come from form entry. The actual template expression would be {% url cyclops-depict %}?smiles={{input_smiles}}&width={{size}}&height={{size}} . That's harder to understand and explain than the invalid/hypothetical {% query-url cyclops-depict smiles=input_smiles width=size height=size %}. Having the patterns in the URL is of course possible but 7 of the 8 parameters are optional and there's no natural ordering, making it rather forced. (Grrr. And Django's supposed to be for perfectionists.) – Andrew Dalke May 06 '10 at 11:00
  • If you want to encapsulate the logic for building URLs you could just write your own [custom templatetag][1]. Make it take parameters such as `entry` or even a complete context and return built URL. This way you could even emulate `url` tag and have the syntax you like.. [1]: http://docs.djangoproject.com/en/dev/howto/custom-template-tags/#custom-template-tags-and-filters – Davor Lucic May 06 '10 at 11:44
  • I'm getting back to this project. One thing is I'm teaching this to non-software developers (they are computational chemists who do some programming) and I don't want to explain all this. I'll have to think about this some more. Thanks for the followup! – Andrew Dalke May 13 '10 at 17:23
19

I recommend to use builtin django's QueryDict. It also handles lists properly. End automatically escapes some special characters (like =, ?, /, '#'):

from django.http import QueryDict
from django.core.urlresolvers import reverse

q = QueryDict('', mutable=True)
q['some_key'] = 'some_value'
q.setlist('some_list', [1,2,3])
'%s?%s' % (reverse('some_view_name'), q.urlencode())
# '/some_url/?some_list=1&some_list=2&some_list=3&some_key=some_value'

q.appendlist('some_list', 4)
q['value_with_special_chars'] = 'hello=w#rld?'
'%s?%s' % (reverse('some_view_name'), q.urlencode())
# '/some_url/?value_with_special_chars=hello%3Dw%23rld%3F&some_list=1&some_list=2&some_list=3&some_list=4&some_key=some_value'

To use this in templates you will need to create custom template tag

imposeren
  • 4,142
  • 1
  • 19
  • 27
7

Working variation of previous answers and my experience with dealing this stuff.

from django.urls import reverse
from django.utils.http import urlencode

def build_url(*args, **kwargs):
    params = kwargs.pop('params', {})
    url = reverse(*args, **kwargs)
    if params:
        url += '?' + urlencode(params)
    return url

How to use:

>>> build_url('products-detail', kwargs={'pk': 1}, params={'category_id': 2})
'/api/v1/shop/products/1/?category_id=2'
michal-michalak
  • 827
  • 10
  • 6
5

Neither of the original answers addresses the related issue resolving URLs in view code. For future searchers, if you are trying to do this, use kwargs, something like:

reverse('myviewname', kwargs={'pk': value})
ghickman
  • 5,893
  • 9
  • 42
  • 51
Aitch
  • 934
  • 11
  • 20
5

The answer that used urllib is indeed good, however while it was trying to avoid strings concatenation, it used it in path + '?' + urllib.urlencode(kwargs). I believe this may create issues when the path has already some query parmas.

A modified function would look like:

def url_with_querystring(url, **kwargs):
    url_parts = list(urlparse.urlparse(url))
    query = dict(urlparse.parse_qsl(url_parts[4]))
    query.update(kwargs)
    url_parts[4] = urllib.urlencode(query)
    return urlparse.urlunparse(url_parts)
Nour Wolf
  • 2,140
  • 25
  • 24