npx claudepluginhub sergio-bershadsky/ai --plugin django-devThis skill uses the workspace's default tool permissions.
Opinionated Django development toolkit enforcing consistent, production-ready patterns.
Provides 2025 Django patterns for project structure, settings, naming conventions, models with type hints and indexes, async support. Activates on Django models, views, URLs, forms, templates, commands, structure.
Provides Django 5.x expertise for async views, DRF, Celery, Django Channels; builds scalable apps with architecture, ORM optimization, testing, deployment.
Provides Django patterns, best practices, and guides for database models/querysets, security/auth/permissions, DRF APIs/serializers/viewsets, testing, error handling, and performance/caching. Auto-activates on manage.py, settings.py, or django package.
Share bugs, ideas, or general feedback.
Opinionated Django development toolkit enforcing consistent, production-ready patterns.
Base*), virtual (Virtual*), proxy (Proxy*)/docker folderAlways use uv for package management with split dependencies:
# Initialize project
uv init myproject
cd myproject
# Add dependencies by group
uv add django dynaconf django-unfold django-ninja
uv add --group dev ruff mypy django-stubs
uv add --group test pytest pytest-django factory-boy pytest-cov
pyproject.toml:
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"django>=5.0",
"dynaconf[toml]>=3.2",
"django-unfold>=0.30",
"django-ninja>=1.0",
"psycopg[binary]>=3.1",
"whitenoise>=6.6",
]
[dependency-groups]
dev = [
"ruff>=0.3",
"mypy>=1.8",
"django-stubs>=4.2",
"ipython>=8.0",
]
test = [
"pytest>=8.0",
"pytest-django>=4.8",
"factory-boy>=3.3",
"pytest-cov>=4.1",
"freezegun>=1.4",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.mypy]
plugins = ["mypy_django_plugin.main"]
strict = true
Standard Django project layout:
project/
├── pyproject.toml # Dependencies (uv)
├── uv.lock # Lock file
├── docker/
│ ├── Dockerfile # Main Dockerfile
│ ├── Dockerfile.dev # Development Dockerfile
│ ├── docker-compose.yml # Main compose
│ ├── docker-compose.dev.yml
│ ├── nginx/
│ │ └── nginx.conf
│ └── scripts/
│ ├── entrypoint.sh
│ └── wait-for-it.sh
├── config/
│ ├── __init__.py
│ ├── settings.py # Dynaconf integration
│ ├── .secrets.toml # Gitignored secrets
│ └── settings.toml # Environment config
├── apps/
│ └── myapp/
│ ├── models/ # Package, not single file
│ │ ├── __init__.py
│ │ ├── base.py # Base classes
│ │ └── user.py # One model per file
│ ├── forms/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── managers/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── api/ # Django Ninja (see django-dev-ninja)
│ └── admin/ # Unfold admin (see django-dev-unfold)
├── tests/ # See django-dev-test
└── manage.py
All Docker artifacts in /docker folder:
# docker/Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Install dependencies
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# Copy application
COPY . .
# Collect static files
RUN uv run python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["uv", "run", "gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker/docker-compose.yml
services:
web:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "8000:8000"
environment:
- DJANGO_ENV=production
env_file:
- ../.env
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=myproject
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
postgres_data:
Create base classes in models/base.py:
import uuid
from django.db import models
from django.utils import timezone
class BaseTimeStamped(models.Model):
"""Adds created_at and updated_at timestamps."""
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
get_latest_by = "created_at"
class BaseSoftDelete(models.Model):
"""Adds soft delete capability with deleted_at field."""
deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
self.deleted_at = timezone.now()
self.save(update_fields=["deleted_at"])
def hard_delete(self):
super().delete()
@property
def is_deleted(self) -> bool:
return self.deleted_at is not None
class BaseUUID(models.Model):
"""Uses UUID as primary key instead of auto-increment."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class BaseModel(BaseUUID, BaseTimeStamped, BaseSoftDelete):
"""Standard base model with UUID, timestamps, and soft delete."""
class Meta:
abstract = True
| Prefix | Type | Example |
|---|---|---|
Base* | Abstract base class | BaseTimeStamped, BaseModel |
Virtual* | In-memory only (not persisted) | VirtualCart, VirtualSession |
Proxy* | Proxy model | ProxyActiveUser, ProxyAdmin |
| (none) | Regular model | User, Product, Order |
All classes follow strict member ordering:
class Meta - ALWAYS FIRST in the classobjects = Manager()@property) - Alphabetical order_method, __str__) - Alphabetical orderclass User(BaseModel):
"""User account model."""
# 1. class Meta - ALWAYS FIRST
class Meta:
db_table = "users"
ordering = ["-created_at"]
# 2. Fields
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# 3. Manager
objects = UserManager()
# 4. Properties (alphabetical)
@property
def display_name(self) -> str:
return self.name or self.email.split("@")[0]
@property
def is_verified(self) -> bool:
return self.email_verified_at is not None
# 5. Private/dunder methods (alphabetical)
def __repr__(self) -> str:
return f"<User {self.email}>"
def __str__(self) -> str:
return self.email
def _calculate_score(self) -> int:
return len(self.orders.all())
def _validate_status(self) -> bool:
return self.is_active
# 6. Public methods (alphabetical)
def activate(self) -> None:
self.is_active = True
self.save(update_fields=["is_active"])
def can_place_order(self) -> bool:
return self.is_active and not self.is_deleted
def deactivate(self) -> None:
self.is_active = False
self.save(update_fields=["is_active"])
Each model in its own file (models/user.py):
from django.db import models
from .base import BaseModel
from ..managers.user import UserManager
class User(BaseModel):
"""User account model."""
# 1. class Meta - ALWAYS FIRST
class Meta:
db_table = "users"
verbose_name = "User"
verbose_name_plural = "Users"
ordering = ["-created_at"]
# 2. Fields
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# 3. Manager
objects = UserManager()
# 4. Properties
@property
def display_name(self) -> str:
return self.name or self.email.split("@")[0]
# 5. Private/dunder methods
def __str__(self) -> str:
return self.email
Re-export all models in models/__init__.py:
from .base import BaseModel, BaseTimeStamped, BaseSoftDelete, BaseUUID
from .user import User
from .product import Product
__all__ = [
"BaseModel",
"BaseTimeStamped",
"BaseSoftDelete",
"BaseUUID",
"User",
"Product",
]
Place managers in managers/ package:
# managers/user.py
from django.db import models
class UserQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True, deleted_at__isnull=True)
def by_email(self, email: str):
return self.filter(email__iexact=email)
class UserManager(models.Manager):
def get_queryset(self) -> UserQuerySet:
return UserQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def by_email(self, email: str):
return self.get_queryset().by_email(email)
Always use Dynaconf for Django settings. See references/dynaconf.md for complete setup.
Quick setup:
pip install dynaconf
dynaconf init -f toml
Update config/settings.py:
from dynaconf import Dynaconf
settings = Dynaconf(
envvar_prefix="DJANGO",
settings_files=["settings.toml", ".secrets.toml"],
environments=True,
env_switcher="DJANGO_ENV",
)
Forms follow the same 1-file-per-form pattern. See references/forms.md for details.
# forms/user.py
from django import forms
from ..models import User
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ["email", "name"]
To create a new Django app with proper structure:
mkdir -p apps/myapp/{models,forms,managers,api,admin}
touch apps/myapp/__init__.py
touch apps/myapp/{models,forms,managers,api,admin}/__init__.py
models/base.pyINSTALLED_APPS using DynaconfFor detailed patterns and setup guides:
references/models.md - Advanced model patterns, relationships, constraintsreferences/forms.md - Form organization, validation, widgetsreferences/dynaconf.md - Complete Dynaconf setup and environment configuration