Learning Objectives
By the end of this section, you will be able to:
- Create a Todo web application with Bootstrap and Django
- Create a Django project
- Create and register a Todo app
- Define the Todo model
- Set up the Django REST APIs
- Create the user interface
In this module, you will create a simple, responsive Todo application. To accomplish this, you will use Bootstrap and Django. Bootstrap is an open-source, responsive web application framework, and Django is a Python-based web application development framework. Both frameworks are highly popular due to their ease of use.
Creating a Todo Web Application with Bootstrap and Django
The Todo web application in this subsection uses Bootstrap and Django. Bootstrap’s CSS templates are used for the UI features. Django serves as both the front end and back end. Django templates are part of the user interface to get and set data via HTTP requests. Incoming requests are then handled by an API built using the Django REST Framework.
Prerequisites
To build the Todo application, you must install Python, PIP, Django, Django REST Framework, Bootstrap, and jQuery. The Todo application on the following pages was developed and tested with specific software versions. To avoid errors, please ensure you install the same versions: Python v3.9.4, PIP v21.3.1, Django v4.0.1, Django REST Framework v3.13.1, Bootstrap v4.5.0, and jQuery v3.5.1.
The steps to build the Todo application are as follows:
- Install and set up a local programming environment for Python 3.
- Download and install Python 3.9.4.
- Figure 11.14 shows adding Python and its Scripts subfolder to your path environment data (on Windows, the path updates might be: C:\Users\your_username\AppData\Local\Programs\Python\Python39 and C:\Users\your_username\AppData\Local\Programs\Python\Python39\Scripts).
- Create a Python venv:
python -m venv py394venv
- Activate the venv:
cd py394venv
Windows:.\Scripts\activate.bat
macOS:source ./bin/activate
- Install in the local programming environment Django:
pip install Django==4.0.1
- Install in the local programming environment Django REST Framework:
pip install djangorestframework==3.13.1]
Figure 11.15 shows the sequence of steps needed to install the Python environment for working with Django and the Django REST Framework.
Creating the Django Project
The first step to building a Django web application is to create a Django project, which is a high-level directory used to contain the directories and files necessary to run a Django web application. To create a Django project, run the following commands:
$ mkdir BootstrapDjangoToDoApp
$ cd BootstrapDjangoToDoApp
$ django-admin startproject ToDoApp.
The period at the end of the last command is very important to ensure that Django-dependent files are generated in the current directory. By following these commands, the directory, BootstrapDjangoToDoApp/, will be created and Django-dependent files will be generated as shown in Figure 11.16.
Django built-in database tables are used to manage users, groups, migrations, and so forth in a web application. To generate these tables, run the migrate command:
$ python manage.py migrate
The final step to creating a Django project is to confirm that the setup was completed. To do this, run the runserver command shown here:
$ python manage.py runserver
To further confirm that the Django project setup was completed, launch a browser and navigate to http://localhost:8000. Figure 11.17 shows the Django page.
Link to Learning
Django is a free and open-source Python web framework that can make web development more efficient and less time-consuming. With an emphasis on streamlining web development and making it easier for web developers to meet deadlines, Django also requires less code.
Creating and Registering the Todo App
Once the Django project is successfully completed and set up, the next step is to create the Todo application and register it in the Django project. To accomplish this, run the following command:
$ python manage.py startapp todo
The todo/ directory will be generated under the Django project directory, BootstrapDjangoToDoApp/. Files related to the Todo application will be generated as shown in Figure 11.18.
Next, the Todo application must be registered in the Django project as an installed app so that Django can recognize it. To do this, open the ToDoApp/settings.py file. Look for the INSTALLED_APPS variable as seen in Figure 11.19. Add ‘todo’ to the list as shown. Because the Django REST Framework will be used, also add ‘rest_framework’ to the list.
Define the Todo Model
Once you register and install the ToDo web application, you will be able to create todo tasks, which can be assigned categories. The next step is to create the models. Two models will be created for Category and TodoList. Open the todo/models.py file and add the code shown.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils import timezone
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name = ("Category")
verbose_name_plural = ("Categories")
def __str__(self):
return self.name
class TodoList(models.Model):
title = models.CharField(max_length=250)
content = models.TextField(blank=True)
created = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))
due_date = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))
category = models.ForeignKey(Category, default="General", on_delete=models.DO_NOTHING)
class Meta:
ordering = ["-created"] # order by most recently created
def __str__(self):
return self.title
The Category and TodoList Python classes describe the properties for the models for Category and TodoList tables, respectively. The TodoList model contains a ForeignKey field to the Category model. Each todo item will be associated with one category. After creating the models, a migration file needs to be generated to create the physical tables in the database. To generate the migration file, run the following command:
$ python manage.py makemigrations todo
This command will generate a migration file in todo/migrations/, which will look like the following code.
# Generated by Django 4.0.1 on 2022-01-30 21:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
options={
'verbose_name': 'Category',
'verbose_name_plural': 'Categories',
},
),
migrations.CreateModel(
name='TodoList',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=250)),
('content', models.TextField(blank=True)),
('created', models.DateField(default='2022-01-30')),
('due_date', models.DateField(default='2022-01-30')),
('category', models.ForeignKey(default='General', on_delete=django.db.models.deletion.DO_NOTHING, to='todo.category')),
],
options={
'ordering': ['-created'],
},
),
]
The next step is to apply the changes in the migration file to the database, which is accomplished by running the following command:
$ python manage.py migrate
For this application, Django uses the default sqlite3 (db.sqlite3) database. Please note that Django supports other databases as well, including MySQL and PostgreSQL. To define the Todo model, you can use the default Django admin interface to perform CRUD operations on the database. To use the admin interface, open the todo/admin.py file and register the models as seen in the following code snippet.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from . import models
# Register your models here.
class TodoListAdmin(admin.ModelAdmin):
list_display = ("title", "created", "due_date")
class CategoryAdmin(admin.ModelAdmin):
list_display = ("name",)
admin.site.register(models.TodoList, TodoListAdmin)
admin.site.register(models.Category, CategoryAdmin)
The next step is to create a superuser account that allows access to the admin interface. To do this, run the following command and follow the prompts to enter a username, email address, and password for the superuser.
$ python manage.py createsuperuser
Once this step is complete, restart the server using the following command:
$ python manage.py runserver
After this is complete, open a browser and navigate to http://localhost:8000/admin/. To access the admin interface, log in with the credentials that you set up for the superuser. When you log in to the admin interface, you should see the following page from Figure 11.20. On this page, you will have the ability to create, edit, and delete categories and todo items.
Setting Up the Django REST APIs
Previously, you installed the Django REST Framework, which provides a toolkit to build APIs. In this section, you’ll create the serializers, View functions, and routers required for the API.
Creating the Serializers
The next step is to implement two serializers, one for the Category model and one for the TodoList model. A serializer is a tool to control response outputs and convert complex data into content, such as JSON. In the todo/ directory, create the file, serializers.py, and add the code shown in the code snippet that follows. The serializer classes inherit from serializers.ModelSerializer because this class creates the serializer, with fields corresponding to the Model fields defined earlier. Each serializer specifies the Model to work with and the fields to be included in the JSON object, which are all fields.
# todo/serializers.py
from rest_framework import serializers
from .models import TodoList, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = TodoList
fields = "__all__"
Creating the View
After you create each serializer, the next step is to create a corresponding View function for both the Category serializer and the Todo serializer. To do this, open todo/views.py and add the code shown in the code snippet that follows. The viewsets class, which provides a default implementation of the CRUD operations, is imported from rest_framework. The CategoryView and TodoView classes provide a queryset of categories and todo items, respectively. They also specify the serializer_class defined in the previous section.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render,redirect
from rest_framework import viewsets
from .serializers import TodoSerializer, CategorySerializer
from .models import TodoList, Category
import datetime
# Create your views here.
class CategoryView(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class TodoView(viewsets.ModelViewSet):
queryset = TodoList.objects.all()
serializer_class = TodoSerializer
Creating the Routers
The next step is to create the routers that provide URL paths for the API. To do this, open the todo/urls.py file and add the code shown in the code snippet that follows:
from django.urls import path, include
#from todo import views as todo_views
from rest_framework import routers
from todo.views import *
router = routers.DefaultRouter()
router.register(r'categories', CategoryView, basename='Categories')
router.register(r'todos', TodoView, basename='Todos')
urlpatterns = [
path('', index, name="TodoList"),
path(r'api/', include(router.urls)),
]
Once you complete this step, launch the Django server using the following command:
$ python manage.py runserver
To access the API, launch a browser and navigate to http://127.0.0.1:8000/api/. As shown in Figure 11.21, you should see two API paths listed, one for categories and another for todo items.
Todo items are dependent on categories. To perform CRUD operations on the Category table, click on the categories API path, as shown in Figure 11.22.
Once you access the Category List page, enter a category name in the field near the bottom of the page and click the Post button to save it. You can add additional category names. Once you save each name using the Post button, the categories will appear in JSON format, as illustrated in Figure 11.23.
To ensure that categories can be updated or deleted as needed, the primary key, which is “id”, must be included in the API path (e.g., /api/categories/{id}/). For example, to update or delete the category with id=3, the API path is /api/categories/3/. As shown in Figure 11.24, once this category is pulled up on the Category Instance page, the DELETE button is visible at the top to delete the category. If the category needs to be updated, the PUT button visible near the bottom can be used for updates.
Next, navigate back to the API root. Click on the todos API path to see, as outlined in Figure 11.25, that a Todo item has a Category field that appears as a drop-down list of categories. The CRUD operations also can be performed on the TodoList table. This will be done next through the user interface via templates.
Creating the User Interface
To use and control the Todo application, you must create a user interface (UI). This section will outline the steps to do this.
Installing Bootstrap
The first step to create a UI is to install Bootstrap, which, as an open-source, responsive web application framework, can be used in web applications like Django to create UIs. To install Bootstrap in a Django application, you have several options. In this scenario, an efficient method is to download the Bootstrap CSS and JS files and add them to the static/ directory, as shown in Figure 11.26. To do this, first create the static/ directory in the Django project directory. In addition, to install jQuery, download the JS file and add it to the static/ directory. Finally, add a custom CSS file to include individual style in the Django web application.
After adding the CSS and JS files to the static/ directory, the next step is to open the ToDoApp/settings.py file, navigate to the bottom of the file, and add the path variables shown in the following code snippet.
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
Creating the View
The next step is to create the View. The View function is needed for template pages, which are created in the next section. The View function is also needed to interact with the database to both create and delete todo items.
To create the View, open the todo/views.py file and add the code shown in the following code snippet. This code takes an HTTP request object. If the request method is POST, a todo item is either created or deleted, depending on which button is clicked. Otherwise, the request method is GET and the todo items are displayed to the user.
def index(request): # index view
todos = TodoList.objects.all() # query all todos with object manager
categories = Category.objects.all() # get all categories with object manager
if request.method == "POST": # check if request method is POST
if "taskAdd" in request.POST: # check if request is to add a todo item
title = request.POST["description"] # title
date = str(request.POST["date"]) # date
category = request.POST["category_select"] # category
content = title + " -- " + date + " " + category # content
Todo = TodoList(title=title, content=content, due_date=date, category=Category.objects.get(name=category))
Todo.save() # save todo item
return redirect("/") # reload page
if "taskDelete" in request.POST: # check if request is to delete a todo
checkedlist = request.POST["checkedbox"] # checked todos to be deleted
for todo_id in checkedlist:
todo = TodoList.objects.get(id=int(todo_id)) # get todo id
todo.delete() # delete todo
return render(request, "index.html", {"todos": todos, "categories":categories})
Creating the Templates
The final step to build a Django web application is to create templates, which are HTML files that can also contain embedded Python code. Create a file called style.css under the /static/css directory and insert the code shown here into that file. The templates discussed in the following sections require this style sheet.
/* basic reset */
*,
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/* app */
html {
font-size: 100%;
}
body {
background: #e6f9ff;
font-family: "Open Sans", sans-serif;
}
/* super basic grid structure */
.container {
width: 600px;
margin: 0 auto;
background: #ffffff;
padding: 20px 0;
-webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
}
.row {
display: block;
padding: 10px;
text-align: center;
width: 100%;
clear: both;
overflow: hidden;
}
.half {
width: 50%;
float: left;
}
.content {
background: #fff;
}
/* logo */
h1 {
font-family: "Rokkitt", sans-serif;
color: #666;
text-align: center;
font-weight: 400;
margin: 0;
}
.tagline {
margin-top: -10px;
text-align: center;
padding: 5px 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: #777;
}
/* inputs */
.inputContainer {
height: 60px;
border-top: 1px solid #e5e5e5;
position: relative;
overflow: hidden;
}
.inputContainer.last {
border-bottom: 1px solid #e5e5e5;
margin-bottom: 20px;
}
.inputContainer.half.last.right {
border-left: 1px solid #efefef;
}
input[type="date"],
input[type="text"],
select {
height: 100%;
width: 100%;
padding: 0 20px;
position: absolute;
top: 0;
vertical-align: middle;
display: inline-block;
border: none;
border-radius: none;
font-size: 13px;
color: #777;
margin: 0;
font-family: "Open Sans", sans-serif;
font-weight: 600;
letter-spacing: 0.5px;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
input[type="date"] {
cursor: pointer;
}
input[type="date"]:focus,
input[type="text"]:focus,
select:focus {
outline: none;
background: #ecf0f1;
}
::-webkit-input-placeholder {
color: lightgrey;
font-weight: normal;
-webkit-transition: all 0.3s;
transition: all 0.3s;
}
::-moz-placeholder {
color: lightgrey;
font-weight: normal;
transition: all 0.3s;
}
::-ms-input-placeholder {
color: lightgrey;
font-weight: normal;
transition: all 0.3s;
}
input:-moz-placeholder {
color: lightgrey;
font-weight: normal;
transition: all 0.3s;
}
input:focus::-webkit-input-placeholder {
color: #95a5a6;
font-weight: bold;
}
input:focus::-moz-input-placeholder {
color: #95a5a6;
font-weight: bold;
}
.inputContainer label {
padding: 5px 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: #777;
display: block;
position: absolute;
}
button {
font-family: "Open Sans", sans-serif;
background: transparent;
border-radius: 2px;
border: none;
outline: none;
height: 50px;
font-size: 14px;
color: #fff;
cursor: pointer;
text-transform: uppercase;
position: relative;
-webkit-transition: all 0.3s;
transition: all 0.3s;
padding-left: 30px;
padding-right: 15px;
}
.icon {
position: absolute;
top: 30%;
left: 10px;
font-size: 20px;
}
.taskAdd {
background: #444;
padding-left: 31px;
}
.taskAdd:hover {
background: #303030;
}
.taskDelete {
background: #e74c3c;
padding-left: 30px;
}
.taskDelete:hover {
background: #c0392b;
}
/* task styles */
.taskList {
list-style: none;
padding: 0 20px;
}
.taskItem {
border-top: 1px solid #e5e5e5;
padding: 15px 0;
color: #777;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.5px;
}
.taskList .taskItem:nth-child(even) {
background: #fcfcfc;
}
.taskCheckbox {
margin-right: 1em;
}
.complete-true {
text-decoration: line-through;
color: #bebebe;
}
.taskList .taskDate {
color: #95a5a6;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
display: block;
margin-left: 41px;
}
.fa-calendar {
margin-right: 10px;
font-size: 16px;
}
[class*="category-"] {
display: inline-block;
font-size: 10px;
background: #444;
vertical-align: middle;
color: #fff;
padding: 10px;
width: 75px;
text-align: center;
border-radius: 2px;
float: right;
font-weight: normal;
text-transform: uppercase;
margin-right: 20px;
}
.category- {
background: transparent;
}
.category-Personal {
background: #2980b9;
}
.category-Work {
background: #8e44ad;
}
.category-School {
background: #f39c12;
}
.category-Cleaning {
background: #16a085;
}
.category-Other {
background: #d35400;
}
footer {
text-align: center;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: #777;
}
footer a {
color: #f39c12;
}
/* custom checkboxes */
.taskCheckbox {
-webkit-appearance: none;
appearance: none;
-webkit-transition: all 0.3s;
transition: all 0.3s;
display: inline-block;
cursor: pointer;
width: 19px;
height: 19px;
vertical-align: middle;
}
.taskCheckbox:focus {
outline: none;
}
.taskCheckbox:before,
.taskCheckbox:checked:before {
font-family: "FontAwesome";
color: #444;
font-size: 20px;
-webkit-transition: all 0.3s;
transition: all 0.3s;
}
.taskCheckbox:before {
content: "\f096";
}
.taskCheckbox:checked:before {
content: "\f14a";
color: #16a085;
}
/* custom select menu */
.taskCategory {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
padding-left: 16.5px; /*specific positioning due to difficult behavior of select element*/
background: #fff;
}
.selectArrow {
position: absolute;
z-index: 10;
top: 35%;
right: 0;
margin-right: 20px;
color: #777;
pointer-events: none;
}
.taskCategory option {
background: #fff;
border: none;
outline: none;
padding: 0 100px;
}
The first template file is base.html, which is the file that includes links to Bootstrap and jQuery. To illustrate these links, a snippet of the base.html file is shown in the code. The Bootstrap navigation bar is implemented in this file.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>TodoApp - Django</title> {% load static %} <!-- Bootstrap CSS --> <link rel="stylesheet" href="/static/css/bootstrap.min.css" /> <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" /> <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}" /> <!-- jQuery --> <script type="text/javascript" src="{% static 'js/jquery-3.5.1.min.js' %}"></script> <!-- Popper --> <script type="text/javascript" src="{% static 'js/popper.min.js' %}"></script> <!-- Bootstrap Core JavaScript --> <script type="text/javascript" src="{% static 'js/bootstrap.bundle.min.js' %}"></script> <script </head> <body> <nav class="navbar navbar-dark bg-dark justify-content-between"> <a class="navbar-brand">ToDo</a> <form class="form-inline"> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" /> <button class="btn btn-outline-info my-2 my-sm-0" type="submit"> Search </button> </form> </nav> <div>{% block content %} {% endblock content %}</div> </body> </html>
The second template file is index.html, which lists any existing todo items and includes the form to create or delete a todo item. A snippet of the index.html file is shown in the following code. The index.html file also extends the base.html template, which allows the Bootstrap navigation bar implemented in base.html to be inserted in this page and every page that extends it.
{% extends "./base.html" %} {% load static %} {% block content %}
<div django-app="TaskManager">
<div class="container mt-5">
<div class="content">
<h1>Todo List</h1>
<!--<p class="tagline">Django Todo App</p>-->
<form action="" method="post">
{% csrf_token %}
<!-- csrf token for basic security -->
<div class="inputContainer">
<input
type="text"
id="description"
class="taskName"
placeholder="What do you need to do?"
name="description"
required
/>
<label for="description">Description</label>
</div>
<div class="inputContainer half last">
<i class="fa fa-caret-down selectArrow"></emphasis>
<select id="category" class="taskCategory" name="category_select">
<option class="disabled" value="">Choose a category</option>
{% for category in categories %}
<option
class=""
value="{{ category.name }}"
name="{{ category.name }}"
>
{{ category.name }}
</option>
{% endfor %}
</select>
<label for="category">Category</label>
</div>
<div class="inputContainer half last right">
<input type="date" id="dueDate" class="taskDate" name="date" />
<label for="dueDate">Due Date</label>
</div>
<div class="row">
<button class="taskAdd" name="taskAdd" type="submit">
<i class="fa fa-plus icon"></emphasis>Add task
</button>
<button
class="taskDelete"
name="taskDelete"
formnovalidate=""
type="submit"
onclick="$('input#sublist').click();"
>
<i class="fa fa-trash-o icon"></emphasis>Delete Tasks
</button>
</div>
<ul class="taskList">
{% for todo in todos %}
<!-- django template lang - for loop -->
<li class="taskItem">
<input
type="checkbox"
class="taskCheckbox"
name="checkedbox"
id="{{ todo.id }}"
value="{{ todo.id }}"
/>
<label for="{{ todo.id }}"
><span class="complete-">{{ todo.title }}</span></label>
<span class="category-{{ todo.category }}"
>{{ todo.category }}</span>
>{{ todo.category }}</span
<strong class="taskDate"
><i class="fa fa-calendar"></emphasis>Created: {{todo.created}} - Due:
>{{ todo.category }}</span
{{todo.due_date}}</strong>
>{{ todo.category }}</span
</li>
{% endfor %}
</ul>
<!-- taskList -->
</form>
</div>
<!-- content -->
</div>
<!-- container -->
</div>
{% endblock %}
The first step to access the Todo Django web application is to restart the Django server using the following command:
$ python manage.py runserver
Next, launch a browser and navigate to http://localhost:8000. The page highlighted in Figure 11.27 should appear.
To create a todo list, fill out the form and click the Add Task button as shown in Figure 11.28.
In addition, the todo item should also be viewable in the API that was created. This should appear as outlined in Figure 11.29.