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

При создании веб-приложений мы часто снова и снова повторяем определенные шаблоны в разработке, что делает саму разработку скучной и монотонной. Django старается избавить нас от этой монотонности при работе с моделями и шаблонами, но и на уровне создания представлений часто возникают такие же проблемы.

Общие представления(generic views) Django были разработаны, чтобы избавить нас от этой скуки. В их основе лежит набор идиом и шаблонов, базирующийся на практическом опыте создания представлений, который дает нам абстрактный каркас для быстрого создания собственных представлений, без необходимости писать лишний, повторяющийся код.

Мы можем выделить некоторые общие задачи, такие как отображение списка объектов, и написать код, который будет отображать список любых объектов. Затем мы можем указать модель, хранящую нужные объекты, и передать ее как дополнительный аргумент в URLconf.

Django содержит встроенный набор общих представлений, которые могут делать следующее:

  • Отображать список и страницу подробной информации для одиночного объекта. Если бы мы создавали приложение для управления обсуждениями, то TalkListView и RegisteredUserListView могли бы использоваться как представления для отображения списков. Отдельная страница с обсуждениями, может быть примером того, что мы зазываем «подробное(detail)» представление.
  • Представлять объекты-даты в указателях год/месяц/день в архиве страниц, связанные детальные данные, и страницы «последние(latest)».
  • Позволять пользователям(как авторизованным так и нет) создавать, обновлять и удалять объекты.

Taken together, these views provide interfaces to perform the most common tasks developers encounter.

Расширение общих представлений

Нет сомнений, что использование общих представлений может существенно ускорить разработку . Однако, в большинстве проектов, наступает момент, когда общие представления перестают быть эффективными и достаточными. Действительно, наиболее распространенный вопрос, который задают новые Django разработчики, как заставить базовые представления обрабатывать более широкий спектр ситуаций.

This is one of the reasons generic views were redesigned for the 1.3 release - previously, they were view functions with a bewildering array of options; now, rather than passing in a large amount of configuration in the URLconf, the recommended way to extend generic views is to subclass them, and override their attributes or methods.

Тем не менее, общие представления имеют свои пределы. Если реализация вашего представления, как подкласса общего представления вызывает большие трудности, то вероятнее более эффективным будет написание необходимого вам кода с использованием ваших собственных представлений-классов или представлений-функций.

Больше примеров использования общих представлений вы можете найти в некоторых приложениях сторонних разработчиков, или создать свое собственное, выполняющее ваши задачи.

Общие представления и отображение объектов

TemplateView certainly is useful, but Django’s generic views really shine when it comes to presenting views of your database content. Because it’s such a common task, Django comes with a handful of built-in generic views to help generate list and detail views of objects.

Давайте начнем с рассмотрения примеров, показывающих получение списка объектов или индивидуального объекта.

Мы будем использовать следующие модели:

# 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()),
]

That’s all the Python code we need to write. We still need to write a template, however. We could explicitly tell the view which template to use by adding a template_name attribute to the view, but in the absence of an explicit template Django will infer one from the object’s name. In this case, the inferred template will be "books/publisher_list.html" – the «books» part comes from the name of the app that defines the model, while the «publisher» bit is the lowercased version of the model’s name.

Примечание

Таким образом, если (например) опция APP_DIRS для бэкенда DjangoTemplates равна True, то путь к шаблону будет следующим : /path/to/project/books/templates/books/publisher_list.html

This template will be rendered against a context containing a variable called object_list that contains all the publisher objects. A template might look like this:

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

Это действительно все, что нужно сделать. Все крутые «фичи» общих представлений-классов можно получить лишь устанавливая значения определенных атрибутов в представлении. В разделе общие представления вы найдете детальное описание всех общих представлений; в оставшейся части раздела мы рассмотрим общие подходы в расширении и модификации общих представлений.

Создание «дружелюбного» контента для шаблона

Вы должны были обратить внимание, что в нашем примере список издателей хранится в переменной с именем object_list. И хотя все прекрасно работает, с нашей стороны это не сильно «дружелюбно» к разработчикам шаблонов: они должны «как-то понять», что имеют здесь дело со списком издателей.

Well, if you’re dealing with a model object, this is already done for you. When you are dealing with an object or queryset, Django is able to populate the context using the lowercased version of the model class“ name. This is provided in addition to the default object_list entry, but contains exactly the same data, i.e. 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'

Задать переменной контекста понятное имя(отражающее его предназначение) - это всегда замечательно. Ваши коллеги, занимающиеся разработкой шаблонов, будут вам благодарны.

Добавление дополнительного контента

Often you need to present some extra information beyond that provided by the generic view. For example, think of showing a list of all the books on each publisher detail page. The DetailView generic view provides the publisher to the context, but how do we get additional information in that template?

The answer is to subclass DetailView and provide your own implementation of the get_context_data method. The default implementation adds the object being displayed to the template, but you can override it to send more:

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), если необходимо гарантировать полное переопределение данных родителей. Если у вас возникли проблемы, просмотрите mro(method resolution order) вашего представления.

Также обратите внимание, что данные контекста представления-классов могут перезаписывать данные контекстных процессоров, для примера смотрите 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()

Specifying model = Publisher is shorthand for saying queryset = Publisher.objects.all(). However, by using queryset to define a filtered list of objects you can be more specific about the objects that will be visible in the view (see Выполнение запросов for more information about QuerySet objects, and see the class-based views reference for the complete details).

To pick an example, we might want to order a list of books by publication date, with the most recent first:

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'

That’s a pretty minimal example, but it illustrates the idea nicely. Of course, you’ll usually want to do more than just reorder objects. If you want to present a list of books by a particular publisher, you can use the same technique:

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, убедитесь, что для модели Publisher существует издатель с именем „ACME Publishing“. Общие представления-классы предоставляют на этот случай параметр allow_empty. Смотрите подробнее в class-based-views reference .

Динамическое фильтрование

Другой часто встречающейся задачей является необходимость отфильтровать список объектов по переданному в URL ключу. Ранее мы жестко прописывали имя издателя в URLconf, но что если нам необходимо написать представление, отображающее все книги некоторого произвольного издателя?

Handily, the ListView has a get_queryset() method we can override. By default, it returns the value of the queryset attribute, but we can use it to add more logic.

Ключевым моментом в выполнении этой работы, является понимание того, что при вызове представления-класса, в его ссылке на экземпляр self сохраняется много «полезных вещей». Н-р, там сохраняется экземпляр текущего запроса (request (в 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)

Using get_queryset to add logic to the queryset selection is as convenient as it is powerful. For instance, if we wanted, we could use self.request.user to filter using the current user, or other more complex logic.

Одновременно мы можем также добавить издателя в контекст, и затем использовать это значение в шаблоне

# ...

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'),
]

Then we’d write our new view – get_object is the method that retrieves the object – so we override it and wrap the call:

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 для определения значения первичного ключа и последующей фильтрации объектов в запросе (в queryset).

Если вы хотите задать для группы другое имя, вы можете указать его в атрибуте pk_url_kwarg представления. За подробностями обращайтесь к дополнительной информации для DetailView.