DEV Community

Cover image for Get Done ✅ : A step-by-step guide in building a Django To Do List
Dhanush D
Dhanush D

Posted on

Get Done ✅ : A step-by-step guide in building a Django To Do List

Introduction

Managing tasks effectively is crucial in our busy lives, and a to-do list application can be an excellent tool for staying organized. This blog post walks you through the development of a to-do list application using Django, a powerful and versatile web framework in Python. The project, titled django-todoList, is designed to help users create, manage, and track their daily tasks seamlessly.

Prerequisites

Before we begin, ensure that you have the following:

  • Python installed (preferably version 3.8 or above).

  • Django installed. If not, you can install it using the following command.

pip install django
Enter fullscreen mode Exit fullscreen mode
  • A basic understanding of how Django works and familiarity with Python and HTML.

Step 1: Setting Up the Project

1.1 Create a Django Project

First, create a new Django project using the command:

django-admin startproject mysite
Enter fullscreen mode Exit fullscreen mode

Navigate into your project folder:

cd mysite
Enter fullscreen mode Exit fullscreen mode

1.2 Create a Django App

Next, create an app within the project. We’ll call it todoList:

python manage.py startapp todoList
Enter fullscreen mode Exit fullscreen mode

Step 2: Define Models

In Django, models are used to define the structure of your data. For the GetDone To-Do app, we need a model to represent a Task.

Navigate to todoList/models.py and define the Task model:

from django.db import models
from django.contrib.auth.models import User

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    complete = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    deadline = models.DateTimeField(null=True, blank=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

Enter fullscreen mode Exit fullscreen mode

This model includes fields like title, description, deadline, and complete to store details of each task. We also associate each task with a user via the user foreign key.

2.1 Migrate the Database

Once the model is ready, run the migrations to create the table for this model in the database:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Forms

We need forms to handle user input for creating and updating tasks. In todoList/forms.py, create the TaskForm:

from django import forms
from .models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'description', 'deadline', 'complete']
        widgets = {
            'title': forms.TextInput(attrs={'placeholder': 'Enter task title'}),
            'description': forms.Textarea(attrs={'placeholder': 'Enter task description', 'rows': 4}),
            'deadline': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
            'complete': forms.CheckboxInput(),
        }

    def clean_title(self):
        title = self.cleaned_data.get('title')
        if not title:
            raise forms.ValidationError('Title is required')
        return title

    def clean_description(self):
        description = self.cleaned_data.get('description')
        if not description:
            raise forms.ValidationError('Description is required')
        return description

    def clean_deadline(self):
        deadline = self.cleaned_data.get('deadline')
        if not deadline:
            raise forms.ValidationError('Deadline is required')
        return deadline

Enter fullscreen mode Exit fullscreen mode

The TaskForm uses Django’s ModelForm to automatically create form fields for the Task model.

Step 4: Define Views

Next, we need to create views to handle user requests, such as creating tasks, updating them, and listing them.

In todoList/views.py, define the views:

from django.shortcuts import render, redirect
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.contrib.auth.views import LoginView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from .models import Task
from .forms import TaskForm
from django.contrib import messages
from django.utils import timezone


# Task List View
class TodoListView(LoginRequiredMixin, ListView):
    model = Task
    context_object_name = 'tasks'
    template_name = 'task_list.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_tasks = Task.objects.filter(user=self.request.user)
        context['tasks'] = Task.objects.filter(user=self.request.user)
        context['incomplete_tasks_count'] = user_tasks.filter(complete=False).count()  # Count incomplete tasks
        context['now'] = timezone.now()
        return context

# Task Create View
class TaskCreate(LoginRequiredMixin, CreateView):
    model = Task
    form_class = TaskForm
    template_name = 'todoList/task_create.html'
    success_url = reverse_lazy('todoList')

    def form_valid(self, form):
        form.instance.user = self.request.user
        messages.success(self.request, 'Task created successfully!')
        return super(TaskCreate, self).form_valid(form)

# Task Update View
class TaskUpdate(LoginRequiredMixin, UpdateView):
    model = Task
    form_class = TaskForm
    template_name = 'todoList/task_update.html'
    success_url = reverse_lazy('todoList')
    def form_valid(self, form):
        messages.success(self.request, 'Task updated successfully!')
        return super(TaskUpdate, self).form_valid(form)

# Task Delete View
class TaskDelete(LoginRequiredMixin, DeleteView):
    model = Task
    context_object_name = 'task'
    template_name = 'todoList/task_delete.html'
    success_url = reverse_lazy('todoList')
    def dispatch(self, request, *args, **kwargs):
        response = super().dispatch(request, *args, **kwargs)
        if response.status_code == 302:
            messages.success(self.request, 'Task deleted successfully!')
        return response

