Встроенные общие представления на основе классов

Написание веб-приложений может быть монотонным, потому что мы повторяем определенные шаблоны снова и снова. 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, а также см. class-based views reference для получения полной информации).

В качестве простого примера можно привести список книг по дате публикации, причем самая последняя должна быть первой:

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)

Как вы можете видеть, довольно легко добавить больше логики в выбор кверисета; если бы мы захотели, мы могли бы использовать 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

Вернуться на верх