Create TODO App with Django - Step-by-Step Guide

Faraz

By Faraz -

Learn how to create a simple TODO app using Django. This beginner-friendly guide walks you through each step to build and style your own task management app.


create-todo-app-with-django-step-by-step-guide.webp

Creating a TODO app is a great way to learn Django, one of the most popular Python-based web frameworks. In this tutorial, you'll learn step-by-step how to build a simple task management app. Whether you're a beginner or have some experience, this guide will help you understand the basics of Django while creating something practical and useful.

Prerequisites

Before we dive in, make sure you have a basic understanding of Django and web development. You'll need the following tools and libraries installed on your machine:

  • Python (version 3.6 or higher)
  • Django (version 3.0 or higher)
  • A text editor or IDE (such as VS Code or PyCharm)
  • Basic knowledge of HTML and CSS

Setting Up the Development Environment

Installing Django

Before starting, ensure you have Python installed. You can install Django using pip:

pip install django

Creating a new Django project

Create a new Django project using the following command:

django-admin startproject todoproject

Setting up the project structure

Navigate into your project directory:

cd todoproject

Create a Django App

Create a new app within your project:

python manage.py startapp todoapp

Adding the app to the project

In todoproject/settings.py, add todoapp to the INSTALLED_APPS list:

INSTALLED_APPS = [
    'todoapp',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Creating the ToDo App

Create Models

In todoapp/models.py, define the model for the TODO items:

from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

Create Forms

In todoapp, create a new file named forms.py for the TODO model and add the following function:

from django import forms
from .models import Todo

class TodoForm(forms.ModelForm):
    class Meta:
        model = Todo
        fields = ['title', 'description', 'completed']

Create Views

Next, we’ll create views for listing, adding, updating, and deleting TODO items. Open todoapp/views.py and add the following:

from django.shortcuts import render, redirect, get_object_or_404
from .models import Todo
from .forms import TodoForm

def todo_list(request):
    todos = Todo.objects.all().order_by('-created_at')
    return render(request, 'todo_list.html', {'todos': todos})

def todo_create(request):
    if request.method == 'POST':
        form = TodoForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('todo_list')
    else:
        form = TodoForm()
    return render(request, 'todo_form.html', {'form': form})

def todo_update(request, pk):
    todo = get_object_or_404(Todo, pk=pk)
    if request.method == 'POST':
        # Check if the 'completed' field is in the POST data
        todo.completed = 'completed' in request.POST
        todo.save()
        return redirect('todo_list')
    else:
        # Render the form if needed (not necessary for the checkbox functionality)
        form = TodoForm(instance=todo)
        return render(request, 'todo_form.html', {'form': form})

def todo_delete(request, pk):
    todo = Todo.objects.get(pk=pk)
    if request.method == 'POST':
        todo.delete()
        return redirect('todo_list')
    return render(request, 'todo_confirm_delete.html', {'todo': todo})

Set Up URL Routing

Add a URL pattern for your view. In todoapp/urls.py (create this file if it doesn't exist), add:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.todo_list, name='todo_list'),
    path('create/', views.todo_create, name='todo_create'),
    path('update/<int:pk>/', views.todo_update, name='todo_update'),
    path('delete/<int:pk>/', views.todo_delete, name='todo_delete'),
]

Include the app's URLs in your project's URL configuration. Open todoproject/urls.py and include the ToDo app URLs:

from django.contrib import admin
from django.urls import path, include

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

Create Templates

Create a templates directory within the TODO app and then create base.html, todo_list.html, todo_confirm_delete.html and todo_form.html inside this directory.

Base Template (base.html):

{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODO App</title>
    <link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&amp;display=swap'>
    <link rel="stylesheet" href="{% static 'styles.css' %}">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
</head>
<body>
    <div class="container">
        <nav class="navbar">
            <h1>My TODO App</h1>
            <a href="{% url 'todo_create' %}" class="btn btn-add">+ Add TODO</a>
        </nav>
        <div class="content">
            {% block content %}{% endblock %}
        </div>
    </div>
<script>
    document.querySelectorAll('.todo-checkbox-form').forEach(form => {
    form.addEventListener('change', function(event) {
        event.preventDefault();
        const formData = new FormData(this);
        fetch(this.action, {
            method: 'POST',
            body: formData,
            headers: {
                'X-CSRFToken': formData.get('csrfmiddlewaretoken')
            }
        }).then(response => {
            if (response.ok) {
                location.reload();
            }
        });
    });
});
</script>
</body>
</html>

TODO List Template (todo_list.html):

{% extends 'base.html' %}
{% block content %}
<div class="todo-container">
    {% for todo in todos %}
    <div class="todo-card {% if todo.completed %}completed{% endif %}">
        <div class="todo-header">
            <h2>{{ todo.title }}</h2>
            <form method="post" action="{% url 'todo_update' todo.pk %}" class="todo-checkbox-form">
                {% csrf_token %}
                <label class="custom-checkbox">
                    <input type="checkbox" name="completed" onchange="this.form.submit()" {% if todo.completed %}checked{% endif %}>
                    <span class="checkmark"></span>
                </label>
            </form>
        </div>
        <p>{{ todo.description }}</p>
        <div class="todo-actions">
            <a href="{% url 'todo_update' todo.pk %}" class="btn btn-edit"><i class="fas fa-edit"></i> Edit</a>
            <a href="{% url 'todo_delete' todo.pk %}" class="btn btn-delete"><i class="fas fa-trash"></i> Delete</a>
        </div>
    </div>
    {% endfor %}
</div>
{% endblock %}

TODO Form Template (todo_form.html):

{% extends 'base.html' %}

{% block content %}
<div class="add-form-container">
    <h2>{{ form.instance.pk|yesno:"Update TODO,Create TODO" }}</h2>
    <form method="post">
        {% csrf_token %}
        <div class="form-group">
            <label for="id_title">Title</label>
            {{ form.title }}
        </div>
        <div class="form-group">
            <label for="id_description">Description</label>
            {{ form.description }}
        </div>
        <div class="form-group">
            <div class="task-complete-area">
            <label for="id_completed">Completed</label>
                <label class="custom-checkbox">
                    {{ form.completed }}
                    <span class="checkmark"></span>
                </label>
                </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">{{ form.instance.pk|yesno:"Update,Create" }}</button>
            <a href="{% url 'todo_list' %}" class="btn btn-secondary">Cancel</a>
        </div>
    </form>
</div>
{% endblock %}

TODO Delete Confirmation Template (todo_confirm_delete.html):

{% extends 'base.html' %}

{% block content %}
<div class="delete-container">
    <h2>Are you sure you want to delete "{{ todo.title }}"?</h2>
    <form method="post">
        {% csrf_token %}
        <div class="form-actions">
            <button type="submit" class="btn btn-danger">Yes, delete it</button>
            <a href="{% url 'todo_list' %}" class="btn btn-secondary">Cancel</a>
        </div>
    </form>
</div>
{% endblock %}

Add CSS Styling

Create a CSS file named styles.css in todoapp/static/ and add the following styles:

body {
    font-family: 'Poppins', sans-serif;
    background-color: #f0f2f5;
    margin: 0;
    padding: 0;
}

.container {
    width: 80%;
    max-width: 1200px;
    margin: auto;
    padding: 20px;
}

.navbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px;
    background-color: #343a40;
    color: white;
    border-radius: 10px;
    margin-bottom: 20px;
}

.navbar h1 {
    margin: 0;
    font-size: 24px;
}

.navbar .btn-add {
    background-color: #28a745;
    color: white;
    padding: 10px 15px;
    border-radius: 5px;
    text-decoration: none;
}

.navbar .btn-add:hover {
    background-color: #218838;
}

.content {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.todo-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
}

.todo-card {
    background: white;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s, box-shadow 0.3s;
}

.todo-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}

