Встроенные общие представления на основе классов¶
Написание веб-приложений может быть монотонным, потому что мы повторяем определенные шаблоны снова и снова. 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
Наконец, подключите это представление к вашим урлам:
# 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 %}
Вот, собственно, и все. Все крутые возможности общих представлений появляются благодаря изменению атрибутов, установленных для общего представления. В generic views reference подробно описаны все общие представления и их опции; в остальной части этого документа будут рассмотрены некоторые из распространенных способов настройки и расширения общих представлений.
Создание «дружественных» контекстов шаблонов¶
Вы могли заметить, что в нашем примере шаблона списка издателей все издатели хранятся в переменной с именем 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, и более чем для нескольких издательств это будет неразумно. Мы рассмотрим эту проблему в следующем разделе.
Примечание
Если вы получаете 404 при запросе /books/acme/
, проверьте, действительно ли у вас есть издатель с именем „ACME Publishing“. Общие представления имеют параметр allow_empty
для этого случая. Более подробную информацию см. в class-based-views reference.
Динамическая фильтрация¶
Другая распространенная потребность - отфильтровать объекты, представленные на странице списка, по некоторому ключу в 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
в представлении.