Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial django-htmx post #1638

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/assets/blog/starter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
340 changes: 340 additions & 0 deletions src/content/blog/a-django-htmx-starter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
---
title: "A Django & HTMX Starter: With Commentary"
date: "2024-07-01"
tags:
- "tutorial"
- "django"
- "htmx"
slug: "django-htmx-starter-with-commentary"
heroImage: "@assets/blog/starter.jpg"
unsplash: "Sebastian Coman Photography"
unsplashURL: "sebastiancoman"
description: "<DESCRIPTION>"
---

I am very interested in building some side projects lately.
But I have barely ever got past the initial setup of a project; getting the initial styles, user authentication, etc.
So I thought it was time to create a skeleton that gets me prototyping beyond user authentication.

As you might have know, I have really been interested in HTMX.
So it is no suprized that this skeleton leverages HTMX.
What might be more surprizing is that it also uses Django for the backend.

I have been exploring other languages for a while now; even falling in love with Go for websites.
However, when I want to do rapid prototyping, there is nothing better than using something you are intimentaly familiar with.
For me, that is Python.
And I really love all the decisions Django takes away from me allowing me to just build.

For the TLDR folks, here is a link to the repo with all the code: [Django HTMX Starter](https://github.com/joshfinnie/django-htmx-starter)

## Setup

The first thing you notice is that this skeleton uses [Poetry](https://python-poetry.org/).
I have been using Poetry for all my python applications for a while now and just feel like it is undespencible.
I am also very opinonated on using [Django Allauth](https://docs.allauth.org/en/latest/).
I feel like this package gives you the easiest user intergration.
Also allowing for social logins is a nice touch!
Other than that, [django-htmx](https://django-htmx.readthedocs.io/en/latest/) and [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks) for some quality-of-life.
And that's it.
As easy as I could make it.


```toml
[tool.poetry]
name = "django-htmx-starter"
version = "0.1.0"
description = "A skeleton for rapid prototyping, built with Django and HTMX"
authors = ["Josh Finnie <josh@jfin.us>"]
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.11"
Django = "^5.0.6"
django-htmx = "^1.17.3"
django-allauth = {extras = ["socialaccount"], version = "^0.62.1"}
django-widget-tweaks = "^1.5.0"

[tool.poetry.group.dev.dependencies]
ruff = "^0.4.4"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

In addition to leveraging Poetry, I also am experimenting with [ruff](https://docs.astral.sh/ruff/).
What I like about ruff is that the configuration can just be added to your `pyproject.toml` file that is generated by Poetry.
These are the settings I use:

```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".git",
".ruff_cache",
".venv",
"node_modules",
]

# Same as Black.
line-length = 119
indent-width = 4

# Assume Python 3.11
target-version = "py311"

[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []

# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"

# Like Black, indent with spaces, rather than tabs.
indent-style = "space"

# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false

# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
```

These are just a standard I have grown accustom to.
But feel free to update them how you see fit!

## Main Django Application

Next let us take a look at the main Django application.
It is pretty standard; as I said above I am a fan of the default Django configurations.
But we do want to update some things to make it easier to use templates and Django Allauth.

To ensure that our application has all the required settings for Django Allauth and our `users` application, we modify `settings.py` to have the following:

```python
INSTALLED_APPS = [
# Our Apps
"users.apps.UsersConfig",

# Django Default Apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",

# Third Party Apps
"allauth",
"allauth.account",
"allauth.socialaccount",
"widget_tweaks",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
]

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]

SITE_ID = 1

ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_USER_MODEL_USERNAME_FIELD = "username"
ACCOUNT_AUTHENTICATION_METHOD = "username"
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_SESSION_REMEMBER = True

AUTH_USER_MODEL = "users.CustomUser"
LOGIN_REDIRECT_URL = "index"

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
```

These settings setup the template infrastructure (to use HTMX properly).
And it adds the required settings needed for Django Allauth.
For example, I like to require both an email and a username for my applications.
And if we are in `DEBUG` mode, I send emails to the console for verification.

One additional thing to point out is that we create a `BaseAbstractModel` which I use to create all other models from.
This is handy as it builds in a `created_at` and `updated_at` and allows for soft deletes.
I find having soft deletes to be very useful.
Keeping data around, but unaccessible saves you more often than you would think.


```python
class BaseAbstractModel(models.Model):
pkid = models.BigAutoField(primary_key=True, editable=False)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
is_deleted = models.BooleanField(default=False)

def soft_delete(self):
"""soft delete a model instance"""
self.is_deleted=True
self.save()

class Meta:
abstract = True
ordering = ['-created_at']
```

## Tailwind and Templates

I often find that my applications take on a very similar look.
I am not known for my design skills, so making something usable is better than making something beautiful.
This application leverages [TailwindCSS](https://tailwindcss.com/) just for that reason.
My blog where you are reading this post is TailwindCSS.
I have become comfortable with it, thus it adds to the speed of prototyping.

I am also not going to toot my own horn here about the designs.
I found most of the code at [Flowbite](https://flowbite.com/blocks/) and have edited it to fit my needs.

Keeping the templates neat and easily extendable was a very important goal with this project.
You can see below the layout of my templates file:

```
templates/
├── account
│   ├── email_confirm.html
│   ├── login.html
│   ├── logout.html
│   ├── password_reset.html
│   ├── password_reset_done.html
│   ├── password_reset_from_key.html
│   ├── password_reset_from_key_done.html
│   ├── signup.html
│   ├── signup_closed.html
│   └── verification_sent.html
├── base.html
├── index.html
└── partials
├── cta.html
├── footer.html
├── header.html
├── hero.html
└── pricing.html
```

I keep all my templates organized where names won't collide with each other and be easily found.
I think this is the most difficult change coming from a React based application.
A little more brainpower is needed to figure out something that works for you.
But once you find it, everything goes so smoothly.

I have downloaded and served the HTMX library.
I tend not to want to use CDNs if I do not have to.
Luckily, Django solves static files pretty easily.
Here is my `base.html` file:

```html
{% load static %}
{% load widget_tweaks %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Django HTMX Starter - {% block head_title %}Build fast!{% endblock %}</title>
<script src="{% static 'js/htmx.min.js' %}"></script>
<link href="{% static 'css/styles.css' %}" rel="stylesheet" />
{% block extra_head %}{% endblock %}
</head>
<body
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
class="min-h-screen bg-white dark:bg-gray-900"
>
{% include "partials/header.html" %}
{% if messages %}
<div>
{% for message in messages %}
<div
class="flex items-center px-4 py-3 text-sm font-bold text-white bg-blue-500"
role="alert"
>
<p>{{ message }}</p>
</div>
{% endfor %}
</div>
{% endif %}
<main>{% block content %}{% endblock %}</main>
{% include "partials/footer.html" %}
</body>
</html>
```

Larger logic like the header, footer and navbar are broken out into partials.
And both HTMX and TailwindCSS is served locally.
For those not as familiar with Django, the `{% load %}` keyword at the top of this file tells the Django templates to use the `static` and `widget_tweaks` as part of the templates.
And it is important to note the `hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'` attribute on the body tag.
This helps with HTMX posting to the Django backend.
The `django-htmx` package will be looking for this!

## Conclusion

I hope you found it interesting to read through my thought on how I created this skeleton project.
I have found quite a difference in my prototyping since I have removed all the overhead of setting up a project.
If you have something similar or have something you want to chat about, feel free to find me on [Threads](https://threads.net/@joshfinnie).
I love connecting with people there!

Please note that the above project is licensed with the MIT License.
Take it, use it as your own and improve it for your needs...