.todo-card.completed {
    background-color: #d4edda;
    border-left: 5px solid #28a745;
}

.todo-card h2 {
    margin: 0;
    font-size: 20px;
    color: #343a40;
}

.todo-card p {
    color: #6c757d;
}

.todo-actions {
    margin-top: 10px;
    display: flex;
    gap: 10px;
}

.btn {
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 5px;
    text-decoration: none;
    color: white;
    transition: background-color 0.3s;
    font-family: 'Poppins', sans-serif;
}

.btn-edit {
    background-color: #ffc107;
}

.btn-edit:hover {
    background-color: #e0a800;
}

.btn-delete {
    background-color: #dc3545;
}

.btn-delete:hover {
    background-color: #c82333;
}

.form-container {
    background: white;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.add-form-container {
    max-width: 600px;
    margin: 20px auto;
    padding: 20px;
    background-color: #f8f9fa;
    border-radius: 8px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.form-group {
    margin-bottom: 15px;
}

.form-group .task-complete-area{
    display: flex;
    gap: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
    color: #333;
}

.form-group input,
.form-group textarea {
    width: 100%;
    padding: 10px;
    border: 1px solid #ced4da;
    border-radius: 5px;
    box-sizing: border-box;
}

.form-group textarea {
    height: 100px;
    resize: none;
}

.form-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.btn-primary {
    background-color: #007bff;
    color: #fff;
    border: none;
    font-size: 16px;
}

.btn-primary:hover {
    background-color: #0056b3;
}

.btn-secondary {
    background-color: #6c757d;
    color: #fff;
    border: none;
}

.btn-secondary:hover {
    background-color: #5a6268;
}

.delete-container {
    max-width: 500px;
    margin: 20px auto;
    padding: 20px;
    background-color: #f8d7da;
    border-radius: 8px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.delete-container h2 {
    color: #721c24;
}

.btn-danger {
    background-color: #dc3545;
    color: #fff;
    border: none;
    font-size: 16px;
    font-weight: bold;
}

.btn-danger:hover {
    background-color: #c82333;
}

.todo-form input, .todo-form textarea {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border-radius: 5px;
    border: 1px solid #ced4da;
}

.btn-save {
    background-color: #28a745;
    color: white;
    padding: 10px 15px;
    border-radius: 5px;
    border: none;
    cursor: pointer;
}

.btn-save:hover {
    background-color: #218838;
}

.custom-checkbox {
    position: relative;
    display: inline-block;
    width: 22px;
    height: 22px;
}

.custom-checkbox input {
    opacity: 0;
    width: 0;
    height: 0;
}

.checkmark {
    position: absolute;
    top: 0;
    left: 0;
    height: 22px;
    width: 22px;
    background-color: #eee;
    border-radius: 5px;
    border: 2px solid #ced4da;
    transition: background-color 0.3s;
}

.custom-checkbox input:checked ~ .checkmark {
    background-color: #28a745;
    border-color: #28a745;
}

.checkmark:after {
    content: "";
    position: absolute;
    display: none;
}

.custom-checkbox input:checked ~ .checkmark:after {
    display: block;
}

.custom-checkbox .checkmark:after {
    left: 7px;
    top: 3px;
    width: 5px;
    height: 10px;
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.todo-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.todo-card.completed {
    background-color: #d4edda;
    border-left: 5px solid #28a745;
    text-decoration: line-through;
    opacity: 0.7;
}

Include Static Files

Make sure Django is configured to serve static files in development. Ensure you have these settings in todoproject/settings.py:

STATIC_URL = '/static/'

Run Your Server

Run migrations:

python manage.py makemigrations
python manage.py migrate

Run the development server:

python manage.py runserver

Open your browser and go to http://127.0.0.1:8000/ to see your TODO app in action.

Full Todo App Project Structure

todoproject/
│
├── manage.py
├── db.sqlite3
├── todoproject/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wsgi.py
│
├── todoapp/
│   ├── migrations/
│   │   ├── __init__.py
│   │   └── 0001_initial.py
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── views.py
│   ├── forms.py
│   ├── urls.py
│   ├── templates/
│   │   │   ├── base.html
│   │   │   ├── todo_list.html
│   │   │   ├── todo_form.html
│   │   │   ├── todo_confirm_delete.html
│   ├── static/
│   │   │   ├── styles.css

Conclusion

By following this guide, you’ve learned how to create a functional TODO app using Django. From setting up your project to designing and styling your app, you’ve covered all the key steps. This project not only enhances your Django skills but also gives you a handy tool for managing your tasks. Keep experimenting with more features to continue improving your app!

That’s a wrap!

I hope you enjoyed this article

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Post

Please allow ads on our site🥺