OverIQ.com

Creating URLs in Django

Last updated on July 27, 2020


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

The 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.

Currently, urls.py file in the blog app looks like this:

TGDB/django_project/blog/urls.py

1
2
3
4
5
6
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 the URL for the post_list URL pattern.

{% url 'post_list' %}

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

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

1
2
3
4
5
6
7
>>>
>>> from django import template
>>> t = template.Template("{% url 'post_list' %}")
>>> c = template.Context({})
>>> t.render(c)
'/'
>>>

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

1
2
3
4
5
6
7
8
9
>>>
>>> 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() function #

What if a 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 the reverse() function. It accepts the name of the URL pattern. To use this method we first have to import it from django.urls. Just like the url tag on success it returns part of the URL without the host, otherwise, a NoReverseMatch exception is thrown.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>>
>>> 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 the url tag and reverse() function #

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 as before, 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 the 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' %}
1
2
3
4
5
>>>
>>> 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 that there are no quotation marks around cat.

1
2
3
4
5
>>>
>>> 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 pattern needs, we can pass additional arguments to the reverse() function as follows:

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

Here is an example.

1
2
3
4
5
>>>
>>> reverse('post_by_category', args=['css'])
'/category/css/'
>>>
>>>

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

1
2
3
4
5
>>>
>>> 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.

We now know how to create URLs. Let's update post_list.html, post_detail.html, post_by_category.html and post_by_tag.html templates to use the url tags as follows (changes are highlighted).

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#...
{% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#...
{% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#...
{% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#...
{% 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 templates share the same p.post-info paragraph. The problem is that if we add or update something inside p.post-info paragraph 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 the contents of p.posts-info and then include this template in the each of our existing templates, this way we only have to update the contents of p.post-info paragraph 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

1
2
3
4
5
6
7
8
9
<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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
    {% 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

1
2
3
4
5
6
7
8
9
#...
    <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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...
    {% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...
    {% 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 %}
#...

The get_absolute_url() method #

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

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

TGDB/django_project/blog/models.py

1
2
3
4
5
6
7
#...
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...
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.

1
2
3
4
5
6
7
8
9
>>>
>>> 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. #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>>
>>> 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() method is functionally equivalent to the url tag and the reverse() method, but 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() method 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 the link does? This link will take you to the public view of the object. For example, let's say you are editing a category named python then the "VIEW ON SITE" link 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.

While we are at it. Let's define get_absolute_url() method for Post and Tag models.

TGDB/django_project/blog/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#...
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])
#...

Let's update our templates to use get_absolute_url() method instead of url tag.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
         {% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...
         {% 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...
        {% 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

1
2
3
4
5
6
7
8
9
<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>

Note: To checkout this version of the repository type git checkout 17a.