Создание представлений для публичного интерфейса

Beginner

Введение

Этот туториал начинается там, где закончился раздел Настройка базы данных. Мы продолжаем работу над веб-опросом и сосредоточимся на создании публичного интерфейса - «представлений».

Обзор

Представление - это "тип" веб-страницы в вашем приложении Django, который обычно выполняет определенную функцию и имеет определенный шаблон. Например, в приложении блога вы можете иметь следующие представления:

  • Главная страница блога - отображает несколько последних записей.
  • Страница "деталей" записи - постоянная ссылка на отдельную запись.
  • Архивная страница по годам - отображает все месяцы с записями за заданный год.
  • Архивная страница по месяцам - отображает все дни с записями за заданный месяц.
  • Архивная страница по дням - отображает все записи за заданный день.
  • Действие комментария - обрабатывает публикацию комментариев к заданной записи.

В нашем приложении опросов мы будем иметь следующие четыре представления:

  • Страница "индекса" вопросов - отображает несколько последних вопросов.
  • Страница "деталей" вопроса - отображает текст вопроса, без результатов, но с формой для голосования.
  • Страница "результатов" вопроса - отображает результаты для определенного вопроса.
  • Действие голосования - обрабатывает голосование за определенный вариант в определенном вопросе.

В Django веб-страницы и другое содержимое доставляются представлениями. Каждое представление представлено функцией на Python (или методом, в случае классовых представлений). Django будет выбирать представление, изучая запрашиваемый URL (точнее, часть URL после имени домена).

Теперь, за время вашего пребывания в сети, вы, возможно, столкнулись с такими прекрасностями, как ME2/Sites/dirmod.htm?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B. Вам будет приятно знать, что Django позволяет нам гораздо более элегантные шаблоны URL.

Шаблон URL - это общая форма URL - например: /newsarchive/<year>/<month>.

Для перехода от URL к представлению Django использует то, что называется "URL-конфигурациями". URL-конфигурация сопоставляет шаблоны URL представлениям.

В этом туториале предоставляются основные инструкции по использованию URL-конфигураций, и вы можете обратиться к /topics/http/urls для получения дополнительной информации.

Создание дополнительных представлений

Теперь добавим несколько дополнительных представлений в polls/views.py. Эти представления немного отличаются, потому что они принимают аргумент:

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)


def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)


def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

Свяжем эти новые представления с модулем polls.urls, добавив следующие вызовы ~django.urls.path:

Откройте файл polls/urls.py и добавьте следующие строки:

from django.urls import path

from. import views

