Руководство часть 6: Отображение списков и детальной информации

Данная часть расширяет наш сайт LocalLibrary, добавляя в него списки и страницы, путём предоставления подробной информации о книгах и авторах. В текущей части мы подробно изучим обобщённые базовые классы отображения и покажем как они могут существенно сократить количество кода, который вы должны были бы написать в обычной ситуации. Кроме того, мы более подробно рассмотрим управление и настройки URL-адресов, показывая как выполнить простое сопоставление какой-либо строки паттерну регулярного выражения.

Требования: Завершить все предыдущие части руководства, включая Руководство Django Часть 5: Создание домашней страницы.
Цель: Понимать где и как применять обобщённые базовые классы отображения, и как применять паттерны URL-адресов для передачи информации в отображения.

Обзор

В данном руководстве мы завершим первую версию сайта LocalLibrary, с помощью добавления страницы перечисления и подробной информации о книгах и авторах (или, если быть более точными, мы покажем как вам реализовать соответствующие страницы для книг, а для авторов вы сможете сделать их самостоятельно!)

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

Завершающая часть данного руководства будет посвящена демонстрации постраничного показа ваших данных (pagination) при применении обобщённого класса отображения списка.

Страница со списком книг

Страница со списком книг показывает все книги в наличии и будет доступна по адресу: catalog/books/. Эта страница для каждой записи выводит заголовок и автора, при этом каждый заголовок является гиперссылкой на соответствующую страницу подробной информации о книге. Данная страница будет иметь ту же структуру и навигацию как и все остальные страницы сайта, таким образом мы сможем расширить базовый шаблон сайта (base_generic.html), который мы создали в предыдущей части руководства.

Преобразования URL-адресов

Откройте файл /catalog/urls.py и скопируйте в него путь до 'books/', как в примере ниже. Практически так же, как и для главной страницы сайта, функция path() определяет регулярное выражение, по которому проверяется адрес ('books/'), функцию отображения, которая вызовется при совпадении адресов (views.BookListView.as_view()), и название для этого конкретного преобразования.

python
urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
]

Как было отмечено в предыдущей части руководства, URL-адрес уже должен содержать /catalog, и таким образом полный адрес, на самом деле, имеет вид /catalog/books/.

Функция отображения имеет другой формат, чем прежде — это связано с тем, что данное отображение реализуется через класс. Мы будем наследоваться от существующей общей функции из view, которая уже делает большую часть того, что мы хотим, вместо того, чтобы писать свою собственную функцию во view с нуля.

При использовании обобщённых классов отображения в Django мы получаем доступ к соответствующей функции отображения при помощи вызова метода as_view(). Таким образом выполняется вся работа по созданию экземпляра класса и гарантируется вызов правильных методов для входящих HTTP-запросов.

Отображение (на основе базового класса)

Мы могли бы достаточно просто реализовать отображение списка книг при помощи обычной функции (также, как мы сделали это для главной страницы сайта), которая должны была бы выполнить запрос получения всех книг из базы данных, затем вызвать функцию render(), в которую передать данный список, в соответствующий шаблон страницы. Тем не менее, вместо это мы будем использовать обобщённый класс отображения списка — класс, который наследуется от существующего отображения (ListView). Поскольку обобщённый класс уже реализует большую часть того, что нам нужно, и следуя лучшим практикам Django, мы сможем создать более эффективный список при помощи меньшего количества кода, меньшего количества повторений и гораздо лучшей поддержкой.

Откройте catalog/views.py и скопируйте следующий код, в нижнюю часть данного файла:

python
from django.views import generic

class BookListView(generic.ListView):
    model = Book

Это всё! Обобщённое отображение выполнит запрос к базе данных, получит все записи заданной модели (Book), затем отрендерит (отрисует) соответствующий шаблон, расположенный в /locallibrary/catalog/templates/catalog/book_list.html (который мы создадим позже). Внутри данного шаблона вы можете получить доступ к списку книг при помощи переменной шаблона object_list ИЛИ book_list (если обобщить, то "the_model_name_list").

Примечание: Этот, выглядящий странно, путь к файлу шаблона не является опечаткой — обобщённое отображение ищет файл шаблона /application_name/the_model_name_list.html (catalog/book_list.html, в данном случае) внутри директории приложения /application_name/templates/ (у нас - /catalog/templates/).

