Build A Todo App Using Django


A friend asked me how she could use Django to develop a Todo App and I showed her. A week later, another friend asked for the same thing! So, I decided to write about it just in case anyone else needed to know how I would implement it.

We would need the following models:

  1. Task : to input each task item
  2. SubTask : to input items under each task item

We would open our terminal and change the directory to where we want to work.

In The Terminal…

Create a directory to store our application and move into it
run mkdir taskproject
run cd taskproject
Now that we are in the taskproject directory, we need a virtual environment to keep the version of Django and the dependencies required to build this app contained.
We might need to build another application that requires a different version of Django. When you install the new version of Django, the existing one is replaced and if there are changes in the version that the Task application depends on, the Task application could stop working.
That said, run python3 -m venv todovenv to create a virtual environment called todovenv
To activate it run source todovenv/bin/activate
(for windows: run todovenv\Scripts\activate)
To install django, run python -m pip install Django
To setup our new project called taskproject, run django-admin startproject taskproject .
Take note of the trailing period; it tells django to setup the project inside the current directory.
To create our task app, run python manage.py startapp task
To start the Django inbuilt server, run python manage.py runserver
Please note that this server and the database that comes with this application are for testing purposes and are not suitable for production

In the browser

In your browser, visit: 127.0.0.1:8000. You should see a similar picture

Project Structure

Right now, our taskproject folder structure should look something like:
-taskproject
-taskproject
init.py
asgi.py
settings.py
urls.py
wsgi.py
-task
-migrations
init.py
admin.py
apps.py
models.py
tests.py
views.py
manage.py

In The Code Editor

Now we move into how code editor to write some “hello world!”. In the task.models.py let’s create our models

...
class Task(models.Model):
    item = models.CharField(max_length=250)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.item


class SubTask(models.Model):
    task = models.ForeignKey(Task, on_delete=models.CASCADE)
    item = models.CharField(max_length=250)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.item

Add the application to “installed apps” in the project settings.py file so that our project becomes aware of the application