urlpatterns = [
    ## ex: /polls/
    path("", views.index, name="index"),
    ## ex: /polls/5/
    path("<int:question_id>/", views.detail, name="detail"),
    ## ex: /polls/5/results/
    path("<int:question_id>/results/", views.results, name="results"),
    ## ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

Теперь запустите сервер снова:

cd ~/project/mysite
python manage.py runserver 0.0.0.0:8080

Переключитесь на вкладку Web 8080 и перейдите по адресу /polls/34/. Сервер запустит метод detail() и отобразит любой ID, который вы укажете в URL. Попробуйте также /polls/34/results/ и /polls/34/vote/ - они отобразят заглушку результатов и страницу голосования.

Диаграмма маршрутизации URL в Django

Когда кто-то запрашивает страницу на вашем сайте - скажем, /polls/34/, Django загрузит Python-модуль mysite.urls, потому что он указан в настройке ROOT_URLCONF. Он находит переменную с именем urlpatterns и последовательно обходит шаблоны. После нахождения совпадения по 'polls/' он удаляет совпадающий текст ("polls/") и отправляет оставшийся текст - "34/" - в URL-конфигурацию 'polls.urls' для дальнейшей обработки. Там оно совпадает с '<int:question_id>/', что приводит к вызову представления detail() следующим образом:

detail(request=<HttpRequest object>, question_id=34)

Часть question_id=34 берется из <int:question_id>. Использование угловых скобок "захватывает" часть URL и передает ее в качестве именованного аргумента в функцию представления. Часть question_id в строке определяет имя, которое будет использоваться для идентификации совпавшего шаблона, а часть int - это конвертер, который определяет, какие шаблоны должны совпадать с этой частью пути URL. Двоеточие (:) отделяет конвертер и имя шаблона.

Создание представлений, которые действительно что-то делают

Каждое представление отвечает за выполнение одной из двух задач: возвращение объекта ~django.http.HttpResponse, содержащего содержимое запрошенной страницы, или генерацию исключения, такого как ~django.http.Http404. Остальное зависит от вас.

Ваш вид может читать записи из базы данных или не читать. Может он использовать систему шаблонов, такую как шаблонизатор Django, или сторонний шаблонизатор на Python, или не использовать. Может он генерировать PDF-файл, выводить XML, создавать ZIP-файл в режиме реального времени - что угодно, используя любые библиотеки Python, которые хотите.

Все, что требуется Django - это ~django.http.HttpResponse. Или исключение.

Поскольку это удобно, давайте используем собственный API базы данных Django, который мы рассматривали в Уроке 2. Вот попытка создать новый вид index(), который отображает последние 5 вопросов опроса в системе, разделенных запятыми, в порядке убывания даты публикации:

Откройте файл polls/views.py и измените его так, чтобы он выглядел так:

from django.http import HttpResponse

from.models import Question


def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    output = ", ".join([q.question_text for q in latest_question_list])
    return HttpResponse(output)


## Оставьте остальные представления (detail, results, vote) без изменений

Однако здесь есть проблема: дизайн страницы жестко закодирован в представлении. Если вы хотите изменить внешний вид страницы, вам придется редактировать этот Python-код. Поэтому давайте используем систему шаблонов Django, чтобы отделить дизайн от Python, создав шаблон, который может использовать представление.

Сначала создайте директорию с именем templates в директории polls. Django будет искать шаблоны в этой директории.

Настройка TEMPLATES вашего проекта описывает, как Django будет загружать и отображать шаблоны. Файл настроек по умолчанию настраивает бэкенд DjangoTemplates, для которого параметр APP_DIRS <TEMPLATES-APP_DIRS> установлен в True. По соглашению DjangoTemplates ищет поддиректорию "templates" в каждом из INSTALLED_APPS.

Внутри директории templates, которую вы только что создали, создайте другую директорию с именем polls, а внутри нее создайте файл с именем index.html. Другими словами, ваш шаблон должен находиться по адресу polls/templates/polls/index.html. В силу того, как работает загрузчик шаблонов app_directories, описанный выше, вы можете ссылаться на этот шаблон в Django как polls/index.html.

Именование шаблонов

Теперь мы возможно могли бы обойтись без создания дополнительной директории polls внутри polls/templates (и поместить шаблоны прямо в polls/templates), но на самом деле это было бы плохой идеей. Django выберет первый найденный шаблон с совпадающим именем, и если у вас есть шаблон с таким же именем в другом приложении, Django не сможет различить между ними.

Нам нужно уметь указывать Django на правильный шаблон, и лучший способ этого сделать - это именование шаблонов. То есть, поместить эти шаблоны внутри другой директории, названной именем самого приложения.

Вставьте следующий код в этот шаблон:

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Примечание:

Для сокращения урока все примеры шаблонов используют неполный HTML. В своих проектах вы должны использовать полные HTML-документы.

Теперь обновим наш вид index в polls/views.py, чтобы использовать шаблон:

from django.http import HttpResponse
from django.template import loader

from.models import Question


def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    template = loader.get_template("polls/index.html")
    context = {
        "latest_question_list": latest_question_list,
    }
    return HttpResponse(template.render(context, request))

Этот код загружает шаблон с именем polls/index.html и передает ему контекст. Контекст - это словарь, который сопоставляет имена переменных шаблона объектам Python.

Запустите сервер снова:

python manage.py runserver 0.0.0.0:8080

Откройте страницу, указав в браузере адрес "/polls/", и вы должны увидеть маркированный список, содержащий вопрос "Что нового" из Урока 2. Ссылка ведет на страницу с деталями вопроса.

Главная страница опросов Django

Короткая запись: ~django.shortcuts.render

Очень часто используется следующая последовательность действий: загрузить шаблон, заполнить контекст и вернуть объект ~django.http.HttpResponse с результатом отрендеренного шаблона. Django предоставляет короткую запись для этого. Вот полный вид index(), переписанный:

from django.shortcuts import render

from.models import Question


def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    context = {"latest_question_list": latest_question_list}
    return render(request, "polls/index.html", context)

Обратите внимание, что после того, как мы сделали это во всех представлениях, мы больше не нуждаемся в импорте ~django.template.loader и ~django.http.HttpResponse (если у вас остались заглушки методов detail, results и vote, вы по-прежнему будете использовать HttpResponse).

Функция ~django.shortcuts.render принимает объект запроса в качестве первого аргумента, имя шаблона в качестве второго аргумента и словарь в качестве необязательного третьего аргумента. Она возвращает объект ~django.http.HttpResponse с отрендеренным с данным контекстом заданным шаблоном.

Генерация ошибки 404

Теперь давайте рассмотрим представление деталей вопроса - страницу, на которой отображается текст вопроса для заданного опроса. Вот представление:

from django.http import Http404
from django.shortcuts import render

from.models import Question


#...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, "polls/detail.html", {"question": question})

Новая концепция здесь: представление вызывает исключение ~django.http.Http404, если вопрос с запрошенным ID не существует.

Мы поговорим о том, что можно поместить в шаблон polls/detail.html чуть позже, но если вы хотите быстро запустить вышеприведенный пример, файл, содержащий только:

{{ question }}

будет для вас началом работы на данный момент.

Короткая запись: ~django.shortcuts.get_object_or_404

Очень часто используется следующая последовательность действий: использовать ~django.db.models.query.QuerySet.get и вызывать ~django.http.Http404, если объект не существует. Django предоставляет короткую запись для этого. Вот представление detail(), переписанное:

from django.shortcuts import get_object_or_404, render

from.models import Question


#...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/detail.html", {"question": question})

Функция ~django.shortcuts.get_object_or_404 принимает модель Django в качестве первого аргумента и произвольное количество именованных аргументов, которые она передает в функцию ~django.db.models.query.QuerySet.get менеджера модели. Она вызывает исключение ~django.http.Http404, если объект не существует.

Почему мы используем вспомогательную функцию ~django.shortcuts.get_object_or_404, а не автоматически ловим исключения ~django.core.exceptions.ObjectDoesNotExist на более высоком уровне или заставляем API модели вызывать ~django.http.Http404 вместо ~django.core.exceptions.ObjectDoesNotExist?

Поскольку это привяжет модельный слой к представительному слою. Одной из главных целей дизайна Django является поддержание слабой связи. Некоторое контролируемое связывание вводится в модуле django.shortcuts.

Также существует функция ~django.shortcuts.get_list_or_404, которая работает так же, как ~django.shortcuts.get_object_or_404, за исключением того, что она использует ~django.db.models.query.QuerySet.filter вместо ~django.db.models.query.QuerySet.get. Она вызывает исключение ~django.http.Http404, если список пуст.

Использование системы шаблонов

Вернемся к представлению detail() для нашего приложения опросов. С учетом переменной контекста question, вот, как может выглядеть шаблон polls/detail.html:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

Система шаблонов использует синтаксис с использованием точки для доступа к атрибутам переменных. В примере {{ question.question_text }} сначала Django пытается выполнить поиск в словаре по объекту question. Если это не удается, он пытается выполнить поиск по атрибуту - что в этом случае работает. Если поиск по атрибуту неудается, он бы попытался выполнить поиск по индексу списка.

Вызов методов происходит в цикле {% for %}<for>: question.choice_set.all интерпретируется как Python-код question.choice_set.all(), который возвращает итерируемый объект Choice и подходит для использования в теге {% for %}<for>.

Удаление жестко закодированных URL-адресов в шаблонах

Помните, когда мы писали ссылку на вопрос в шаблоне polls/index.html, ссылка была частично жестко закодирована следующим образом:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

Проблема с таким жестко закодированным и тесно связанным подходом заключается в том, что становится сложно изменить URL-адреса на проектах с большим количеством шаблонов. Однако, поскольку вы определили аргумент name в функциях ~django.urls.path в модуле polls.urls, вы можете избавиться от зависимости от конкретных URL-пути, определенных в конфигурации URL, используя тег шаблона {% url %}:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

Работает это так, что ищет определение URL, указанное в модуле polls.urls. Вы можете точно увидеть, где определено имя URL 'detail' ниже:

## значение 'name', которое вызывается тегом шаблона {% url %}
path("<int:question_id>/", views.detail, name="detail"),

Если вы хотите изменить URL представления деталей опроса на что-то другое, например, на polls/specifics/12/ вместо изменения его в шаблоне (или шаблонах), вы измените его в polls/urls.py:

Вы вообще не нужно изменять шаблон.

## добавлено слово'specifics'
path("specifics/<int:question_id>/", views.detail, name="detail"),

Именование URL-адресов с учетом пространства имен

Проект-туториал имеет только одно приложение, polls. В реальных проектах на Django может быть пять, десять, двадцать приложений или больше. Как Django различает имена URL-адресов между ними? Например, приложение polls имеет представление detail, и такое же представление может быть в приложении на том же проекте, которое посвящено блогу. Как заставить Django знать, какое представление приложения создать для URL-адреса при использовании тега шаблона {% url %}?

Ответ - добавить пространства имен в конфигурацию URL. В файле polls/urls.py добавьте app_name, чтобы установить пространство имен приложения:

from django.urls import path

from. import views

app_name = "polls"
urlpatterns = [
    ## пример: /polls/
    path("", views.index, name="index"),
    ## пример: /polls/5/
    path("<int:question_id>/", views.detail, name="detail"),
    ## пример: /polls/5/results/
    path("<int:question_id>/results/", views.results, name="results"),
    ## пример: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

Теперь измените шаблон polls/index.html из:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

на ссылку на представление с учетом пространства имен:

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Пример именования URL-адресов с учетом пространства имен

Когда вы будете уверены в написании представлений, прочитайте раздел Обработка форм и сокращение нашего кода, чтобы узнать основы обработки форм и обобщенных представлений.

Резюме

Поздравляем! Вы завершили лабораторную работу по созданию представлений для публичного интерфейса. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.