Маршрутизация, обработка исключений запросов, перенаправления

Курс по Django: https://stepik.org/a/183363

Архив проекта: lesson-3-coolsite.zip

Продолжаем тему маршрутизации. И для начала уберем фрагмент ‘women/’ из пути URL’ов для приложения women:

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

Теперь за главную страницу будет отвечать функция представления index, а за вывод разделов – функция categories. Но у нас может быть множество категорий и хотелось бы, например, их отображать с помощью таких URL:

127.0.0.1:8000/cats/1/

127.0.0.1:8000/cats/2/

127.0.0.1:8000/cats/3/

...

Как прописать такой шаблон для URL в Django? Для этого в списке адресов приложения следует указать числовой параметр, следующим образом:

urlpatterns = [
    path('', index),
    path('cats/<int:catid>/', categories),
]

Смотрите, здесь в угловых скобках записан параметр catid, который имеет тип int – целочисленный. Такой путь будет соответствовать любым комбинациям URL с фрагментом ‘cats/число/’. И, далее, в функции представления categories мы уже можем использовать этот параметр:

def categories(request, catid):
    return HttpResponse(f"<h1>Статьи по категориям</h1>{catid}</p>")

Как видите, все предельно просто. Помимо типа int в Django можно использовать и такие типы:

  • str – любая не пустая строка, исключая символ ‘/’;
  • int – любое положительное целое число, включая 0;
  • slug – слаг, то есть, латиница ASCII таблицы, символы дефиса и подчеркивания;
  • uuid – цифры, малые латинские символы ASCII, дефис;
  • path – любая не пустая строка, включая символ ‘/’.

Например, вместо int часто указывают slug, так URL становится более информативным:

path('cats/<slug:cat>/', categories),

Изменим функцию представления:

def categories(request, cat):
    return HttpResponse(f"<h1>Статьи по категориям</h1>{cat}</p>")

И теперь в качестве категории можно указывать не просто числа, а строку в виде слага, например, так:

127.0.0.1:8000/cats/sport/

127.0.0.1:8000/cats/music/

Как видите, это понятнее для пользователя и поисковых систем. Сайты с такими URL, в среднем, лучше индексируются и занимают более высокие позиции в поисковой выдаче.

Если по каким-то причинам представленных типов для URL недостаточно, то в Django имеется функция

re_path()

которая делает все то же самое, но с использованием регулярных выражений. Например, определим URL, в котором можно указывать год в виде четырех чисел:

re_path(r'^archive/(?P<year>[0-9]{4})/', archive),

Добавим функцию представления archive:

def archive(request, year):
    return HttpResponse(f"<h1>Архив по годам</h1>{year}</p>")

И теперь мы можем обращаться к этому URL, указывая ровно 4 цифры:

http://127.0.0.1:8000/archive/2020/

Если же указать меньше или больше чисел, то шаблон URL не совпадет и возникнет исключение 404 – страница не найдена. Вот так, достаточно гибко можно задавать список URL для механизма маршрутизации в Django.

Обработка GET и POST запросов

Те из вас, кто занимался веб-разработкой знают, что структура URL-адреса может содержать дополнительные параметры в виде GET-запроса. Например, вот так:

http://127.0.0.1:8000/?name=Gagarina&cat=music

или так:

127.0.0.1:8000/cats/music/?name=Gagarina&type=pop

И так далее. Здесь у нас идет специальный разделитель – знак вопроса, после которого через амперсанд перечисляются различные параметры в виде пар ключ-значение. Как можно выделять такие значения и обрабатывать их в функциях представления? Для этого, как раз и используется ссылка request на объект HttpRequest. Мы через нее можем обратиться к специальному словарю:

request.GET

где и сохраняются все эти данные:

def categories(request, cat):
    print(request.GET)
    return HttpResponse(f"<h1>Статьи по категориям</h1>{cat}</p>")

Например, по запросу:

127.0.0.1:8000/cats/music/?name=Gagarina&type=pop

мы увидим в консоли значения:

<QueryDict: {'name': ['Gagarina'], 'type': ['pop']}>

Или, можем сначала проверить: есть ли в словаре какие-либо данные и только потом выводить их в консоль:

def categories(request, cat):
    if(request.GET):
        print(request.GET)
 
    return HttpResponse(f"<h1>Статьи по категориям</h1>{cat}</p>")

Как видите, проверка делается очень просто. В дальнейшем мы будем использовать этот функционал.

Помимо GET-запросов в веб-программировании часто используются и POST-запросы. Они, обычно, связаны с передачей данных из форм и позже мы их тоже будем использовать. Здесь же я просто отмечу, что в этом случае вместо коллекции GET используется коллекция POST, а все остальное остается без изменений:

def categories(request, cat):
    if(request.POST):
        print(request.POST)
 
    return HttpResponse(f"<h1>Статьи по категориям</h1>{cat}</p>")

Обработка исключений при запросах к серверу

Следующий важный аспект – это обработка исключений при запросах к серверу. Самым распространенным из них является обращение к несуществующей странице, когда сервер возвращает страницу с кодом 404. Например, если для нашего сайта указать путь:

http://127.0.0.1:8000/aaa

то мы увидим исключение 404 – страница не найдена. Такую информацию мы видим исключительно в процессе отладки нашего сайта, когда глобальная константа DEBUG в пакете конфигурации (файл settings.py) установлена в True.

