Django To-Do App Project

Django To-Do App Project

Building a to-do app is one of those classic projects that almost every developer tackles at some point. It’s a perfect way to learn a new framework because it covers so many fundamental concepts: creating models, setting up views, handling forms, and working with templates. In this walkthrough, we’re going to build a fully functional to-do application using Django. By the end, you’ll have a solid understanding of how Django works and how to structure a basic web app.

Setting Up Your Django Project

First things first, let’s make sure you have Django installed. If you don’t, you can install it using pip. Open your terminal and run:

pip install django

Once that’s done, we can start a new Django project. Let’s call it todo_project:

django-admin startproject todo_project
cd todo_project

Now, within your project, you need to create an app. In Django, a project can contain multiple apps. Our to-do functionality will live inside an app we’ll call tasks:

python manage.py startapp tasks

Don’t forget to add your new app to the INSTALLED_APPS list in todo_project/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tasks',  # Add this line
]
Django Command Purpose
startproject Creates a new Django project
startapp Creates a new app within the project
runserver Starts the development server
makemigrations Creates migration files for model changes
migrate Applies migrations to the database

Now, let’s test that everything is working. Start the development server:

python manage.py runserver

Open your browser and go to http://127.0.0.1:8000/. You should see the Django welcome page. If you do, you’re all set up correctly!

Designing the Task Model

The heart of our to-do app is the Task model. This is where we define what a task looks like in our database. Open tasks/models.py and define the model like this:

from django.db import models

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

    def __str__(self):
        return self.title

Here’s what each field does: - title: A character field to store the task description. - completed: A boolean to track whether the task is done. - created: A datetime that automatically records when the task was created.

After defining the model, we need to create and apply migrations:

python manage.py makemigrations
python manage.py migrate

This sets up the database table for our Task model.

Creating Views and URLs

Now, let’s make our app do something! We’ll start by creating a view to list all tasks. Open tasks/views.py:

from django.shortcuts import render
from .models import Task

def task_list(request):
    tasks = Task.objects.all()
    return render(request, 'tasks/task_list.html', {'tasks': tasks})

This view fetches all tasks from the database and passes them to a template. Next, we need to map this view to a URL. Create a urls.py file inside the tasks directory:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.task_list, name='task_list'),
]

Then, include these URLs in the project’s main urls.py (located in todo_project/):

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

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

Now, if you visit the homepage, it will use the task_list view. But we haven’t made the template yet—let’s do that next.

Building the Templates

Django uses templates to generate HTML dynamically. Create a directory named templates inside your tasks app, and inside that, another directory named tasks. Then, create task_list.html:

<!DOCTYPE html>
<html>
<head>
    <title>To-Do List</title>
</head>
<body>
    <h1>Your To-Do List</h1>
    <ul>
    {% for task in tasks %}
        <li>{{ task.title }} - {% if task.completed %}Done{% else %}Not Done{% endif %}</li>
    {% endfor %}
    </ul>
</body>
</html>

This template loops through all tasks and displays them in a list. Now, when you visit the homepage, you should see your tasks (though there aren’t any yet because we haven’t added any).

Key things to remember when working with templates: - Always place templates in appname/templates/appname/. - Use {% %} for logic like loops and conditionals. - Use {{ }} to output variables.

Adding New Tasks with a Form

A to-do app isn’t very useful if you can’t add tasks! Let’s create a form for that. First, we’ll make a form in tasks/forms.py (create the file if it doesn’t exist):

from django import forms
from .models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title']

This form is based on our Task model and only includes the title field. Now, update views.py to handle form submission:

from django.shortcuts import render, redirect
from .models import Task
from .forms import TaskForm

def task_list(request):
    tasks = Task.objects.all()
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('task_list')
    else:
        form = TaskForm()
    return render(request, 'tasks/task_list.html', {'tasks': tasks, 'form': form})

This view now handles both GET and POST requests. If it’s a POST request (form submission), it validates the form and saves the new task. Then, update the template to include the form:

<body>
    <h1>Your To-Do List</h1>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Add Task</button>
    </form>
    <ul>
    {% for task in tasks %}
        <li>{{ task.title }} - {% if task.completed %}Done{% else %}Not Done{% endif %}</li>
    {% endfor %}
    </ul>
</body>

