Creating URLs in Django

Up until now, we have been hardcoding URLs in our templates. At a later date, If we want to update the URL structure we have manually visit each and every templates to update the URL. This problem can be solved easily by using url tag in our templates.

url tag #

The url tag helps us to generate links in the templates. It has the following syntax:

Syntax: {% url 'url_name' arg1 arg2 %}

where url_name is the value we passed to the name keyword argument of the url() function. The arg1 and arg1 are additional arguments required by the view function. On success, it returns part of the URL without host portion. If it can't create URL NoReverseMatch exception is thrown.

At this point urls.py file in blog app looks like this:

TGDB/django_project/blog/urls.py

urlpatterns = [
    url(r'^category/(?P<category_slug>[\w-]+)/$', views.post_by_category, name='post_by_category'),
    url(r'^tag/(?P<tag_slug>[\w-]+)/$', views.post_by_tag, name='post_by_tag'),
    url(r'^(?P<pk>\d+)/$', views.post_detail, name='post_detail'),
    url(r'^$', views.post_list, name='post_list'),
]

The following code will create URL for post_list URL pattern.

{% url 'post_list' %}

Notice that we are not passing any arguments to url tag because post_list URL pattern doesn't accepts any. In other words, post_list() view function doesn't not accept any additional arguments apart from request.

Inside the template the above code output would output '/'. We can verify this in the Django Shell.

>>>
>>> from django import template
>>> t = template.Template("{% url 'post_list' %}")
>>> c = template.Context({})
>>> t.render(c)
'/'
>>>

In case specified URL pattern doesn't exists, then the url tag throws a NoReverseMatch exception.

>>>
>>> t = template.Template("{% url 'url_pattern_dont_exists' %}")
>>> c = template.Context({})
>>> t.render(c)
Traceback (most recent call last):
...
django.urls.exceptions.NoReverseMatch: Reverse for 'url_pattern_dont_exits' with
 arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []
>>>

The reverse() method #

What if need arises to generate URLs in the in the Python code, for example in a view function ? To create URLs in Python code we use reverse() method. It accepts name attribute of the URL pattern. To use this method we first have to import it from django.urls. Just like url tag on success it returns part of the URL without host, otherwise NoReverseMatch exception is thrown.

>>>
>>> from django.urls import reverse
>>> reverse('post_list')
'/'
>>>
>>> reverse('url_pattern_dont_exists')
Traceback (most recent call last):
...  
django.urls.exceptions.NoReverseMatch: Reverse for 'url_pattern_dont_exits' with
 arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []
>>>

Passing arguments to url tag and reverse method #

Consider the following URL pattern.

url(r'^category/(?P<category>[\w-]+)/$', views.posts_by_category, name='posts_by_category'),

At first you might think, just like above, we can easily create URL for this pattern in our template like this:

{% url 'post_by_category' %}

But you would be wrong. In this case (?P<category>[\w-]+) part of regular expression is unknown, so we have to pass one additional parameter to the url tag to generate the complete URL.

{% url 'post_by_category' 'tag' %}
>>>
>>> t = template.Template("{% url 'post_by_category' 'python' %}")
>>> t.render(template.Context({}))
'/category/python/'
>>>

We can also pass additional arguments as variables like this:

{% url 'post_by_category' cat %}

Notice there is no quotation marks around cat.

>>>
>>> t = template.Template("{% url 'post_by_category' cat %}")
>>> t.render(template.Context({'cat': 'java'}))
'/category/java/'
>>>

If URL pattern requires more than one arguments, then separate each argument by a space character.

{% url 'new_patter' arg1 arg2 arg2 %}

Similarly, In case the URL patterns needs, we can pass additional arguments to the reverse() method as follows:

reverse('url_pattern', args=['arg1', 'arg2'])

Here is an example.

>>>
>>> reverse('post_by_category', args=['css'])
'/category/css/'
>>>
>>>

We can also pass additional argument as keyword arguments like this:

>>>
>>> reverse('post_by_category', kwargs={'category_slug': 'css'})
'/category/css/'
>>>
>>>

Notice that the name of the key in the kwargs dictionary should match the named group in the URL pattern. Otherwise, you would get NoReverseMatch exception.

Let's update post_list.html, post_detail.html, post_by_category.html and post_by_tag.html templates to use url tags as follows. Note that only the changed code is shown.

TGDB/django_project/blog/templates/blog/post_list.html

  ...
{% block content %}    
    <div class="content">
        <div class="section-inner clearfix">        

        {% for post in posts %}
            <h3>
                <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                <span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
                <span>Category : <a href="{% url 'post_by_category' post.category.slug %}">{{ post.category }}</a></span> |
                <span>Tag :
                    {% for tag in post.tags.all %}
                        <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
                    {% empty %}
                        None
                    {% endfor %}
                </span>
            </p>
        {% empty %}
            <p>No posts exits</p>
        {% endfor %}
        </div>
    </div>         
{% endblock %}
...

TGDB/django_project/blog/templates/blog/post_detail.html

...
{% block content %}
    <div class="content">
        <div class="section-inner clearfix">
            <h1>{{ post.title|capfirst }}</h1>
            <p class="post-info">
                <span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
                <span>Category : <a href="{% url 'post_by_category' post.category.slug %}">{{ post.category }}</a></span> |
                <span>Tag :
                    {% for tag in post.tags.all %}
                        <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
                    {% empty %}
                        None
                    {% endfor %}
                </span>
            </p>
            <p>
                {{ post.content }}
            </p>
        </div>
    </div>    
{% endblock %}
...

TGDB/django_project/blog/templates/blog/post_by_category.html

...
{% block content %}
    <div class="content">
        <div class="section-inner clearfix">
        <h4>All the posts under category - {{ category.name }}</h4>
        {% for post in posts %}
            <h3>
                <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                <span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
                <span>Category : <a href="{% url 'post_by_category' post.category.slug %}">{{ post.category }}</a></span> |
                <span>Tag :
                {% for tag in post.tags.all %}
                    <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
                {% empty %}
                    None
                {% endfor %}
                </span>
            </p>
        {% endfor %}
        </div>
    </div>         
{% endblock %}
...

TGDB/django_project/blog/templates/blog/post_by_tag.html

...
{% block content %}
    <div class="content">
        <div class="section-inner clearfix">
            <h4>All the posts tagged with - {{ tag.name }}</h4>
            {% for post in posts %}
            <h3>
                <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                <span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
                <span>Category : <a href="{% url 'post_by_category' post.category.slug %}">{{ post.category }}</a></span> |
                <span>Tag :
                    {% for tag in post.tags.all %}
                        <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
                    {% empty %}
                        None
                    {% endfor %}
                </span>
            </p>
            {% endfor %}
        </div>
    </div>         
{% endblock %}
...

Eliminating Redundancy #

If you pay close attention to the templates we just modified you will find that all of the four template shares the same .post-info class. The problem is that if we add or update something inside .post-info class then we would have to revisit every template to apply the changes, which is very bad. Instead, It would be much better to create a separate template for .posts-info and then include this template in the each of our existing templates, this way we only have to update .post-info class at one place and all other templates will pick the changes automatically.

Create a new template called post_info.html and add the following code to it.

TGDB/django_project/blog/templates/blog/post_info.html

<span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
<span>Category : <a href="{% url 'post_by_category' post.category.slug %}">{{ post.category }}</a></span> |
<span>Tag :
  {% for tag in post.tags.all %}
      <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
  {% empty %}
      None
  {% endfor %}
</span>

Then include this template in all other templates using the include tag as follows:

TGDB/django_project/blog/templates/blog/post_list.html

    ...
    {% for post in posts %}
        <h3>                        
            <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
        </h3>
        <p class="post-info">
            {% include 'blog/post_info.html' %}
        </p>
    {% empty %}
        <p>There are no posts</p>
    {% endfor %}
    ...

TGDB/django_project/blog/templates/blog/post_detail.html

    ...
    <h1>{{ post.title|capfirst }}</h1>
    <p class="post-info">
        {% include 'blog/post_info.html' %}
    </p>
    <p>
        {{ post.content }}
    </p>
    ...

TGDB/django_project/blog/templates/blog/post_by_category.html

    ...
    {% for post in posts %}
        <h3>
            <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
        </h3>
        <p class="post-info">
            {% include 'blog/post_info.html' %}
        </p>
    {% endfor %}
    ...

TGDB/django_project/blog/templates/blog/post_by_tag.html

    ...
    {% for post in posts %}
        <h3>
            <a href="{% url 'post_detail' post.id %}">{{ post.title|capfirst }}</a>
        </h3>
        <p class="post-info">
            {% include 'blog/post_info.html' %}
        </p>
    {% endfor %}
    ...