Давайте посмотрим, что произойдет, если временно перевести ее в значение False. Перейдем в пакет конфигурации, откроем файл settings.py, найдем константу DEBUG и присвоим ей значение False. При запуске тестового сервера у нас возникнет ошибка, что мы должны указать разрешенные хосты. Так как мы сейчас используем хост 127.0.0.1, то его в виде строки и укажем:

ALLOWED_HOSTS = ['127.0.0.1']

Теперь сервер успешно запустился и при обновлении страницы мы уже видим более дружественную страницу без лишней отладочной информации. Но как нам ее поправить, чтобы отобразить более понятную информацию? Для этого в файле urls.py пакета конфигурации можно переопределить обработчик исключения 404. Для этого ему достаточно присвоить ссылку на функцию, которая и будет формировать ответ для отсутствующих страниц, например, так:

handler404 = pageNotFound

Мы здесь передаем ссылку на функцию pageNotFound, а саму функцию определим в приложении women:

def pageNotFound(request, exception):
    return HttpResponseNotFound('<h1>Страница не найдена</h1>')

Обратите внимание, функция принимает два аргумента и возвращает ответ в виде экземпляра класса HttpResponseNotFound, которому передается HTML-страница, отображаемая при неверных запросах. Если теперь мы обновим страницу, то увидим заголовок «Страница не найдена».

В дальнейшем мы увидим, как вместо строки можно возвращать шаблон страницы 404. Сейчас главное понять принцип работы механизма обработки исключений.

Итак, мы предполагаем, что функция pageNotFound будет вызываться всякий раз при возникновении исключения 404. И это важный момент. Смотрите, если в какой-либо другой функции представления сгенерировать это исключение, то будет автоматическое перенаправление на функцию pageNotFound и пользователь увидит все ту же страницу 404. Например, в функции archive мы сделаем проверку:

from django.http import HttpResponse, HttpResponseNotFound, Http404
 
def archive(request, year):
    if(int(year) > 2020):
        raise Http404()
 
    return HttpResponse(f"<h1>Архив по годам</h1>{year}</p>")

Если год больше 2020-го, то генерируется исключение 404 как экземпляр класса Http404 и выполняется перенаправление на функцию pageNotFound. Это нам позволяет описывать логику отображения неверных запросов в одном месте программы – в функции pageNotFound.

Аналогичным образом можно переопределять обработчики других исключений, например:

  • handler500 – ошибка сервера;
  • handler403 – доступ запрещен;
  • handler400 – невозможно обработать запрос.

Но все они работают в боевом режиме при DEBUG = False. При отладке мы увидим расширенную служебную информацию, помогающую исправлять ошибки при разработке сайта. Более подробную информацию можно посмотреть на странице русскоязычной документации:

https://djbook.ru/rel3.0/topics/http/views.html

Создание 301 и 302 редиректов

Очень часто при развитии сайта некоторые его страницы переносятся на другой URL-адрес. И, чтобы не потерять позиции этих страниц в поисковой выдаче, поисковым системам нужно явно указать, что страница перемещена либо временно, либо постоянно на новый URL. Это делается с помощью перенаправления с кодами:

  • 301 – страница перемещена на другой постоянный URL-адрес;
  • 302 – страница перемещена временно на другой URL-адрес.

В Django такие редиректы достаточно просто выполняются с помощью функции:

django.shortcuts.redirect

Давайте для примера сделаем перенаправление со страницы архива, если год больше 2000:

def archive(request, year):
    if(int(year) > 2020):
        return redirect('/')
 
    return HttpResponse(f"<h1>Архив по годам</h1>{year}</p>")

Здесь в качестве первого параметра указывается страница, на которую происходит перенаправление, в данном случае – это главная страница сайта. Если теперь выполнить запрос:

127.0.0.1:8000/archive/2021/

то мы попадем на главную страницу с кодом перенаправления 302 (см. консоль). Если же нам нужно указать постоянный редирект с кодом 301, то записывается дополнительный параметр:

return redirect('/', permanent=True)

Параметр name функции path

Однако, указывать в функции redirect, да и вообще где бы то ни было в приложении конкретный URL-адрес (кроме их списка в коллекции urlpatterns) – это порочная практика, или, как еще говорят – хардкодинг. Вместо этого каждому шаблону пути можно присвоить свое уникальное имя и использовать его в рамках всего проекта.

Давайте определим имена для наших URL-запросов. Для этого перейдем в файл women/urls.py и в каждой функции path пропишем параметр name с уникальными именами:

urlpatterns = [
    path('', index, name='home'),
    path('cats/<slug:cat>/', categories, name='cats'),
    re_path(r'^archive/(?P<year>[0-9]{4})/', archive, name='archive'),
]

Конечно, эти имена вы можете выбрать и другие – это лишь пример. И далее, в функции redirect мы можем выполнить перенаправление на главную страницу, указав имя home:

return redirect('home', permanent=True)

Как видите, это гораздо понятнее и безопаснее использования конкретных URL-адресов. Если в дальнейшем маршрут изменится, то автоматически изменится и адрес перенаправления для home.

На этом мы завершим это обзорное занятие по маршрутизации и обработке исключений в Django. Конечно, это лишь базовые возможности, которые предлагает данный фреймворк. Но эта база позволит вам сделать первые шаги в создании сайтов, постепенно знакомясь с другими возможностями Django, опираясь на его документацию или опыт других разработчиков. Рассказать обо всем в рамках начального курса просто нереально. Иначе это превратится в справочную информацию и большого эффекта от такого курса уже не будет.

Курс по Django: https://stepik.org/a/183363

Видео по теме