Вы можете использовать атрибуты для того, чтобы изменить поведение по умолчанию. Например, вы могли бы указать другой файл шаблона, например, если в вашем распоряжении имеется несколько отображений, которые используют одну и ту же модель, или вам позарез захотелось бы использовать другое имя переменной шаблона, если book_list не является интуитивно понятным. Возможно, наиболее полезным вариантом является изменение/отфильтрованные результата запроса к базе данных — таким образом, вместо перечисления всех книг вы могли бы показывать 5 наиболее популярных.

python
class BookListView(generic.ListView):
    model = Book
    context_object_name = 'my_book_list'   # ваше собственное имя переменной контекста в шаблоне
    queryset = Book.objects.filter(title__icontains='war')[:5] # Получение 5 книг, содержащих слово 'war' в заголовке
    template_name = 'books/my_arbitrary_template_name_list.html'  # Определение имени вашего шаблона и его расположения

Переопределение методов в классах отображения

Пока что вам не приходилось этого делать, но у вас имеется возможность переопределять некоторые методы класса отображения.

Например, мы можем переопределить метод получения списка всех записей get_queryset(). Данный подход является более гибким, чем использование атрибута queryset, как мы сделали в предыдущем фрагменте кода (хотя, в данном случае и нет никакой разницы):

python
class BookListView(generic.ListView):
    model = Book

    def get_queryset(self):
        return Book.objects.filter(title__icontains='war')[:5] # Получить 5 книг, содержащих 'war' в заголовке

Мы также могли бы переопределить метод get_context_data() для того, чтобы в контексте (в переменной контекста) передавать шаблону дополнительные переменные (например, список книг передаётся по умолчанию). Фрагмент, представленный ниже, показывает как добавить переменную с именем "some_data" в контекст (затем она будет доступна как переменная шаблона).

python
class BookListView(generic.ListView):
    model = Book

    def get_context_data(self, **kwargs):
        # В первую очередь получаем базовую реализацию контекста
        context = super(BookListView, self).get_context_data(**kwargs)
        # Добавляем новую переменную к контексту и инициализируем её некоторым значением
        context['some_data'] = 'This is just some data'
        return context

В процессе выполнения всего этого важно придерживаться определённой последовательности действий:

  • В первую очередь - получить существующий контекст из нашего суперкласса.
  • Затем добавить в контекст новую информацию.
  • Затем вернуть новый (обновлённый) контекст.

Примечание: Посмотрите Встроенные обобщённые классы отображения (Django docs) для ознакомления с большим количеством примеров того, что вы могли бы сделать.

Создание шаблона Отображения Списка

Создайте HTML-файл /locallibrary/catalog/templates/catalog/book_list.html и скопируйте в него текст, указанный ниже. Как было отмечено ранее, это файл шаблона по умолчанию, который будет "искать" обобщённый класс отображения списка (для модели с именем Book в приложении с именем catalog).

Шаблоны для обобщённых отображений такие же как все остальные шаблоны (хотя, естественно, передаваемые в них контекст, или информация могут отличаться). Так же как и с нашим шаблоном для главной страницы, в первой строке мы расширяем наш базовый шаблон, а затем определяем и замещаем блок с именем content.

django
{% extends "base_generic.html" %}

{% block content %}
    <h1>Book List</h1>

    {% if book_list %}
    <ul>

      {% for book in book_list %}
      <li>
        <a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
      </li>
      {% endfor %}

    </ul>
    {% else %}
      <p>There are no books in the library.</p>
    {% endif %}
{% endblock %}

По умолчанию отображение передаёт контекст (список книг) как object_list и book_list (синонимы; оба варианта будут работать).

Условные ветвления

Мы применяем теги шаблона if, else и endif для того, чтобы проверить определена ли переменная book_list и содержит ли она данные. Если список НЕ пуст, тогда мы выполняем итерации по списку книг. Если список пуст (else-случай) тогда мы показываем текст, поясняющий, что в наличии нет книг.

django
{% if book_list %}
  <!-- здесь наш код "бежит" по списку книг -->
{% else %}
  <p>В библиотеке книг нет.</p>
{% endif %}

В данном фрагменте проверяется только одно условие, но вы можете протестировать другие варианты при помощи тэга шаблона elif (например, {% elif var2 %} ). Для дополнительной информации по данной теме смотрите: if, ifequal/ifnotequal и ifchanged в главе Встроенные тэги и фильтры шаблона (Django Docs).