The {% csrf_token %} is necessary for security in Django forms. Now you can add new tasks directly from the web page!

Form Field Type Description
CharField For short text input
BooleanField For yes/no values
DateTimeField For date and time

Marking Tasks as Complete

Right now, we can add tasks, but we can’t mark them as completed. Let’s add that functionality. We’ll do this by creating a new view that toggles the completed status. Add this to views.py:

from django.shortcuts import get_object_or_404

def toggle_task(request, task_id):
    task = get_object_or_404(Task, id=task_id)
    task.completed = not task.completed
    task.save()
    return redirect('task_list')

Then, add a URL pattern for this view in tasks/urls.py:

urlpatterns = [
    path('', views.task_list, name='task_list'),
    path('task/<int:task_id>/toggle/', views.toggle_task, name='toggle_task'),
]

Now, update the template to make each task clickable:

<ul>
{% for task in tasks %}
    <li>
        <a href="{% url 'toggle_task' task.id %}">
            {{ task.title }} - {% if task.completed %}Done{% else %}Not Done{% endif %}
        </a>
    </li>
{% endfor %}
</ul>

Now, clicking on a task will toggle its completed status. This is a simple way to handle task completion.

Deleting Tasks

It’s also useful to be able to delete tasks. Let’s add a delete view. In views.py:

def delete_task(request, task_id):
    task = get_object_or_404(Task, id=task_id)
    task.delete()
    return redirect('task_list')

Add the URL pattern:

path('task/<int:task_id>/delete/', views.delete_task, name='delete_task'),

And update the template to include a delete link for each task:

<li>
    <a href="{% url 'toggle_task' task.id %}">
        {{ task.title }} - {% if task.completed %}Done{% else %}Not Done{% endif %}
    </a>
    <a href="{% url 'delete_task' task.id %}" style="color: red; margin-left: 10px;">Delete</a>
</li>

Now you can delete tasks directly from the list.

Important considerations when building delete functionality: - Always use POST requests for destructive actions like delete in production. - For simplicity here we use GET, but you should use forms with POST method for better security.

Adding Style with CSS

Our app works, but it doesn’t look very nice. Let’s add some basic CSS to improve the appearance. Create a static directory inside your tasks app, and inside that, a css directory. Create a file named style.css:

body {
    font-family: Arial, sans-serif;
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

h1 {
    color: #333;
}

form {
    margin-bottom: 20px;
}

ul {
    list-style-type: none;
    padding: 0;
}

li {
    padding: 10px;
    border-bottom: 1px solid #ccc;
}

a {
    text-decoration: none;
    color: #007bff;
}

a:hover {
    text-decoration: underline;
}

Load the static files in your template by adding this at the top of task_list.html:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>To-Do List</title>
    <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
</head>

Now your app should look much cleaner!

Using Django’s Admin Interface

Django comes with a powerful admin interface that lets you manage your data easily. Let’s set it up for our Task model. First, register the model in tasks/admin.py:

from django.contrib import admin
from .models import Task

admin.site.register(Task)

Now, create a superuser account:

python manage.py createsuperuser

Follow the prompts to set up a username and password. Then, start the server and visit http://127.0.0.1:8000/admin/. Log in with your superuser credentials, and you’ll see an interface where you can add, edit, and delete tasks directly.

Admin Action Purpose
Add Create new tasks manually
Change Edit existing tasks
Delete Remove tasks from the database

Adding User Authentication

To make our app more practical, let’s add user authentication so each user can have their own private to-do list. First, modify the Task model to include a user foreign key:

from django.contrib.auth.models import User

class Task(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

After modifying the model, create and apply migrations:

python manage.py makemigrations
python manage.py migrate

Now, update the task_list view to only show tasks for the logged-in user:

from django.contrib.auth.decorators import login_required

@login_required
def task_list(request):
    tasks = Task.objects.filter(user=request.user)
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            task = form.save(commit=False)
            task.user = request.user
            task.save()
            return redirect('task_list')
    else:
        form = TaskForm()
    return render(request, 'tasks/task_list.html', {'tasks': tasks, 'form': form})

The @login_required decorator ensures that only logged-in users can access the page. We also filter tasks by the current user and set the user when creating new tasks.

Finally, let’s add login/logout functionality. Create login and logout views in tasks/views.py:

from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import render, redirect

def login_view(request):
    if request.method == 'POST':
        form = AuthenticationForm(data=request.POST)
        if form.is_valid():
            user = form.get_user()
            login(request, user)
            return redirect('task_list')
    else:
        form = AuthenticationForm()
    return render(request, 'tasks/login.html', {'form': form})

def logout_view(request):
    logout(request)
    return redirect('login')

Create templates for login and add URLs for these views. Now users can log in and have their own private to-do lists.

Key benefits of adding user authentication: - Security: Users only see their own tasks. - Personalization: Each user has their own data. - Scalability: Ready for multiple users.

Adding Task Editing functionality

Let’s make our app even more useful by allowing users to edit task titles. Create an edit view in views.py:

def edit_task(request, task_id):
    task = get_object_or_404(Task, id=task_id, user=request.user)
    if request.method == 'POST':
        form = TaskForm(request.POST, instance=task)
        if form.is_valid():
            form.save()
            return redirect('task_list')
    else:
        form = TaskForm(instance=task)
    return render(request, 'tasks/edit_task.html', {'form': form})

Add the URL pattern:

path('task/<int:task_id>/edit/', views.edit_task, name='edit_task'),

Create an edit_task.html template:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>Edit Task</title>
    <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
</head>
<body>
    <h1>Edit Task</h1>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Save Changes</button>
    </form>
    <a href="{% url 'task_list' %}">Cancel</a>
</body>
</html>

Add an edit link to your task list:

<li>
    <a href="{% url 'toggle_task' task.id %}">
        {{ task.title }} - {% if task.completed %}Done{% else %}Not Done{% endif %}
    </a>
    <a href="{% url 'edit_task' task.id %}">Edit</a>
    <a href="{% url 'delete_task' task.id %}" style="color: red;">Delete</a>
</li>

Now users can edit their tasks without having to delete and recreate them.

Adding Task Categories

Let’s make our app more organized by adding categories to tasks. First, create a Category model in models.py:

class Category(models.Model):
    name = models.CharField(max_length=100)
    color = models.CharField(max_length=7, default='#007bff')  # HEX color code

    def __str__(self):
        return self.name

Then add a category field to the Task model:

class Task(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)

    def __str__(self):
        return self.title

Create and apply migrations, then update the TaskForm to include category:

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'category']

Update the template to show categories and allow filtering. This makes your to-do app much more organized and useful!

Category Field Purpose
name The name of the category
color A color to visually distinguish categories
ForeignKey Links tasks to their categories

Adding Due Dates to Tasks

Another useful feature is due dates for tasks. Add a due_date field to the Task model:

class Task(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    due_date = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return self.title

Update the form to include due_date:

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'category', 'due_date']
        widgets = {
            'due_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
        }

Update your templates to display due dates and create migrations. Now users can set deadlines for their tasks!

Adding Search Functionality

As the number of tasks grows, search becomes essential. Let’s add a search form to filter tasks. First, add a search form to forms.py:

class TaskSearchForm(forms.Form):
    search = forms.CharField(required=False, label='Search tasks')

Update the task_list view to handle search:

def task_list(request):
    tasks = Task.objects.filter(user=request.user)
    search_form = TaskSearchForm(request.GET or None)

    if search_form.is_valid() and search_form.cleaned_data['search']:
        search_term = search_form.cleaned_data['search']
        tasks = tasks.filter(title__icontains=search_term)

    # Rest of the view remains the same
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            task = form.save(commit=False)
            task.user = request.user
            task.save()
            return redirect('task_list')
    else:
        form = TaskForm()

    return render(request, 'tasks/task_list.html', {
        'tasks': tasks, 
        'form': form,
        'search_form': search_form
    })

Add the search form to your template:

<form method="get">
    {{ search_form.as_p }}
    <button type="submit">Search</button>
</form>

Now users can search through their tasks easily.

Three key benefits of adding search: - Efficiency: Quickly find specific tasks. - Organization: Better task management. - Usability: Improved user experience.

Adding Task Prioritization

Let’s add priority levels to tasks. First, add a priority field to the Task model:

class Task(models.Model):
    PRIORITY_CHOICES = [
        ('low', 'Low'),
        ('medium', 'Medium'),
        ('high', 'High'),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    due_date = models.DateTimeField(null=True, blank=True)
    priority = models.CharField(max_length=6, choices=PRIORITY_CHOICES, default='medium')

    def __str__(self):
        return self.title

Update the form to include priority:

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'category', 'due_date', 'priority']
        widgets = {
            'due_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
        }

Update your templates to display priority and create migrations. Now users can prioritize their tasks!

Priority Level Description
Low Not urgent tasks
Medium Normal priority tasks
High Urgent tasks that need attention

Adding Task Notes

Sometimes tasks need additional details. Let’s add a notes field. Update the Task model:

class Task(models.Model):
    # ... existing fields ...
    notes = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.title

Update the form:

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'category', 'due_date', 'priority', 'notes']
        widgets = {
            'due_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
            'notes': forms.Textarea(attrs={'rows': 3}),
        }

