Cheatsheet Django
Framework web Python de alto nível, rápido e seguro
Django
Installation and Setup
Install Django
pip install django django-admin startproject my_site cd my_site python manage.py runserver
startproject creates the base structure. runserver starts at localhost:8000. The project includes settings.py, urls.py and manage.py. Use a virtual environment.
Project Structure
my_site/ settings.py urls.py wsgi.py blog/ models.py views.py urls.py admin.py tests.py manage.py
settings.py centralizes configuration. The root urls.py distributes routes. wsgi.py is the production entry point. Each app has its own models.py, views.py and urls.py.
Media (Uploads)
# settings.py MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" # urls.py (dev): from django.conf import settings urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
MEDIA_ROOT stores uploaded files. MEDIA_URL is the access prefix. In production use S3 or external storage. Never serve media with Django in production.
Create App
python manage.py startapp blog
# register in settings.py:
INSTALLED_APPS = [
...
"blog",
]startapp creates models, views, tests and admin. Register it in INSTALLED_APPS to activate. Each app is an independent module. Convention: one app per feature.
Essential Settings
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "my_db",
}
}DEBUG = False in production (security). ALLOWED_HOSTS restricts domains. SECRET_KEY must come from env. DATABASES configures PostgreSQL, MySQL or SQLite.
Environment Variables
# pip install python-decouple
from decouple import config
SECRET_KEY = config("SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
DATABASE_URL = config("DATABASE_URL")python-decouple reads environment variables with defaults. cast=bool converts types. Never commit secrets in the code. Use a local .env and env vars in production.
Migrations
python manage.py makemigrations python manage.py migrate python manage.py showmigrations python manage.py sqlmigrate blog 0001
makemigrations generates migration files. migrate applies them to the DB. showmigrations lists the state. sqlmigrate shows the generated SQL. Never edit the DB manually.
Virtual Environment
python -m venv .venv source .venv/bin/activate # Linux/Mac .venv\Scripts\activate # Windows pip install django pip freeze > requirements.txt
venv isolates the project dependencies. activate enables the environment. pip freeze generates requirements. Always use virtual environments — one per project.
Custom Commands
# blog/management/commands/seed_posts.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Creates sample posts"
def handle(self, *args, **options):
self.stdout.write("Done!")Custom commands live in management/commands/. BaseCommand is the base class. handle() contains the logic. Run with python manage.py seed_posts.
CLI Commands
python manage.py shell python manage.py createsuperuser python manage.py collectstatic python manage.py test python manage.py dbshell
shell opens a REPL with Django loaded. createsuperuser creates an admin. collectstatic gathers static files. test runs the suite. dbshell opens the DB client.
Static Files
# settings.py
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
# in the template:
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">STATIC_URL is the URL prefix. STATICFILES_DIRS are the source folders. collectstatic copies everything to STATIC_ROOT. In production serve with Nginx or a CDN.
Models e ORM
Define Model
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.titlemodels.Model is the base of all models. Each field is a column in the DB. __str__ defines a readable representation. auto_now_add fills on creation.
Advanced Filters (lookups)
Post.objects.filter(title__icontains="hello") Post.objects.filter(created__year=2025) Post.objects.filter(views__gte=100) Post.objects.filter(title__startswith="How") Post.objects.filter(id__in=[1, 2, 3]) Post.objects.filter(content__isnull=True)
Lookups use __: icontains (case-insensitive), gte (greater or equal), year, in, isnull. Chain multiple filters with AND. Use Q() for OR.
ManyToMany
class Post(models.Model):
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
# usage:
post.tags.add(tag1, tag2)
post.tags.remove(tag1)
post.tags.all()
tag.posts.count()ManyToManyField creates an automatic intermediate table. add() and remove() manage associations. blank=True makes it optional in forms. Access works both ways.
Custom Managers
class PostManager(models.Manager):
def published(self):
return self.filter(active=True, date__lte=timezone.now())
class Post(models.Model):
objects = PostManager()
Post.objects.published()A custom Manager adds methods to objects. Encapsulates reusable queries. Replaces the default manager. Multiple managers per model are possible.
Field Types
CharField(max_length=100) TextField() IntegerField() FloatField() BooleanField(default=False) DateField() DateTimeField(auto_now=True) EmailField() SlugField(unique=True) UUIDField(default=uuid.uuid4)
CharField requires max_length. TextField for long text. auto_now updates on every save. SlugField generates friendly URLs. UUIDField for non-sequential IDs.
Ordering and Slicing
Post.objects.order_by("-created")
Post.objects.order_by("title", "-created")
Post.objects.all()[:10]
Post.objects.all()[10:20]
# in the model Meta:
class Meta:
ordering = ["-created"]order_by() with - is descending. Slice [:10] generates LIMIT in SQL. Meta.ordering sets the default order. You cannot combine slicing with chained filters.
Q Objects and F Expressions
from django.db.models import Q, F
Post.objects.filter(Q(title__icontains="django") | Q(tags__name="python"))
Post.objects.filter(views__gt=F("likes"))
Post.objects.annotate(ratio=F("likes") / F("views"))Q() enables OR and complex conditions. | is OR, & is AND. F() compares fields with each other. annotate() adds computed fields to the QuerySet.
Create Records
Post.objects.create(title="Hello", content="Text")
# or:
p = Post(title="X", content="Y")
p.save()
# multiple:
Post.objects.bulk_create([
Post(title="A"), Post(title="B"),
])create() inserts and returns the object. save() persists manually. bulk_create() inserts in bulk (faster). The ID is assigned automatically after save.
Update and Delete
p.title = "New" p.save() # bulk update: Post.objects.filter(active=False).update(archived=True) # delete: p.delete() Post.objects.filter(created__year=2020).delete()
save() updates all fields. update() does a bulk update without loading objects. delete() removes with cascade. update() does not trigger signals or save().
Aggregations
from django.db.models import Count, Avg, Sum, Max
Post.objects.aggregate(total=Count("id"), average=Avg("views"))
Post.objects.values("author").annotate(total=Count("id"))
Post.objects.aggregate(highest=Max("views"))aggregate() returns a dictionary with results. values() + annotate() does GROUP BY. Count, Avg, Sum, Max are the main functions. Generates optimized SQL.
Basic Queries
Post.objects.all() Post.objects.get(id=1) Post.objects.filter(active=True) Post.objects.exclude(id=2) Post.objects.first() Post.objects.last() Post.objects.count() Post.objects.exists()
all() returns everything. get() expects exactly 1 (error if 0 or 2+). filter() returns a QuerySet. exists() is efficient to check presence. QuerySets are lazy.
Relations (ForeignKey)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
# usage:
post.comments.all()
comment.post.titleForeignKey creates a many-to-one relation. on_delete=CASCADE deletes dependents. related_name defines the reverse access. SET_NULL preserves with null.
select_related and prefetch_related
# ForeignKey (JOIN):
Comment.objects.select_related("post", "author").all()
# ManyToMany / reverse (extra query):
Post.objects.prefetch_related("tags", "comments").all()select_related() does a JOIN for ForeignKey (1 query). prefetch_related() does a separate query for M2M. Eliminates the N+1 problem. Essential for performance with relations.
Views e CBVs
Function-based View
from django.shortcuts import render
def home(request):
posts = Post.objects.filter(active=True)
return render(request, "blog/home.html", {"posts": posts})render() combines template + context + request. The view receives request and returns an HttpResponse. Simple and explicit. Ideal for custom logic or small endpoints.
CreateView and UpdateView
from django.views.generic.edit import CreateView, UpdateView
class PostCreate(CreateView):
model = Post
fields = ["title", "content"]
success_url = reverse_lazy("post_list")
class PostUpdate(UpdateView):
model = Post
fields = ["title", "content"]CreateView generates an automatic creation form. UpdateView pre-fills with existing data. fields defines editable fields. success_url is the redirect after save.
Redirect
from django.shortcuts import redirect
from django.urls import reverse
return redirect("post_list")
return redirect("/blog/post/1/")
return redirect(reverse("post_detail", args=[post.id]))redirect() accepts a URL name, path or object. reverse() generates a URL by name. Always use names instead of hardcoded paths. Returns 302 by default.
GET and POST
def contact(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return redirect("success")
else:
form = ContactForm()
return render(request, "contact.html", {"form": form})request.method checks the HTTP verb. POST processes data, GET shows an empty form. request.POST contains the form data. Always redirect after POST (PRG pattern).
DeleteView
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
class PostDelete(DeleteView):
model = Post
success_url = reverse_lazy("post_list")
template_name = "blog/confirm_delete.html"DeleteView shows a confirmation and deletes via POST. Requires a confirmation template. reverse_lazy() because URLs are not loaded yet. Protects against accidental deletion.
get_object_or_404
from django.shortcuts import get_object_or_404, get_list_or_404 post = get_object_or_404(Post, id=pk) posts = get_list_or_404(Post, active=True)
get_object_or_404() returns the object or raises Http404. Replaces try/except DoesNotExist. get_list_or_404() for lists. Cleaner than objects.get() with handling.
ListView
from django.views.generic import ListView
class PostList(ListView):
model = Post
template_name = "blog/list.html"
context_object_name = "posts"
paginate_by = 10
ordering = ["-created"]ListView lists objects with automatic pagination. context_object_name renames the template variable. paginate_by enables pagination. Less code than the equivalent FBV.
TemplateView
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "about.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["team"] = ["Anna", "Brian"]
return ctxTemplateView renders a template without a model. get_context_data() adds extra context. Ideal for static pages with dynamic data. The simplet Django view.
Mixins
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
class PostCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Post
permission_required = "blog.add_post"
login_url = "/login/"LoginRequiredMixin requires authentication. PermissionRequiredMixin checks permission. Mixins add behavior to CBVs. Order matters: mixins before the base view.
DetailView
from django.views.generic import DetailView
class PostDetail(DetailView):
model = Post
template_name = "blog/detail.html"
slug_field = "slug"
slug_url_kwarg = "slug"DetailView shows one object by pk or slug. Returns 404 automatically if it does not exist. slug_field defines the lookup field. The object is available the object or post in the template.
HTTP Responses
from django.http import JsonResponse, HttpResponse, HttpResponseNotFound
return JsonResponse({"ok": True, "total": 42})
return HttpResponse("Plain text", content_type="text/plain")
return HttpResponseNotFound("Not found")JsonResponse returns JSON with the correct headers. HttpResponse is the base response. HttpResponseNotFound is 404. For APIs, prefer DRF serializers.
View Decorators
from django.views.decorators.http import require_POST, require_GET from django.views.decorators.cache import cache_page @require_POST def delete_post(request, id): ... @cache_page(60 * 15) def post_list(request): ...
@require_POST rejects other methods (405). @cache_page caches the response. Decorators modify FBV behavior. Combine multiple decorators freely.
URLs e Routing
Root URLconf
# my_site/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
path("", include("pages.urls")),
]include() delegates routes to apps. path() defines URL patterns. The root distributes by prefix. Keeps each app with its own URLs.
Reverse and url()
from django.urls import reverse
reverse("blog:list")
reverse("blog:detail", args=[1])
reverse("blog:detail", kwargs={"pk": 1})reverse() generates a URL from the name. args for positional parameters. kwargs for named ones. With a namespace use app:name. Never hardcode URLs.
Error Views (404, 500)
# views.py
def handler404(request, exception):
return render(request, "404.html", status=404)
def handler500(request):
return render(request, "500.html", status=500)
# urls.py
handler404 = "pages.views.handler404"Custom views for error pages. handler404 receives the exception. handler500 does not receive the full request. Templates at the templates root. Only works with DEBUG=False.
App Routes
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="list"),
path("<int:pk>/", views.detail, name="detail"),
path("new/", views.create, name="create"),
]app_name defines a namespace to avoid conflicts. name identifies the route for reverse. Reference becomes blog:list. Each app has its own urls.py.
URLs in Templates
<a href="{% url 'blog:list' %}">Posts</a>
<a href="{% url 'blog:detail' post.pk %}">{{ post.title }}</a>
<a href="{% url 'blog:detail' pk=post.pk %}">View</a>{% url %} generates URLs in templates. Accepts positional args or kwargs. With a namespace: app:name. If the route does not exist, an error in dev. Always use it instead of fixed paths.
include with kwargs
urlpatterns = [
path("blog/", include("blog.urls")),
path("api/v1/", include("api.urls")),
path("api/v2/", include("api.urls_v2")),
]include() with different prefixes for versioning. Each version has its own urls.py. The prefix is stripped before passing to the app. Makes API evolution easier.
Path Converters
path("post/<int:id>/", views.post)
path("user/<str:username>/", views.profile)
path("article/<slug:slug>/", views.article)
path("folder/<path:filepath>/", views.file_view)
path("uuid/<uuid:id>/", views.by_uuid)Converters: int, str, slug, path, uuid. They validate and convert automatically. The value is passed the a kwarg to the view. 404 if it does not match.
Namespaces
# root urls.py:
path("blog/", include("blog.urls", namespace="blog")),
path("api/blog/", include("blog.urls", namespace="api-blog")),
# reverse:
reverse("blog:list")
reverse("api-blog:list")namespace lets you reuse the same urls.py with different prefixes. Avoids name conflicts between apps. Essential for versioned APIs. Define app_name in the app urls.py.
URL Naming Conventions
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<int:pk>/", views.detail, name="post_detail"),
path("new/", views.create, name="post_create"),
path("<int:pk>/edit/", views.update, name="post_update"),
path("<int:pk>/delete/", views.delete_post, name="post_delete"),
]Convention: model_action (post_list, post_detail). CRUD: list, detail, create, update, delete. Descriptive and consistent names. Makes reverse and maintenance easier.
re_path (regex)
from django.urls import re_path
re_path(r"^articles/(?P<year>[0-9]{4})/$", views.archive)
re_path(r"^post/(?P<slug>[-\w]+)/$", views.post)re_path() uses regex for complex patterns. Named groups (?P<name>) become kwargs. Prefer path() when possible. Regex for specific validations.
Redirect and Generic Views
from django.views.generic import RedirectView
urlpatterns = [
path("old/", RedirectView.as_view(url="/new/", permanent=True)),
path("docs/", RedirectView.as_view(pattern_name="blog:list")),
]RedirectView redirects without a custom view. permanent=True returns 301. pattern_name uses internal reverse. Useful for legacy URLs after restructuring.
Templates
Variables
<h1>{{ title }}</h1>
<p>{{ post.author.name }}</p>
<p>{{ items|length }} items</p>
<p>{{ value|default:"N/A" }}</p>{{ }} prints variables. Attribute access with a dot. |length counts elements. |default provides a fallback for empty values. Auto-escaping is on by default (safe against XSS).
Inheritance
<!-- base.html -->
<html>
<body>
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>
<!-- page.html -->
{% extends "base.html" %}
{% block content %}<h1>Title</h1>{% endblock %}{% extends %} inherits from a base layout. {% block %} defines overridable sections. {{ block.super }} includes the parent content. A template can only have one extends.
Custom Template Tags
# blog/templatetags/blog_tags.py
from django import template
register = template.Library()
@register.simple_tag
def total_posts():
return Post.objects.count()
# in the template:
{% load blog_tags %}
{% total_posts %} postsCustom tags live in templatetags/. @register.simple_tag creates a simple tag. @register.filter creates a filter. {% load %} imports it in the template. Requires the app in INSTALLED_APPS.
Conditionals
{% if user.is_authenticated %}
Hello, {{ user.username }}
{% elif user.is_staff %}
Staff
{% else %}
<a href="{% url 'login' %}">Sign in</a>
{% endif %}{% if %} supports elif and else. Access attributes and methods without parentheses. is_authenticated checks login. Operators: and, or, not, in, comparisons.
Include and Partials
{% include "partials/menu.html" %}
{% include "partials/card.html" with title="X" %}
{% for post in posts %}
{% include "partials/post_item.html" %}
{% endfor %}{% include %} inserts partials with the current context. with passes extra variables. Inside loops, loop variables are available. Ideal for reusable components.
Comments and Debug
{# single-line comment #}
{% comment %}
Multi-line comment
not rendered in the HTML
{% endcomment %}
{% debug %} <!-- shows context (dev) -->{# #} is an inline comment. {% comment %} for blocks. They do not appear in the final HTML. {% debug %} shows all context variables. Remove before production.
For Loop
{% for post in posts %}
<li>{{ forloop.counter }}. {{ post.title }}</li>
{% empty %}
<li>No posts</li>
{% endfor %}{% for %} iterates QuerySets and lists. {% empty %} shows a fallback if empty. forloop.counter is 1-based. Also: forloop.first, forloop.last, forloop.revcounter.
Static and Media
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">
<img src="{% static 'img/logo.png' %}" alt="Logo">
<script src="{% static 'js/app.js' %}"></script>
<!-- media (uploads): -->
<img src="{{ post.image.url }}">{% load static %} loads the tag. {% static %} generates a URL with versioning. .url on FileField/ImageField gives the media path. Always use the static tag, never fixed paths.
Widthratio and Cycles
{% widthratio value max 100 %}%
{% cycle 'row-even' 'row-odd' the rowclass %}
<tr class="{{ rowclass }}">...</tr>
{% with total=items|length %}
{{ total }} items
{% endwith %}{% widthratio %} does math calculations (percentages). {% cycle %} alternates values in loops. {% with %} creates a temporary variable. Useful for presentation logic.
Filters
{{ name|upper }}
{{ name|lower|capfirst }}
{{ date|date:"d/m/Y" }}
{{ text|truncatewords:30 }}
{{ price|floatformat:2 }}
{{ items|join:", " }}
{{ html|safe }}|upper, |lower transform case. |date formats dates. |truncatewords cuts by words. |safe disables escaping (careful). Filters chain with pipe.
CSRF and URLs
<form method="post">
{% csrf_token %}
<input name="title">
<button>Send</button>
</form>
<a href="{% url 'blog:detail' post.pk %}">View</a>{% csrf_token %} is required in POST forms. It generates a hidden input with a token. Without it, Django rejects with 403. {% url %} generates links by route name.
Forms and Validation
ModelForm
from django import forms
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "tags"]
widgets = {
"content": forms.Textarea(attrs={"rows": 10}),
}ModelForm generates a form from a model. fields lists included fields. widgets customizes rendering. Automatic validation based on the model fields.
Custom Validation (forms)
class PostForm(forms.ModelForm):
def clean_title(self):
title = self.cleaned_data["title"]
if len(title) < 5:
raise forms.ValidationError("Minimum 5 characters")
return title
def clean(self):
data = super().clean()
if data.get("date") and data["date"] < timezone.now():
raise forms.ValidationError("Date in the past")
return dataclean_field() validates a specific field. clean() validates multiple fields together. ValidationError adds an error to the form. Always return the cleaned value.
CBV with Forms (FormView)
from django.views.generic.edit import FormView
class ContactView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = "/thanks/"
def form_valid(self, form):
form.send_email()
return super().form_valid(form)FormView handles GET/POST automatically. form_valid() is called when valid. success_url is the redirect. Less boilerplate than an FBV for simple forms.
Manual Form
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
accept = forms.BooleanField(required=False)forms.Form for forms without a model. Fields validate automatically. required=False makes it optional. Ideal for contact, search and external forms.
Errors and Messages
{{ form.non_field_errors }}
{{ form.title.errors }}
{% for field in form %}
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors.0 }}</span>
{% endif %}
{% endfor %}non_field_errors are errors from clean(). field.errors are per field. Iterating form gives full layout control. errors.0 shows the first error.
File Uploads
class UploadForm(forms.Form):
file = forms.FileField()
# in the view:
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
f = request.FILES["file"]
with open(f"media/{f.name}", "wb+") as dest:
for chunk in f.chunks():
dest.write(chunk)request.FILES contains uploaded files. chunks() reads in blocks (memory efficient). The form needs enctype="multipart/form-data". In models use FileField/ImageField.
Validate in the View
def create_post(request):
if request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect("blog:list")
else:
form = PostForm()
return render(request, "form.html", {"form": form})is_valid() runs all validation. commit=False does not save yet (allows adding fields). request.FILES for uploads. cleaned_data holds validated data.
Widgets and attrs
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = "__all__"
widgets = {
"date": forms.DateInput(attrs={"type": "date"}),
"color": forms.TextInput(attrs={"type": "color"}),
"content": forms.Textarea(attrs={"class": "editor"}),
}widgets customizes the fields' HTML. attrs adds HTML attributes. type: date uses the native datepicker. class for CSS styling. Improves UX without JS.
Reusable Validators
from django.core.validators import MinValuisValidtor, RegexValidator
class ProductForm(forms.Form):
price = forms.FloatField(validators=[MinValuisValidtor(0)])
sku = forms.CharField(validators=[
RegexValidator(r"^[A-Z]{3}-\d{4}$", "Format: ABC-1234")
])validators are reusable across forms and models. MinValuisValidtor limits values. RegexValidator validates patterns. Create custom validators for business rules.
Form in the Template
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Send</button>
</form>
<!-- or field by field: -->
{{ form.title.label_tag }}
{{ form.title }}
{{ form.title.errors }}as_p renders in paragraphs. as_table and as_ul are alternatives. Field by field gives full control. enctype is needed for uploads. Always include csrf_token.
Formsets
from django.forms import modelformset_factory
CommentFormSet = modelformset_factory(
Comment, fields=["text"], extra=2
)
# in the view:
formset = CommentFormSet(request.POST or None, queryset=post.comments.all())
if formset.is_valid():
formset.save()modelformset_factory creates multiple forms at once. extra adds empty forms. Ideal for editing lists of objects. inlineformset_factory for FK relations.
Admin
Register Model
# admin.py from django.contrib import admin from .models import Post admin.site.register(Post)
admin.site.register() enables the model in the panel. Access it at /admin/. Requires a superuser. Full automatic CRUD (list, create, edit, delete).
Custom Actions
@admin.action(description="Mark the published")
def publish(modeladmin, request, queryset):
queryset.update(active=True)
class PostAdmin(admin.ModelAdmin):
actions = [publish]@admin.action() creates bulk actions. queryset is the selected objects. They appear in the actions dropdown. description is the label. Ideal for bulk operations.
Admin Custom Views
from django.urls import path
from django.http import HttpResponse
class PostAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
custom = [path("export/", self.admin_site.admin_view(self.export))]
return custom + urls
def export(self, request):
return HttpResponse("CSV here", content_type="text/csv")get_urls() adds custom routes to the admin. admin_view() protects with login. Useful for exports, reports and dashboards. Access it at /admin/app/model/export/.
ModelAdmin
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "created", "active"]
search_fields = ["title", "content"]
list_filter = ["active", "created"]
ordering = ["-created"]@admin.register() is the modern decorator. list_display defines columns. search_fields enables search. list_filter adds side filters. ordering sorts by default.
Admin Permissions
class PostAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
def has_delete_permission(self, request, obj=None):
return request.user.is_superuserget_queryset() filters what the user sees. has_delete_permission() controls deletion. Also: has_add_permission(), has_change_permission(). Restrict by role.
Superuser and Staff
python manage.py createsuperuser
# programmatically:
from django.contrib.auth.models import User
User.objects.create_superuser("admin", "admin@site.com", "pass")
User.objects.create_user("editor", is_staff=True)createsuperuser creates via CLI. is_staff=True allows admin access. is_superuser has all permissions. Staff without superuser needs explicit permissions.
Form Fields
class PostAdmin(admin.ModelAdmin):
fields = ["title", "content", "tags"]
readonly_fields = ["created", "updated"]
exclude = ["slug"]
# or with fieldsets:
fieldsets = [
("Info", {"fields": ["title", "content"]}),
("Meta", {"fields": ["tags", "active"], "classes": ["collapse"]}),
]fields controls order and visibility. readonly_fields prevents editing. fieldsets groups into sections. collapse hides by default. Organizes complex forms.
List Editing
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "active", "views"]
list_editable = ["active"]
list_per_page = 25
date_hierarchy = "created"
save_on_top = Truelist_editable allows editing fields in the listing. list_per_page controls pagination. date_hierarchy adds date navigation. save_on_top duplicates the save button.
Custom Admin Site
# admin.py admin.site.site_header = "Site Management" admin.site.site_title = "Admin" admin.site.index_title = "Control Panel"
site_header is the title at the top. site_title is the tab title. index_title is the home page heading. Simple customization without custom templates.
Inlines
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]TabularInline edits related objects in a table. StackedInline in stacked format. extra sets empty forms. Edit FK and M2M directly in the parent form.
Autocomplete and raw_id
class CommentAdmin(admin.ModelAdmin):
autocomplete_fields = ["post"]
raw_id_fields = ["author"]
class PostAdmin(admin.ModelAdmin):
search_fields = ["title"] # required for autocompleteautocomplete_fields creates a searchable select (FK/M2M). The related model needs search_fields. raw_id_fields shows an ID input. Essential for FKs with many records.
Advanced and Deploy
Middleware
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time
start = time.time()
response = self.get_response(request)
response["X-Time"] = str(time.time() - start)
return response
# settings.py MIDDLEWARE = [..., "app.middleware.TimingMiddleware"]Middleware intercepts all requests/responses. __call__ processes each request. Code before get_response is pre-view, after is post-view. Register it in MIDDLEWARE.
Tests
from django.test import TestCase, Client
class PostTest(TestCase):
def setUp(self):
self.post = Post.objects.create(title="Test")
def test_list(self):
response = self.client.get("/blog/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Test")
def test_create(self):
self.client.post("/blog/new/", {"title": "New"})
self.assertTrue(Post.objects.filter(title="New").exists())TestCase creates an isolated test DB. self.client simulates requests. assertEqual, assertContains check responses. setUp() prepares data. Run with manage.py test.
WSGI / ASGI
# Gunicorn (WSGI): gunicorn my_site.wsgi:application --bind 0.0.0.0:8000 --workers 4 # Uvicorn (ASGI): uvicorn my_site.asgi:application --host 0.0.0.0 --port 8000 # Nginx the a reverse proxy in front
Gunicorn serves Django in production (WSGI). Uvicorn for async (ASGI). --workers sets the processes. Always use Nginx the a reverse proxy. Never use runserver in production.
Signals
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Post)
def notify(sender, instance, created, **kwargs):
if created:
send_notification(instance)post_save fires after save. created indicates if it is new. sender filters by model. Also: pre_save, post_delete. Register in apps.py ready(). Decouples logic.
Logging
import logging
logger = logging.getLogger(__name__)
def my_view(request):
logger.info("Accessed the page", extra={"user": request.user.id})
logger.error("Error processing", exc_info=True)
# settings.py LOGGING = {...}Stdlib logging with Django config. extra adds context. exc_info=True includes the traceback. Configure handlers in LOGGING. Essential for debugging in production.
Docker
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN python manage.py collectstatic --noinput EXPOSE 8000 CMD ["gunicorn", "my_site.wsgi", "--bind", "0.0.0.0:8000"]
python:3.12-slim is a light image. collectstatic at build time. gunicorn the CMD. Use docker-compose with PostgreSQL and Redis. Multi-stage to optimize size.
Cache
from django.core.cache import cache
from django.views.decorators.cache import cache_page
cache.set("key", value, timeout=3600)
result = cache.get("key")
cache.delete("key")
@cache_page(60 * 15)
def post_list(request): ...cache is the low-level API (Redis, Memcached). set()/get() with TTL. @cache_page caches the whole view. Configure CACHES in settings. Essential for performance.
Context Processors
# context_processors.py
def categories(request):
return {"menu_categories": Category.objects.all()}
# settings.py:
TEMPLATES = [{
"OPTIONS": {
"context_processors": [..., "app.context_processors.categories"],
},
}]context_processors inject variables into all templates. They return a dictionary. Ideal for menus, counters and global data. Register in TEMPLATES settings.
Management and Fixtures
# export data: python manage.py dumpdata blog > backup.json # import: python manage.py loaddata backup.json # shell with data: python manage.py shell -c "from blog.models import *; print(Post.objects.count())"
dumpdata exports to JSON (fixtures). loaddata imports. Useful for seeding and data migration. shell -c runs one-liners. Combine with custom commands.
Celery (async tasks)
# tasks.py
from celery import shared_task
@shared_task
def send_email(to, subject, body):
# slow task
send_mail(subject, body, "noreply@site.com", [to])
# in the view:
send_email.delay("ana@site.com", "Hello", "Body")@shared_task defines a Celery task. .delay() sends it to the queue. Runs in a separate worker. Requires a broker (Redis/RabbitMQ). Ideal for emails, reports and heavy processing.
Deploy Checklist
DEBUG = False ALLOWED_HOSTS = ["domain.com"] SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 X_FRAME_OPTIONS = "DENY"
DEBUG=False in production. SECURE_SSL_REDIRECT forces HTTPS. HSTS prevents downgrade. X_FRAME_OPTIONS avoids clickjacking. Run manage.py check --deploy.
Best Practices
# 1. Fat models, thin views # 2. CBVs for CRUD, FBVs for custom logic # 3. select_related/prefetch_related always # 4. Tests for every feature # 5. Settings via env vars
Business logic in models and services, not in views. Use CBVs for standard CRUD. select_related avoids N+1. Tests with TestCase. Config via python-decouple.
Authentication and Permissions
Login and Logout
from django.contrib.auth import login, logout, authenticate
user = authenticate(request, username="ana", password="123")
if user:
login(request, user)
logout(request)authenticate() checks credentials. login() creates the session. logout() destroys the session. Django handles cookies and CSRF automatically. Ready-made views in django.contrib.auth.views.
Permissions
from django.contrib.auth.decorators import permission_required
@permission_required("blog.add_post", raise_exception=True)
def create(request): ...
# check manually:
request.user.has_perm("blog.delete_post")
request.user.has_perms(["blog.add_post", "blog.change_post"])Permissions follow the app.action_model pattern. raise_exception=True returns 403 instead of a redirect. has_perm() checks programmatically. Created automatically per model (add, change, delete, view).
Password Hashing
from django.contrib.auth.hashers import make_password, check_password
hashed = make_password("password123")
check_password("password123", hashed) # True
# settings.py:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.Argon2PasswordHasher",
]make_password() generates a salted hash. check_password() verifies. Django uses PBKDF2 by default. Argon2 is more secure (install argon2-cffi). Never store plain text.
Ready-made Auth Views
from django.contrib.auth import views as auth_views
urlpatterns = [
path("login/", auth_views.LoginView.as_view(template_name="login.html"), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
path("password_reset/", auth_views.PasswordResetView.as_view(), name="pw_reset"),
]LoginView and LogoutView are ready-made CBVs. PasswordResetView sends a reset email. Includes validation and security. Just create the templates. Saves a lot of code.
Groups
from django.contrib.auth.models import Group, Permission
editors = Group.objects.create(name="Editors")
perm = Permission.objects.get(codename="add_post")
editors.permissions.add(perm)
user.groups.add(editors)
user.has_perm("blog.add_post") # True via groupGroup bundles permissions for multiple users. permissions.add() assigns permissions to the group. user.groups.add() adds the user to the group. More scalable than individual permissions.
Flash Messages
from django.contrib import messages
messages.success(request, "Post created!")
messages.error(request, "Error saving")
messages.warning(request, "Warning!")
# in the template:
{% for message in messages %}
<div class="alert {{ message.tags }}">{{ message }}</div>
{% endfor %}messages shows one-time notifications. Tags: debug, info, success, warning, error. Stored in the session. They appear on the next request. Standard for post-action feedback.
Check the User
request.user.is_authenticated
request.user.username
request.user.email
request.user.is_staff
request.user.is_superuser
# in the template:
{% if user.is_authenticated %}
Hello, {{ user.username }}
{% endif %}request.user is the current user (or AnonymousUser). is_authenticated checks login. is_staff and is_superuser for permissions. Available in all views and templates.
Custom User Model
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True)
# settings.py:
AUTH_USER_MODEL = "accounts.User"AbstractUser extends the default user with extra fields. AUTH_USER_MODEL must be set before the first migration. Alternative: AbstractBaseUser for full control. Always go custom from the start.
CSRF Protection
# enabled by default (MIDDLEWARE)
# in the template:
{% csrf_token %}
# for AJAX:
headers = {"X-CSRFToken": "{{ csrf_token }}"}
# exempt (careful):
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request): ...csrf_token protects against cross-site request forgery. The middleware validates POST automatically. For AJAX send the token in the header. @csrf_exempt disables it (only for webhooks).
login_required
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
@login_required(login_url="/login/")
def profile(request): ...
class ProfileView(LoginRequiredMixin, TemplateView):
login_url = "/login/"
redirect_field_name = "next"@login_required protects FBVs. LoginRequiredMixin protects CBVs. Redirects to login with ?next=. After login, returns to the original page. Essential for private areas.
Auth Signals
from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.dispatch import receiver
@receiver(user_logged_in)
def on_login(sender, request, user, **kwargs):
user.last_ip = request.META.get("REMOTE_ADDR")
user.save()user_logged_in fires after login. user_login_failed on failed attempts. @receiver registers the handler. Useful for logging, auditing and profile updates.
API REST (DRF)
Install DRF
pip install djangorestframework
# settings.py:
INSTALLED_APPS = [..., "rest_framework"]
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}djangorestframework is the DRF package. Register it in INSTALLED_APPS. REST_FRAMEWORK configures global defaults. Pagination, authentication and permissions are configurable.
Routers
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
router.register("users", UserViewSet)
urlpatterns = [
path("api/", include(router.urls)),
]DefaultRouter generates REST URLs automatically. Creates list, detail and root. basename for URL names. Includes the browsable API at the root. Standard for APIs with ViewSets.
Filters and Search
# pip install django-filter
REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
}
# usage: /api/posts/?active=true&search=django&ordering=-createdDjangoFilterBackend filters by exact fields. SearchFilter searches in search_fields. OrderingFilter orders by ordering_fields. Combine all three for flexible APIs.
Serializers
from rest_framework import serializers
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = Post
fields = ["id", "title", "content", "author_name", "created"]
read_only_fields = ["created"]ModelSerializer generates a serializer from the model. fields controls the output. source accesses related fields. read_only prevents writes. Automatic validation included.
Authentication (tokens)
# settings.py:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
}
# get a token:
# POST /api-token-auth/ {"username": "ana", "password": "123"}TokenAuthentication uses the Authorization: Token xxx header. SessionAuthentication for the browser. For JWT use SimpleJWT. Configure per view or globally.
Custom Validation (DRF)
class PostSerializer(serializers.ModelSerializer):
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Minimum 5 characters")
return value
def validate(self, data):
if data.get("publish_date") and data["publish_date"] < timezone.now():
raise serializers.ValidationError("Date in the past")
return datavalidate_field() validates a specific field. validate() for multi-field. serializers.ValidationError returns 400 with details. Same pattern the Django Forms.
APIView
from rest_framework.views import APIView
from rest_framework.response import Response
class PostList(APIView):
def get(self, request):
posts = Post.objects.all()
data = PostSerializer(posts, many=True).data
return Response(data)
def post(self, request):
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)APIView is DRF's base view. Methods per HTTP verb. many=True for lists. request.data parses JSON/form. Response negotiates the format (JSON/HTML).
DRF Permissions
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.permissions import BasePermission
class IsAuthor(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.author == request.user
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsAuthor]IsAuthenticated requires login. BasePermission creates custom permissions. has_object_permission() checks per object. Combine several with a list (AND).
Nested Serializers
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "text", "author"]
class PostSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True, read_only=True)
total_comments = serializers.SerializerMethodField()
def get_total_comments(self, obj):
return obj.comments.count()SerializerMethodField adds computed fields. Nested serializers include relations. many=True for lists. read_only for derived data. Controls response depth.
ViewSets
from rest_framework import viewsets
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filterset_fields = ["active", "author"]
search_fields = ["title"]
ordering_fields = ["created", "title"]ModelViewSet generates full CRUD (list, create, retrieve, update, destroy). queryset and serializer_class are required. Reduces hundreds of lines to a single class.
Pagination
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
class PostPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "size"
max_page_size = 100
class PostViewSet(viewsets.ModelViewSet):
pagination_class = PostPaginationPageNumberPagination uses ?page=2. LimitOffsetPagination uses ?limit=10&offset=20. page_size_query_param lets the client control it. The response includes count and next/previous.
Throttling
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
},
}AnonRateThrottle limits anonymous users. UserRateThrottle limits authenticated ones. DEFAULT_THROTTLE_RATES sets the limits. Returns 429 when exceeded. Essential for public APIs.