Цикл For

Шаблон использует тэги for и endfor для того, чтобы "пробежаться" по списку книг, как показано ниже. На каждой итерации (каждом цикле) в переменную шаблона book передаётся информация текущего элемента списка.

django
{% for book in book_list %}
  <li><!-- здесь код, который использует информацию из каждого элемента book списка--></li>
{% endfor %}

Мы не применяем здесь, но внутри каждого цикла Django создаёт переменные, которые вы можете использовать при итерации. Например, вы можете проверять переменную forloop.last (указывает на последнюю итерацию в цикле) для выполнения каких-либо завершающих действий для данного цикла.

Доступ к переменным

Код внутри цикла создаёт экземпляр для каждой книги из списка, при помощи которой показывается заголовок (как ссылка на "скоро-будет-сделано" подробное отображение) и автора книги.

html
<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})

Мы получаем доступ к полям соответствующей записи о книге при помощи "дот-нотации", то есть через точку (например, book.title и book.author), где текст, который идёт после book, является именем поля (так, как определено в модели).

Кроме того, внутри нашего шаблона, мы можем вызывать функции модели — в данном случае, мы вызываем Book.get_absolute_url() для получения URL-адреса, который мы используем для показа детальной информации о книге. Данный вызов работает только для функции у которой нет аргументов (в шаблоне не существует возможности передать аргументы в функцию!)

Примечание: Мы должны быть достаточно осмотрительными для того, чтобы избегать "сторонних эффектов" когда мы вызываем функции из шаблона. В данном случае мы просто получаем URL-адрес, но функции могут делать всё что угодно — мы не хотели бы "убить" наша базу данных (например) просто отрендеривая наш шаблон!

Обновление базового шаблона

Откройте файл базового шаблона (/locallibrary/catalog/templates/base_generic.html) и вставьте {% url 'books' %} в URL-ссылку для пункта All books, как показано ниже. Тем самым, мы создали "переход" на страницу с книгами (теперь мы можем смело это сделать, поскольку у нас имеется соответствующее "книжное" url-преобразование).

python
<li><a href="{% url 'index' %}">Home</a></li>
<li><a href="{% url 'books' %}">All books</a></li>
<li><a href="">All authors</a></li>

Как же теперь все это выглядит?

Пока что у вас нет возможности создать список книг, потому что мы не учли ещё необходимые зависимости — преобразование URL-адреса для страниц с подробной информации о книге, которое необходимо для ссылок на отдельные книги. Мы покажем страницы со списком и подробной информацией о книге после следующего раздела.

Страница с подробной информацией о книге

Доступ к странице с подробной информацией о книге осуществляется при помощи URL-адреса catalog/book/<id> (где <id> является первичным ключом для данной книги). В дополнение к полям модели Book (автор, краткое содержание, ISBN, язык и жанр), также мы перечислим детали доступных экземпляров книги (BookInstances) включая их статус, ожидаемую дату возврата, штамп (imprint) и id. Это должно позволить нашим читателям не просто узнать о книге, но также убедиться, имеется ли она в наличии и/или когда будет доступна.

URL-преобразования

Откройте /catalog/urls.py и добавьте 'book-detail' URL-преобразование, отмеченное жирным в следующем фрагменте. Эта функция url() определяет паттерн, связанный с обобщённым классом отображения детальной информации, а также имя для данной связи.

python
from django.urls import path
from . import views
from django.conf.urls import url

urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^books/$', views.BookListView.as_view(), name='books'),
    url(r'^book/(?P<pk>\d+)$', views.BookDetailView.as_view(), name='book-detail'),
]

В отличие от предыдущих преобразований, в данном случае мы применяем наше регулярное выражение (РВ) для сопоставления "настоящего паттерна", а не просто строки. Данное РВ сопоставляет любой URL-адрес, который начинается с book/, за которым до конца строки (до маркера конца строки - $) следуют одна, или более цифр. В процессе выполнения данного преобразования, оно "захватывает" цифры и передаёт их в функцию отображения как параметр с именем pk.

Примечание: Как было отмечено ранее, наш преобразуемый URL-адрес в реальности выглядит вот так catalog/book/<digits> (потому что мы находимся в приложении catalog, то подразумевается каталог /catalog/).

