Встроенные обобщенные представления на основе классов
Написание веб-приложений может быть монотонным, потому что мы постоянно повторяем одни и те же шаблоны. 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" — часть «книги» взята из имени приложения, которое определяет модель, а часть «издатель» — просто нижний регистр имени модели.
Примечание
Таким образом, когда (например) параметр 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». Обобщенные представления имеют параметр 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.2/topics/class-based-views/generic-display/