-
Notifications
You must be signed in to change notification settings - Fork 0
Extending the Boilerplate
Feature code lives under modules/.
Each Django module uses packages instead of growing large models.py, views.py, admin.py, or tests.py files.
This tutorial adds a complete blog module.
Run:
make create-app NAME=blogThe command creates:
modules/blog/
├── admin/
│ └── __init__.py
├── migrations/
│ └── __init__.py
├── models/
│ └── __init__.py
├── tests/
│ └── __init__.py
├── views/
│ └── __init__.py
├── __init__.py
└── apps.py
The generated AppConfig.name is modules.blog.
Add the module before django_cleanup.apps.CleanupConfig in app/settings.py:
INSTALLED_APPS = [
# Existing apps...
"modules.blog",
"django_cleanup.apps.CleanupConfig",
]Keep django_cleanup.apps.CleanupConfig last so its signal handlers run after other apps.
Create modules/blog/models/post.py:
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.titleExport every model from modules/blog/models/__init__.py so Django discovers it:
from modules.blog.models.post import PostCreate and apply migrations:
uv run python manage.py makemigrations blog
uv run python manage.py migrateCommit generated migrations. Never edit migrations already applied in shared or production environments.
Create modules/blog/admin/post.py:
from django.contrib import admin
from unfold.admin import ModelAdmin
from modules.blog.models.post import Post
@admin.register(Post)
class PostAdmin(ModelAdmin):
list_display = ("title", "published", "created_at")
list_filter = ("published",)
search_fields = ("title", "body")
readonly_fields = ("created_at",)Import each admin module from modules/blog/admin/__init__.py so Django admin autodiscovery loads it:
from modules.blog.admin.post import PostAdminCreate modules/blog/views/post.py:
from django.shortcuts import render
from modules.blog.models.post import Post
def post_list(request):
posts = Post.objects.filter(published=True).order_by("-created_at")
return render(request, "blog/post_list.html", {"posts": posts})Export the view from modules/blog/views/__init__.py:
from modules.blog.views.post import post_listCreate modules/blog/urls.py:
from django.urls import path
from modules.blog.views import post_list
app_name = "blog"
urlpatterns = [
path("", post_list, name="post-list"),
]Include it from app/urls.py:
urlpatterns = [
path("blog/", include("modules.blog.urls")),
path("", admin.site.urls),
]Keep feature URLs before the root admin route because Django admin has a final catch-all view.
Create modules/blog/templates/blog/post_list.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blog</title>
</head>
<body>
<main>
<h1>Blog</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</article>
{% empty %}
<p>No published posts.</p>
{% endfor %}
</main>
</body>
</html>Create modules/blog/tests/test_post_list.py:
from django.test import TestCase
from django.urls import reverse
from modules.blog.models.post import Post
class PostListTests(TestCase):
def test_shows_only_published_posts(self):
Post.objects.create(title="Visible", body="Published", published=True)
Post.objects.create(title="Hidden", body="Draft", published=False)
response = self.client.get(reverse("blog:post-list"))
self.assertContains(response, "Visible")
self.assertNotContains(response, "Hidden")Test modules do not need exports in tests/__init__.py.
Run:
make test
make checkUse one file per cohesive concept, not automatically one file per class.
Example:
modules/blog/
├── admin/
│ ├── __init__.py
│ ├── category.py
│ └── post.py
├── models/
│ ├── __init__.py
│ ├── category.py
│ └── post.py
├── tests/
│ ├── __init__.py
│ ├── test_category.py
│ └── test_post.py
└── views/
├── __init__.py
├── category.py
└── post.py
Explicitly export models, admin registrations, and public views from their package __init__.py files.
Avoid empty services/, selectors/, or repositories/ packages until feature code needs those boundaries.
Build and recreate services:
docker compose up --build -dContainer startup applies committed migrations and collects static files before Gunicorn starts.
Verify the release:
docker compose exec web .venv/bin/python manage.py check --deploy
docker compose logs webSee deployment.md for HTTPS, updates, and backups.
- Module lives under
modules/. - Module is registered before
django_cleanup.apps.CleanupConfig. - Models are exported from
models/__init__.py. - Admin modules are imported from
admin/__init__.py. - Model changes include generated migrations.
- Feature URLs are included by
app/urls.py. - User-visible behavior has tests under
tests/. - New environment variables are documented in
.env.example. -
make testandmake checkpass.
Django Boilerplate