Предупреждение: Важно: Обобщённый класс отображения подробной информации ожидает получить параметр с именем pk. Если вы пишете свою собственную функцию отображения, то тогда вы можете использовать параметр с любым именем, который пожелаете, или вообще передавать информацию в безымянном аргументе.

Отдельный пример с регулярными выражениями

Паттерны регулярного выражения является невероятно мощным инструментом преобразования. Пока что, мы не очень много говорили о них, поскольку мы сопоставляли URL-адреса с простыми строками (а не паттернами), и потому что они не интуитивны и пугающий для начинающих.

Примечание: Без паники! Мы будем рассматривать и использовать достаточно простые паттерны и при этом хорошо задокументированные!

В первую очередь вы должны знать что обычно регулярные выражения объявляются при помощи строкового литерала (то есть, они заключены в кавычки: r'<ваше регулярное выражение>').

Главными элементами синтаксиса объявления паттерна, который вы должны знать, являются:

Символ Значение
^ Соответствует началу строки
$ Соответствует концу строки
\d Соответствует цифре (0, 1, 2, ... 9)
\w Соответствует любому символу из алфавита в верхнем- или нижнем- регистре, цифре, или символу подчёркивания (_)
+ Соответствует одному, или более предыдущему символу. Например, для соответствия одной, или более цифре вы должны использовать \d+. Для одного и более символа "a", вы можете использовать a+
* Соответствует отсутствию вообще, или присутствию одного, или более предыдущему символу. Например, для соответствия "ничему", или слову (то есть, любому символу) вы можете использовать \w*
( ) Захват части паттерна внутри скобок. Любое захваченное значение будет передано отображению как безымянный параметр (если захватывается множество паттернов, то соответствующие параметры будут поставляться в порядке их объявления).
(?P<name>...) Захват части паттерна (обозначенного через ...) как именованной переменной (в данном случае <name>). Захваченные значения передаются в отображение с определённым именем. Таким образом, ваше отображение должно объявить аргумент с тем же самым именем!
[ ] Соответствует одному символу из множества. Например, [abc] будет соответствовать либо 'a', или 'b', или 'c'. [-\w] будет соответствовать либо символу '-' , или любому другому словарному символу.

Большинство других символов могут быть заданы буквально!

Давайте рассмотрим несколько реальных примеров паттернов:

Паттерн Описание
r'^book/(?P<pk>\d+)$' Это РВ применяется в нашем url-преобразовании. Оно соответствует строке, которая начинается с book/ (^book/), затем имеет одну, или более цифр (\d+), а затем завершается (цифрой и только цифрой).Оно также захватывает все цифры (?P<pk>\d+) и передаёт их в отображение, в параметре с именем 'pk'. **Захваченные значения всегда передаются как строка!**Например, данному паттерну должна соответствовать следующая строка book/1234 , которая отправляет переменную pk='1234' в отображение.
r'^book/(\d+)$' Этот паттерн соответствует тем же самым URL-адресам как и в предыдущем случае. Захваченная информация будет отправлена в отображение как безымянный параметр.
r'^book/(?P<stub>[-\w]+)$' Данный паттерн соответствует строке, которая начинается с book/ (^book/), затем идут один, или более символов либо '-', или словарные символы ([-\w]+), а затем завершается. Он также захватывает данное множество символов и передаёт их в отображение в параметре с именем 'stub'.Это довольно типичный паттерн для "стаба". Стабы являются дружественными URL-адресами - первичными ключами для данных. Вы могли бы применить стаб, если вы захотели бы, чтобы URL-адрес вашей книги был более информативным. Например, /catalog/book/the-secret-garden, выглядит немного лучше чем /catalog/book/33.

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

Примечание: В качестве дополнительного задания, рассмотрите возможность того, как вы могли бы закодировать url на список всех книг, вышедших в определённый год, месяц, день и какое РВ (паттерн) должно соответствовать этому.

Передача дополнительных настроек в ваши преобразования URL-адресов

Одной возможностью, которую мы не применяли здесь, но которая могла бы быть вам полезной, является то, что вы можете объявлять и передавать дополнительные настройки в отображения. Данные настройки объявляются как словарь, который вы передаёте как третий безымянный аргумент функции url(). Этот способ может быть полезен, если вы хотите воспользоваться тем же самым отображением для нескольких ресурсов и передавать данные для изменения его поведения в каждом отдельном случае (ниже, мы передаём разные имена шаблонов).