get_absolute_url() method #

The get_absolute_url() is just another method commonly employed by Django developers to generate urls. Unlike the url tag, we can use get_absolute_url() both in our python code as well as in templates. To use this function you must implement it in the model's class.

Let's define it in the Category model first, just below __str__() method.

class Category(models.Model):    
    ...

    def get_absolute_url(self):
        return '/category/{0}/'.format(self.name)

Instead of manually creating URL you could use reverse() method like this:

from django.urls import reverse

class Category(models.Model):    
    ...

    def get_absolute_url(self):
        return reverse('post_by_category', args=[self.slug])

Calling get_absolute_url() in python code. #

Make sure you restart Django Shell before typing the following code.

>>>
>>> from blog.models import Category
>>>
>>> c = Category.objects.get(name='python')
>>> c
<Category: python>
>>> c.get_absolute_url()
'/category/python/'
>>>

Calling get_absolute_url() in templates. #

>>>
>>> from django import template
>>>
>>> cat = Category.objects.get(name='python')
>>>
>>> t = template.Template("{{ cat.get_absolute_url }}")
>>> c = template.Context({'cat': cat})
>>> t.render(c)
'/category/python/'
>>>

It may seem like get_absolute_url() is functionally equivalent to the url tag and the reverse() method, this is not entirely true. The get_absolute_url() method has the following advantages:

  1. Django documentation advocates that you should use get_absolute_url() in templates. The reason is that, at a later date, if we want to change the structure of the URL, then we just need to modify
    get_absolute_url() and all the templates will pick up the changes automatically. It also means that you don't have to remember whether Category or Tag object takes id or any other argument.

  2. The redirect() method which we will discuss in Redirecting URLs in Django also uses the URL returned by get_absolute_url() method to perform a temporary redirect.

  3. Various third-party libraries available at PyPI (https://pypi.python.org/pypi) also uses get_absolute_url() for different tasks.

  4. If get_absolute_url() is defined in the model class, then Django Admin site uses it in the object editing page to create a "VIEW ON SITE" link. So what this link does ? This link will take you to the public view of the object. For example, let's say you are editing python category in Django Admin then "VIEW ON SITE" will take you to the Category Page which displays all post published under python category.

    Too see "VIEW ON SITE" link in action, login to Django Admin and visit Change Category page. You will see a link "VIEW ON SITE" in the upper right corner of the page. Click the link and Django Admin will redirect you to the category page for that category.

    view-on-site-link-in-object-edit-page.png

    post-by-category-python.png

Let's define get_absolute_url() method for Post and Tag model.

class Tag(models.Model):
    ...

    def get_absolute_url(self):
        return reverse('post_by_tag', args=[self.slug])


class Post(models.Model):
    ...

    def get_absolute_url(self):
      return reverse('post_detail', args=[self.id])

Lets update our templates to use get_absolute_url() method instead of url tag.

TGDB/django_project/blog/templates/blog/post_list.html

...
         {% for post in posts %}
            <h3>
                <a href="{{ post.get_absolute_url }}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                {% include 'blog/post_info.html' %}
            </p>
        {% empty %}
            <p>There are no posts</p>
        {% endfor %}
...

TGDB/django_project/blog/templates/blog/post_by_category.html

...
         {% for post in posts %}
            <h3>
                <a href="{{ post.get_absolute_url }}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                {% include 'blog/post_info.html' %}
            </p>
        {% endfor %}
...

TGDB/django_project/blog/templates/blog/post_by_tag.html

...
        {% for post in posts %}
            <h3>
                <a href="{{ post.get_absolute_url }}">{{ post.title|capfirst }}</a>
            </h3>
            <p class="post-info">
                {% include 'blog/post_info.html' %}
            </p>
        {% endfor %}
...

TGDB/django_project/blog/templates/blog/post_info.html

<span>Date: {{ post.pub_date|date:"d M y h:i a" }}</span> |
<span>Category : <a href="{{ post.category.get_absolute_url }}">{{ post.category }}</a></span> |
<span>Tag :
  {% for tag in post.tags.all %}
      <a href="{{ tag.get_absolute_url }}">{{ tag }}</a>
  {% empty %}
      None
  {% endfor %}
</span>