-
Notifications
You must be signed in to change notification settings - Fork 0
Render HTML templates to response
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.
git checkout bp-templates
- Download the latest template pack.
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 `-- ...
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")
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",
],
},
},
]
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")
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}
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 `-- ...
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>
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> 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>
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> 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>
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 %}
- Set up new Django project
- Add project views
- Configure PostgreSQL database
- Add task model
- Fetch data from the database to response
- Render HTML templates to response
- Create auth forms
- Implement user authentication
- Create custom user model
- Create task model form
- Apply permissions to views
- Refactoring project views to CBVs
- Create serializers
- Implement API views
- Apply API views permissions