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

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

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

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

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

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

  • Представлять объекты-даты в указателях год/месяц/день в архиве страниц, связанные детальные данные, и страницы “последние(latest)”.

  • Позволять пользователям(как авторизованным так и нет) создавать, обновлять и удалять объекты.

Собранные вместе, эти представления предоставляют разработчику интерфейс для решения наиболее часто встречаемых задач.

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

Нет сомнений, что использование общих представлений может существенно ускорить разработку . Однако, в большинстве проектов, наступает момент, когда общие представления перестают быть эффективными и достаточными. Действительно, наиболее распространенный вопрос, который задают новые 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):              # __unicode__ on Python 2
        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):              # __unicode__ on Python 2
        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.conf.urls import url
from books.views import PublisherList

urlpatterns = [
    url(r'^publishers/$', PublisherList.as_view()),
]

Это весь код на Python, который нам необходимо написать. Хотя, нам еще необходим шаблон. Мы можем явно указать в представлении, какой шаблон мы хотим использовать. Для этого мы должны добавить в представление атрибут template_name, с указанием имени шаблона. Если явно не указывать этот атрибут, Django “вычислит” его из названия объекта. В данном случае, таким “вычисленным” шаблоном будет "books/publisher_list.html" – часть “books” берется из имени приложения, определяющего модель, а часть “publisher” - это просто название модели в нижнем регистре.

Примечание

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

При обработке шаблона (рендеринге), будет использоваться контекст, содержащий переменную object_list. Это переменная хранит список всех объектов издателей(publisher). Очень простой шаблон мог бы выглядеть так:

{% extends "base.html" %}

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

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

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

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

Что ж, если вы имеете дело с моделью, - то считайте что все уже сделано. Если вы оперируете запросом(queryset) или объектом, 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'

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

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

Часто возникает потребность передать в контекст некоторые дополнительные данные, помимо тех, что автоматически предоставляются представлением. Н-р, представим что нам необходимо отобразить список всех книг на странице с детальной информацией об издателе. Представление-класс DetailView предоставляет нам только переменную контекста, содержащую данные об издателе, но как нам передать дополнительную информацию в шаблон?

Вот решение: вы можете создать подкласс от DetailView и переопределить в нем метод get_context_data. Реализация метода по умолчанию просто добавляет объект, который будет доступен в шаблоне. Но переопределив метод, вы можете добавить любые дополнительные данные(расширить контекст):

from django.views.generic import DetailView
from books.models import Publisher, Book

class PublisherDetail(DetailView):

    model = Publisher

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super(PublisherDetail, self).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()

Запись 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, а добавление более чем несколько издателей обернется непомерно высокими накладными расходами. Мы разберемся с этой проблемой в следующем разделе.

Примечание

Если при запросе к /books/acme/ вы получаете ошибку 404, убедитесь, что для модели Publisher существует издатель с именем ‘ACME Publishing’. Общие представления-классы предоставляют на этот случай параметр allow_empty. Смотрите подробнее в class-based-views reference .

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

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

Для этого удобно воспользоваться методом get_queryset() класса ListView, который мы можем переопределить для наших целей. Без переопределения метод возвращает значение атрибута queryset, но мы собираемся расширить его поведение.

Ключевым моментом в выполнении этой работы, является понимание того, что при вызове представления-класса, в его ссылке на экземпляр self сохраняется много “полезных вещей”. Н-р, там сохраняется экземпляр текущего запроса (request (в self.request)), а также список позиционных (self.args) и именованных (self.kwargs) аргументов, которые “отлавливаются” из строки запроса в URLconf.

Вот, например, у нас есть строка в URLconf, “захватывающая” одиночную группу:

# urls.py
from django.conf.urls import url
from books.views import PublisherBookList

urlpatterns = [
    url(r'^books/([\w-]+)/$', 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.args[0])
        return Book.objects.filter(publisher=self.publisher)

Как вы можете видеть, добавление дополнительной логики в генерацию queryset, - это совсем несложно. Если бы нам потребовалось, мы могли бы использовать значение self.request.user чтобы отфильтровать данные с учетом текущего пользователя, или добавить любую другую более сложную логику.

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

# ...

def get_context_data(self, **kwargs):
    # Call the base implementation first to get a context
    context = super(PublisherBookList, self).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.conf.urls import url
from books.views import AuthorDetailView

urlpatterns = [
    #...
    url(r'^authors/(?P<pk>[0-9]+)/$', AuthorDetailView.as_view(), name='author-detail'),
]

Затем мы создадим наше новое представление: get_object– это метод, который получает и возвращает “рабочий” объект, таким образом, нам нужно лишь переопределить его(не забыв вызвать метод родительского класса):

from django.views.generic import DetailView
from django.utils import timezone
from books.models import Author

class AuthorDetailView(DetailView):

    queryset = Author.objects.all()

    def get_object(self):
        # Call the superclass
        object = super(AuthorDetailView, self).get_object()
        # Record the last accessed date
        object.last_accessed = timezone.now()
        object.save()
        # Return the object
        return object

Примечание

В URLconf указана именованная группа pk. Это имя является именем по умолчанию, которое использует DetailView для определения значения первичного ключа и последующей фильтрации объектов в запросе (в queryset).

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