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

Writing web applications can be monotonous, because we repeat certain patterns again and again. Django tries to take away some of that monotony at the model and template layers, but web developers also experience this boredom at the view level.

Для облегчения этой боли были разработаны генеративные представления в 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 PublisherListView(ListView):
    model = Publisher

Наконец, подключите это представление к вашим урлам:

# urls.py
from django.urls import path
from books.views import PublisherListView

urlpatterns = [
    path('publishers/', PublisherListView.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 PublisherListView(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 PublisherDetailView(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 PublisherDetailView(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 BookListView(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 AcmeBookListView(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 PublisherBookListView

urlpatterns = [
    path('books/<publisher>/', PublisherBookListView.as_view()),
]

Next, we’ll write the PublisherBookListView view itself:

# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher

class PublisherBookListView(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 использует для поиска значения первичного ключа, используемого для фильтрации набора запросов.

If you want to call the group something else, you can set pk_url_kwarg on the view.

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