home / skills / carlos-asg / tu-voz-en-ruta / django

django skill

/skills/django

This skill enforces Django CBV best practices and type-safe forms, improving maintainability, security, and scalability across views, URLs, and commands.

npx playbooks add skill carlos-asg/tu-voz-en-ruta --skill django

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.0 KB
---
name: django
description: >
  Django modern patterns and best practices for views, forms, URL routing, and management commands.
  Trigger: When implementing or refactoring Django views, forms, URLs, or commands.
license: Apache-2.0
metadata:
  author: Carlos
  version: "1.1"
  scope: [root]
  auto_invoke:
    - "Writing Django views/forms/URLs"
    - "Implementing survey form views"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---

## ⚠️ CRITICAL: Class-Based Views ONLY

**This project uses EXCLUSIVELY Class-Based Views (CBV). Function-Based Views (FBV) are PROHIBITED.**

**Why?** Better reusability, built-in mixins (permissions, login), cleaner GET/POST separation, easier to extend.

## Django Forms with Type Hints

```python
from django import forms
from django.core.exceptions import ValidationError
from typing import Any

class UserForm(forms.ModelForm):
    """Type-safe ModelForm with validation."""
    
    class Meta:
        model = User
        fields = ["name", "email", "status"]
        widgets = {"email": forms.EmailInput(attrs={"class": "form-control"})}
        labels = {"name": "Full Name"}
        help_texts = {"email": "We'll never share your email."}
    
    def clean_email(self) -> str:
        """Validate unique email."""
        email = self.cleaned_data.get("email", "")
        qs = User.objects.filter(email=email)
        if self.instance and self.instance.pk:
            qs = qs.exclude(pk=self.instance.pk)
        if qs.exists():
            raise ValidationError("Email already exists")
        return email.lower()
    
    def clean(self) -> dict[str, Any]:
        """Cross-field validation."""
        cleaned_data = super().clean()
        # Add cross-field validation logic here
        return cleaned_data
```

**Common field types:** `CharField`, `IntegerField`, `ChoiceField`, `MultipleChoiceField`, `BooleanField`, `DateField`, `FileField`, `EmailField`

## Class-Based Views (CBV)

```python
from django.views.generic import ListView, CreateView, UpdateView, DetailView, TemplateView, FormView
from django.urls import reverse_lazy
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from typing import Any

# TemplateView - Static/simple pages
class HomeView(TemplateView):
    template_name = "home.html"
    
    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        context["total_users"] = User.objects.count()
        return context

# ListView - Display list of objects
class UserListView(ListView):
    model = User
    template_name = "users/list.html"
    context_object_name = "users"
    paginate_by = 20
    
    def get_queryset(self):
        return User.objects.filter(status="active")

# DetailView - Display single object
class UserDetailView(DetailView):
    model = User
    template_name = "users/detail.html"
    context_object_name = "user"
    pk_url_kwarg = "user_id"

# CreateView - Create new object
class UserCreateView(CreateView):
    model = User
    form_class = UserForm
    template_name = "users/create.html"
    success_url = reverse_lazy("user_list")
    
    def form_valid(self, form):
        messages.success(self.request, f"User {form.instance.name} created!")
        return super().form_valid(form)

# UpdateView - Update existing object
class UserUpdateView(UpdateView):
    model = User
    form_class = UserForm
    template_name = "users/update.html"
    pk_url_kwarg = "user_id"
    
    def form_valid(self, form):
        messages.success(self.request, "User updated!")
        return super().form_valid(form)
    
    def get_success_url(self):
        return reverse_lazy("user_detail", kwargs={"user_id": self.object.pk})

# FormView - Form without model
class ContactFormView(FormView):
    template_name = "contact.html"
    form_class = ContactForm
    success_url = reverse_lazy("home")
    
    def form_valid(self, form):
        form.send_email()
        messages.success(self.request, "Message sent!")
        return super().form_valid(form)

# Protected view with permissions
class DashboardView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
    template_name = "dashboard.html"
    permission_required = "app.can_view_dashboard"
    login_url = "/login/"
    
    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        context["organization"] = self.request.tenant  # Multi-tenant access
        return context
```

**Key Methods to Override:**
- `get_context_data()` - Add extra context
- `get_queryset()` - Filter queryset
- `form_valid()` - Handle valid form submission
- `form_invalid()` - Handle invalid form submission
- `get_success_url()` - Dynamic success URL
- `dispatch()` - Pre-process request

## URL Configuration

```python
# users/urls.py
from django.urls import path
from . import views

app_name = "users"  # Enable namespacing

urlpatterns = [
    path("", views.UserListView.as_view(), name="user_list"),
    path("<int:user_id>/", views.UserDetailView.as_view(), name="user_detail"),
    path("create/", views.UserCreateView.as_view(), name="user_create"),
    path("<int:user_id>/update/", views.UserUpdateView.as_view(), name="user_update"),
]

# Reverse URLs
success_url = reverse_lazy("users:user_detail", kwargs={"user_id": 123})
# Template: {% url 'users:user_detail' user_id=123 %}
```

## Multi-Tenancy with django-tenants

```python
# settings.py
TENANT_MODEL = "organization.Organization"
TENANT_DOMAIN_MODEL = "organization.Domain"
PUBLIC_SCHEMA_URLCONF = "buzon_quejas.urls_public"  # Public URLs
ROOT_URLCONF = "buzon_quejas.urls_tenant"  # Tenant URLs

# urls_public.py - No tenant required
urlpatterns = [
    path("", views.LandingView.as_view(), name="landing"),
    path("login/", views.LoginView.as_view(), name="login"),
]

# urls_tenant.py - Tenant context required
urlpatterns = [
    path("", views.DashboardView.as_view(), name="dashboard"),
    path("users/", include("apps.users.urls")),
]

# Tenant-aware view
class DashboardView(LoginRequiredMixin, TemplateView):
    template_name = "dashboard.html"
    
    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        tenant = self.request.tenant  # Access current tenant
        context["tenant_name"] = tenant.name
        return context
```

## Management Commands

```python
# app/management/commands/command_name.py
from django.core.management.base import BaseCommand, CommandError
from django_tenants.utils import schema_context
from typing import Any

class Command(BaseCommand):
    help = "Command description"
    
    def add_arguments(self, parser) -> None:
        parser.add_argument("count", type=int, help="Number to process")
        parser.add_argument("--schema", type=str, help="Tenant schema")
    
    def handle(self, *args: Any, **options: Any) -> None:
        count = options["count"]
        schema = options.get("schema")
        
        # Tenant-specific operation
        if schema:
            with schema_context(schema):
                self._process(count)
        else:
            self._process(count)
    
    def _process(self, count: int) -> None:
        try:
            # Your logic here
            self.stdout.write(self.style.SUCCESS(f"Processed {count} items"))
        except Exception as e:
            raise CommandError(f"Error: {e}")

# Usage: python manage.py command_name 10 --schema=tenant1
```

## Project-Specific: Survey Form Pattern

**URL Structure:**
- `/survey/` - Unit selector (auto-redirect if only 1 unit)
- `/survey/<transit_number>/` - Survey form for specific unit
- `/survey/<transit_number>/submit/` - Submit survey
- `/survey/thank-you/` - Thank you page

```python
# apps/interview/views.py
class SelectUnitForSurveyView(TemplateView):
    """Auto-redirect if 1 unit, show selector if multiple."""
    template_name = "interview/select_unit.html"
    
    def get(self, request, *args, **kwargs):
        units = Unit.objects.all()
        if units.count() == 0:
            self.template_name = "interview/no_units.html"
        elif units.count() == 1:
            return redirect("interview:survey_form", transit_number=units.first().transit_number)
        return super().get(request, *args, **kwargs)
    
    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        context["units"] = Unit.objects.all()
        return context

class SurveyFormView(TemplateView):
    """Display survey form for specific unit."""
    template_name = "interview/form_section.html"
    
    def get_context_data(self, transit_number: str, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        unit = get_object_or_404(Unit, transit_number=transit_number)
        context.update({
            "unit": unit,
            "questions": Question.objects.filter(is_active=True).order_by("order"),
        })
        return context

class SubmitSurveyView(FormView):
    """Process survey submission with rate limiting."""
    form_class = SurveyForm
    template_name = "interview/form_section.html"
    
    def dispatch(self, request, *args, **kwargs):
        if request.method != "POST":
            return redirect("interview:survey_form", transit_number=kwargs["transit_number"])
        return super().dispatch(request, *args, **kwargs)
    
    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["unit"] = get_object_or_404(Unit, transit_number=self.kwargs["transit_number"])
        return kwargs
    
    def form_valid(self, form):
        form.save()
        messages.success(self.request, "¡Gracias! Tu encuesta ha sido enviada.")
        return redirect("interview:survey_thank_you")
    
    def form_invalid(self, form):
        messages.error(self.request, "Por favor corrige los errores.")
        return super().form_invalid(form)

# apps/interview/urls.py
app_name = "interview"
urlpatterns = [
    path("survey/", views.SelectUnitForSurveyView.as_view(), name="select_unit_for_survey"),
    path("survey/<str:transit_number>/", views.SurveyFormView.as_view(), name="survey_form"),
    path("survey/<str:transit_number>/submit/", views.SubmitSurveyView.as_view(), name="submit_survey"),
    path("survey/thank-you/", views.SurveyThankYouView.as_view(), name="survey_thank_you"),
]
```

## Admin Customization (django-jazzmin)

```python
# settings.py
INSTALLED_APPS = [
    "jazzmin",  # MUST be before django.contrib.admin
    "django.contrib.admin",
    # ...
]

JAZZMIN_SETTINGS = {
    "site_title": "Admin Panel",
    "site_header": "Tu Voz en Ruta",
    "site_logo": "images/logo.png",
    "icons": {
        "auth.user": "fas fa-user",
        "transport.Unit": "fas fa-bus",
    },
    "theme": "flatly",
}

# Custom Tenant Admin
from django.contrib.admin import AdminSite

class TenantAdminSite(AdminSite):
    site_header = "Panel de Administración"
    site_title = "Admin"

tenant_admin_site = TenantAdminSite(name="tenant_admin")

@admin.register(Unit, site=tenant_admin_site)
class UnitAdmin(admin.ModelAdmin):
    list_display = ["transit_number", "route", "is_active"]
    list_filter = ["is_active", "route"]
    search_fields = ["transit_number"]
```

## Best Practices Checklist

**ALWAYS:**
- ✅ **Use Class-Based Views (CBV) - MANDATORY**
- ✅ Type hints on all views and forms
- ✅ Use `get_object_or_404` instead of try/except
- ✅ Use `reverse_lazy` in CBV (not `reverse`)
- ✅ Add `app_name` in urls.py for namespacing
- ✅ Use Django messages framework for feedback
- ✅ Validate data in forms, not views
- ✅ Separate public and tenant URLs
- ✅ Access tenant via `request.tenant`
- ✅ Use mixins: `LoginRequiredMixin`, `PermissionRequiredMixin`

**NEVER:**
- ❌ **Use Function-Based Views (FBV) - PROHIBITED**
- ❌ Hard-code URLs (use `reverse_lazy()`)
- ❌ Put business logic in views (use models/managers/services)
- ❌ Skip form validation
- ❌ Mix public and tenant logic
- ❌ Use `reverse()` in class attributes (use `reverse_lazy()`)

Overview

This skill captures Django modern patterns and best practices for Class-Based Views, forms with type hints, URL routing, multi-tenant handling, and management commands. It focuses on maintainability, reusability, and secure patterns for a complaint box web app serving tenant organizations. Use it when implementing or refactoring views, forms, URLs, or commands to conform with project conventions.

How this skill works

The skill enforces Class-Based Views only and recommends specific generic CBVs (TemplateView, ListView, DetailView, CreateView, UpdateView, FormView) with named override points (get_context_data, get_queryset, form_valid, get_success_url, dispatch). It shows type-hinted ModelForms with per-field and cross-field validation, tenant-aware URL configurations, and management commands that can run per-tenant using schema_context. It also demonstrates admin customization and messages-based user feedback.

When to use it

  • Creating or refactoring any view — always implement as a CBV with proper mixins
  • Building forms tied to models or standalone forms where type hints and validation matter
  • Defining URL patterns that must be namespaced and tenant-aware
  • Writing management commands that may operate across tenant schemas
  • Implementing multi-step flows like surveys with redirects and rate limiting

Best practices

  • MANDATORY: Use Class-Based Views; do not use function-based views
  • Use reverse_lazy in class attributes and get_success_url for dynamic redirects
  • Keep business logic out of views; use models/managers/services for processing
  • Validate all input in forms (clean_<field> and clean) and use type hints
  • Use get_object_or_404 for object lookup and messages framework for user feedback
  • Use LoginRequiredMixin and PermissionRequiredMixin for access control and access tenant with request.tenant

Example use cases

  • List active users with UserListView that overrides get_queryset to filter by status
  • Create and update users with ModelForm type hints and unique-email clean_email logic
  • Survey flow: selector view that auto-redirects, a form view for a unit, and a FormView to process submissions
  • Tenant-safe management command that runs logic inside django_tenants schema_context
  • Custom admin site per tenant with jazzmin settings and ModelAdmin registrations

FAQ

Can I use function-based views for small endpoints?

No. This project mandates Class-Based Views only to ensure consistency, reusability, and mixin support.

When should I use reverse_lazy vs reverse?

Use reverse_lazy in class attributes and anywhere evaluated at import time; use reverse inside methods executed at runtime.