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:
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 modifyget_absolute_url()
and all the templates will pick up the changes automatically. It also means that you don't have to remember whetherCategory
orTag
object takes id or any other argument.The
redirect()
method which we will discuss in Redirecting URLs in Django also uses the URL returned byget_absolute_url()
method to perform a temporary redirect.Various third-party libraries available at PyPI https://pypi.python.org/pypi also uses
get_absolute_url()
for different tasks.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 namedpython
then the"VIEW ON SITE"
link will take you to the Category page which displays all post published underpython
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
.
Load Comments