python
url(r'^/url/$', views.my_reused_view, {'my_template_name': 'some_path'}, name='aurl'),
url(r'^/anotherurl/$', views.my_reused_view, {'my_template_name': 'another_path'}, name='anotherurl'),

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

Отображение (на основе класса)

Откройте catalog/views.py, и скопируйте следующий код в нижнюю часть файла:

python
class BookDetailView(generic.DetailView):
    model = Book

Это всё! Все что вам надо теперь сделать это создать шаблон с именем /locallibrary/catalog/templates/catalog/book_detail.html, а отображение передаст ему информацию из базы данных для определённой записи Book, выделенной при помощи URL-преобразования. Внутри шаблона вы можете получить доступ к списку книг при помощи переменной с именем object или book (обобщённо "the_model_name").

Если у вас имеется необходимость, то вы можете изменить текущий шаблон и/или имя объекта контекста, используемого для ссылки на книгу в шаблоне. Кроме того, вы можете переопределить методы, например, для добавления дополнительной информации к контексту.

Что произойдёт, если записи не существует?

Если запрашиваемой записи не существует, тогда обобщённый класс отображения подробной информации автоматически "выкинет" исключение Http404 — в продакшене это приведёт к автоматическому отображению страницы с текстом "resource not found" ("ресурс не найден"), которую, конечно же, вы можете настроить по своему усмотрению.

Просто для иллюстрации идеи как это могло бы работать, мы приведём фрагмент кода, демонстрирующего возможную реализацию отображения в виде функции, если по каким-либо причинам вы не используете отображение на основе обобщённого класса.

python
def book_detail_view(request,pk):
    try:
        book_id=Book.objects.get(pk=pk)
    except Book.DoesNotExist:
        raise Http404("Book does not exist")

    #book_id=get_object_or_404(Book, pk=pk)

    return render(
        request,
        'catalog/book_detail.html',
        context={'book':book_id,}
    )

В первую очередь отображение пытается получить определённую запись о книге из модели. Если ей это не удаётся, то "выбрасывается" исключение Http404, которое сигнализирует, что данная книга не найдена "not found". Последним шагом является, как обычно, вызов функции render() с именем соответствующего шаблона и данных о книге, передаваемых в параметре с именем context (в виде словаря).

Примечание: Функция get_object_or_404() (показана закомментированной) является удобным "ярлыком" для генерации исключения Http404 если запись не найдена.

Создание шаблона детальной информации

Создайте HTML файл /locallibrary/catalog/templates/catalog/book_detail.html и скопируйте в него содержимое, представленное ниже. Как было указано ранее, это шаблон "по умолчанию" (имя шаблона), который "ожидается"обобщённым классом отображения детальной информации (для модели с именем Book в приложении с именем catalog).

django
{% extends "base_generic.html" %}

{% block content %}
  <h1>Title: {{ book.title }}</h1>

  <p><strong>Author:</strong> <a href="">{{ book.author }}</a></p> <!-- author detail link not yet defined -->
  <p><strong>Summary:</strong> {{ book.summary }}</p>
  <p><strong>ISBN:</strong> {{ book.isbn }}</p>
  <p><strong>Language:</strong> {{ book.language }}</p>
  <p><strong>Genre:</strong> {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}</p>

  <div style="margin-left:20px;margin-top:20px">
    <h4>Copies</h4>

    {% for copy in book.bookinstance_set.all %}
    <hr>
    <p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'd' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
    {% if copy.status != 'a' %}<p><strong>Due to be returned:</strong> {{copy.due_back}}</p>{% endif %}
    <p><strong>Imprint:</strong> {{copy.imprint}}</p>
    <p class="text-muted"><strong>Id:</strong> {{copy.id}}</p>
    {% endfor %}
  </div>
{% endblock %}

Примечание: Ссылка на автора в шаблоне содержит пустой URL-адрес, потому что мы ещё не создали страницу детальной информации об авторе. Когда это произойдёт, вы должны будете обновить данный URL-адрес как указано ниже:

django
<a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a>