Create migrations and update your templates. Now users can add detailed notes to their tasks!

Adding Task Sorting

As the number of tasks grows, sorting becomes important. Let’s add sorting options. First, create a sorting form in forms.py:

class TaskSortForm(forms.Form):
    SORT_CHOICES = [
        ('created', 'Date Created'),
        ('due_date', 'Due Date'),
        ('priority', 'Priority'),
        ('title', 'Title'),
    ]

    sort_by = forms.ChoiceField(choices=SORT_CHOICES, required=False)
    ascending = forms.BooleanField(required=False, initial=True)

Update the task_list view to handle sorting:

def task_list(request):
    tasks = Task.objects.filter(user=request.user)

    # Handle search
    search_form = TaskSearchForm(request.GET or None)
    if search_form.is_valid() and search_form.cleaned_data['search']:
        search_term = search_form.cleaned_data['search']
        tasks = tasks.filter(title__icontains=search_term)

    # Handle sorting
    sort_form = TaskSortForm(request.GET or None)
    if sort_form.is_valid():
        sort_field = sort_form.cleaned_data.get('sort_by')
        ascending = sort_form.cleaned_data.get('ascending', True)

        if sort_field:
            if not ascending:
                sort_field = '-' + sort_field
            tasks = tasks.order_by(sort_field)

    # Rest of the view remains the same
    # ...

Add the sort form to your template and create the appropriate UI. Now users can sort their tasks by different criteria!

Adding Pagination

When users have many tasks, pagination becomes necessary. Django has built-in pagination support. Update your task_list view:

from django.core.paginator import Paginator

def task_list(request):
    tasks = Task.objects.filter(user=request.user)

    # Handle search, sorting, etc. first
    # ...

    # Add pagination
    paginator = Paginator(tasks, 10)  Show 10 tasks per page
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    # Update context to use page_obj instead of tasks
    return render(request, 'tasks/task_list.html', {
        'page_obj': page_obj,
        'form': form,
        'search_form': search_form,
        'sort_form': sort_form
    })

Update your template to use the pagination object and add pagination controls:

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">next</a>
            <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
</div>

Now your app can handle large numbers of tasks efficiently!

Three key benefits of pagination: - Performance: Faster loading with fewer tasks per page. - Usability: Easier to navigate through many tasks. - Organization: Better visual structure for task lists.

Adding Task Export Functionality

Sometimes users want to export their tasks. Let’s add CSV export functionality. Create an export view:

import csv
from django.http import HttpResponse

def export_tasks(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="tasks.csv"'

    writer = csv.writer(response)
    writer.writerow(['Title', 'Completed', 'Created', 'Category', 'Due Date', 'Priority'])

    tasks = Task.objects.filter(user=request.user)
    for task in tasks:
        writer.writerow([
            task.title,
            task.completed,
            task.created,
            task.category.name if task.category else '',
            task.due_date,
            task.priority
        ])

    return response

Add the URL pattern and a link in your template. Now users can download their tasks as a CSV file!

Adding Task Import Functionality

Let’s also allow users to import tasks from CSV. Create an import view and form. This is more complex but very useful for users who want to migrate from other systems.

Adding Task Statistics

Finally, let’s add some statistics about the user's tasks. Create a statistics view that shows things like: - Total tasks - Completed tasks - Tasks by priority - Tasks by category

This gives users insights into their productivity and task distribution.

Congratulations! You've built a fully-featured to-do app with Django. You've learned about models, views, templates, forms, authentication, and many other Django concepts. This project provides a solid foundation that you can extend with even more features as you continue learning Django.