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.
Install
npx skillscat add carlos-asg/tu-voz-en-ruta/django Install via the SkillsCat registry.
SKILL.md
⚠️ 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
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_dataCommon field types: CharField, IntegerField, ChoiceField, MultipleChoiceField, BooleanField, DateField, FileField, EmailField
Class-Based Views (CBV)
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 contextKey Methods to Override:
get_context_data()- Add extra contextget_queryset()- Filter querysetform_valid()- Handle valid form submissionform_invalid()- Handle invalid form submissionget_success_url()- Dynamic success URLdispatch()- Pre-process request
URL Configuration
# 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
# 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 contextManagement Commands
# 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=tenant1Project-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
# 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)
# 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_404instead of try/except - ✅ Use
reverse_lazyin CBV (notreverse) - ✅ Add
app_namein 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 (usereverse_lazy())