Шпаргалка по Django: модели, views, ORM, админка
Django — «фреймворк для перфекционистов с дедлайнами». Полноценный веб-фреймворк: ORM, админка, формы, аутентификация, шаблоны — всё из коробки. Под капотом синхронный, но с Django 4+ есть частичная поддержка async.
Быстрый старт
Установка Django, создание проекта и приложения, запуск dev-сервера. startproject создаёт каркас с настройками и корневыми маршрутами. startapp добавляет модуль с моделями, views и админкой. migrate применяет миграции БД.
pip install django
django-admin startproject mysite .
python manage.py startapp blog
python manage.py runserver # http://127.0.0.1:8000
python manage.py migrate # применить миграции
python manage.py createsuperuser # админ: /admin
Структура проекта
mysite/
manage.py
mysite/
__init__.py
settings.py # настройки
urls.py # корневые маршруты
wsgi.py / asgi.py
blog/
models.py # модели
views.py # представления
urls.py # маршруты приложения
admin.py # регистрация в админке
templates/ # шаблоны
Модели и ORM
Определение моделей
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Автор'
verbose_name_plural = 'Авторы'
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Черновик'),
('published', 'Опубликовано'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='posts')
tags = models.ManyToManyField('Tag', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [models.Index(fields=['status', '-created_at'])]
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
# После изменения моделей:
# python manage.py makemigrations
# python manage.py migrate
Основные типы полей
# Строки
models.CharField(max_length=200)
models.TextField()
models.SlugField()
# Числа
models.IntegerField()
models.FloatField()
models.DecimalField(max_digits=10, decimal_places=2)
models.PositiveIntegerField()
# Даты
models.DateField(auto_now_add=True)
models.DateTimeField(auto_now=True)
models.TimeField()
# Логика
models.BooleanField(default=True)
models.NullBooleanField() # True/False/None
# Файлы
models.FileField(upload_to='uploads/')
models.ImageField(upload_to='images/')
# Email/URL
models.EmailField()
models.URLField()
# Связи
models.ForeignKey(Other, on_delete=models.CASCADE)
models.OneToOneField(Other, on_delete=models.CASCADE)
models.ManyToManyField(Other)
# on_delete варианты
# CASCADE — удалить связанные
# PROTECT — запретить удаление
# SET_NULL — установить NULL
# SET_DEFAULT — установить значение по умолчанию
# DO_NOTHING — ничего не делать
ORM — запросы
from django.db.models import Q, Count, Avg, Sum, F
# Создание
author = Author.objects.create(name="Пушкин", email="pushkin@ex.com")
post = Post(title="Руслан и Людмила", author=author, content="...")
post.save()
# Чтение
Post.objects.all() # все записи
Post.objects.filter(status='published') # WHERE status = 'published'
Post.objects.exclude(status='draft') # WHERE NOT
Post.objects.get(id=1) # одна запись (DoesNotExist если нет)
Post.objects.get_or_create(title="Test") # найти или создать
Post.objects.first() # первая
Post.objects.last() # последняя
Post.objects.count() # количество
# Фильтрация
Post.objects.filter(title__contains='Django') # LIKE '%Django%'
Post.objects.filter(title__icontains='django') # ILIKE (без учёта регистра)
Post.objects.filter(title__startswith='How') # LIKE 'How%'
Post.objects.filter(created_at__year=2026) # по году
Post.objects.filter(created_at__gte='2026-01-01') # >= дата
Post.objects.filter(id__in=[1, 2, 3]) # IN
Post.objects.filter(id__range=(1, 100)) # BETWEEN
Post.objects.filter(content__isnull=True) # IS NULL
# Q-объекты — сложные условия
Post.objects.filter(Q(status='published') & Q(author__name='Пушкин'))
Post.objects.filter(Q(status='published') | Q(status='draft'))
Post.objects.filter(~Q(status='deleted')) # NOT
# F-выражения — операции с полями
Post.objects.update(view_count=F('view_count') + 1) # атомарный инкремент
Post.objects.filter(author=F('editor')) # сравнение полей
# Сортировка
Post.objects.order_by('created_at') # ASC
Post.objects.order_by('-created_at') # DESC
Post.objects.order_by('status', '-created_at') # по двум полям
# Агрегация
Post.objects.aggregate(Avg('rating')) # {'rating__avg': 4.2}
Post.objects.aggregate(Count('id'), Sum('views'))
# Группировка
Author.objects.annotate(post_count=Count('posts'))
Author.objects.annotate(avg_rating=Avg('posts__rating'))
# Связанные объекты
author = Author.objects.get(id=1)
author.posts.all() # related_name='posts'
author.posts.filter(status='published') # фильтрация связанных
post = Post.objects.get(id=1)
post.author.name # прямой доступ к FK
# select_related / prefetch_related — оптимизация N+1
Post.objects.select_related('author').all() # JOIN для FK/OneToOne
Post.objects.prefetch_related('tags').all() # доп. запрос для M2M
# Срезы и пагинация
Post.objects.all()[:10] # первые 10
Post.objects.all()[10:20] # с 11 по 20
Views и URL-маршруты
Views обрабатывают HTTP-запросы и возвращают ответы. Function-based views проще для начала, class-based views (ListView, DetailView, CreateView) дают готовую функциональность из коробки. URL-маршруты связывают path с view через urlpatterns.
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'), # /
path('posts/', views.post_list, name='post_list'), # /posts/
path('posts/<int:pk>/', views.post_detail, name='detail'), # /posts/1/
path('posts/<slug:slug>/', views.post_by_slug),
path('posts/<int:year>/<int:month>/', views.archive),
]
# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse
from .models import Post
# Function-based view
def post_list(request):
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {'posts': posts})
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk, status='published')
return render(request, 'blog/post_detail.html', {'post': post})
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
# Class-based views (CBV)
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
queryset = Post.objects.filter(status='published')
paginate_by = 10
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
class PostCreateView(CreateView):
model = Post
fields = ['title', 'content', 'status', 'tags']
template_name = 'blog/post_form.html'
success_url = reverse_lazy('post_list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
# CBV: ключевые методы для переопределения
# ListView: get_queryset(), get_context_data()
# DetailView: get_object(), get_context_data()
# CreateView: form_valid(), get_form(), get_form_kwargs()
# UpdateView: get_object(), form_valid(), get_initial()
# DeleteView: get_object(), get_success_url()
# TemplateView: get_context_data()
# Все CBV: dispatch() — точка входа для всех HTTP-методов
# CBV-декораторы через @method_decorator
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
@method_decorator(cache_page(60), name='dispatch')
class PostListView(ListView):
...
Формы
Django Forms берут на себя генерацию HTML-формы и валидацию данных. ModelForm автоматически создаёт форму на основе модели. Валидация: clean_<field>() для одного поля, clean() для перекрёстной проверки нескольких полей.
from django import forms
from .models import Post
# Форма из модели
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'status', 'tags']
widgets = {
'content': forms.Textarea(attrs={'rows': 10}),
'tags': forms.CheckboxSelectMultiple(),
}
# Валидация
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 10:
raise forms.ValidationError("Заголовок слишком короткий")
return title
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
title = cleaned_data.get('title')
if status == 'published' and len(title) < 20:
raise forms.ValidationError(
"Для публикации заголовок должен быть не менее 20 символов"
)
return cleaned_data
# Обычная форма (не из модели)
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
cc_myself = forms.BooleanField(required=False)
Шаблоны
Django Template Language: {% extends %} для наследования, {% block %} для переопределения, {% include %} для вставки фрагментов. Фильтры (|date, |truncatechars) форматируют значения. {% url %} генерирует URL по имени маршрута — не хардкодьте ссылки.
<!-- base.html -->
<!DOCTYPE html>
<html>
<body>
{% include 'includes/header.html' %}
<main>
{% block content %}{% endblock %}
</main>
{% include 'includes/footer.html' %}
</body>
</html>
<!-- post_list.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Посты</h1>
{% for post in posts %}
<article>
<h2><a href="{% url 'detail' post.pk %}">{{ post.title }}</a></h2>
<p>{{ post.content|truncatewords:30 }}</p>
<small>{{ post.author.name }} — {{ post.created_at|date:"d.m.Y" }}</small>
</article>
{% empty %}
<p>Постов пока нет</p>
{% endfor %}
<!-- Пагинация -->
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">← Назад</a>
{% endif %}
<span>Стр. {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Вперёд →</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
<!-- Фильтры -->
{{ value|default:"ничего" }}
{{ name|lower }}
{{ text|truncatechars:100 }}
{{ posts|length }}
{{ created_at|date:"d.m.Y H:i" }}
{{ content|linebreaks }}
{{ html_content|safe }} <!-- без экранирования -->
Админка
Автоматически сгенерированный интерфейс управления данными. Регистрируете модель через @admin.register() — получаете CRUD с поиском, фильтрами, пагинацией. Настраивается: list_display (колонки), list_filter (фильтры), search_fields, prepopulated_fields.
# admin.py
from django.contrib import admin
from .models import Post, Author, Tag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'status', 'author', 'created_at']
list_filter = ['status', 'created_at', 'tags']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'created_at'
ordering = ['-created_at']
list_per_page = 25
# Действия
actions = ['make_published', 'make_draft']
@admin.action(description='Опубликовать выбранные')
def make_published(self, request, queryset):
queryset.update(status='published')
@admin.action(description='В черновик')
def make_draft(self, request, queryset):
queryset.update(status='draft')
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
list_display = ['name', 'email', 'post_count']
search_fields = ['name']
def post_count(self, obj):
return obj.posts.count()
post_count.short_description = 'Постов'
Middleware
Цепочка обработчиков, через которую проходит каждый запрос ДО view и каждый ответ ПОСЛЕ view. Стандартные middleware: сессии, аутентификация, CSRF-защита, безопасность. Кастомный middleware добавляет заголовки, логирует запросы, измеряет время обработки.
# middleware.py
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Код ДО view
response = self.get_response(request)
# Код ПОСЛЕ view
response['X-Custom-Header'] = 'MyValue'
return response
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'myapp.middleware.SimpleMiddleware', # кастомный
]
Аутентификация
Django включает готовую систему пользователей: вход/выход, права доступа, группы. @login_required защищает function-based view, LoginRequiredMixin — class-based. user.is_authenticated в шаблоне проверяет, залогинен ли пользователь.
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
# Вход
def login_view(request):
user = authenticate(request, username=username, password=password)
if user:
login(request, user)
return redirect('index')
# Выход
def logout_view(request):
logout(request)
return redirect('index')
# Защита view (function-based)
@login_required
def profile(request):
return render(request, 'profile.html')
# Защита view (class-based)
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'profile.html'
# Проверка в шаблоне
{% if user.is_authenticated %}
Привет, {{ user.username }}!
{% endif %}
# Группы и права
user.groups.add(group)
user.has_perm('app.change_model')
request.user.is_staff # доступ в админку
request.user.is_superuser # суперпользователь
# PermissionRequiredMixin (CBV)
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
class AdminView(PermissionRequiredMixin, TemplateView):
permission_required = 'app.can_view_admin'
class OwnerOnlyView(UserPassesTestMixin, UpdateView):
def test_func(self):
return self.get_object().author == self.request.user
# Кастомная модель пользователя — всегда используйте get_user_model()
from django.contrib.auth import get_user_model
User = get_user_model() # НЕ: from django.contrib.auth.models import User
# settings.py: AUTH_USER_MODEL = 'myapp.CustomUser'
# Задавайте ДО первой миграции!
Django REST Framework (DRF)
Добавляет REST API поверх Django-моделей. ModelSerializer автоматически создаёт сериализатор по модели. ModelViewSet даёт полный CRUD (list, create, retrieve, update, delete) одним классом. DefaultRouter генерирует URL-маршруты автоматически.
# pip install djangorestframework
from rest_framework import serializers, viewsets, routers
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# Сериализатор
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.ReadOnlyField(source='author.name')
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'status', 'author_name', 'created_at']
# ViewSet
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.filter(status='published')
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'posts', PostViewSet)
urlpatterns += router.urls
# Endpoints:
# GET /posts/ — список
# POST /posts/ — создать
# GET /posts/{id}/ — получить
# PUT /posts/{id}/ — обновить
# PATCH /posts/{id}/ — частично обновить
# DELETE /posts/{id}/ — удалить
Сигналы
Позволяют коду реагировать на события в других частях приложения без жёсткой связи. @receiver(post_save, sender=Model) — выполнить функцию после сохранения модели. Используйте умеренно: сигналы делают поток управления неочевидным.
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
@receiver(post_save, sender=Post)
def notify_on_publish(sender, instance, created, **kwargs):
if instance.status == 'published':
print(f"Опубликован пост: {instance.title}")
@receiver(pre_delete, sender=Post)
def cleanup_on_delete(sender, instance, **kwargs):
instance.tags.clear() # очистить связи перед удалением
# Регистрация: signals.py + ready() в AppConfig
# ⚠️ Антипаттерн: не используйте сигналы там, где можно вызвать функцию напрямую.
# Сигналы — только для межприложенческого взаимодействия или переопределяемых действий.
Транзакции
Атомарные операции: либо все изменения сохраняются, либо ни одно. Критично для операций, затрагивающих несколько объектов или баланс/счётчики.
from django.db import transaction
# Декоратор
@transaction.atomic
def create_order(request):
order = Order.objects.create(user=request.user)
OrderItem.objects.create(order=order, product=product, quantity=qty)
product.stock -= qty
product.save()
# Контекстный менеджер
def transfer_money(from_acc, to_acc, amount):
with transaction.atomic():
from_acc.balance -= amount
from_acc.save()
to_acc.balance += amount
to_acc.save()
# Обработка IntegrityError внутри atomic
from django.db import IntegrityError
@transaction.atomic
def create_unique_item(request):
parent = Parent.objects.create()
try:
with transaction.atomic():
Child.objects.create(parent=parent, name="unique_name")
except IntegrityError:
parent.delete()
raise
M2M: операции со связями «многие ко многим»
API ManyToManyField отличается от ForeignKey: метод .add()/.set()/.remove()/.clear() вместо прямого присваивания.
post = Post.objects.get(id=1)
tag1, tag2, tag3 = Tag.objects.filter(id__in=[1, 2, 3])
# ForeignKey — прямое присваивание
post.author = author
post.save()
# ManyToManyField — особый API
post.tags.add(tag1) # добавить один
post.tags.add(tag1, tag2, tag3) # добавить несколько
post.tags.set([tag1, tag2]) # заменить весь набор
post.tags.remove(tag1) # убрать один
post.tags.clear() # убрать все
post.tags.create(name="новый тег") # создать и связать
# через related_name
author.posts.add(post) # author.posts — related_name из FK
post.tags.filter(name__startswith='Django') # фильтрация связанных
Полезные команды manage.py
manage.py — точка входа для всех операций с Django-проектом. Миграции (makemigrations/migrate), интерактивная консоль (shell), тесты (test), экспорт/импорт данных (dumpdata/loaddata), сбор статики (collectstatic).
python manage.py startapp <name> # новое приложение
python manage.py makemigrations # создать миграции
python manage.py migrate # применить миграции
python manage.py showmigrations # статус миграций
python manage.py sqlmigrate app 0001 # SQL миграции
python manage.py shell # интерактивная консоль
python manage.py shell_plus # + автоимпорт (django-extensions)
python manage.py dbshell # консоль БД
python manage.py createsuperuser # суперпользователь
python manage.py changepassword user # сменить пароль
python manage.py collectstatic # собрать статику
python manage.py test # запустить тесты
python manage.py dumpdata app.Model > data.json # экспорт данных
python manage.py loaddata data.json # импорт данных
python manage.py runserver 0.0.0.0:8000 # запуск на всех интерфейсах
Статика и медиа-файлы
Критическое различие: в режиме разработки (DEBUG=True) Django раздаёт статику сам, в продакшене (DEBUG=False) — нет. Статика (CSS, JS, картинки проекта) собирается collectstatic. Медиа (загруженные пользователем файлы) хранятся отдельно.
# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' # сюда collectstatic всё копирует
STATICFILES_DIRS = [BASE_DIR / 'static'] # дополнительные папки со статикой
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media' # загруженные пользователем файлы
# В шаблоне
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'img/logo.png' %}">
# Для медиа в development: добавить в корневой urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [...]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
python manage.py collectstatic # собрать статику для продакшена
python manage.py collectstatic --clear # очистить STATIC_ROOT и собрать заново
python manage.py collectstatic --dry-run # посмотреть, что будет скопировано
python manage.py findstatic css/style.css # найти, откуда берётся статический файл
Settings и переменные окружения
Никогда не храните секреты в коде. Используйте переменные окружения для SECRET_KEY, DEBUG, паролей БД, API-ключей.
# settings.py — безопасный подход
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # обязательно!
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Разделение настроек по окружениям (settings/base.py, dev.py, prod.py)
# settings/__init__.py
import os
ENVIRONMENT = os.environ.get('DJANGO_ENV', 'development')
if ENVIRONMENT == 'production':
from .production import *
else:
from .development import *
Ключевые настройки, которые нужно знать:
| Настройка | Назначение |
|---|---|
SECRET_KEY |
Криптографическая подпись (сессии, CSRF, токены) |
DEBUG |
Режим отладки: всегда False в продакшене |
ALLOWED_HOSTS |
Список доменов/ip, которые может обслуживать Django |
INSTALLED_APPS |
Список приложений (не забывайте добавлять новые!) |
MIDDLEWARE |
Цепочка middleware (порядок важен!) |
DATABASES |
Конфигурация БД (SQLite, PostgreSQL, MySQL) |
TIME_ZONE |
Часовой пояс ('Europe/Moscow') |
USE_TZ |
True — использовать timezone-aware datetime (рекомендуется) |
LANGUAGE_CODE |
Язык по умолчанию ('ru-ru') |
AUTH_USER_MODEL |
Кастомная модель пользователя (задать ДО manage.py migrate) |
Частые ошибки
| Ошибка | Правильно |
|---|---|
Забыт on_delete у ForeignKey |
Явно указать on_delete=models.CASCADE |
| QuerySet ленивый — не выполняется | Обернуть в list() или итерировать |
| N+1 запросов в цикле | select_related() / prefetch_related() |
null=True для CharField/TextField |
Использовать blank=True для форм (или оба) |
migrate при конфликтах |
makemigrations --merge или удалить дубли |
settings.DEBUG = True в продакшене |
DEBUG = os.environ.get('DEBUG', 'False') == 'True' |
Забыт {% csrf_token %} в форме |
Всегда внутри <form method="post"> |
Не добавлено приложение в INSTALLED_APPS |
Добавить 'app.apps.AppConfig' в settings |
Model.objects.get() падает с DoesNotExist |
Использовать get_object_or_404() или try/except |
datetime.now() вместо timezone-aware |
from django.utils import timezone; timezone.now() |
| Статика не грузится в продакшене | collectstatic + nginx, Django не раздаёт статику при DEBUG=False |
| FK-присваивание для M2M-полей | post.tags.add(tag), а не post.tags = tag |
Создание формы без instance=obj при редактировании |
PostForm(instance=post) для предзаполнения |
| Сигналы для кода внутри одного модуля | Вызвать функцию напрямую, сигналы — для межприложенческого |
Ресурсы
- Django Project — официальная документация, туториалы, новости
- Django на GitHub — исходный код
- Django Packages — реестр reusable-приложений и утилит
- Classy Class-Based Views — полная справка по CBV с методами и атрибутами
- Django REST Framework — документация DRF
- Two Scoops of Django — бестселлер по best practices
- Django Debug Toolbar — панель отладки (SQL-запросы, кэш, шаблоны)
- django-extensions —
shell_plus,runserver_plus,graph_models
Что дальше
- Шпаргалка по Python: от основ до профи
- Шпаргалка по FastAPI: REST API, Pydantic, WebSocket
- Шпаргалка по параллелизму: потоки, процессы, asyncio
- SQL шпаргалка для начинающих
Связанные шпаргалки: Python, FastAPI, Параллелизм, SQL для начинающих