Django templates – Introduction
Django is a high-level Python web framework designed for rapid web application development. It has a clean, pragmatic design and follows the Model-View-Controller (MVC) architectural pattern. It provides many valuable tools and functionalities, such as an Object-Relational Mapping (ORM) system, form handling, authentication, and an intuitive admin interface. Django uses templates to generate static HTML from the outputs of a Django View. A Django View is a Python function or class that takes a web request and returns a web response.
In practice, Django’s templates are simply HTML files, with some special syntax and tools that let Django render the HTML page on-the-fly for the visiting User from the Views. The templates use a templating language that allows developers to insert dynamic data into the HTML page, such as variables, loops, and conditional statements. Let’s deep dive and discuss some ways of efficiently working with Django templates.
Getting Started with Django Templates
The Django template system provides built-in tags and filters that make it easy to perform common repeatable tasks, such as formatting dates or generating URLs in our Django apps. A template tag is a syntax that allows developers to provide arbitrary logic in the rendering process. This might sound vague, but it is left intentionally so by Django because they perform a lot of functions.
Template Tags
Template tags are indicated by curly braces with percent signs, like this: {% tag %}. Some tasks performed by template tags include; outputting content, getting content from databases and other storage media, performing control operations like if statements and for loops, generating URLs, formatting dates, and other custom-made complex functions.
There are a lot of built-in template tags (https://docs.djangoproject.com/en/4.1/ref/templates/builtins/) in Django, and below are some of the common ones:
- if
The {% if %} tag allows developers to add conditional logic to templates. It tests if a variable is True, False, None, or if it exists.
<div> {% if student_attendance_list %} Number of active students: {{ student_attendance_list|length }} {% elif students_pending %} students will confirm attendance soon! {% else %} No athletes. {% endif %} </div>
- for
The {% for %} tag empowers the developer to loop over a list of items, for example, a query set from the database or a list of objects passed from a view.
<ul> {% for student in student_attendance_list %} <li>{{ student.names }}: ${{ student.tution_paid }}</li> {% endfor %} </ul>
- include
the {% include %} tag allows one to include another template within another template.
{% include '_student_profile.html' %}
- extends
The {% extends %} tag commands Django to inherit from another template and override specific content blocks.
{% extends '_student_profile.html' %}
- load
The {% load %} tag allows developers to load custom template tags maybe from another library, just like using import in Python.
{% load static %}
- block
The {% block %} tag defines a block of content that a function can override in a child template.
{% block title %}Student Dashboard{% endblock title %}
- csfr_token
The {% csrf_token %} tag is significant in Django for form handling. It generates a hidden input field containing a CSRF token, which is required for secure form submissions.
<form class="user" enctype="multipart/form-data" action="{% url 'users:profile_picture_update' %}" method="POST"> {% csrf_token %} {% for field in form.visible_fields %} <div class="form-group mb-5 text-center-md"> {{ field.label_tag }} {% render_field field class="form-control form-control-user mb-4" %} {% for error in field.errors %} <span style="color:red">{{ render_field.error }}</span> {% endfor %} </div> {% endfor %} </form>
- url
The {% url %} tag generates an absolute path reference( URL without domain name) based on the name of a view or a URL pattern.
<a class="btn btn-primary btn-sm d-none d-sm-inline-block" role="button" href="{% url 'trainer_profile' %}"> <i class="fas fa-download fa-sm text-white-50"></i> Trainer's Profile </a>
The above template tags are the most commonly used in Django, but the list is not exhaustive. Refer to the Django documentation (https://docs.djangoproject.com/en/4.1/ref/templates/builtins/) for more information on available template tags.
Filters and Variables
Besides template tags, there are also variable placeholders in Django templates. The placeholders are represented using {{ some_variable }} and are used to pass in an expression or variable defined from the view function. The template tags and the variable placeholder can have added complex functionalities using filters.
Filters transform the values of variables and arguments. Filters use the pipe (|) operator to apply the specified transformations to the data.
For example:
{% for team_member in directors %} <div class="col-lg-4"> <div class="mx-auto testimonial-item mb-5 mb-lg-0" data-bss-hover-animate="pulse"> <img class="rounded-circle img-fluid mb-3" style="width:400px; height: 200px; max-height: 300px" src="{{ team_member.profile_picture.url }}"> <h5>{{ team_member.salutation }}. {{ team_member.first_name|title }} {{ team_member.last_name }}</h5> <h6>{{ team_member.type|lower }}</h6> <p class="font-weight-light mb-0">"{{ team_member.bio }}"</p> </div> </div> {% endfor %}
The code above converts the member type to lowercase in the HTML template and the first name into title-case.
Other commonly used Django template filters include:
Date Filter
The date filter formats a date given a string of dates with certain format characters.
{{ my_date|date:'D d M Y' }}
Slugify filter
The slugify filter converts spaces of a string into hyphens and then converts the resulting string into lowercase.
value = " I love qodo" {{ value|slugify }}
results into “i-love-qodo.”
Refer to the Django documentation (https://docs.djangoproject.com/en/4.1/ref/templates/builtins/) for more information on available template filters.
Best Practices when using Django templates
- Decide on the Templating Engine to Use
Django has a built-in templating engine, Django Templating Engine. However, it supports other 3rd party templating engines like Jinja2, Mako, Cheetah, and Tenjin. By default, use the Django Templating Engine since it has tight integration with the other Django features and is easy to use. If you need to change the engine, edit the TEMPLATES in the settings.py file to the templating engine of your choice.
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'adci_config/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
- Keep the templates in the Project structure consistent
Django, by default, makes an opinionated assumption about the structure of our project when it’s looking for templates. Therefore, the location is usually in the templates subdirectory in each created application. However, you can define another directory to search for templates. In the example below, the directory to search for templates has been changed to look for templates in the base directory of the entire project. In that case, organize the templates in the templates directory into subdirectories based on their purpose, such as “home” for home templates. This will make it easier to find and maintain your templates.
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'adci_config/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
The choice of where to put the templates is determined by preference and the nature of the project and should be consistent throughout the project.
- Use template inheritance
Django applications are meant to be reusable, and we can apply the same methodology to templates by inheriting regular HTML from other templates. A typical pattern is having a common base template for common aspects of your application, logged-in pages, logged-out pages, or places where significant changes are made to the underlying HTML. From our example below, base.html would contain most of the core structure that would make up each page, including the static CSS and Javascript, with blocks defined for an app or page-specific customizations.
Each child template then extends the base template as needed. For example, the 404.html inherits the base to render using the underlying CSS and JavaScript.
{% extends "_base.html" %} {% block title %}Page not found{% endblock %} {% block content %} <h1>Page not found</h1> <p>{% if exception %}{{ exception }}{% else %}This is not the page you were looking for.{% endif %}</p> {% endblock content %}
Django uses autoescape template tag surrounding blocks where we don’t want Django to auto escape our HTML tags or JavaScript, but rather treat the contents of the block literally.
The {% block content %} in the code allows us to populate any content in a child page.
- Be Mindful of Handling Querysets
Handling queries within your templates can be a performance bottleneck for Django, depending on the complexities of your model definitions.
Django’s templating system is tightly coupled with Django’s object-relational mapping layer, which returns data from the database. Without adequately considering this coupling, you may inadvertently cause the number of queries on each page load to jump to unmaintainable amounts. In some cases, this can cause the database to become too sluggish to operate certain pages on your site, or worse, crash and need to be restarted. Thankfully, Django provides mechanisms and patterns which we can use to make sure our templates are running as fast as possible, and we’re not killing the database server.
For example, consider the below view:UserListView(ListView): template_name = 'accounts/student_list.html' model = User paginate_by = 20 context_object_name = 'students' queryset = User.objects.all()
And a template:
<ul> {% for student in students %} <li>{{ student.names }}: ${{ student.tution_paid }}</li> <li>{{ student.profile.photo_url }}</li> {% endfor %} </ul>
When Django renders the template, it will need an additional query to grab information from the profile object as it’s a related field. In our example view, we’re paginating by 20 users, so this one line in the template could account for an additional 20 queries (on each page request as the profile object, as with all related entities and models in Django), which aren’t included in the original query for the 20 users. Imagine how this could become a prolonged page if we were including fields from other related objects in our table or if we were paginating by 1000 users instead of 20.
Replacing our User.objects.all() with User.objects.select_related(‘profile’), we’re telling Django to include related profile instances when performing its query for our users. This consists of the Student Profile model on each User instance, preventing Django from needing to run an extra query each time we ask for information from the profile within the template.
UserListView(ListView): template_name = 'accounts/student_list.html' model = User paginate_by = 20 context_object_name = 'students' queryset = User.objects.select_related('profile')
Django’s select_related functionality does not work with many-to-many or with many-to-one relationships. In such scenarios, use Django’s prefetch_related method. Unlike select_related, prefetch_related does its magic in Python instead of SQL select statements by joining related objects into instances accessible in templates. It doesn’t perform things in a single query like select_related can, but it’s much more efficient than running a query each time you request a related attribute. For example, a prefetch for related projects and classes and one-to-many relationships off of the User model would look like this:
UserListView(ListView): template_name = 'accounts/student_list.html' model = User paginate_by = 20 context_object_name = 'students' queryset = User.objects.prefetch_related('projects', 'class')
- Keep templates simple and concise
Avoid including complex logic in your templates, as this can make them harder to read and maintain. Instead, the best practice for complex logic is to move as much logic as possible into your views or models. This makes your templates clean, maintainable, and reusable.It’s also a very good idea to break down your templates into smaller, reusable pieces. This makes them more maintainable and easier to reuse in other parts of your application. Use template comments to document your code and make it easier for other developers to understand what’s going on. You can add comments using the {% comment %} tag.
- Avoid hardcoding URLs and strings by using URL namespaces
Use the {% url %} tag to generate URLs based on the name of a view or URL pattern, and use Django’s translation framework to translate strings. Furthermore, use namespaces with Django templates since it simplifies developing inside templates. URL namespaces allow us to have unique URL names, even if another application uses the same URL name (e.g. create, detail, and edit, for example.) For example, without using URL namespaces, a Django project couldn’t have two URLs named create. With namespacing, we can call and reference our URLs without needing long, complex names for each URL in our application.
from django.urls import path from . import views app_name = 'students' urlpatterns = [ path('', views.index, name='index'), path('post/<int:pk>/', views.student_detail, name='student_detail'), ]
In our template, we would call:
{% url 'students:index' %}
Or
{% url 'students:student_detail' pk=post.pk %}
- Consider template caching
Template caching is a valuable method of speeding up the rendering of templates. By caching frequently used pre-populated templates, you can improve the performance of your application and reduce server load.
- Debug your templates
Django provides several tools that can help you debug your templates. By enabling template debugging, you can see each template’s context and output and identify any errors. Use tools like django-debug-toolbar (https://django-debug-toolbar.readthedocs.io/en/latest/) to investigate templates and views in your Django application. Once installed, the django-debug-toolbar can show which queries are run when a view is executed, and a template is loaded. This is incredibly useful for debugging slow pages since you may find a template you’ve written is running hundreds of queries.
- Make use of third-party template libraries
Django has a massive ecosystem of third-party libraries that extend the functionality of templates. Use them to simplify and speed up the code development process. An example of such a tool is the crispy-forms (https://django-crispy-forms.readthedocs.io/en/latest/) tool which provides a |crispy filter and {% crispy %} tag that will let developers control the rendering behavior of Django forms in an exquisite and DRY way.
Conclusion
In conclusion, Django templates aren’t challenging to work with, but as we’ve seen above, there are several ways we can make working with Django templates even more manageable. These ways include:
- Learning and knowing which tags and filters are built-in that can help us out.
- Structuring our template folders in ways that are consistent and predictable for Django and other developers.
- Optimizing page load speeds and database connections using well-thought-out views and templates and caching commonly used templates.
- Namespacing URLs to make them predictable and accessible in templates and readable by other developers.
- Using third-party tools to monitor application performance and improve template functionalities.