27 February

Don’t hard-code URLs

What do we mean by hard-coded URLs? Here’s an example of something you might have in your book.html template:

<li><a href="/user/{{b.user.id}}/">{{b.user}}</a>

That URL in the href is meant to match up with this entry in the urlpatterns:

    url(r'^user/(d+)/', 'app.views.userprofile'),

But what if we want to modify that URL someday? We’d have to remember to do it everywhere that it is used, and it might be hard to find them all.

Instead, we can have Django generate the URL from whatever we specify in urlpatterns. The modified href would look like this:

    <a href="{% url app.views.userprofile b.user.id %}">

This {% url %} syntax refers instead to the name of the view function, and then provides any required parameters afterward, separated by spaces. It expands to /book/1/ just like before, but now if we wanted to change that we can just modify it in urls.py

There’s even a slightly nicer way, if we’d like to specify a name for the URL apart from the name of its view function. In urls.py, you would add a name= parameter, like this:

    url(r'^user/(d+)/', 'app.views.userprofile', name='user'),

And now in the {% url %} tag, we can just refer to that name instead of the full view function name:

    <a href="{% url user b.user.id %}">

The {% url %} syntax only works in a template. If you need to generate the full URL from Python code, you can try something like this:

from django.core.urlresolvers import reverse
url = reverse('user', b.user.id)

Form processing

A form class looks a lot like a model class — here is app/forms.py:

from django import forms

class BookForm (forms.Form):
    isbn = forms.CharField(max_length='20')
    title = forms.CharField(max_length='80')
    paperback = forms.BooleanField(required=False)

We set up a URL which will display the form (on a GET) or process the form (on a POST):

    url(r'^bookform/', 'app.views.bookform', name='bookform'),

Then, in app/views.py, the basic logic for form processing looks like this:

def bookform(request):
    if request.method == 'POST':      # Is user submitting form?
        form = BookForm(request.POST) # Bind with user's data
        if form.is_valid():           # Check validity
            # Form data are valid, process form (add new Book)
            b = Book(isbn = form.cleaned_data['isbn'],
                     title = form.cleaned_data['title'])
            b.save()
            # After successful processing, it's common to redirect
            # to another page
            return HttpResponseRedirect(reverse('book', args=[b.id]))
    else:
        form = BookForm()   # Unbound (blank) form
    # At this point, either form was blank, or it has errors.
    # Either way, display it using a template
    return render_to_response('bookform.html', {'form': form},
                context_instance=RequestContext(request))

And here is templates/bookform.html. In addition to {{form.as_p}}, we have to wrap it in a normal HTML <form> tag and provide the submit button. We also include the csrf_token to prevent cross-site request forgery (a common security hole in web sites).

My book form template.

<form method="POST">
  {{form.as_p}}
  {% csrf_token %}
  <input type="submit" value="Submit" />
</form>

Template inheritance

With this feature, we can define a base.html containing the basic structure of our web pages, including headers, footers etc. Then we can define blocks within that page that can be replaced in derived templates.

Here’s what base.html would look like:

<html>
  <head>
    <title>LIU Bookswap</title>
  </head>
  <body>
    <h1>LIU Bookswap</h1>

    {% if user.is_authenticated %}
      <p>You are logged in as {{user}}. <a href="">Log out</a></p>
    {% else %}
      <p>You are not logged in. <a href="">Log in</a></p>
    {% endif %}

    {% block content %}
    {% endblock %}
    <hr />
    <p>Brought to you by CS164.</p>
  </body>
</html>

And here is a derived template, for example search.html, that defines the material to be plugged into the content block:

{% extends "base.html" %}

{% block content %}
<h2>Search Results</h2>

<ul>
{% for r in results %}
<li>
<a href="/book/{{r.id}}/">{{r.title}}</a>
by {{r.authors}} ({{r.isbn}})
</li>
{% endfor %}
</ul>
{% endblock %}

To support the “You are logged in/not logged in” feature of the base page, we need to always pass a user parameter into render_to_response, like this:

    return render_to_response('search.html', {'results': results,
                                              'user': request.user})

Search query tip (for M4)

Tonight, Josh noticed that if you leave a __contains query blank (such as title or author), then it ends up matching ALL of the books in your DB. This is because the empty string is contained within any string. So author__contains='' is always true.

You can compensate for this (and also do some other fancier tricks) by building up the query parameters incrementally, and using conditions to add parts of it that only happen if a search parameter is non-empty.

For example, in this code, we build the query in a variable q and then combine it with authors__contains only if request.GET['authors'] is non-empty:

    q = (Q(isbn=request.GET['isbn']) |
         Q(title__contains=request.GET['title']) |
         Q(course__prefix=request.GET['prefix']) |
         Q(course__number=request.GET['number']))
    if request.GET['authors']:
        q = q | Q(authors__contains=request.GET['authors'])
    results = Book.objects.filter(q).distinct()
    return render_to_response('search.html', {'results': results})

You can try similar patterns to get more sophisticated searches.

comments powered by Disqus

 

©2012 Christopher League · some rights reserved · CC by-sa