Skip to content

Render HTML templates to response

Serhii Horodilov edited this page Feb 21, 2024 · 11 revisions

At the end of this guide, you will:

  • Configure project views to render HTML pages within responses.
  • Configure project to serve static files in the development environment.

Table of Contents

Getting started

git checkout bp-templates

Guide

Templates pack

  1. Download the latest template pack.
In deed, you require the dist archive only; however handlebars can be useful as well, since it contains the source templates used to build the distribution.

Unzip the archive.

Create the templates directory in the project root, and move all *.html files from the archive in it. Create the assets directory, and move the rest of the templates distribution in it. The files structure should look like:

/
|-- ...
|-- assets/
|   |-- css/
|   |-- fonts/
|   |-- icons/
|   |-- img/
|   `-- js/
|-- templates/
|   |-- profile.html
|   |-- signin.html
|   |-- signup.html
|   |-- task_detail.html
|   |-- task_form.html
|   `-- task_list.html
`-- ...

Render templates to response

Update views to render templates

There is no template to render with task_delete_view function, so it will just redirect back to list view (homepage).

# tasks/views.py
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import redirect, render


def task_list_view(request: HttpRequest) -> HttpResponse:
    """
    Handle requests to tasks list
    """

    return render(request, "task_list.html")


def task_detail_view(request: HttpRequest, pk: int) -> HttpResponse:
    """
    Handle requests to task details
    """

    return render(request, "task_detail.html")


def task_create_view(request: HttpRequest) -> HttpResponse:
    """
    Handle requests to create a new task instance
    """

    return render(request, "task_form.html")


def task_update_view(request: HttpRequest, pk: int) -> HttpResponse:
    """
    Handle requests to update an existing task instance
    """

    return render(request, "task_form.html")


def task_delete_view(request: HttpRequest, pk: int) -> HttpResponse:
    """
    Handle requests to delete an existing task instance
    """

    return redirect("tasks:list")

There is no template to render with sign_out_view function, so it will just redirect back to list view (homepage).

# users/views.py
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import redirect, render


def user_profile_view(request: HttpRequest) -> HttpResponse:
    """
    Handle requests to user's profile
    """

    return render(request, "profile.html")


def sign_up_view(request: HttpRequest) -> HttpResponse:
    """
    Register a new user in the system
    """

    return render(request, "signup.html")


def sign_in_view(request: HttpRequest) -> HttpResponse:
    """
    Authenticate a user
    """

    return render(request, "signin.html")


def sign_out_view(request: HttpRequest) -> HttpResponse:
    """
    Sing out the authenticated user
    """

    return redirect("tasks:list")

Adjust project settings

To serve templates from non-apps directory(ies) you have to add path to the templates directory to TEMPLATES[0]["DIRS"] in the tasktracker/settings.py module.

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "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",
            ],
        },
    },
]

Apply context in render function

Put fake db module to the tasks application, adjust in case of need[1].

git checkout fake-db tasks/_fake_db.py

Modify the tasks application views to create the context objects for each response.

diff --git a/tasks/views.py b/tasks/views.py
index 498aca2..f6df371 100644
--- a/tasks/views.py
+++ b/tasks/views.py
@@ -4,9 +4,12 @@ Tasks application views
 """
 
 from django.http.request import HttpRequest
-from django.http.response import HttpResponse
+from django.http.response import Http404, HttpResponse
 from django.shortcuts import redirect, render
 
+# TODO: blocker GH-62, GH-63
+from tasks import _fake_db
+
 
 def task_list_view(request: HttpRequest) -> HttpResponse:
     """
@@ -14,7 +17,11 @@ def task_list_view(request: HttpRequest) -> HttpResponse:
 
     """

+    ctx = {
+        "object_list": _fake_db.tasks,
+    }
+
     return render(request, "task_list.html")
 
 
 def task_detail_view(request: HttpRequest, pk: int) -> HttpResponse:
@@ -23,7 +30,15 @@ def task_detail_view(request: HttpRequest, pk: int) -> HttpResponse:
 
     """
 
+    task = _fake_db.get_task(pk)
+    if task is None:
+        raise Http404
+
+    ctx = {
+        "object": task,
+    }
+
     return render(request, "task_detail.html")
 
 
 def task_create_view(request: HttpRequest) -> HttpResponse:
@@ -41,6 +56,10 @@ def task_update_view(request: HttpRequest, pk: int) -> HttpResponse:
 
     """
 
+    task = _fake_db.get_task(pk)
+    if task is None:
+        raise Http404
+
     return render(request, "task_form.html")
 
 
@@ -50,4 +69,8 @@ def task_delete_view(request: HttpRequest, pk: int) -> HttpResponse:
 
     """
 
+    task = _fake_db.get_task(pk)
+    if task is None:
+        raise Http404
+
     return redirect("tasks:list")


@@ -14,7 +14,14 @@ def user_profile_view(request: HttpRequest) -> HttpResponse:

     """

+    ctx = {
+        "first_name": "Dora",
+        "last_name": "Headstrong",
+        "email": "[email protected]",
+        "get_full_name": lambda: "Dora Headstrong",
+    }
+
     return render(request, "users/profile.html")

Custom template tags

Create templatetags package inside of tasks app.

tasks/templatetags/tasks.py

"""
Tasks application templatetags
"""

from typing import Any, Dict

from django import template

register = template.Library()


@register.filter(name="is_completed")
def is_completed(obj):
    # TODO: GH-77
    return "true" if obj["completed"] else "false"


@register.inclusion_tag("tasks/_task_tr.html", takes_context=True)
def task_row(context: Dict[str, Any], obj):
    # TODO: GH-78
    return {"object": obj}

Inherited and inclusion templates

There is a convention to store related files within their apps. But first, let's adjust templates' content to avoid repeating groups.

/
|-- ...
|-- assets/
|-- tasks/
|   |-- ...
|   `-- templates/
|       |-- tasks/
|       |   |-- _task_tr.html
|       |   |-- task_detail.html
|       |   |-- task_form.html
|       |   `-- task_list.html
|       `-- widgets
|           |-- modal_task_delete.html
|           `-- pagination.html
|-- templates/
|   |-- _navbar.html
|   `-- base.html
|-- users/
|   |-- ...
|   `-- templates/
|       |-- auth/
|       |   |-- signin.html
|       |   `-- signup.html
|       `-- users/
|           `-- profile.html
`-- ...

Base template

templates/base.html

<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="description" content="Task tracker templates package - EDU Python course">
  <meta name="author" content="Serhii Horodilov <[email protected]>">
  <link rel="icon" type="image/svg+xml" href="../img/favicon.svg">
  <link rel="icon" type="image/png" href="../img/favicon.png">
  <title>{% block title %}Task Tracker{% endblock %}</title>
  <script defer src="../js/main.bundle.js"></script>
  <link href="../css/main.min.css" rel="stylesheet">
</head>
<body>
{% include "_navbar.html" %}
<div class="container">
  <div class="row row-cols-1 justify-content-center">
    <main class="col col-md-10 col-lg-9">
      {% block main %}{% endblock %}
    </main>
  </div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
  {% block messages %}{% endblock %}
</div>
</body>
</html>

Navbar inclusion template

templates/_navbar.html

<div class="container" role="navigation">
  <nav class="navbar navbar-expand-lg mb-3">
    <div class="container-fluid">
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarTogglerDemo01"
              aria-controls="navbarTogglerDemo01" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarTogglerDemo01">
        <a href="{% url "tasks:list" %}" class="navbar-brand"><i class="bi bi-list-task"></i>&nbsp;Task Tracker</a>
        <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
          <li class="nav-item"><a href="{% url "admin:index" %}" class="nav-link">Administration</a></li>
          <li class="nav-item"><a href="{% url "tasks:create" %}" class="nav-link">Create new</a></li>
          <li class="nav-item"><a href="{% url "users:profile" %}" class="nav-link">Profile</a></li>
          <li class="nav-item">
            <form action="{% url "users:sign-out" %}">
              <input type="submit" class="nav-link" value="Sign Out">
            </form>
          </li>
          <li class="nav-item"><a href="{% url "users:sign-up" %}" class="nav-link">Sign Up</a></li>
          <li class="nav-item"><a href="{% url "users:sign-in" %}" class="nav-link">Sign In</a></li>
        </ul>
      </div>
    </div>
  </nav>
</div>

Tasks application templates

Create a new directory to store tasks app templates - tasks/templates/tasks. Move tasks related templates to this directory.

tasks/templates/tasks/task_detail.html

{% extends "base.html" %}
{% load tasks %}
{% block title %}Task Details | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <div class="card shadow" id="taskDetailContainer">
    <div class="card-header">
      <h1 class="card-title h3 text-center" id="summary">
        <span>{{ object.summary }}</span>
      </h1>
      <hr class="border">
      <div class="row row-cols-1 row-cols-md-2">
        <div class="col">
          <strong class="me-1">Assignee:</strong>
          <span>{{ object.assignee|default_if_none:"Unassigned" }}</span>
        </div>
        <div class="col">
          <strong class="me-1">Reporter:</strong>
          <span>{{ object.reporter }}</span>
        </div>
        <div class="col">
          <strong class="me-1">Created:</strong>
          <span>{{ object.created_at|task_timestamp:5 }}</span>
        </div>
        <div class="col">
          <strong class="me-1">Updated:</strong>
          <span>{{ object.updated_at|task_timestamp:5 }}</span>
        </div>
      </div>
    </div>
    <div class="card-body">
      {{ object.description|linebreaksbr }}
    </div>
    <div class="card-footer">
      <div class="d-flex justify-content-end align-items-center">
        <button class="btn btn-outline-success mx-3"
                hx-patch="{# TODO: GH-78 #}" hx-swap="none"
                hx-vals="js:{completed:true}">
          Complete
        </button>
        <div class="btn-group" role="group">
          <a href="{% url "tasks:update" object.pk %}" class="btn btn-outline-primary" role="button">
            Update
          </a>
          <button class="btn btn-outline-danger"
                  data-bs-toggle="modal"
                  data-bs-target="#modalTaskDelete">
            Delete
          </button>
        </div>
      </div>
    </div>
  </div>
  {% include "widgets/modal_task_delete.html" with object=object %}
{% endblock %}

tasks/templates/tasks/task_form.html

{% extends "base.html" %}
{% block title %}Task Form | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <h1 class="h3 fw-normal mb-3 text-center">Task details</h1>
  <form class="w-75 m-auto" aria-label="TaskForm">
    <div class="mb-3">
      <label for="summary" class="form-label">Summary</label>
      <input type="text" class="form-control" id="summary" name="summary">
    </div>
    <div class="mb-3">
      <div class="mb-3 form-check">
        <input type="checkbox" class="form-check-input" id="completed" name="completed">
        <label for="completed" class="form-check-label">Completed</label>
      </div>
    </div>
    <div class="mb-3">
      <label for="description" class="form-label">Description</label>
      <textarea class="form-control" id="description" name="description" rows="10"></textarea>
    </div>
    <div class="mb-3">
      <label for="assignee" class="form-label">Assignee</label>
      <select class="form-select" id="assignee" name="assignee">
        <option value="" class="selected">-- Select assignee --</option>
        <option value="[email protected]">Pippin Sackville-Baggins</option>
        <option value="[email protected]">Toby Mugwort</option>
        <option value="[email protected]">Wilcome Brownlock</option>
      </select>
    </div>
    <div class="mb-3 justify-content-center">
      <input type="submit" class="btn btn-primary w-100 mt-3" name="submit" value="Submit">
    </div>
  </form>
  <div class="d-flex flex-row justify-content-center">
    <div class="w-75">
      <a href="" class="w-100 btn btn-secondary">Cancel</a>
    </div>
  </div>
{% endblock %}

tasks/templates/tasks/task_list.html

{% extends "base.html" %}
{% load tasks %}
{% block title %}Tasks List | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <div class="card shadow">
    <div class="card-header d-flex flex-row justify-content-center">
      <h1 class="card-title h3 text-capitalize mx-auto my-2"><i class="bi bi-list-task"></i>&nbsp;task list</h1>
    </div>
    <div class="card-body">
      <table class="table">
        <caption class="visually-hidden">task list</caption>
        <thead>
        <tr>
          <th class="col col-3" id="taskAssignee" scope="col">Assignee</th>
          <th class="col col-8" id="taskDetail" scope="col">Task Details</th>
          <th class="col d-flex flex-row justify-content-center" id="taskActions" scope="col">Actions</th>
        </tr>
        </thead>
        <tbody id="taskContainer">
        {% for object in object_list %}
          {% task_row object %}
        {% endfor %}
        </tbody>
      </table>
    </div>
    <div class="card-footer d-flex flex-row justify-content-center">
      {# TODO: GH-66 #}
      {% include "widgets/pagination.html" %}
    </div>
  </div>
{% endblock %}

tasks/templates/tasks/_task_tr.html

{% load tasks %}
<tr class="task" id="{{ object.pk }}" data-task-completed="{{ object|is_completed }}">
  <td class="align-middle task-assignee" aria-labelledby="taskAssignee">
    <img src="{# TODO: GH-67 #}" alt="avatar" class="rounded-circle shadow avatar">
    <br>
    <span class="d-none d-md-inline ms-2 text-nowrap">
      {{ object.assignee.get_full_name }}
    </span>
  </td>
  <td class="align-middle task-detail" aria-labelledby="taskDetail">
    <div>
      <a href="{% url "tasks:detail" object.pk %}" class="text-decoration-none fw-bold task-detail-ref">
        {{ object.summary }}
      </a>
    </div>
    <p class="d-none d-lg-block fst-italic text-muted">
      {{ object.description|truncatewords:20 }}
    </p>
  </td>
  <td class="align-middle task-actions" aria-labelledby="taskActions">
    <div class="d-flex flex-row justify-content-between align-items-center">
      {% if object.completed %}
        <i class="bi bi-arrow-repeat{# TODO: GH-78 #}" role="button"
           hx-patch="{# TODO: GH-78 #}" hx-swap="none"
           hx-vals="js:{completed:false}" hx-headers="js:{}"></i>
      {% else %}
        <i class="bi bi-check-lg{# TODO: GH-78 #}" role="button"
           hx-patch="{# TODO: GH-78 #}" hx-swap="none"
           hx-vals="js:{completed:true}" hx-headers="js:{}"></i>
      {% endif %}
      <i class="bi bi-trash{# TODO: GH-78 #}" role="button"
         hx-delete="{# TODO: GH-78 #}" hx-target="closest tr" hx-swap="outerHTML"></i>
    </div>
  </td>
</tr>

tasks/templates/widgets/modal_task_delete.html

<div class="modal fade" id="modalTaskDelete" tabindex="-1"
     aria-labelledby="modalTaskDeleteLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="modalTaskDeleteLabel">Are you sure you want to delete task?</h1>
      </div>
      <div class="modal-body">
        <p>Task will be permanently deleted.</p>
        <p class="fw-bold text-center">{{ object.summary }}</p>
        <div class="row row-cols-2">
          <div class="col">Reporter: {{ object.reporter }}</div>
          <div class="col">Assignee: {{ object.assignee|default_if_none:"Unassigned" }}</div>
        </div>
      </div>
      <div class="modal-footer">
        <form action="">
          <button class="btn btn-secondary mx-1" data-bs-dismiss="modal">Cancel</button>
          <button class="btn btn-danger mx-1" type="submit">Confirm</button>
        </form>
      </div>
    </div>
  </div>
</div>

tasks/templates/widgets/pagination.html

<nav aria-label="TaskPagination">
  <ul class="pagination">
    <li class="page-item disabled"><a href="" class="page-link"><i class="bi bi-chevron-left"></i></a></li>
    <li class="page-item active"><a href="" class="page-link">1</a></li>
    <li class="page-item"><a href="" class="page-link">2</a></li>
    <li class="page-item"><a href="" class="page-link">3</a></li>
    <li class="page-item"><a href="" class="page-link">4</a></li>
    <li class="page-item"><a href="" class="page-link">5</a></li>
    <li class="page-item"><a href="" class="page-link"><i class="bi bi-chevron-right"></i></a></li>
  </ul>
</nav>

Users application templates

Create new directories to store users app templates: users/templates/auth and users/templates/users. Move auth related templates to auth templates directory, and move user profile template to users template directory.

users/templates/auth/signin.html

{% extends "base.html" %}
{% block title %}Sign In | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <form class="w-50 m-auto" id="formAuth" aria-label="Login Form">
    <h1 class="h3 fw-normal mb-3 text-center">Enter your credentials</h1>
    <div class="form-floating mb-3">
      <input type="text" class="form-control shadow" id="username" name="username" placeholder="Username">
      <label for="username">Username</label>
    </div>
    <div class="form-floating mb-3">
      <input type="password" class="form-control shadow" id="password" name="password" placeholder="Password">
      <label for="password">Password</label>
    </div>
    <button class="btn btn-primary w-100 my-2 fs-5">Sign In</button>
    <p class="text-center mt-3">
      Have no account?
      <a href="" class="text-body fw-bold">Register now</a>
    </p>
  </form>
{% endblock %}

users/templates/auth/signup.html

{% extends "base.html" %}
{% block title %}Sign Up | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <form class="w-50 m-auto" id="formAuth" aria-label="Login Form">
    <h1 class="h3 fw-normal mb-3 text-center">Register a new account</h1>
    <div class="form-floating mb-3">
      <input type="text" class="form-control shadow" id="username" name="username" placeholder="Username">
      <label for="username">Username</label>
    </div>
    <div class="form-floating mb-3">
      <input type="email" class="form-control shadow" id="email" name="email" placeholder="Email">
      <label for="email">Email</label>
    </div>
    <div class="form-floating mb-3">
      <input type="password" class="form-control shadow" id="password" name="password" placeholder="Password">
      <label for="password">Password</label>
    </div>
    <div class="form-floating mb-3">
      <input type="password" class="form-control shadow" id="confirmPassword" name="confirm_password"
             placeholder="Confirm Password">
      <label for="confirmPassword">Confirm Password</label>
    </div>
    <button class="btn btn-primary w-100 my-2 fs-5">Sign Up</button>
    <p class="text-center mt-3">
      Already have an account?
      <a href="" class="text-body fw-bold">Login</a>
    </p>
  </form>
{% endblock %}

users/templates/users/profile.html

{% extends "base.html" %}
{% block title %}User Profile | {{ block.super|default_if_none:"Task Tracker" }}{% endblock %}
{% block main %}
  <div class="card shadow">
    <div class="card-header">
      <h1 class="card-title text-center">{{ get_full_name }}</h1>
      <p class="h5 text-center">{{ email }}</p>
    </div>
    <div class="card-body">
      <div class="row row-cols-1 row-cols-lg-2">
        <div class="col d-flex justify-content-center">
          <img src="https://i.pravatar.cc/?u={{ email }}" alt="avatar"
               class="rounded-circle shadow avatar">
        </div>
        <div class="col mt-3 mt-lg-0" aria-labelledby="formUserdata">
          <h2 class="text-center">Change user data</h2>
          <form id="formUserdata">
            <div class="mb-3">
              <label for="firstName" class="form-label">First name</label>
              <input type="text" class="form-control" id="firstName" name="first_name" value="{{ first_name }}">
            </div>
            <div class="mb-3">
              <label for="lastName" class="form-label">Last name</label>
              <input type="text" class="form-control" id="lastName" name="last_name" value="{{ last_name }}">
            </div>
            <div class="mb-3">
              <label for="image" class="form-label">User image</label>
              <input type="file" class="form-control" id="image" name="image">
            </div>
            <div>
              <button type="submit" class="w-100 mt-2 btn btn-primary">Save</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
{% endblock %}

Changes

  1. Full change log
  2. Fake DB
  3. Templates

References

  1. ^ Fake DB