smithery/jpoutrin

django

Django development patterns and conventions (2025). Auto-loads when working with Django models, views, URLs, forms, templates, management commands, or project structure. Includes async support and type hints.

Installation

$ npx skills add smithery/jpoutrin --skill django

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/jpoutrin · top by installs.

npx skills add smithery/jpoutrin

Browse all from smithery/jpoutrin

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,557 B
  • docs SUMMARY.md 273 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Django Development (2025)

Project Structure

project_name/
├── config/                 # Project config (rename from project_name/)
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py             # Required for async
├── apps/
│   ├── __init__.py
│   └── core/               # Shared utilities, base models
├── templates/
├── static/
├── manage.py
├── pyproject.toml          # Modern Python packaging
└── requirements/
    ├── base.txt
    ├── dev.txt
    └── prod.txt

Environment & Settings

# config/settings/base.py
import environ

env = environ.Env(
    DEBUG=(bool, False),
)
environ.Env.read_env()

SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
DATABASES = {"default": env.db()}
# .env
SECRET_KEY=your-secret-key
DEBUG=True
DATABASE_URL=postgres://user:pass@localhost:5432/dbname

Naming Conventions

Component Convention Example
App singular, lowercase blog, user_profile
Model singular PascalCase Article, UserProfile
View (function) noun_action article_detail
View (class) NounActionView ArticleDetailView
URL name app:noun-action blog:article-detail
Template app/noun_action.html blog/article_detail.html

Models

from django.db import models
from django.urls import reverse


class TimestampedModel(models.Model):
    """Abstract base for created/updated timestamps."""
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Article(TimestampedModel):
    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        PUBLISHED = "published", "Published"

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    author = models.ForeignKey(
        "auth.User",
        on_delete=models.CASCADE,
        related_name="articles",
    )
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.DRAFT,
        db_index=True,
    )

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["status", "created_at"]),
        ]

    def __str__(self) -> str:
        return self.title

    def get_absolute_url(self) -> str:
        return reverse("blog:article-detail", kwargs={"slug": self.slug})

Key patterns:

  • Use TextChoices / IntegerChoices for choices (not tuples)
  • Type hints on methods
  • Always set relatedname, dbindex on filtered fields

Views

Sync Views (Standard)

from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q


class ArticleListView(ListView):
    model = Article
    template_name = "blog/article_list.html"
    context_object_name = "articles"
    paginate_by = 20

    def get_queryset(self):
        qs = super().get_queryset().select_related("author")
        if q := self.request.GET.get("q"):
            qs = qs.filter(Q(title__icontains=q) | Q(body__icontains=q))
        return qs

Async Views

Use async for I/O-bound operations (external APIs, file ops). Requires ASGI server.

import httpx
from django.http import JsonResponse
from asgiref.sync import sync_to_async


async def weather_view(request):
    """Async view calling external API."""
    city = request.GET.get("city", "London")
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.weather.com/{city}")
    return JsonResponse(response.json())


async def article_list_async(request):
    """Async view with ORM (requires sync_to_async wrapper)."""
    articles = await sync_to_async(list)(
        Article.objects.select_related("author")[:20]
    )
    return JsonResponse({"articles": [a.title for a in articles]})

When to use async views:

  • External HTTP calls → use httpx (async) not requests
  • Multiple concurrent I/O operations
  • High-concurrency endpoints

Note: Django ORM is not fully async. Wrap ORM calls with synctoasync().

URLs

# apps/blog/urls.py
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.ArticleListView.as_view(), name="article-list"),
    path("<slug:slug>/", views.ArticleDetailView.as_view(), name="article-detail"),
]

# config/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("apps.blog.urls")),
]

Forms

from django import forms
from .models import Article


class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body", "status"]
        widgets = {
            "body": forms.Textarea(attrs={"rows": 10}),
        }

    def clean_title(self) -> str:
        title = self.cleaned_data["title"]
        if len(title) < 5:
            raise forms.ValidationError("Title must be at least 5 characters.")
        return title

Templates

templates/
├── base.html
├── includes/
│   ├── _pagination.html
│   └── _messages.html
└── blog/
    ├── article_list.html
    └── article_detail.html

Prefix partials with underscore. Use {% url %} not hardcoded paths:

{% extends "base.html" %}

{% block content %}
<a href="{% url 'blog:article-detail' slug=article.slug %}">
  {{ article.title }}
</a>
{% endblock %}

Type Hints

Add type hints throughout for mypy / IDE support:

from django.http import HttpRequest, HttpResponse
from django.db.models import QuerySet


def article_list(request: HttpRequest) -> HttpResponse:
    articles: QuerySet[Article] = Article.objects.filter(status="published")
    return render(request, "blog/article_list.html", {"articles": articles})
# pyproject.toml
[tool.mypy]
plugins = ["mypy_django_plugin.main"]
django_settings_module = "config.settings.dev"

# Run
mypy apps/

Running with ASGI (for async)

# Install
pip install uvicorn

# Development
uvicorn config.asgi:application --reload

# Production
gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker -w 4

