Back to list
jpoutrin

django

by jpoutrin

Claude Code Marketplace Plugin to dev like a pro in a multi-agent fashion

4🍴 0📅 Jan 22, 2026

SKILL.md


name: django description: Django web application development (2025). Use when creating or modifying Django projects, apps, models, views, URLs, forms, templates, or management commands. Covers project structure, naming conventions, async support, type hints, and modern best practices.

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

ComponentConventionExample
Appsingular, lowercaseblog, user_profile
Modelsingular PascalCaseArticle, UserProfile
View (function)noun_actionarticle_detail
View (class)NounActionViewArticleDetailView
URL nameapp:noun-actionblog:article-detail
Templateapp/noun_action.htmlblog/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 related_name, db_index 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 sync_to_async().

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 select_related (FK) and prefetch_related (M2M). Check with django-debug-toolbar.

  2. Sync calls in async views: Wrap ORM with sync_to_async(). Use httpx not requests.

  3. Missing migrations: Run makemigrations after model changes. Commit migrations.

  4. Hardcoded URLs: Use {% url %} in templates, reverse() in Python.

  5. No indexes: Add db_index=True or Meta.indexes for filtered/ordered fields.

  6. 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

LayerResponsibility
CommandParse args, call service, format output, exit codes
ServiceBusiness 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

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon