In this series of posts we will create a To-Do app using HTMX and Django, following a TDD (Test-driven development) workflow.
The goal of this series of posts is to document my own learning process of using HTMX with Django.
If you just want to have a look on how to integrate HTMX with Django, you can jump to part 3.
The app will start with a textbox on top to add to-do items, and a list of current to-do items as horizontal components. The first version will simply support the creation and completion of the items.
The next two features we want to have is the concept of due dates and a sidebar which allows us to filter by tasks due today, tomorrow and overdue, and the use of natural language to add to-do items, for example writing "Call doctor tomorrow 8:30" should create an item with the title "Call doctor" and due date on the next day at 8:30 AM.
These are the types of features that can benefit immensely from TDD as it allows us to focus on how to process natural language while not worrying too much on how to integrate it with the frontend.
Creating the project using uv
We will kickoff the project using uv
, which is defined on its website as
"An extremely fast Python package and project manager, written in Rust [...] a single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more."
I am particularly interested on having a simple way to manage dependencies using pyproject.toml
and a virtual environment.
To install uv on your machine, follow the instructions in its website.
❯ uv init
Initialized project `todo-mx`
❯ uv venv
Using CPython 3.12.8
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
❯ source .venv/bin/activate
Now that we have a project and a virtual environment we can add our first two dependencies: Django and pytest. We're also adding an optional dependency, pytest-sugar, which provides a nicer interface to our tests.
❯ uv add django
Resolved 5 packages in 248ms
Installed 3 packages in 350ms
+ asgiref==3.8.1
+ django==5.1.4
+ sqlparse==0.5.3
❯ uv add pytest pytest-sugar --dev
Resolved 12 packages in 206ms
Installed 6 packages in 14ms
+ iniconfig==2.0.0
+ packaging==24.2
+ pluggy==1.5.0
+ pytest==8.3.4
+ pytest-sugar==1.0.0
+ termcolor==2.5.0
Creating the django project
To create the Django project in the current directory, we can run the following uv command:
uv run django-admin startproject todomx .
Note: We're preprending the command with uv run
to ensure that we will always run the commands in the correct virtual environment for the project; it's a good habit to develop when we're dealing with several projects and environments, but it's not mandatory.
We can ensure that the installation worked by running the runserver
command:
❯ uv run python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 01, 2025 - 16:12:00
Django version 5.1.4, using settings 'todomx.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Opening the link shows us the Django welcome screen
Overriding the default user authentication model
Before we run the pending migrations, as suggested when we executed the runserver
command, we will override Django's default user model, which is a best-practice for new projects. There's a lot of literature on why this is a good idea, including the official documentation.
Later we may want to incorporate django-allauth
in the project, but we want to keep it simple and have something up and running soon, so let's just create an app called core
, create a UserProfile
class inheriting from AbstractUser
, and set our project to use this class as its authentication model.
❯ uv run python manage.py startapp core
Let's add the model in core/models.py
:
from django.contrib.auth.models import AbstractUser
class UserProfile(AbstractUser):
pass
In settings.py
, let's add our new app and change the authentication user model:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core', # <-- NEW
]
AUTH_USER_MODEL = 'core.UserProfile' # <-- NEW
We can now make the migrations, apply them, and create a superuser for the app:
❯ uv run python manage.py makemigrations
Migrations for 'core':
core/migrations/0001_initial.py
+ Create model UserProfile
❯ uv run python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying core.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
❯ uv run python manage.py createsuperuser
Username: admin
Email address: admin@email.com
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
❯
Running the app again with runserver
and opening localhost:8000/admin
will show us Django's admin page:
To wrap up part 1, we can register an admin view for UserProfile
, :
# core/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import UserProfile
@admin.register(UserProfile)
class UserProfileAdmin(UserAdmin):
model = UserProfile
list_display = ['email', 'username']
Refreshing the admin site will show us the admin interface for users
That's it for the initial setup of our project! In part 2, we will use pytest to implement the first version of the Todo model
Top comments (2)
Looking forward for the next part !
Thanks! I'm writing this to document my own learning but it's awesome if it's useful for more people :)