В прошлом курсе мы разбирали HTTP подробно — методы, статус-коды, структуру запроса и ответа. Здесь не будем повторять теорию, а сразу посмотрим, как это выглядит в Django.
Два метода, с которыми работает большинство веб-приложений:
- GET — запрос данных. Параметры передаются в строке URL:
/films/?genre=drama&year=2020. Ничего не меняет на сервере. - POST — отправка данных. Параметры передаются в теле запроса, не видны в URL. Используется для создания и изменения данных: отправка формы, добавление записи.
В Django оба метода приходят в одно и то же представление. Чтобы обработать их по-разному, проверяем request.method:
from django.http import HttpResponse
def add_film(request):
if request.method == 'POST':
# данные из формы — в request.POST
title = request.POST.get('title')
return HttpResponse(f'Фильм «{title}» добавлен.')
# GET-запрос — показываем форму
return HttpResponse('Здесь будет форма добавления фильма.')Сейчас мы возвращаем простой текст вместо HTML-формы — шаблоны и настоящие формы появятся в модулях 2 и 5. Но логика ветвления по request.method остаётся именно такой.
Параметры из строки запроса доступны через request.GET:
def film_list(request):
genre = request.GET.get('genre', '') # ?genre=drama
year = request.GET.get('year', '') # ?year=2020
response_text = f'Жанр: {genre}, год: {year}' if genre or year else 'Все фильмы'
return HttpResponse(response_text)Запрос /films/?genre=drama&year=2020 вернёт: Жанр: drama, год: 2020.
Используем .get() с дефолтным значением, а не обращение по ключу напрямую — если параметра нет в запросе, request.GET['genre'] выбросит KeyError.
Самая частая ошибка в работе с данными — запрос несуществующей записи. Пользователь заходит на /films/999/, а фильма с таким id нет.
Сейчас у нас ещё нет базы данных, поэтому покажем на примере со словарём — структура будет точно такой же, когда появятся модели:
from django.http import HttpResponse, Http404
# Временная замена БД — словарь с фильмами
FILMS = {
1: 'Крёстный отец',
2: 'Список Шиндлера',
3: 'Побег из Шоушенка',
}
def film_detail(request, film_id):
if film_id not in FILMS:
raise Http404(f'Фильм с id={film_id} не найден')
return HttpResponse(f'Фильм: {FILMS[film_id]}')Исключение Http404 — это сигнал Django вернуть ответ 404 Not Found. В режиме разработки (DEBUG = True) Django покажет подробную отладочную страницу. В продакшне — стандартную страницу 404, которую можно кастомизировать.
Когда в модуле 3 мы подключим базу данных, Django даст нам готовый инструмент — get_object_or_404(). Он делает то же самое, но короче:
# Так это будет выглядеть с моделями (модуль 3):
from django.shortcuts import get_object_or_404
from .models import Film
def film_detail(request, film_id):
film = get_object_or_404(Film, id=film_id)
return HttpResponse(f'Фильм: {film.title}')Запомни эту функцию — она появится в каждом представлении, которое работает с конкретной записью.
По умолчанию Django показывает стандартные страницы для ошибок 404 и 500. Их можно заменить собственными — для этого достаточно создать шаблоны с именами 404.html и 500.html в корневой папке шаблонов. Это сделаем в модуле 2, когда займёмся шаблонами.
Но есть и программный способ — переопределить обработчики в корневом urls.py:
# filmsite/urls.py
from django.contrib import admin
from django.urls import path, include
from films import views as film_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('films.urls')),
]
# Пользовательские обработчики ошибок
handler404 = film_views.page_not_found
handler500 = film_views.server_error# films/views.py
def page_not_found(request, exception):
return HttpResponse('Страница не найдена. Вернитесь на главную.', status=404)
def server_error(request):
return HttpResponse('Произошла ошибка сервера. Мы уже работаем над этим.', status=500)Обрати внимание: handler404 принимает два аргумента — request и exception, а handler500 — только request.
Пользовательские обработчики handler404 и handler500 работают только при DEBUG=False. В режиме разработки (DEBUG=True) Django показывает собственные отладочные страницы ошибок.
Если вы решили проверить работу переопределенных обработчиков, замените вот эти две настройки в settings.py:
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']Перенаправление — это ответ сервера, который говорит браузеру: «перейди по другому адресу». Используется, например, после отправки формы: пользователь отправил данные через POST, сервер сохранил их и перенаправляет на страницу с результатом.
Это стандартный паттерн веб-разработки — Post/Redirect/Get (PRG). Без него при обновлении страницы браузер повторно отправит POST-запрос, и данные запишутся дважды.
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
def add_film(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
if not title:
return HttpResponse('Название фильма не может быть пустым.', status=400)
# Здесь будет сохранение в БД (модуль 3)
# После сохранения — перенаправляем на список фильмов
return redirect('/films/')
return HttpResponse('Здесь будет форма добавления фильма.')Функция redirect() принимает URL в виде строки и возвращает ответ с кодом 302 Found.
Передавать URL жёстко закодированной строкой — плохая практика. Если маршрут изменится, нужно будет искать все вхождения этой строки по всему проекту. Для этого существует reverse().
Функция reverse() принимает имя маршрута и возвращает соответствующий URL:
from django.urls import reverse
reverse('film_list') # вернёт '/films/'
reverse('index') # вернёт '/'Если маршрут динамический и требует параметров, передаём их через kwargs:
reverse('film_detail', kwargs={'film_id': 7}) # вернёт '/films/7/'Теперь перепишем redirect() с использованием reverse():
from django.shortcuts import redirect
from django.urls import reverse
def add_film(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
if not title:
return HttpResponse('Название фильма не может быть пустым.', status=400)
# Перенаправляем по имени маршрута, а не по жёсткой строке
return redirect(reverse('film_list'))
return HttpResponse('Здесь будет форма добавления фильма.')На практике redirect() умеет принимать имя маршрута напрямую, без явного вызова reverse():
return redirect('film_list') # статический маршрут
return redirect('film_detail', film_id=7) # динамический маршрутЭто просто синтаксический сахар — внутри Django всё равно вызывает reverse(). Но явный reverse() пригодится, когда нужно получить URL не для редиректа, а для других целей — например, передать в шаблон или в JSON-ответ.
Если представление обработало POST-запрос и вернуло HttpResponse напрямую — при обновлении страницы браузер предложит повторно отправить данные. Это приводит к дублированию записей в базе. Всегда используй паттерн PRG: после POST делай redirect().
reverse() можно вызывать в представлениях и шаблонах, но не на уровне модуля — в момент импорта маршруты могут быть ещё не загружены:
# Не работает — вызывается при импорте модуля, до загрузки маршрутов
from django.urls import reverse
FILM_LIST_URL = reverse('film_list')
# Правильно — вызывается внутри функции, когда маршруты уже загружены
def some_view(request):
url = reverse('film_list')
return redirect(url)Если передать в reverse() несуществующее имя маршрута или забыть передать обязательный параметр — Django выбросит исключение NoReverseMatch. Это частая ошибка при переименовании маршрутов:
# В urls.py переименовали 'film_list' в 'films'
# В views.py забыли обновить:
reverse('film_list') # NoReverseMatch
reverse('films') # Перенаправление получилось# films/views.py
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from django.urls import reverse
FILMS = {
1: 'Крёстный отец',
2: 'Список Шиндлера',
3: 'Побег из Шоушенка',
}
def index(request):
return HttpResponse('Добро пожаловать на сайт фильмов!')
def about(request):
return HttpResponse('Сайт-каталог фильмов. Здесь собраны лучшие фильмы всех времён.')
def film_list(request):
genre = request.GET.get('genre', '')
year = request.GET.get('year', '')
response_text = f'Жанр: {genre}, год: {year}' if genre or year else 'Все фильмы'
return HttpResponse(response_text)
def film_detail(request, film_id):
if film_id not in FILMS:
raise Http404(f'Фильм с id={film_id} не найден')
return HttpResponse(f'Фильм: {FILMS[film_id]}')
def add_film(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
if not title:
return HttpResponse('Название фильма не может быть пустым.', status=400)
return redirect(reverse('film_list'))
return HttpResponse('Здесь будет форма добавления фильма.')
def page_not_found(request, exception):
return HttpResponse('Страница не найдена.', status=404)
def server_error(request):
return HttpResponse('Произошла ошибка сервера.', status=500)# films/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('about/', views.about, name='about'),
path('films/', views.film_list, name='film_list'),
path('films/<int:film_id>/', views.film_detail, name='film_detail'),
path('films/add/', views.add_film, name='add_film'),
path('directors/<int:director_id>/', views.director_detail, name='director_detail'),
]- Чем отличается GET от POST? В каких случаях используется каждый из них?
- Что такое паттерн Post/Redirect/Get и зачем он нужен?
- В чём преимущество
reverse()перед жёстко заданной строкой URL? - Как передать параметры в
reverse()для динамического маршрута? - Что произойдёт, если вызвать
raise Http404внутри представления?
Тип: допиши код
Тебе дано представление search_film. Оно должно:
- Принимать поисковый запрос из параметра
qв строке URL (/films/search/?q=крёстный) - Если параметр
qпустой или отсутствует — возвращать ответ'Введите название фильма для поиска.'со статусом400 - Если параметр есть — возвращать строку вида
'Результаты поиска по запросу: крёстный' - Маршрут доступен по адресу
/films/search/с именемsearch_film
Допиши пропущенные части:
# films/views.py
def search_film(request):
query = ___________________________
if not query:
return ___________________________
return HttpResponse(f'Результаты поиска по запросу: {query}')# films/urls.py — добавь маршрут
urlpatterns = [
...
___________________________
]