#settings.py
...
INSTALLED_APPS = [
'task',
...

Back In The Terminal

In your terminal,
Run python manage.py makemigrations
Run python manage.py migrate

The above creates the Task and the SubTask tables in your database with the necessary column names. As advertised, “Django makes it easier to build better Web apps more quickly and with less code.”

The Django documentation advices that applications be portable, that is, applications should contain their files. So, the templates and urls of our task application will be created within the task directory.
We will create a urls.py file and in the task.urls.py let’s add…

#task.urls.py
from django.urls import path
from .views import *

app_name = 'task'

urlpatterns = [
  path('', index, name='home'), #1
  path('add-task/', add_task, name='add-task'), #2
  path('edit-task/<int:pk>/', edit_task, name='edit-task'), #3
    path('add-subtask/<int:pk>/', add_subtask, name='add-subtask'), #4
  path('edit-subtask/<int:pk>/', edit_subtask, name='edit-subtask'), #5

]

1 Displays our task list

2 Allows us add new tasks

3 Allows us edit tasks

4 Allows us add subtasks to each task

5 Allows us edit subtasks

In our application directory, create a “templates” directory and a “task” directory within it. In this directory create task_list.html for a page to view all our tasks. In the task_list.html let’s add…

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {% for task in task_list %}
        {{ task }}<a href="{% url 'task:edit-task' task.id %}">Update Task</a><br>
        <ul>
        {% for subtask in task.subtask_set.all %}
            <li>{{ subtask }}</li>
        {% endfor %}
        </ul>
    {% endfor %}
    <a href="{% url 'task:add-task' %}">Add New Task</a>
</body>
</html>

Create add_task.html for a page to add tasks and in the add_task.html let’s add…

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form method="POST" action="">{% csrf_token%}
        {{ form }}
        <input type="submit" name="Add Task" />
    </form>
</body>
</html>

Create add_subtask.html for a page to add tasks and in the add_subtask.html let’s add…

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    Add Subtask to {{ task }}:<br>
    <form method="POST" action="">{% csrf_token%}
        {{ form }}
        <input type="submit" name="Add Subtask" />
    </form>
</body>
</html>

Create edit_task.html for a page to add tasks and in the edit_task.html let’s add…

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>Task</p>
    <form id="taskform" method="POST" action="">{% csrf_token%}
        {{ form }}
        <input form="taskform" type="submit" name="submit" value="Update Task" />
    </form>
    <ul id="subtasklist">
        <p>subtasks:</p>
        {% for subtask in task.subtask_set.all %}
            <li>
                <form id="subtaskform{{forloop.counter}}" action="{% url 'task:edit-subtask' subtask.id  %}" method="POST">{% csrf_token %}
                    <input type="hidden" name="item" value="{{ task.id }}" />
                    Title: <input type="text" name="title" value="{{ subtask }}" />
                    Completed: <input type="checkbox" name="completed">
                    <input form="subtaskform{{forloop.counter}}" type="submit" value="Update" />
                </form>
            </li>
        {% endfor %}
        </ul>
        <p id="newsubtask"></p>

    <br>
    <a href="{% url 'task:add-subtask' task.id  %}">Add Subtask Form</a><br><br>
    <button type="button" onclick="addSubTask()">Add Subtask JavaScript</button>
    {% include 'task/subtaskform.html' %}
    
    <script>
        function addSubTask() {
            var taskBody = document.getElementById('addsubtaskformdiv')
      taskBody.style.display = "block"
        }
    </script>
</body>
</html>

Here, we have two options to add subtasks to tasks:
First, “Add Subtask Form” redirects us to a new page to add subtasks and redirects us back upon successful submision.
“Add Subtask” uses some JavaScript to display the hidden subtask form in the same page to be filled and submitted on the same page. So, you can choose which button you prefer and remove the other 😃. Also, create subtaskform.html for a page to add subtasks

In the subtaskform.html let’s add…

<div id="addsubtaskformdiv" style="display: none;">
    <p>Add Subtask</p>
    <form id="add-subtaskform" method="POST" action="{% url 'task:add-subtask' task.id  %}">{% csrf_token%}
        Title: <input type="text" name="title">
        Completed: <input type="checkbox" name="completed">
        <input form="add-subtaskform" type="submit" name="Add Subtask" />
    </form>
</div>

With all that done, let’s write the logic for our application. In the task.views.py for the task application, we will have…

#views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.shortcuts import render, get_object_or_404, reverse

from .models import *
from .forms import *
# Create your views here.

def index(request):
    task_list = Task.objects.all()
    return render(request, "task/task_list.html", {"task_list":task_list,})

def add_task(request):
    form = TaskForm()
    if request.method == "POST":
        form = TaskForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/")
    return render(request, "task/add_task.html", {'form':form,})

def edit_task(request, pk):
    task = Task.objects.get(pk=pk)
    form = TaskForm(request.POST or None, instance=task)
    if request.method == "POST":
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/")
    return render(request, "task/edit_task.html", {'form':form, 'task':task,})

def add_subtask(request, pk):
    task = Task.objects.get(pk=pk)
    form = SubTaskForm()
    if request.method == "POST":
        form = SubTaskForm(request.POST)
        if form.is_valid():
            subtask = form.save(commit=False)
            subtask.task = task
            form.save()
            return HttpResponseRedirect(f"/edit-task/{subtask.task.id}/")
    return render(request, "task/add_subtask.html", {'form':form, 'task':task})


def edit_subtask(request, pk):
    subtask = SubTask.objects.get(pk=pk)
    if request.method == "POST":
        subtask.title = request.POST.get("title")
        if request.POST.get("completed") == 'on':
            subtask.completed = True
        else:
            subtask.completed = False
        subtask.save()
        return HttpResponseRedirect(f"/edit-task/{subtask.task.id}/")

The name of each function tells us what it does. This helps other people us our codes especially when documentation is included. Also, when you go over your codes, after some time, you can still tell what each module does.

Now we need forms for adding and editing objects. let’s create a forms.py in our application directory
In the task.forms.py, add…

#task.forms.py
from django import forms


from .models import *

class TaskForm(forms.ModelForm):

    class Meta:
        model = Task
        fields = '__all__'


class SubTaskForm(forms.ModelForm):
    class Meta:
        model = SubTask
        exclude = ('task',)

Right now, our taskproject folder structure should look something like:
-taskproject
-taskproject
init.py
asgi.py
settings.py
urls.py
wsgi.py
-task
-migrations
init.py
-templates
-task
-add_subtask.html
-add_task.html
-edit_task.html
-task_list.html
admin.py
apps.py
forms.py
models.py
tests.py
urls.py
views.py
manage.py

Finally, in the project urls.py, let’s anchor our task application urls.
In urls.py add… path('', include('task.urls'))
So, our urlpatterns will now be…

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('task.urls'))
]

Let us start our server. In the terminal,
Run python manage.py runserver
Now when you refresh your browser, you should see a button prompting you to add new task item.
Try it out and let me know how it goes!
Here, we used bear html pages with zero style or design; I left that for you to handle.
Let me know of any improvements that can be made to this application; I’ll love to learn.
Enjoy!

Olaide Alaka
Python, JavaScript, full-stack developer with over 4 years' experience I am a full-stack mid-level developer versed in Python/Django with JavaScript, HTML, CSS, jQuery/Ajax. I have developed several web and desktop applications, including school management, quiz, school learning, eCommerce, and inventory applications, landing pages, etc., and worked on other complex projects involving Web scraping, Python scripting, JavaScript.

Related Articles

Leave A Reply

Please enter your comment!
Please enter your name here

Stay on Top - Get the daily news in your inbox