Хотя и несколько больше, но почти все в данном шаблоне нам уже встречалось ранее:

  • Мы расширяем наш базовый шаблон и переопределяем блок content.
  • Мы используем условие if для показа того, или иного содержимого.
  • Мы используем циклы for того, чтобы пробежаться по элементам (объектам) в соответствующих списках.
  • Мы получаем доступ к полям контекста при помощи "дот-нотации" (поскольку мы использовали обобщённый класс отображения детальной информации, то контекст имеет имя book; также можем использовать имя object)

Одной интересной вещью, которую мы не видели ранее, является функция book.bookinstance_set.all(). Данный метод является "автомагически"-сконструированным Django для того, чтобы вернуть множество записей BookInstance, связанных с данной книгой Book.

django
{% for copy in book.bookinstance_set.all %}
<!-- итерации по каждой копии/экземпляру книги -->
{% endfor %}

Этот метод создан, потому что вы, на стороне "многим" данной связи, объявили поле ForeignKey (один-ко многим). Поскольку вы ничего не объявили на другой стороне ("один") данной модели (то есть, модель Book "ничего не знает" про модель BookInstance), то она не имеет никакой возможности (по умолчанию) для получения множества соответствующих записей. Для того, чтобы обойти эту проблему, Django конструирует соответствующую функцию "обратного просмотра" ("reverse lookup"), которой вы можете воспользоваться. Имя данной функции создаётся в нижнем регистре и состоит из имени модели, в которой был объявлен ForeignKey (то есть, bookinstance), за которым следует _set (то есть функция, созданная для Book будет иметь вид bookinstance_set()).

Примечание: Здесь мы используем all() для получения всех записей (по умолчанию). Вы, наверное, могли бы использовать метод filter() для получения подмножества записей в коде, но, к сожалению, вы НЕ можете применить данный вызов в шаблоне, потому что вы не можете передать в нем (в шаблоне) аргументы в функцию.

Обратите внимание, что если вы не определяете порядок выдачи данных (в вашем отображении, или в модели), то сервер разработки "выкинет" сообщения об ошибках, похожие на следующие:

[29/May/2017 18:37:53] "GET /catalog/books/?page=1 HTTP/1.1" 200 1637
/foo/local_library/venv/lib/python3.5/site-packages/django/views/generic/list.py:99: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <QuerySet [<Author: Ortiz, David>, <Author: H. McRaven, William>, <Author: Leigh, Melinda>]>
  allow_empty_first_page=allow_empty_first_page, **kwargs)

Это случилось потому что, paginator object (далее объект постраничного вывода) ожидает видеть некую упорядоченность ORDER BY при запросе к базе данных. Без этого, он не сможет гарантировать правильный вывод полученных данных!

Данное руководство пока не дошло до описания Pagination (пока, но скоро будет), и поскольку вы не можете использовать функцию sort_by() и передавать параметр (по той же причине, что и filter()) вы должны выбрать один из трёх вариантов дальнейших действий:

  1. Добавить атрибут ordering внутри Meta-класса объявленного в вашей модели.
  2. Добавить атрибут queryset в вашей реализации класса отображения, определяющего order_by().
  3. Добавить метод get_queryset в вашу реализацию класса отображения и также определить метод order_by().

Если вы выбрали пункт номер один с Meta-классом для модели Author (вероятно, не такой гибкий как вариант с настройкой класса отображения, но тем не менее, достаточно простой), вы должны прийти к чему-то похожему на следующее:

python
class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

    class Meta:
        ordering = ['last_name']

Конечно же, поле не обязательно должно иметь имя last_name: оно может быть любым.

И последнее, но не окончательное, вы должны сортировать по атрибуту/колонке, которая была проиндексирована (уникально, или нет) в вашей базе данных для того, чтобы избежать проблем с быстродействием. Конечно, это не является необходимым в данном примере (и мы, вероятно, забегаем далеко вперёд), если у нас такое небольшое количество книг (и пользователей!), но это необходимо помнить для будущих проектов.

Как это теперь выглядит?

На данный момент мы должны были создать все необходимое для показа страниц со списком книг и детальной информацией. Запустите сервер (python3 manage.py runserver) и откройте ваш браузер http://127.0.0.1:8000/.

Предупреждение: Не кликайте на каком-либо авторе, - ссылки пока не заданы — это будет вашим дополнительным заданием!

Кликните ссылку All books для перехода на страницу со списком книг.

Book List Page

