Django-Admin.ru

URL в Django: path, reverse, статика и медиа

Как устроены URL в Django: urls.py и include, path и конвертеры, reverse и тег url, отправка POST из формы, STATIC_URL и MEDIA_URL, разбор NoReverseMatch и 404.

Редакция Django-Admin.ru
· 17 мин чтения

Содержание · 11 разделов
  1. Как Django выбирает view по URL
  2. urls.py проекта и приложений
  3. path() и конвертеры
  4. Встроенные конвертеры
  5. Свой конвертер
  6. re_path: регулярные выражения
  7. Порядок маршрутов
  8. Имена маршрутов и пространства имён
  9. reverse, reverse_lazy и тег url: обратное построение URL
  10. reverse() во view
  11. reverse_lazy() в атрибутах классов
  12. Тег url в шаблоне
  13. get_absolute_url: канонический адрес объекта
  14. POST-запрос на URL: форма, action и CSRF
  15. Статика: STATIC_URL, STATICFILES_DIRS и STATIC_ROOT
  16. Статика в разработке и в продакшне
  17. Медиа: MEDIA_URL и MEDIA_ROOT
  18. Раздача медиа в режиме разработки
  19. Статика и медиа: в чём разница
  20. Частые ошибки с URL в Django
  21. NoReverseMatch
  22. 404 на статику или медиа
  23. RuntimeError: You called this URL via POST, but the URL doesn’t end in a slash
  24. 403 CSRF verification failed
  25. Страница отдаёт 404, хотя маршрут есть
  26. Частые вопросы
  27. Как посмотреть все URL проекта?
  28. Как получить текущий URL и имя маршрута?
  29. Как передать в URL GET-параметры?
  30. Как настроить редирект со старого адреса на новый?
  31. Как сделать свою страницу 404?
  32. Как поменять адрес админки?
  33. Итог

URL в Django задаются в файле urls.py: список urlpatterns сопоставляет адрес с view через функцию path(), а каждому маршруту дают имя name. По имени адрес строят обратно — функцией reverse() в Python и тегом {% url %} в шаблоне, поэтому ссылки не приходится писать руками. Статические файлы доступны по префиксу STATIC_URL, загруженные пользователями — по MEDIA_URL, и в режиме разработки медиа подключают к маршрутам через static().

# blog/urls.py
from django.urls import path

from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('<slug:slug>/', views.post_detail, name='post_detail'),
]
from django.urls import reverse

reverse('blog:post_detail', kwargs={'slug': 'hello'})  # → '/blog/hello/'

Ниже разберём маршруты проекта и приложений, конвертеры, пространства имён, отправку POST-формы на нужный URL, настройку статики и медиа и типичные ошибки. Примеры проверены на Django 6.1 в виртуальном окружении; отличия от 5.2 LTS отмечены в тексте.

Как Django выбирает view по URL

Когда приходит запрос, Django берёт корневой модуль маршрутов из настройки ROOT_URLCONF (после startproject это config.urls) и перебирает urlpatterns сверху вниз. Первый совпавший шаблон вызывает свой view, остальные уже не проверяются. Если ничего не совпало, Django отвечает 404, а при DEBUG = True показывает список маршрутов, которые он перебрал.

При сопоставлении учитывается только путь:

  • домен и строка запроса не участвуют: для https://example.com/blog/?page=2 Django сравнивает с шаблонами строку blog/, а page читают во view из request.GET;
  • HTTP-метод тоже не учитывается: GET и POST на один адрес попадают в один и тот же view, и различать их нужно внутри view;
  • ведущий слэш в шаблонах не пишут: path('blog/', ...), не path('/blog/', ...). Иначе manage.py check выдаст предупреждение urls.W002.

Весь путь запроса от сервера через middleware до view описан в руководстве по Django для начинающих.

urls.py проекта и приложений

В корневом urls.py держат только подключения: админку и маршруты приложений через include(). Маршруты конкретного раздела живут в urls.py своего приложения — этот файл startapp не создаёт, его добавляют вручную (подробно — в статье «Как создать приложение в Django»).

# config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
]

include() отрезает совпавшую часть адреса и передаёт остаток во вложенный модуль. Запрос /blog/hello/ сначала совпадает с префиксом blog/, затем строка hello/ сравнивается с шаблонами из blog/urls.py. Префикс задаётся в одном месте: поменяете 'blog/' на 'articles/' — изменятся все адреса приложения.

path() и конвертеры

Сигнатура функции — path(route, view, kwargs=None, name=None). В route части адреса в угловых скобках захватываются и передаются во view именованными аргументами: шаблон <int:pk> превратит /blog/42/ в вызов post_detail(request, pk=42).

Встроенные конвертеры

КонвертерЧто совпадаетТип во viewПример
strлюбая непустая строка без /str<str:username>
intноль или положительное целоеint<int:pk>
slugлатинские буквы, цифры, дефис и подчёркиваниеstr<slug:slug>
uuidUUID в нижнем регистре с дефисамиuuid.UUID<uuid:token>
pathнепустая строка, включая /str<path:file_path>

Запись без конвертера, например <username>, равна <str:username>. Если значение не подходит под конвертер, маршрут просто не совпадает: /blog/abc/ не попадёт в <int:pk>/, и Django проверит следующие шаблоны.

Важно: конвертер slug не принимает кириллицу. Для адреса вида /blog/привет/ нужен str или свой конвертер.

Свой конвертер

Когда встроенных не хватает, пишут класс с атрибутом regex и методами to_python() и to_url(). Конвертер года из четырёх цифр:

# blog/converters.py
class YearConverter:
    regex = r'[0-9]{4}'

    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return f'{value:04d}'
# blog/urls.py
from django.urls import path, register_converter

from . import converters, views

register_converter(converters.YearConverter, 'yyyy')

urlpatterns = [
    path('archive/<yyyy:year>/', views.archive, name='archive'),
]

/blog/archive/2026/ передаст во view year=2026 типа int, а /blog/archive/26/ вернёт 404. Метод to_url() работает при построении ссылок: reverse('blog:archive', args=[7]) даст /blog/archive/0007/. Если to_python() или to_url() выбрасывает ValueError, Django считает, что маршрут не совпал.

Начиная с Django 6.0 register_converter() не даёт переопределить уже зарегистрированное имя: попытка зарегистрировать свой класс как int завершится ошибкой ValueError: Converter 'int' is already registered.

re_path: регулярные выражения

Для сложных шаблонов есть re_path() — он принимает регулярное выражение с именованными группами:

from django.urls import re_path

urlpatterns = [
    re_path(
        r'^archive/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$',
        views.archive_month,
        name='archive_month',
    ),
]

Группы из re_path() всегда приходят во view строками — '2026' и '05', без преобразования в числа. Если задачу решает path() со своим конвертером, выбирайте его: шаблон короче, а значение приходит нужного типа.

Порядок маршрутов

Общие шаблоны ставьте ниже конкретных. В таком списке адрес /blog/subscribe/ никогда не дойдёт до своего view: его раньше перехватит <slug:slug>/, потому что subscribe — допустимый slug.

urlpatterns = [
    path('<slug:slug>/', views.post_detail, name='post_detail'),
    path('subscribe/', views.subscribe, name='subscribe'),  # недостижим
]

Правильный порядок — сначала subscribe/, затем <slug:slug>/. То же касается «ловушек» вида <path:page>/: поставленные выше static(), они перехватят запросы к /media/.

Имена маршрутов и пространства имён

Аргумент name даёт маршруту имя, по которому строят ссылки. В разных приложениях часто встречаются одинаковые имена — index, detail, create, — поэтому маршруты приложения помещают в пространство имён (namespace) атрибутом app_name в его urls.py:

# blog/urls.py
app_name = 'blog'

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('subscribe/', views.subscribe, name='subscribe'),
    path('archive/<yyyy:year>/', views.archive, name='archive'),
    path('<slug:slug>/', views.post_detail, name='post_detail'),
]

Теперь к маршрутам обращаются как 'blog:post_list' и 'blog:post_detail'. Без префикса reverse('post_list') вызовет NoReverseMatch.

Аргумент namespace в include('blog.urls', namespace='news') задаёт имя экземпляра: так одно приложение подключают дважды под разными префиксами. Без app_name во вложенном модуле Django откажется запускаться с ошибкой ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported.

reverse, reverse_lazy и тег url: обратное построение URL

Обратное построение (reverse) — получение адреса по имени маршрута и аргументам. Оно защищает от битых ссылок: адрес меняют в urls.py, а ссылки во view и шаблонах обновляются сами.

reverse() во view

from django.urls import reverse

reverse('blog:post_list')                                # → '/blog/'
reverse('blog:post_detail', args=['hello'])              # → '/blog/hello/'
reverse('blog:post_detail', kwargs={'slug': 'hello'})    # → '/blog/hello/'
reverse('blog:post_list', query={'page': 2, 'q': 'django url'}, fragment='top')
# → '/blog/?page=2&q=django+url#top'

args и kwargs вместе передавать нельзя. Параметры query и fragment появились в Django 5.2; в более старых версиях строку запроса склеивают вручную через urllib.parse.urlencode().

Для перенаправления удобнее redirect(): он принимает имя маршрута с аргументами и сам вызывает reverse().

from django.shortcuts import redirect

return redirect('blog:post_detail', slug=post.slug)

Полный адрес с протоколом и доменом — для писем, API, ссылок в мессенджерах — получают из запроса: request.build_absolute_uri(reverse('blog:post_detail', args=[post.slug])).

reverse_lazy() в атрибутах классов

reverse() нельзя вызывать при импорте модуля — в атрибутах class-based views, аргументах декораторов, настройках. В этот момент urls.py ещё не загружен, и Django падает с ошибкой:

django.core.exceptions.ImproperlyConfigured: The included URLconf 'config.urls' does not appear to have any patterns in it. If you see the 'urlpatterns' variable with valid patterns in the file then the issue is probably caused by a circular import.

Для таких мест есть reverse_lazy(): он вычисляет адрес при первом использовании.

from django.urls import reverse_lazy
from django.views.generic import CreateView

from .models import Post


class PostCreate(CreateView):
    model = Post
    fields = ['title', 'slug']
    success_url = reverse_lazy('blog:post_list')

Тег url в шаблоне

В шаблонах ссылки строят тегом {% url %}. Имя маршрута пишут в кавычках, аргументы — через пробел позиционно или по имени:

<a href="{% url 'blog:post_list' %}">Все посты</a>
<a href="{% url 'blog:post_detail' post.slug %}">{{ post.title }}</a>
<a href="{% url 'blog:post_detail' slug=post.slug %}">{{ post.title }}</a>

{% url 'blog:archive' 2026 as archive_url %}
<a href="{{ archive_url }}">Архив за 2026 год</a>

Форма as archive_url сохраняет адрес в переменную и не выбрасывает ошибку, если маршрут не найден, — переменная остаётся пустой. Остальные теги шаблонов разобраны в статье «Шаблоны Django».

get_absolute_url: канонический адрес объекта

Модели, у которых есть своя страница, получают метод get_absolute_url(). Он хранит правило построения адреса в одном месте:

# blog/models.py
from django.db import models
from django.urls import reverse


class Post(models.Model):
    title = models.CharField('Заголовок', max_length=200)
    slug = models.SlugField(unique=True)
    cover = models.ImageField('Обложка', upload_to='covers/', blank=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={'slug': self.slug})

Метод используют сам Django и сторонние пакеты:

  • в шаблоне: <a href="{{ post.get_absolute_url }}"> — метод вызывается без скобок;
  • redirect(post) перенаправляет на post.get_absolute_url();
  • CreateView и UpdateView без success_url после сохранения ведут на страницу объекта;
  • в админке на странице объекта появляется кнопка «Смотреть на сайте».

Поля моделей и Meta подробно описаны в статье «Модели Django».

POST-запрос на URL: форма, action и CSRF

Отправить POST на нужный адрес — значит указать его в атрибуте action формы. Адрес строят тегом {% url %}, а внутрь формы добавляют {% csrf_token %}:

<form method="post" action="{% url 'blog:subscribe' %}">
  {% csrf_token %}
  <input type="email" name="email" required>
  <button type="submit">Подписаться</button>
</form>

Браузер получит такой HTML:

<form method="post" action="/blog/subscribe/">
  <input type="hidden" name="csrfmiddlewaretoken" value="..." />
  <input type="email" name="email" required />
  <button type="submit">Подписаться</button>
</form>

View, который принимает только POST:

# blog/views.py
from django.shortcuts import redirect
from django.views.decorators.http import require_POST


@require_POST
def subscribe(request):
    email = request.POST.get('email', '')
    # ...сохранить подписку...
    return redirect('blog:post_list')

Что здесь важно:

  • Метод проверяет view. Маршрут один для всех методов. Декоратор @require_POST отвечает 405 Method Not Allowed на GET; если форма и обработчик живут в одном view, ветвятся по request.method == 'POST'.
  • Пустой action или отсутствие атрибута отправляет форму на текущий адрес страницы. Это обычный вариант, когда один view и показывает форму, и обрабатывает её.
  • Относительный action вида action="subscribe/" браузер склеит с адресом текущей страницы: со страницы /blog/form/ форма уйдёт на /blog/form/subscribe/. Пишите адрес через {% url %}.
  • Без {% csrf_token %} Django отклонит запрос с 403 Forbidden и сообщением CSRF verification failed. Request aborted. Защиту от CSRF (подделки межсайтовых запросов) выполняет CsrfViewMiddleware.
  • После успешного POST делайте redirect(). Если вернуть HTML сразу, обновление страницы в браузере отправит форму повторно.
  • Слэш в конце адреса обязателен, если маршрут объявлен со слэшем, — иначе POST упадёт, подробности в разделе об ошибках ниже.

Для загрузки файлов у формы должен быть атрибут enctype="multipart/form-data", а во view файлы приходят в request.FILES. Классы Form и ModelForm, валидацию и вывод ошибок разбирает статья «Формы в Django».

Статика: STATIC_URL, STATICFILES_DIRS и STATIC_ROOT

Статические файлы — CSS, JavaScript, шрифты и картинки оформления, которые вы кладёте в проект сами. За них отвечает приложение django.contrib.staticfiles (подключено в новом проекте) и три настройки:

# config/settings.py
STATIC_URL = 'static/'                      # префикс адреса: /static/css/site.css
STATICFILES_DIRS = [BASE_DIR / 'static']    # общие исходники проекта
STATIC_ROOT = BASE_DIR / 'staticfiles'      # сюда collectstatic собирает всё для продакшна
  • STATIC_URL есть в settings.py сразу после startproject. Значение без ведущего слэша ('static/') Django дополнит до /static/, а при развёртывании в подпапку — префиксом этой подпапки.
  • Файлы приложения кладут в blog/static/blog/ — их Django находит сам. Второй уровень blog/ нужен, чтобы одинаковые имена файлов из разных приложений не перекрывали друг друга.
  • STATICFILES_DIRS нужен для общих файлов вне приложений, например папки static/ в корне проекта.
  • STATIC_ROOT должен отличаться от папок из STATICFILES_DIRS, иначе manage.py check выдаст ошибку staticfiles.E002.

В шаблоне адрес строит тег static:

{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">
<link rel="stylesheet" href="/static/css/site.css" />

Статика в разработке и в продакшне

При DEBUG = True команда runserver раздаёт статику сама — дополнительные маршруты в urls.py для неё не нужны. Найти, откуда Django берёт конкретный файл, помогает findstatic:

python manage.py findstatic css/site.css
Found 'css/site.css' here:
  C:\projects\mysite\static\css\site.css

При DEBUG = False Django перестаёт отдавать статику, и адреса /static/... возвращают 404 — так задумано. В продакшне выполняют collectstatic, который копирует файлы всех приложений и STATICFILES_DIRS в STATIC_ROOT, а раздаёт эту папку Nginx или пакет WhiteNoise:

python manage.py collectstatic --noinput

Флаг runserver --insecure заставляет сервер разработки раздавать статику и при DEBUG = False. Он годится, чтобы проверить страницу ошибки 404 локально, но не для боевого сервера. Настройка Nginx для /static/ описана в статье «Деплой Django: Gunicorn и Nginx».

Медиа: MEDIA_URL и MEDIA_ROOT

Медиафайлы — то, что загружают пользователи и администраторы: аватары, обложки, документы. Их адресом и папкой управляют две настройки:

# config/settings.py
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'

Поле FileField или ImageField сохраняет файл в MEDIA_ROOT с учётом upload_to, а в базе хранит только относительный путь. Для ImageField нужен пакет Pillow (python -m pip install pillow). Адрес файла даёт атрибут .url:

post.cover.name  # → 'covers/cat.jpg'
post.cover.url   # → '/media/covers/cat.jpg'
post.cover.path  # → полный путь на диске внутри MEDIA_ROOT
{% if post.cover %}
  <img src="{{ post.cover.url }}" alt="{{ post.title }}">
{% endif %}

Проверка {% if post.cover %} обязательна для необязательного поля: у пустого файла обращение к .url вызывает ValueError.

Раздача медиа в режиме разработки

Медиа runserver автоматически не раздаёт. В разработке маршрут добавляют функцией static() в корневой urls.py:

# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

static() работает только при DEBUG = True и только с локальным префиксом вида /media/: при выключенном DEBUG или префиксе с доменом (https://cdn.example.com/...) функция возвращает пустой список маршрутов. Проверка if settings.DEBUG подчёркивает это для читателя кода. В продакшне медиа раздаёт веб-сервер или внешнее хранилище, например S3.

Статика и медиа: в чём разница

ПараметрСтатикаМедиа
Что этоCSS, JS, шрифты, картинки оформленияФайлы, загруженные через сайт или админку
Кто добавляетРазработчик, вместе с кодомПользователи во время работы сайта
Префикс адресаSTATIC_URLMEDIA_URL
ПапкаSTATIC_ROOT (после collectstatic)MEDIA_ROOT
В шаблонетег static.url поля модели
Раздача в DEBUGrunserver саммаршрут static() в urls.py
В Gitисходники да, STATIC_ROOT — нетнет

MEDIA_URL и STATIC_URL должны различаться: при одинаковых значениях collectstatic остановится с ImproperlyConfigured: The MEDIA_URL and STATIC_URL settings must have different values. Папки MEDIA_ROOT и STATIC_ROOT тоже держите раздельно: collectstatic --clear очищает STATIC_ROOT целиком.

Частые ошибки с URL в Django

NoReverseMatch

NoReverseMatch: Reverse for 'post_detail' with arguments '('',)' not found. 1 pattern(s) tried: ['blog/(?P<slug>[-a-zA-Z0-9_]+)/\\Z']

Django не смог построить адрес по имени и аргументам. Сообщение подсказывает причину:

  • 'post_list' is not a valid view function or pattern name — опечатка в имени, маршрут не подключён через include() или у приложения есть app_name, а вы обращаетесь без префикса blog:;
  • with no arguments not found — маршрут требует аргумент, а его не передали;
  • with arguments '('',)' — аргумент пустой: переменной нет в контексте шаблона или у объекта не заполнено поле, как post.slug выше;
  • аргумент передан, но не подходит под конвертер: кириллица для slug, буквы для int;
  • Could not parse the remainder: ':post_list' — имя в {% url %} написано без кавычек.

404 на статику или медиа

Проверьте по порядку:

  1. DEBUG = True? При выключенном режиме отладки runserver не отдаёт ни статику, ни медиа.
  2. В шаблоне есть {% load static %}, а путь передан через тег static, без жёстко прописанного href="static/css/site.css". Относительный адрес без начального слэша браузер склеит с текущей страницей, и на /blog/hello/ он превратится в /blog/hello/static/css/site.css.
  3. Файл находится командой findstatic. Если нет — папка не указана в STATICFILES_DIRS или приложение не добавлено в INSTALLED_APPS.
  4. Для медиа в корневом urls.py подключён static(settings.MEDIA_URL, ...), а выше нет «ловушки» вроде <path:page>/, которая перехватывает /media/....
  5. В продакшне выполнен collectstatic, а location /static/ в Nginx указывает на STATIC_ROOT.

RuntimeError: You called this URL via POST, but the URL doesn’t end in a slash

RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to 127.0.0.1:8000/blog/hello/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.

Настройка APPEND_SLASH включена по умолчанию: если адрес без слэша не совпал ни с одним маршрутом, а со слэшем совпал, CommonMiddleware отвечает редиректом 301 на адрес со слэшем. Для GET это незаметно. POST при редиректе превращается в GET и теряет данные формы, поэтому при DEBUG = True Django останавливается с этой ошибкой, а при DEBUG = False молча перенаправляет — и данные пропадают.

Исправление — отправлять форму на адрес со слэшем, то есть строить action через {% url %}. Отключать APPEND_SLASH не нужно. Как устроены CommonMiddleware и другие слои обработки запроса — в статье «Django Middleware».

403 CSRF verification failed

В POST-форме нет {% csrf_token %}, или страницу отрисовали без request — например, через render_to_string() без этого аргумента. Используйте render(request, ...). Для запросов из JavaScript передавайте токен в заголовке X-CSRFToken.

Страница отдаёт 404, хотя маршрут есть

Обычно виноват порядок: маршрут выше перехватил адрес (пример с subscribe/ и <slug:slug>/). Другие причины — не совпал слэш в конце, urls.py приложения не подключён через include() или префикс в include() записан без завершающего слэша: path('blog', include(...)) вместе с path('post/', ...) даст адрес /blogpost/.

Частые вопросы

Как посмотреть все URL проекта?

Встроенной команды для этого нет. При DEBUG = True список маршрутов показывает страница 404. Полный список с именами и view выводит команда python manage.py show_urls из пакета django-extensions.

Как получить текущий URL и имя маршрута?

Во view: request.path — путь без строки запроса (/blog/where/abc/), request.get_full_path() — вместе с ней (/blog/where/abc/?page=3). Имя маршрута хранит request.resolver_match.url_name, полное имя с пространством имён — request.resolver_match.view_name (blog:where). В шаблоне те же значения доступны через request, например для подсветки активного пункта меню.

Как передать в URL GET-параметры?

GET-параметры не описывают в path(). Во view их читают из request.GET.get('page'), а ссылку с ними строят reverse(..., query={'page': 2}) в Django 5.2 и новее или дописывают вручную в шаблоне: {% url 'blog:post_list' %}?page=2.

Как настроить редирект со старого адреса на новый?

Для постоянного переезда подойдёт RedirectView прямо в urls.py — он отдаст 301:

from django.views.generic import RedirectView

urlpatterns = [
    path('news/', RedirectView.as_view(pattern_name='blog:post_list', permanent=True)),
]

Внутри view используйте redirect('blog:post_list', permanent=True).

Как сделать свою страницу 404?

Положите шаблон 404.html в общую папку шаблонов — Django использует его при DEBUG = False. Если нужна логика, объявите в корневом urls.py переменную handler404 = 'blog.views.page_not_found' с view вида page_not_found(request, exception), который возвращает ответ со статусом 404.

Как поменять адрес админки?

Замените префикс в корневом urls.py: path('control-panel/', admin.site.urls). Ссылки внутри админки строятся через reverse(), поэтому обновятся сами. Зачем это делать и какие ещё меры защищают вход в админку — в статье «Защита админки Django».

Итог

Маршруты описывают в urls.py приложений и подключают в корневой urls.py через include(). Для параметров берите path() с конвертерами, для сложных шаблонов — свой конвертер или re_path(), а конкретные маршруты ставьте выше общих. Каждому маршруту давайте name, приложению — app_name, и стройте адреса через reverse(), reverse_lazy(), {% url %} и get_absolute_url(). POST-формы отправляйте на адрес из {% url %} со слэшем в конце и с {% csrf_token %}. Статику подключайте тегом static с настройками STATIC_URL и STATIC_ROOT, загрузки — через MEDIA_URL, MEDIA_ROOT и static() в режиме разработки. Подробный справочник по маршрутизации — в документации Django.

  • #Django
  • #urls.py
  • #reverse
  • #Статика
  • #Медиафайлы
↑ ↓ выборEnter открытьEsc закрыть