DEV Community

rodbv
rodbv

Posted on • Edited on

Creating a To-Do app with Django and HTMX - Part 2: Adding the Todo model with tests

This is the second post of our series on how to build a Todo app using HTMX and Django. Click here for part 1.

In part 2, we will create the Todo model and implement its basic functionality with unit tests.

Creating the Todo model

In models.py let's create the Todo model, with the its basic attributes. We want a Todo item to be associated to a UserProfile, so that a user will only see their own items. A todo item will also have a title and a boolean attribute is_completed. We have a lot of future ideas for the Todo model, such as the ability to set a task as "in progress" besides being completed or not started, and due dates, but that's for later. Let's keep it simple by now to have something on the screen asap.

Note: In a real-world app we should probably consider using UUIDs as primary keys on the UserProfile and Todo models, but we'll keep it simple by now.

# core/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models  # <-- NEW

class UserProfile(AbstractUser):
    pass

# NEW
class Todo(models.Model):
    title = models.CharField(max_length=255)
    is_completed = models.BooleanField(default=False)
    user = models.ForeignKey(
        UserProfile,
        related_name="todos",
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title
Enter fullscreen mode Exit fullscreen mode

Let's run the migrations for the new model:

❯ uv run python manage.py makemigrations
Migrations for 'core':
  core/migrations/0002_todo.py
    + Create model Todo
❯ uv run python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
  Applying core.0002_todo... OK
Enter fullscreen mode Exit fullscreen mode

Writing our first tests

Let's write the first tests on our project. We want to ensure that a user will only see their own todo items, and not items from other users.

To help us writing the tests, we will add a new development dependency to our project, model-bakery, which simplifies the process of creating dummy Django model instances. We'll also add pytest-django.

❯ uv add model-bakery pytest-django --dev
Resolved 27 packages in 425ms
Installed 2 packagez in 12ms
 + model-bakery==1.20.0
 + pytest-django==4.9.0
Enter fullscreen mode Exit fullscreen mode

In pyproject.toml we need to configure pytest, by adding some lines at the end of the file:

# pyproject.toml

# NEW
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "todomx.settings"
python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
Enter fullscreen mode Exit fullscreen mode

Now let's write our first test to ensure users only have access to their own todos.

# core/tests/test_todo_model.py

import pytest


@pytest.mark.django_db
class TestTodoModel:
    def test_todo_items_are_associated_to_users(self, make_todo, make_user):
        [user1, user2] = make_user(_quantity=2)

        for i in range(3):
            make_todo(user=user1, title=f"user1 todo {i}")

        make_todo(user=user2, title="user2 todo")

        assert {todo.title for todo in user1.todos.all()} == {
            "user1 todo 0",
            "user1 todo 1",
            "user1 todo 2",
        }

        assert {todo.title for todo in user2.todos.all()} == {"user2 todo"}

Enter fullscreen mode Exit fullscreen mode

We're using a conftest.py file from pytest to have a place for all fixtures we plan to use in our tests. The model_bakery library makes it simple to create instances of UserProfile and Todo with minimal boilerplate.

#core/tests/conftest.py

import pytest
from model_bakery import baker


@pytest.fixture
def make_user(django_user_model):
    def _make_user(**kwargs):
        return baker.make("core.UserProfile", **kwargs)

    return _make_user


@pytest.fixture
def make_todo(make_user):
    def _make_todo(user=None, **kwargs):
        return baker.make("core.Todo", user=user or make_user(), **kwargs)

    return _make_todo
Enter fullscreen mode Exit fullscreen mode

Let's run our tests!

❯ uv run pytest
Test session starts (platform: darwin, Python 3.12.8, pytest 8.3.4, pytest-sugar 1.0.0)
django: version: 5.1.4, settings: todomx.settings (from ini)
configfile: pyproject.toml
plugins: sugar-1.0.0, django-4.9.0
collected 1 item

 core/tests/test_todo_model.py ✓                                                                                                                                   100% ██████████

Results (0.25s):
       1 passed
Enter fullscreen mode Exit fullscreen mode

Register an admin page for Todos

Finally, we can register an admin page for it:

# core/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import Todo, UserProfile # <-- NEW

# .. previous code

# NEW
@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
    model = Todo
    list_display = ["title", "is_completed", "user"]
    list_filter = ["is_completed"]
    search_fields = ["title"]
    list_per_page = 10
    ordering = ["title"]
Enter fullscreen mode Exit fullscreen mode

We can now add some Todo's from admin!

Django admin site showing the Todo admin page

If you want to check the whole code until the end of part 2, you can check it on Github at the part 02 branch.

In part 3, we will create the view for the todo items, and toggle them with HTMX.

Top comments (0)