Затем кликните на ссылку одной из ваших книг. Если все настроено как надо, то вы должны увидеть то, что указано на картинке.

Book Detail Page

Постраничный вывод (Pagination)

Если у вас всего лишь несколько записей в базе данных, то наша страница вывода списка книг будет выглядеть отлично. Тем не менее, когда у вас появятся десятки, или сотни записей ваша страница станет значительно дольше загружаться (и станет слишком длинной для комфортного просмотра). Решением данной проблемы является добавление постраничного вывода (Pagination) к вашему отображению списка, который будет выводить ограниченное количество элементов на каждой странице.

Django имеет отличный встроенный механизм для постраничного вывода. Даже более того, он встроен в обобщённый класс отображения списков, следовательно вам не нужно проделывать большой объем работы, чтобы воспользоваться возможностями постраничного вывода!

Отображения

Откройте catalog/views.py и добавьте поле paginate_by как показано жирным в следующем фрагменте.

python
class BookListView(generic.ListView):
    model = Book
    paginate_by = 10

Как только у вас появится более 10 записей в базе данных отображение начнёт формировать постраничный вывод данных, которые он передаёт шаблону. К различным страницам данного вывода можно получить доступ при помощи параметров GET-запроса — к странице 2 вы можете получить доступ, используя URL-адрес: /catalog/books/?page=2.

Шаблоны

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

Откройте /locallibrary/catalog/templates/base_generic.html и, ниже блока content, вставьте блок (во фрагменте не выделен жирным), отвечающий за постраничный вывод. Данный код, в первую очередь, проверяет "включён" ли механизм постраничного вывода для данной страницы и если это так, то он добавляет ссылки next и previous, соответственно (а также, номер текущей страницы).

django
{% block content %}{% endblock %}

{% block pagination %}
  {% if is_paginated %}
    <div class="pagination">
      <span class="page-links">
        {% if page_obj.has_previous %}
          <a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="page-current">
          Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
          <a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
        {% endif %}
      </span>
    </div>
  {% endif %}
{% endblock %}

Параметр page_obj является объектом типа Paginator, который будет создаваться каждый раз, когда будет применяться постраничный вывод данных для текущей страницы. Он позволяет получить всю информацию о текущей странице, о предыдущих страницах, сколько всего страниц и так далее.

Мы используем {{ request.path }} для получения URL-адреса текущей страницы, для того, чтобы создать ссылки на соответствующие страницы, обратите внимание, что данный вызов не зависит от объекта page_obj и, таким образом, может использоваться отдельно.

На этом все!

Как это выглядит?

Картинка ниже показывает как выглядит постраничный вывод — если вы не добавили более 10 записей в вашу базу данных, тогда вы можете проверить как это работает, просто уменьшив значение в paginate_by, в файле catalog/views.py. Для получения результата, соответствующего картинке ниже, мы изменили paginate_by = 2.

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

Book List Page - paginated

Проверьте себя

Дополнительным задание в данной статье и для завершения данного этапа проекта будет создание отображений детальной информации об авторе и их списка. Эти отображения должны находиться по следующим адресам:

  • catalog/authors/ — Список авторов.
  • catalog/author/<id> — Детальная информация об авторе со значением первичного ключа равным <id>

Соответствующий код для URL-преобразований и отображений должен быть идентичным коду для списка книг и детальной информации о книге Book, который мы создали ранее. Шаблоны будут отличаться, но будут иметь похожее поведение.

Примечание:

  • Когда вы создадите URL-преобразование для страницы списка авторов вам понадобится обновить ссылку All authors в базовом шаблоне. Следуйте тем же путём, который мы проделали когда обновляли ссылку All books.

  • Когда вы создадите URL-преобразование для страницы с детальной информацией об авторе, вы должны будете обновить шаблон детальной информации о книге (/locallibrary/catalog/templates/catalog/book_detail.html), таким образом, чтобы ссылка автора указывала на страницу с детальной информации о нем (а не быть пустой). Данная ссылка будет иметь вид как указано жирным во фрагменте ниже.

    django
    <p>
      <strong>Author:</strong>
      <a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a>
    </p>
    

Когда вы закончите, ваши страницы должны будут выглядеть как на картинке.

Author List Page

Author Detail Page

Итоги

Поздравляем! Наша базовая функциональность библиотеки готова!

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

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

Дополнительная информация