# User Registration View
class RegisterView(CreateView):
    form_class = UserCreationForm
    template_name = 'todoList/register.html'
    success_url = reverse_lazy('todoList')

    def form_valid(self, form):
        response = super().form_valid(form)
        # Log the user in after successful registration
        from django.contrib.auth import login
        login(self.request, self.object)
        messages.success(self.request, 'Registration successful! Welcome!')
        return response


# Login View
class CustomLoginView(LoginView):
    template_name = 'todoList/login.html'
    fields = '__all__'
    redirect_authenticated_user = True

    def get_success_url(self):
        messages.success(self.request, 'You have logged in successfully!')
        return reverse_lazy('todoList')

Enter fullscreen mode Exit fullscreen mode

-TodoListView: Lists all tasks for the logged-in user.
-TaskCreate: Handles task creation.
-TaskUpdate: Allows users to update a task.
-TaskDelete: Provides a confirmation page for deleting a task.
T-he LoginRequiredMixin ensures that only logged-in users can access these views.

Step 5: Configure URLs

In todoList/urls.py, map URLs to their respective views:

from django.urls import path
from .views import TodoListView, TaskCreate, TaskUpdate, TaskDelete, RegisterView

urlpatterns = [
    path('', TodoListView.as_view(), name='todoList'),
    path('create/', TaskCreate.as_view(), name='create_task'),
    path('edit/<int:pk>/', TaskUpdate.as_view(), name='edit_task'),
    path('delete/<int:pk>/', TaskDelete.as_view(), name='delete_task'),
]

Enter fullscreen mode Exit fullscreen mode

These URL patterns will map each view to a specific URL. For example, the task list is displayed at the root URL of the app, and users can create, edit, or delete tasks by visiting specific URLs.

Step 6: Create Templates

Create the following HTML templates to render the views:

6.1 base.html

The base template provides a consistent layout for all pages:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Task Manager{% endblock %}</title>

    <!-- Load Static files -->
    {% load static %}

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link
        href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
        rel="stylesheet">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>

<body class="container p-4">

    <!-- Display messages -->
    <div class="toast-container position-relative top-0 end-0 p-3 ml-6">
        {% if messages %}
        {% for message in messages %}
        <div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
            <div class="toast-body">
                {{ message }}
            </div>
        </div>
        {% endfor %}
        {% endif %}

    </div>

    <main>
        <h1 class=""> Get Done <span class="done-icon">✔</span></h1>
        {% block content %}{% endblock %}

    </main>

    <!-- Bootstrap JS and Popper.js -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>

    <!-- Toast display on page load -->
    <script>
        window.onload = function () {
            let toastElements = document.querySelectorAll('.toast');
            toastElements.forEach(toastElement => {
                var toast = new bootstrap.Toast(toastElement);
                toast.show();
            });
        };
    </script>
</body>

</html>

Enter fullscreen mode Exit fullscreen mode

6.2 login.html

This template is for logging in of the user:

{% extends 'base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="card p-4">
<h2 class="mb-4">Sign in</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="btn btn-primary" type="submit">Login</button>
</form>
<p class="mt-3">Don't have an account? <a href="{% url 'register' %}">Sign up</a></p>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

6.3 register.html

This template is for new user registration :

{% extends 'base.html' %}
{% block title %}Register{% endblock %}
{% block content %}
<div class="card p-4">
<h2 class="mb-4">Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="btn btn-primary" type="submit">Sign Up</button>
</form>
<p class="mt-3">Already have an account? <a href="{% url 'login' %}">Sign In</a></p>
</div>
{% endblock %}


Enter fullscreen mode Exit fullscreen mode

6.4 task_create.html

This template handle task creation. They contain forms to input task details.:

{% extends 'base.html' %}
{% block title %}Create Task{% endblock %}
{% block content %}

<h2 class="mb-4">Create Task</h2>
<form method="post" class="card p-4">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="btn btn-success" type="submit">Save</button>
    <a href="{% url 'todoList' %}" class="btn btn-secondary mt-1">Cancel</a>
</form>

{% endblock %}
Enter fullscreen mode Exit fullscreen mode

6.5 task_delete.html

This template is for deleting a particular task:

{% extends 'base.html' %}
{% block title %}Delete Task{% endblock %}
{% block content %}
<h2 class="mb-4">Delete Task</h2>
<p class="delete-text">Are you sure you want to delete this task: "{{task.title}}" ?</p>
<form method="post" class="d-inline">
    {% csrf_token %}
    <button class="btn btn-danger" type="submit">Confirm</button>
</form>
<a href="{% url 'todoList' %}" class="btn btn-secondary mt-1">Cancel</a>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

6.6 task_list.html

This template lists all tasks for the logged-in user:

{% extends 'base.html' %}
{% block title %}Your Tasks{% endblock %}
{% block content %}
<div class="logout-section"><a href="{% url 'logout' %}" class="btn btn-secondary mt-1"
        title="Do you want to sign out?">Sign out</a></div>
<div class="header">
    <div class="mb-2 username">
        <h3 class="superusers">Hello, {{ request.user.username }}</h3>
    </div>
    <a href="{% url 'create_task' %}" title="Add a new task">
        <div class="add-icon">
            +
        </div>
</div></a>
<h5 class="count-container">You have <span class="task-count">{{ incomplete_tasks_count }}</span> incomplete tasks.</h5>

{% if tasks %}
<ul class="list-group">
    {% for task in tasks %}
    <li class="list-group-item d-flex justify-content-between align-items-center">
        <a href="{% url 'edit_task' task.id %}" class="text-decoration-none" title="Open the task">
            <span style="text-decoration: {% if task.complete %}line-through{% endif %};">
                <div class="title">{{ task.title }}</div>
            </span>
            <div class="mb-0 description">{{ task.description }}</div>
            <div class="deadline">
                Deadline: {{ task.deadline|date:"M d, Y h:i A" }}
                {% if task.deadline < now %}
                <span class="text-danger ms-2" title="Deadline is past!">❗</span>
                {% endif %}
            </div>
        </a>
        <span>
            <a href="{% url 'delete_task' task.id %}" class="text-danger ms-3" title="Delete the task">❌️</a>
        </span>
    </li>
    {% endfor %}
</ul>
{% else %}
<p>No pending tasks.</p>
{% endif %}
{% endblock %}

Enter fullscreen mode Exit fullscreen mode

6.7 task_update.html

These templates handle task updates. They contain forms to input task details.

{% extends 'base.html' %}
{% block title %}Update Task{% endblock %}
{% block content %}
<h2 class="mb-4">Update Task</h2>
<form method="post" class="card p-4">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="btn btn-success" type="submit">Save</button>
    <a href="{% url 'todoList' %}" class="btn btn-secondary mt-1">Cancel</a>
</form>
{% endblock %}

Enter fullscreen mode Exit fullscreen mode

Step 7: Add CSS for styling

a {
    text-decoration: none;
}

h1 {
    text-align: center;
    margin-bottom: 40px;
    color: #333;
    align-items: center;
    justify-content: center;
    font-weight: bold;
}

h2 {
    text-align: start;
    margin-bottom: 10px;
    color: #333;
    align-items: center;
    justify-content: center;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 0 10px;
}

body {
    font-family: "Lato", serif;
    margin: 0;
    padding: 0;
    background-color: #f8f9fa;
}

.container {
    max-width: 750px;
    margin: 20px auto;
    background: #ffffff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

input[type=text],
input[type=password],
textarea {
    border: 1px solid #6c757d;
}

form label {
    display: block !important;
    margin-bottom: 5px;
}

.done-icon {
    font-size: 38px;
    color: green;
    margin-left: 10px;
}

.card {
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

button {
    border-radius: 5px;
}

.count-container {
    margin-left: 16px;
}

.superusers {
    color: #333;
    display: inline-block;
    overflow: hidden;
    white-space: nowrap;
    border-right: 1px solid #333;
    width: 0;
    animation: typing 2s steps(30) 1s forwards, blink 0.75s step-end infinite;
}

.username {
    width: 370px;
}

/* Typing & Cursor blink animation effect */
@keyframes typing {
    from {
        width: 0;
    }

    to {
        width: 100%;
    }
}

@keyframes blink {
    50% {
        border-color: transparent;
    }
}

.delete-text {
    font-size:16px;
}
.add-icon {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #007bff;
    color: white;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    font-size: 24px;
    font-weight: bold;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
    transition: background-color 0.3s;
}

.add-icon a {
    text-decoration: none;
    color: white;
}

.add-icon:hover {
    background-color: #0056b3;
}

.logout-section {

    position: absolute;
    margin-top: -88px;
}

.task {
    padding: 10px;
    border-bottom: 1px solid #e9ecef;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.task-count {
    font-style: italic;
    color: blue;
    font-weight: bold;
}

.task:last-child {
    border-bottom: none;
}

.title {
    font-size: 22px;
    font-weight: bold;
    color: #007bff;
}

.title:hover {
    color: #0056b3;
    font-size: 24px;
}

.description {
    margin: 5px 0;
    font-size: 16px;
    color: #6c757d;
}

.deadline {
    font-size: 14px;
    color: #adb5bd;
}

.tooltip {
    position: relative;
    cursor: pointer;
}

.tooltip::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: 100%;
    /* Position above the icon */
    left: 50%;
    transform: translateX(-50%);
    background-color: #333;
    color: #fff;
    padding: 5px;
    border-radius: 4px;
    white-space: nowrap;
    font-size: 12px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
}

.tooltip:hover::after {
    opacity: 1;
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Add User Authentication

In views.py, you can use Django’s built-in user authentication views to handle user registration and login. For example, you can use UserCreationForm to allow users to sign up:

class RegisterView(CreateView):
    form_class = UserCreationForm
    template_name = 'registration/signup.html'
    success_url = reverse_lazy('login')

Enter fullscreen mode Exit fullscreen mode

Step 8: Run the Server

Once everything is set up, you can run the server:

python manage.py runserver

Enter fullscreen mode Exit fullscreen mode

Visit http://127.0.0.1:8000/todoList to see your To-Do List app in action!

Understanding settings.py and urls.py in the mysite Folder

settings.py

The settings.py file is a crucial part of every Django project. It contains the configuration settings for your project, such as database settings, installed apps, middleware, static files configuration, and more. This file controls the behavior of your project and allows Django to connect the dots between various components.

Here's a brief overview of key settings in settings.py for your GetDone To-Do List app:

Key Sections in settings.py:

Installed Apps: In the INSTALLED_APPS list, you register all the apps used in your project. For instance:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todoList',  # Our custom app
]

STATIC_URL = '/static/'


STATICFILES_DIRS = [
    BASE_DIR / "todoList/static",
]
Enter fullscreen mode Exit fullscreen mode

Here, we’ve added todoList, which is the app that manages the tasks, alongside the default apps provided by Django for user authentication, admin panel, and static files.

urls.py

In Django, the urls.py file handles the routing of HTTP requests to views. It is where you map URL patterns (such as /tasks/, /login/) to corresponding views that will handle them.

In the mysite/urls.py, you usually include URLs for the whole project and link them to the app-level urls.py file.

Here’s what urls.py looks like in your GetDone app:

Key Sections in urls.py:

Project-Level urls.py (mysite/urls.py): The urls.py file in the mysite folder is the main router for your entire Django project. It includes the URLs for the admin panel, authentication, and links to your app's specific URLs. Here’s an example:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),  # For login/logout
    path('', include('todoList.urls')),  # Include the todoList app URLs
]

Enter fullscreen mode Exit fullscreen mode

path('admin/', admin.site.urls): This line includes the Django admin panel.
path('accounts/', include('django.contrib.auth.urls')): This includes built-in authentication URLs for login, logout, and password management.
path('', include('todoList.urls')): This includes the app-specific URLs (defined in todoList/urls.py), so users can navigate through tasks and other features.
App-Level urls.py (todoList/urls.py): This file maps specific URLs to views within the todoList app. It contains paths for viewing tasks, creating tasks, and other task-related actions. For example:

from django.urls import path
from .views import TodoListView, TaskCreate, TaskUpdate, TaskDelete

urlpatterns = [
    path('', TodoListView.as_view(), name='todoList'),
    path('create/', TaskCreate.as_view(), name='create_task'),
    path('edit/<int:pk>/', TaskUpdate.as_view(), name='edit_task'),
    path('delete/<int:pk>/', TaskDelete.as_view(), name='delete_task'),
]

Enter fullscreen mode Exit fullscreen mode

TodoListView.as_view(): This view lists all tasks for the logged-in user.
TaskCreate.as_view(): This view handles the task creation form.
TaskUpdate.as_view(): This view handles the task update form.
TaskDelete.as_view(): This view handles the task deletion confirmation page.

Communication Between Files

Django’s architecture allows for smooth communication between the different files and components:

URLs and Views:

The urls.py maps URLs to views, such as task creation or list viewing. Views are defined in views.py.

Models and Views:

Views interact with models (defined in models.py) to retrieve and manipulate data (tasks). For example, in TodoListView, the view fetches tasks associated with the logged-in user using Task.objects.filter(user=self.request.user).

Forms and Views:

Forms (like TaskForm in forms.py) handle user input and interact with models to validate and save the data.

Templates:

Templates render the final output in HTML, displaying data passed from views and handling user input through forms.

Conclusion

With these steps, you’ve built a fully functional To-Do list app using Django. You’ve implemented user authentication, task management (create, edit, delete), and learned how Django’s MVC (MTV) architecture facilitates the smooth communication between models, views, templates, and URLs. This guide serves as a solid foundation for building more complex Django applications in the future.

Full code of the application is available to clone at

git clone https://github.com/dhanushd1998/django-todoList.git
cd django-todoList
Enter fullscreen mode Exit fullscreen mode

Happy coding! 🚀

Top comments (0)