Кладовочка

О разном, чтобы было под рукой онлайн

Шпаргалка по 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) для предзаполнения
Сигналы для кода внутри одного модуля Вызвать функцию напрямую, сигналы — для межприложенческого

Ресурсы

Что дальше


Связанные шпаргалки: Python, FastAPI, Параллелизм, SQL для начинающих