Встроенные классовые обобщенные представления
Написание веб-приложений может быть монотонным, потому что мы постоянно повторяем одни и те же шаблоны. 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)
Как вы можете видеть, довольно легко добавить больше логики к выбору набора данных; если бы мы захотели, мы могли бы использовать 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/2.1/topics/class-based-views/generic-display/