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

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. 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 our urls.py file look like this:

urlpatterns = [
    url(r'^category/(?P<category_slug>[\w-]+)/$', views.posts_by_category, name='post_by_category'),
    url(r'^tag/(?P<tag_slug>[\w-]+)/$', views.posts_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 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

Conside 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' 'tag' %}")
>>> t.render(template.Context({}))
'/category/tag/'
>>>

We can also pass additional arguments as variables like this:

{% url 'post_by_category' tag %}

Notice there is no quotation marks around tag.

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

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

{% url 'new_patter' arg1 arg2 arg2 %}

Similarly, To generate url inside Python code we have to 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 expcetion.

Lets update post_list.html, post_detail.html, posts_by_category.html and post_by_tag.html templates to use url tags as follows.

post_list.html

  ...
  {% 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 %}
    ...

post_detail.html

...
{% block content %}
    <h1>{{ post.title|capfirst }}</h1>
    <p>Pub date: {{ post.pub_date }}</p>
    <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>

{% endblock %}
...

posts_by_category.html

    ...
    {% 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>Tag :
              {% for tag in post.tags.all %}
                  <a href="{% url 'post_by_tag' tag.slug %}">{{ tag }}</a>
              {% empty %}
                  None
              {% endfor %}
          </span>
        </p>
    {% empty %}
        <p>There are not posts under {{ category }}</p>
    {% endfor %}
    ...

posts_by_tag.html

    ...
    {% 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>There are not posts tagged with {{ tag }}</p>
    {% endfor %}
    ...

Eliminationg Redundacncy

All our four template shares the same .post-info class. The problem is if we add or updating something to .post-info 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 with the following 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>|
<span>Author: <a href="{% url 'post_by_author' post.author.name %}">{{ post.author|title }}</a></span>

Then include this template in all other templates like this:

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 %}
    ...

post_detail.html

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

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>
    {% empty %}
        <p>There are not posts under {{ category }}</p>
    {% endfor %}
    ...    

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>
    {% empty %}
        <p>There are not posts tagged with {{ tag }}</p>
    {% endfor %}
    ...

post_by_author.html

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

get_absolute_url() method

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.

Lets 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('posts_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 seems like get_absolute_url() is functionally equivalent to url tag and reverse() method, this is not entirely true. The get_absolute_url() method has the following advantages over the other two methods.

1) Django admin site uses get_absolute_url() in the edit form 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.

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.

[django admin]

[after redirect]

[]

2) 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.

3) redirect() method which we will discuss next also uses the url returned by get_absolute_url() method to perform a temporary redirect.

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

Let's define get_absolute_url() method for Author, Post and Category model.

class Author(models.Model):
    ...

    def get_absolute_url(self):
        return reverse('post_by_author', args=[self.username])

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.

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 %}
    ...

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>
    {% empty %}
        <p>There are not posts under {{ category }}</p>
    {% endfor %}
    ...

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>
    {% empty %}
        <p>There are not posts tagged with {{ tag }}</p>
    {% endfor %}
    ...

post_by_author.html

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

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>|
<span>Author: <a href="{{ post.author.get_absolute_url }}">{{ post.author|title }}</a></span>