Встроенные обобщённые представления на основе классов
Создание веб-приложений может быть монотонным, поскольку мы постоянно повторяем одни и те же шаблоны. Django пытается избавить от этой монотонности на уровнях модели и шаблонов, но веб-разработчики также сталкиваются с этим утомлением на уровне представлений.
Обобщённые представления Django были разработаны, чтобы облегчить эту задачу. Они берут некоторые распространённые выражения и шаблоны, используемые при разработке представлений, и абстрагируют их, чтобы вы могли быстро создавать общие представления данных, не написав слишком много кода.
Мы можем выделить определённые общие задачи, например, отображение списка объектов, и написать код, который отображает список любых объектов. Затем соответствующую модель можно передать как дополнительный аргумент в URLconf.
Django поставляется с обобщёнными представлениями для выполнения следующих задач:
- Отображение страниц списка и деталей для одного объекта. Если мы создаём приложение для управления конференциями, то
TalkListViewиRegisteredUserListViewбудут примерами представлений списка. Страница с информацией об отдельном докладе является примером представления «деталей». - Представление объектов, связанных с датами, на страницах архивов по годам/месяцам/дням, связанных страницах деталей и страницах «последних» объектов.
- Разрешение пользователям создавать, обновлять и удалять объекты — с или без авторизации.
В совокупности эти представления предоставляют интерфейсы для выполнения наиболее распространённых задач, с которыми сталкиваются разработчики.
Расширение обобщённых представлений
Нет сомнений, что использование обобщённых представлений существенно ускоряет разработку. Однако в большинстве проектов наступает момент, когда обобщённые представления больше не достаточны. На самом деле, один из самых частых вопросов новых разработчиков Django заключается в том, как заставить обобщённые представления обрабатывать более широкий спектр ситуаций.
Это одна из причин, по которой обобщённые представления были переработаны для выпуска 1.3 — ранее они были функциями представлений с обширным набором опций; теперь, вместо того, чтобы передавать большое количество конфигурации в URLconf, рекомендуемый способ расширения обобщённых представлений — наследование от них и переопределение их атрибутов или методов.
Однако у обобщённых представлений есть предел. Если вы обнаружите, что испытываете трудности с реализацией своего представления в виде подкласса обобщённого представления, то, возможно, более эффективным будет написание нужного вам кода, используя собственные представления на основе классов или функциональные представления.
Дополнительные примеры обобщённых представлений доступны в некоторых сторонних приложениях, или вы можете написать свои собственные по мере необходимости.
Обобщённые представления объектов
TemplateView безусловно полезен, но обобщённые представления Django действительно выделяются, когда речь идёт о представлении данных из вашей базы данных. Поскольку это такая распространённая задача, Django поставляется с несколькими встроенными обобщёнными представлениями, которые помогают генерировать представления списка и деталей объектов.
Давайте начнём с рассмотрения некоторых примеров отображения списка объектов или отдельного объекта.
Мы будем использовать следующие модели:
# models.py
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()
class Meta:
ordering = ["-name"]
def __str__(self):
return self.name
class Author(models.Model):
salutation = models.CharField(max_length=10)
name = models.CharField(max_length=200)
email = models.EmailField()
headshot = models.ImageField(upload_to='author_headshots')
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField('Author')
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
publication_date = models.DateField()
Теперь нам нужно определить представление:
# views.py
from django.views.generic import ListView
from books.models import Publisher
class PublisherList(ListView):
model = Publisher
И, наконец, подключить это представление к вашим URL:
# urls.py
from django.urls import path
from books.views import PublisherList
urlpatterns = [
path('publishers/', PublisherList.as_view()),
]
Это всё Python-код, который нам нужно написать. Однако нам всё ещё нужно написать шаблон. Мы могли бы явно указать представлению, какой шаблон использовать, добавив атрибут template_name к представлению, но в отсутствие явного шаблона Django выведет шаблон из имени объекта. В этом случае выведенным шаблоном будет "books/publisher_list.html" — часть «books» взята из имени приложения, определяющего модель, а часть «publisher» — это нижний регистр имени модели.
Примечание
Таким образом, когда (например) параметр APP_DIRS бэкенда DjangoTemplates установлен в значение True в TEMPLATES, расположение шаблона может быть: /path/to/project/books/templates/books/publisher_list.html
Этот шаблон будет рендериться с контекстом, содержащим переменную под названием object_list , которая содержит все объекты издателей. Шаблон может выглядеть так:
{% extends "base.html" %}
{% block content %}
<h2>Publishers</h2>
<ul>
{% for publisher in object_list %}
<li>{{ publisher.name }}</li>
{% endfor %}
</ul>
{% endblock %}
И на самом деле, это всё. Все интересные возможности обобщённых представлений исходят от изменения атрибутов, заданных в обобщённом представлении. В документации справочника по обобщённым представлениям подробно описаны все обобщённые представления и их параметры; остальная часть этого документа рассмотрит некоторые распространённые способы настройки и расширения обобщённых представлений.
Создание «дружественных» контекстов шаблонов
Вы, возможно, заметили, что в нашем примере шаблона списка издателей все издатели хранятся в переменной с именем object_list. Хотя это работает нормально, это не очень «дружелюбно» для авторов шаблонов: им нужно «просто знать», что они имеют дело с издателями.
Ну, если вы имеете дело с объектом модели, это уже сделано за вас. При работе с объектом или набором запросов Django может заполнить контекст, используя нижний регистр имени класса модели. Это предоставляется дополнительно к стандартной записи object_list, но содержит точно такие же данные, т.е. publisher_list.
Если это всё ещё не подходит, вы можете вручную установить имя переменной контекста. Атрибут context_object_name в обобщённом представлении указывает переменную контекста для использования:
# views.py
from django.views.generic import ListView
from books.models import Publisher
class PublisherList(ListView):
model = Publisher
context_object_name = 'my_favorite_publishers'
Предоставление полезного context_object_name всегда хорошая идея. Ваши коллеги, которые разрабатывают шаблоны, скажут вам спасибо.
Добавление дополнительного контекста
Часто вам необходимо предоставить дополнительную информацию помимо той, которая предоставляется обобщённым представлением. Например, представьте себе отображение списка всех книг на каждой странице деталей издателя. Обобщённое представление DetailView предоставляет издателя в контексте, но как получить дополнительную информацию в этом шаблоне?
Ответ заключается в наследовании от DetailView и обеспечении собственной реализации метода get_context_data. Стандартная реализация добавляет отображаемый объект в шаблон, но вы можете переопределить её, чтобы передать больше данных:
from django.views.generic import DetailView
from books.models import Book, Publisher
class PublisherDetail(DetailView):
model = Publisher
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in a QuerySet of all the books
context['book_list'] = Book.objects.all()
return context
Примечание
Как правило, get_context_data объединит данные контекста всех родительских классов с данными текущего класса. Чтобы сохранить это поведение в собственных классах, где вы хотите изменить контекст, убедитесь, что вы вызываете get_context_data в родительском классе. Когда два класса не пытаются определить один и тот же ключ, это даст ожидаемые результаты. Однако если любой класс пытается переопределить ключ после того, как родительские классы установили его (после вызова super), все дочерние классы этого класса также должны явно установить его после super, если они хотят быть уверены, что переопределяют все родители. Если у вас возникнут проблемы, ознакомьтесь с порядком разрешения методов вашего представления.
Ещё один момент — данные контекста из обобщённых представлений на основе классов перекроют данные, предоставленные обработчиками контекста; см. get_context_data() для примера.
Просмотр подмножеств объектов
Теперь давайте посмотрим поближе на аргумент model , который мы использовали всё время. Аргумент model , который определяет модель базы данных, с которой будет работать представление, доступен во всех обобщённых представлениях, которые работают с одним объектом или набором объектов. Однако аргумент model — это не единственный способ указать объекты, с которыми будет работать представление — вы также можете указать список объектов, используя аргумент queryset:
from django.views.generic import DetailView
from books.models import Publisher
class PublisherDetail(DetailView):
context_object_name = 'publisher'
queryset = Publisher.objects.all()
Указание model = Publisher — это сокращение для queryset =
Publisher.objects.all(). Однако, используя queryset для определения отфильтрованного списка объектов, вы можете более точно указать объекты, которые будут отображаться в представлении (см. Создание запросов для получения дополнительной информации об объектах QuerySet и см. справочник по представлениям на основе классов для получения всех подробностей).
Например, мы можем отсортировать список книг по дате публикации, начиная с самых последних:
from django.views.generic import ListView
from books.models import Book
class BookList(ListView):
queryset = Book.objects.order_by('-publication_date')
context_object_name = 'book_list'
Это довольно минимальный пример, но он хорошо иллюстрирует идею. Конечно, вы обычно захотите сделать больше, чем просто переупорядочить объекты. Если вы хотите представить список книг определённого издателя, вы можете использовать ту же технику:
from django.views.generic import ListView
from books.models import Book
class AcmeBookList(ListView):
context_object_name = 'book_list'
queryset = Book.objects.filter(publisher__name='ACME Publishing')
template_name = 'books/acme_list.html'
Обратите внимание, что наряду с отфильтрованным queryset, мы также используем имя пользовательского шаблона. Если бы мы этого не сделали, обобщённое представление использовало бы тот же шаблон, что и «стандартный» список объектов, что, возможно, нам не нужно.
Также обратите внимание, что это не очень элегантный способ выполнения задач, специфичных для издателя. Если мы хотим добавить ещё одну страницу издателя, нам нужно будет добавить ещё несколько строк в URLconf, а с большим количеством издателей это станет неразумно. Мы решим эту проблему в следующем разделе.
Примечание
Если при запросе /books/acme/ вы получаете 404, убедитесь, что у вас действительно есть издатель с именем «ACME Publishing». Для этого случая у обобщённых представлений есть параметр allow_empty . См. справочник по представлениям на основе классов для получения дополнительной информации.
Динамическая фильтрация
Ещё одна распространённая потребность — фильтрация объектов на странице списка по какому-либо ключу в URL. Ранее мы жёстко задавали имя издателя в URLconf, но что, если мы захотим написать представление, которое отображает все книги некоторого произвольного издателя?
Удобно, что у ListView есть метод get_queryset(), который мы можем переопределить. По умолчанию он возвращает значение атрибута queryset , но мы можем использовать его для добавления дополнительной логики.
Ключевой момент для работы заключается в том, что при вызове представлений на основе классов различные полезные вещи хранятся в self; а также сам запрос (self.request) , включая позиционные (self.args) и основанные на имени (self.kwargs) аргументы, захваченные в соответствии с URLconf.
Здесь у нас есть URLconf с одной захваченной группой:
# urls.py
from django.urls import path
from books.views import PublisherBookList
urlpatterns = [
path('books/<publisher>/', PublisherBookList.as_view()),
]
Далее, мы напишем само представление PublisherBookList:
# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher
class PublisherBookList(ListView):
template_name = 'books/books_by_publisher.html'
def get_queryset(self):
self.publisher = get_object_or_404(Publisher, name=self.kwargs['publisher'])
return Book.objects.filter(publisher=self.publisher)
Использование get_queryset для добавления логики к выбору набора данных так же удобно, как и мощно. Например, если бы мы захотели, мы могли бы использовать self.request.user для фильтрации по текущему пользователю или другой более сложной логике.
Мы также можем добавить издателя в контекст одновременно, чтобы мы могли использовать его в шаблоне:
# ...
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in the publisher
context['publisher'] = self.publisher
return context
Выполнение дополнительных задач
Последний распространённый шаблон, который мы рассмотрим, предполагает выполнение дополнительных задач до или после вызова обобщённого представления.
Представьте, что у нас есть поле last_accessed в нашей модели Author , которое мы использовали для отслеживания последнего времени, когда кто-либо просматривал этого автора:
# models.py
from django.db import models
class Author(models.Model):
salutation = models.CharField(max_length=10)
name = models.CharField(max_length=200)
email = models.EmailField()
headshot = models.ImageField(upload_to='author_headshots')
last_accessed = models.DateTimeField()
Обобщённый класс DetailView , конечно, ничего не знал бы об этом поле, но, опять же, мы могли бы легко написать пользовательское представление для обновления этого поля.
Сначала нам нужно добавить деталь об авторе в URLconf, чтобы указать на пользовательское представление:
from django.urls import path
from books.views import AuthorDetailView
urlpatterns = [
#...
path('authors/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
]
Затем мы напишем наше новое представление — get_object — это метод, который извлекает объект, — поэтому мы переопределяем его и оборачиваем вызов:
from django.utils import timezone
from django.views.generic import DetailView
from books.models import Author
class AuthorDetailView(DetailView):
queryset = Author.objects.all()
def get_object(self):
obj = super().get_object()
# Record the last accessed date
obj.last_accessed = timezone.now()
obj.save()
return obj
Примечание
URLconf здесь использует именованную группу pk — это имя по умолчанию, которое DetailView использует для поиска значения первичного ключа, используемого для фильтрации набора данных.
Если вы хотите назвать группу по-другому, вы можете установить pk_url_kwarg в представлении. Более подробные сведения можно найти в справочнике по DetailView
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/3.0/topics/class-based-views/generic-display/