Logo

dev-resources.site

for different kinds of informations.

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

Published at
1/1/2025
Categories
django
python
htmx
pytest
Author
rodbv
Categories
4 categories in total
django
open
python
open
htmx
open
pytest
open
Author
5 person written this
rodbv
open
Creating a To-Do app with HTMX and Django - 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.

pytest Article's
30 articles in total
Favicon
Handling Unmanaged Models in Pytest-Django
Favicon
Mastering Pytest Monkeypatch: Simplify Your Testing
Favicon
Pytest Fish shell autocompletion
Favicon
Creating a To-Do app with HTMX and Django - Part 2: Adding the Todo model with tests
Favicon
How to encourage developers to fix Python warnings for deprecated features
Favicon
No Country For \0 (Escaping Characters Issues in ReportPortal)
Favicon
Overcoming LLM Testing Challenges with Pytest and Trulens: Ensuring Reliable Responses
Favicon
How ReportPortal "Made" Pytest Run Twice
Favicon
Writing Integration And Unit Tests for a Simple Fast API application using Pytest
Favicon
Pytest exited with code 1 issue
Favicon
Pytest Mocks, o que sΓ£o?
Favicon
Automate your tasks Using Pytest: A practical guide with examples
Favicon
Pytest and PostgreSQL: Fresh database for every test (part II)
Favicon
Setting Up a Comprehensive Python Build Validation Pipeline in Azure DevOps
Favicon
Unit testing in Python with sheepy
Favicon
Review of Kumar S’s β€œPython Automation Testing With Pytest” Udemy Course
Favicon
Mocking Python Classes
Favicon
Pytest and PostgreSQL: Fresh database for every test (part I)
Favicon
Securing Testing Secrets with pytest-mask-secrets
Favicon
CS50P Problem Set 5 - Back to the Bank
Favicon
Simplifying Test Execution with pytest.main()
Favicon
Injecting Fun into Python Testing
Favicon
How to run python based AWS lambda tests using pytest and localstack
Favicon
What I learned in the Real Python testing courses
Favicon
Pytest How to pass .yaml file as argument in pytest command line
Favicon
The project that will make you enjoy writing tests for your Django app
Favicon
CI using GitHub Actions
Favicon
How to Handle pytest Timeouts
Favicon
PyTest unit testing now underway
Favicon
Testing using Pytest!

Featured ones: