Django URLs - templatetags, reversing URLs, namespacing and get_absolute_url()
In this post, we'll dive into URL handling concepts in Django, including looking at best practices around linking to URLs in both HTML templates and Python code. We'll look at namespacing URL configurations in different Django apps within a project, as well as a brief look at the get_absolute_url() method that can be defined on your Django models.
The associated video for this post can be found below:
Objectives
In this post, we'll learn:
- How to create named URLs in our Django app's
urlpatterns - How to use the
urltemplate-tag in Django templates - How to use the reverse function with named URLs
- How to namespace URLs in different apps within a Django project
- The purpose of the
get_absolute_url()method
And as a bonus, we'll learn about the show_urls command from django-extensions!
Setup
Let's assume we have a Django app that contains one model - a Book model - and 3 URLs and views. The setup for these is shown below.
# models.py
from django.db import models
class Book(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('home/', views.index),
path('books/', views.books),
path('book/<int:pk>/', views.book),
]
# views.py
from django.shortcuts import render, get_object_or_404
from core.models import Book
def index(request):
""" Simply renders a template """
context = {}
return render(request, 'index.html', context)
def books(request):
""" Gets all books from the database, renders a list template """
context = {'books': Book.objects.all()}
return render(request, 'booklist.html', context)
def book(request, pk):
""" Gets an individual book object from the database, based on its ID/PK. Renders a detail template """
book = get_object_or_404(Book, pk=pk)
context = {'book': book}
return render(request, 'bookdetail.html', context)
Each of the views loads a template, and each template should extend a base template. The content of the individual templates is unimportant for now.
Creating a Navbar
Let's now create a navbar that contains links to the first two URLs - the third URL contains a dynamic primary key, so it doesn't make sense to have this in a navbar.
For the index page and the book-list page, create the following URLs in a navbar.html template.
<nav>
<ul>
<li>
<a href="/home/">Home</a>
</li>
<li>
<a href="/books/">Books</a>
</li>
</ul>
</nav>
Notice how the links to each page, in the href attribute, are hard-coded to match the URLs we defined in the urls.py file.
This is problematic. Imagine we have links to different pages throughout our application templates, and we decide (or are told by management) that some URLs have to be changed. We would have to find all the hard-coded URLs, and change them individually.
This is error-prone and difficult to maintain. There's a better way in Django - by using named URLs and the url template-tag!
Firstly, let's add names to our URLs, in the urls.py file:
urlpatterns = [
path('home/', views.index, name='index'),
path('books/', views.books, name='book-list'),
path('book/<int:pk>/', views.book, name='book-detail')
]Notice that for each URL, we've added a name keyword-argument to the Django path() function. The name is just a string, but the benefit is that you can then refer to this name when referencing URLs using different utilities in Django.
One place we can do this is in our templates, with the url template-tag. Let's adjust our navbar to use this, with the code below:
<nav>
<ul>
<li>
<a href="{% url 'index' %}">Home</a>
</li>
<li>
<a href="{% url 'book-list' %}">Books</a>
</li>
</ul>
</nav>The href attribute has changed, and we're now using the url template-tag to refer to the named URLs for our index and book-list pages.
This has a major benefit - if we change a URL path, we do NOT need to change all its references in the template, as long as we keep the same name. This is a lot easier and is a best practice in Django templates.
To show this nav-bar in all your pages, make sure it is included in the base template before your content block - code such as the following will do the trick:
<!-- base template -->
<body>
{% include 'navbar.html' %}
{% block content %}
{% endblock %}
...
</body>So now we've learned how to reference named URLs in our templates, with the url template-tag.
But what if we need to attach a dynamic value to the URL? For example, on our book-list page, we might have anchor tags linking to a book-detail page, and we want to attach the primary key of each book to the URL. How can we do this?
Attaching Dynamic Values to URLs
Let's flesh out the template for our book-list. We want to add links to each book that we have in the context - if you look at the view function in the setup, we grab all books using the Book.objects.all() query.
So, in our list template, add the following code to iterate over these books, and create a link to each.
<ul>
{% for book in books %}
<li>
<a href="{% url 'book-detail' book.pk %}">{{ book.name }}</a>
</li>
{% endfor %}
</ul>On line 4, we generate the link using the url template-tag, and this time - after the URL name 'book-detail' - we add an argument attaching the book's primary key. The variable book, in this case, comes from the template for-loop, defined on line 2.
It's as easy as this. If we have additional dynamic values in our URL, we can specify them in order using the template-tag.
Reversing Named URLs
In Django, there's a reverse() function built into the django.urls module. This allows you to reference a named URL from Python code, generating the string for that URL - it's similar to the template-tag, but this can be used in your models, views and other Python files!
Let's create a dummy view that will simply redirect to our book-list. In the views.py file, add this view at the bottom:
from django.shortcuts import redirect
from django.urls import reverse
def redirect_view(request):
return redirect(reverse('book-list'))
Notice here that we are passing the name of the URL to the reverse() function. This generates a string URL-path that is then used by the redirect() helper to redirect us to that specific page in our application.
Don't forget to add a URL for this new view, as below:
path('redirect-me/', views.redirect_view, name='redirect-me')When you navigate to the page, you will be redirected to the book-list page.
We can also pass arguments and keyword arguments to the reverse() function, to attach dynamic values to the URL. Again, it's similar to what we did in the last section with the template-tag.
Let's say we want to redirect to the book-detail page of the first book in the database. We can use the following code.
def redirect_view(request):
book = Book.objects.first()
return redirect(reverse('book-detail', kwargs={'pk': book.pk}))Using the kwargs, we can pass a dictionary whose keys match the dynamic URL parameters. The values will be added to the URL generated by the reverse() function.
Namespacing URLs
Let's introduce another scenario. Imagine we have multiple apps within our Django project. Each app has a urls.py file.
What happens if two URLs from different apps in our project have the same name?
This clash will cause chaos in your application! Note also that your URLs could clash with URLs in 3rd party apps, too - such as django-allauth.
To demonstrate this, create a new Django app in your project called reports. Add to this a urls.py file with the following code:
from django.urls import path
from . import views
urlpatterns = [
path('home/', views.index, name='index')
]Don't forget to include this new URLconf in your main, project urls.py file. And you can create a dummy view to handle this URL, too.
Now, notice that we have two URLs in this project that have name='index'. These will clash, and Django will by default only load one of them. Not ideal!
The solution to this issue is to use namespaces in each of your apps' urls.py file. It's simple to do this: we just add an app_name variable in the urls.py file, defining the namespace for those URLs.
The core app's urls.py file should look like the following after this:
from django.urls import path
from . import views
app_name = 'core'
urlpatterns = [
path('home/', views.index, name='index'),
path('books/', views.books, name='book-list'),
path('book/<int:pk>/', views.book, name='book-detail'),
path('redirect-me/', views.redirect_view, name='redirect-me')
]
And our new reports app's URLs should look like this:
from django.urls import path
from . import views
app_name = 'reports'
urlpatterns = [
path('home/', views.index, name='index')
]
All we have done is add this app_name to distinguish (i.e. namespace) the URLs in each app. Now that we've done this, we can distinguish these when using the url template-tag or the reverse() function.
Let's adjust our nav-bar to link to the reports app's index URL, but also amend the template-tag to use our new namespaces, as below:
<nav>
<ul>
<li>
<a href="{% url 'core:index' %}">Home</a>
</li>
<li>
<a href="{% url 'core:book-list' %}">Books</a>
</li>
<li>
<a href="{% url 'reports:index' %}">Reports</a>
</li>
</ul>
</nav>To reference a namespaced URL, we separate the namespace and the actual URL name with a colon, as can be seen in the url template-tag above.
The major benefit is that we can now safely reference two URLs with the same name, and these will indeed load the correct page. We are highly unlikely now to clash with our other apps, or with third-party apps such as django-allauth.
We can easily do the same with the reverse() function. Amend the code in our redirect view to the following:
def redirect_view(request):
book = Book.objects.first()
return redirect(reverse('core:book-detail', kwargs={'pk': book.pk}))The syntax is the same as with the template-tag - we use a colon to separate the namespace and the URL name.
get_absolute_url()
Let's close this one off by considering the get_absolute_url() method. This is a method that you can optionally add to your Django models, and its purpose is to define the canonical URL for a model instance.
For example, for our Book model, the detail-view (i.e. the view for an individual book) is determined by a URL that has the book's primary key as a dynamic parameter.
We can define this method on the Book model as follows:
from django.db import models
from django.urls import reverse
# Create your models here.
class Book(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('core:book-detail', kwargs={'pk': self.pk})The method, on lines 12-13, uses the reverse() function to generate the URL for the detail page, attaching the primary-key using self.pk in the kwargs.
We can now refer to this in the book-list template that generates links to each book, with the following change:
<ul>
{% for book in books %}
<li>
<a href="{{ book.get_absolute_url }}">{{ book.name }}</a>
</li>
{% endfor %}
</ul>Instead of the URL template-tag with arguments, we simply use the get_absolute_url method on the model instance. Easy and useful!
Bonus: django-extensions "show_urls" command
If you have django-extensions installed in your project, there's a handy command that allows you to inspect your URLs, as well as the URLs of third-party apps (or generated by third-party app routers) that you're using - such as django-allauth, Django REST Framework, Channels, etc.
Simply run the following command:
python manage.py show_urlsYou'll get all the URLs, as well as which app is generating them, and importantly which names have been given to the URLs.
Summary
In this post, we've learned how to work with URLs in Django - named URLs, namespacing, the url template-tag, and usage with the reverse() function.
We've also learned about the get_absolute_url() method that you can define on your Django models.
If you enjoyed this post, please subscribe to our YouTube channel and follow us on Twitter to keep up with our new content!
Please also consider buying us a coffee, to encourage us to create more posts and videos!