Testing

import pytest
from django.test import Client
from django.urls import reverse


@pytest.mark.django_db
def test_article_list_view(client: Client):
    response = client.get(reverse("blog:article-list"))
    assert response.status_code == 200
    assert "articles" in response.context


@pytest.mark.django_db
async def test_async_view():
    """Async test for async views."""
    from django.test import AsyncClient
    client = AsyncClient()
    response = await client.get("/api/weather/?city=Paris")
    assert response.status_code == 200

Use pytest-django + factory_boy for ergonomic testing.

Common Pitfalls

  1. N+1 queries: Use selectrelated (FK) and prefetchrelated (M2M). Check with django-debug-toolbar.
  1. Sync calls in async views: Wrap ORM with synctoasync(). Use httpx not requests.
  1. Missing migrations: Run makemigrations after model changes. Commit migrations.
  1. Hardcoded URLs: Use {% url %} in templates, reverse() in Python.
  1. No indexes: Add db_index=True or Meta.indexes for filtered/ordered fields.
  1. Fat views: Move business logic to model methods or a service layer.

Modern Tooling

# pyproject.toml dev dependencies
django-debug-toolbar    # Query debugging
django-extensions       # shell_plus, show_urls
django-environ          # Environment variables
pytest-django           # Testing
factory-boy             # Test fixtures
mypy + django-stubs     # Type checking
ruff                    # Linting (replaces flake8/isort/black)

Management Commands

Keep commands thin — delegate logic to services.

Structure

apps/blog/
├── management/
│   └── commands/
│       └── publish_scheduled.py
└── services/
    └── publishing.py

Service Layer

# apps/blog/services/publishing.py
from dataclasses import dataclass
from django.utils import timezone
from apps.blog.models import Article


@dataclass
class PublishResult:
    published_count: int
    failed_ids: list[int]


def publish_scheduled_articles(dry_run: bool = False) -> PublishResult:
    """
    Publish all articles scheduled for now or earlier.
    
    Business logic lives here — testable without command scaffolding.
    """
    now = timezone.now()
    articles = Article.objects.filter(
        status=Article.Status.SCHEDULED,
        publish_at__lte=now,
    )
    
    if dry_run:
        return PublishResult(published_count=articles.count(), failed_ids=[])
    
    published = 0
    failed = []
    
    for article in articles:
        try:
            article.status = Article.Status.PUBLISHED
            article.save(update_fields=["status", "updated_at"])
            published += 1
        except Exception:
            failed.append(article.id)
    
    return PublishResult(published_count=published, failed_ids=failed)

Command (Thin Wrapper)

# apps/blog/management/commands/publish_scheduled.py
from django.core.management.base import BaseCommand, CommandError
from apps.blog.services.publishing import publish_scheduled_articles


class Command(BaseCommand):
    help = "Publish articles that are scheduled for now or earlier"

    def add_arguments(self, parser):
        parser.add_argument(
            "--dry-run",
            action="store_true",
            help="Show what would be published without making changes",
        )

    def handle(self, *args, **options):
        dry_run = options["dry_run"]
        
        if dry_run:
            self.stdout.write(self.style.WARNING("DRY RUN — no changes will be made"))
        
        result = publish_scheduled_articles(dry_run=dry_run)
        
        if result.failed_ids:
            self.stderr.write(
                self.style.ERROR(f"Failed to publish: {result.failed_ids}")
            )
        
        self.stdout.write(
            self.style.SUCCESS(f"Published {result.published_count} articles")
        )

Testing

# apps/blog/tests/test_services.py
import pytest
from django.utils import timezone
from apps.blog.services.publishing import publish_scheduled_articles
from apps.blog.models import Article


@pytest.mark.django_db
def test_publish_scheduled_articles(article_factory):
    # Create scheduled article in the past
    article = article_factory(
        status=Article.Status.SCHEDULED,
        publish_at=timezone.now() - timezone.timedelta(hours=1),
    )
    
    result = publish_scheduled_articles()
    
    assert result.published_count == 1
    article.refresh_from_db()
    assert article.status == Article.Status.PUBLISHED


@pytest.mark.django_db
def test_publish_dry_run_no_changes(article_factory):
    article = article_factory(
        status=Article.Status.SCHEDULED,
        publish_at=timezone.now() - timezone.timedelta(hours=1),
    )
    
    result = publish_scheduled_articles(dry_run=True)
    
    assert result.published_count == 1
    article.refresh_from_db()
    assert article.status == Article.Status.SCHEDULED  # Unchanged

Command Guidelines

Layer Responsibility
Command Parse args, call service, format output, exit codes
Service Business logic, DB operations, return typed results

Command should NOT:

  • Contain business logic
  • Query the database directly (beyond simple lookups)
  • Be the only way to run the logic

Benefits:

  • Services are reusable (views, tasks, other commands)
  • Services are testable without management command overhead
  • Commands stay focused on CLI concerns

When to Use Separate Skills

  • django-api: For REST APIs with Django Ninja or DRF
  • django-admin: For complex admin customizations