Использование миксинов с представлениями на основе классов

Осторожно

Это продвинутая тема. Перед изучением этих техник рекомендуется иметь рабочие знания Django’s class-based views.

Встроенные в Django представления на основе классов предоставляют много функциональных возможностей, но некоторые из них вы можете захотеть использовать отдельно. Например, вы можете захотеть написать представление, которое отображает шаблон для создания HTTP ответа, но вы не можете использовать TemplateView; возможно, вам нужно отобразить шаблон только на POST, а GET будет делать что-то совсем другое. Хотя вы можете использовать TemplateResponse напрямую, это, скорее всего, приведет к дублированию кода.

По этой причине Django также предоставляет ряд миксинов, которые обеспечивают более дискретную функциональность. Рендеринг шаблонов, например, инкапсулирован в TemplateResponseMixin. Справочная документация Django содержит full documentation of all the mixins.

Контекст и шаблонные ответы

Предоставляются два центральных миксина, которые помогают обеспечить согласованный интерфейс для работы с шаблонами в представлениях, основанных на классах.

TemplateResponseMixin

Каждое встроенное представление, возвращающее значение TemplateResponse, будет вызывать метод render_to_response(), который предоставляет TemplateResponseMixin. Большую часть времени этот метод будет вызываться за вас (например, он вызывается методом get(), реализованным как TemplateView, так и DetailView); также маловероятно, что вам понадобится переопределить его, хотя если вы хотите, чтобы ваш ответ возвращал что-то не отображаемое через шаблон Django, то вы захотите это сделать. В качестве примера можно привести JSONResponseMixin example.

render_to_response() сам вызывает get_template_names(), который по умолчанию будет искать template_name в представлении на основе класса; два других миксина (SingleObjectTemplateResponseMixin и MultipleObjectTemplateResponseMixin) переопределяют это, чтобы обеспечить более гибкие настройки по умолчанию при работе с реальными объектами.

ContextMixin
Каждый встроенный вид, которому нужны контекстные данные, например, для отрисовки шаблона (включая TemplateResponseMixin выше), должен вызывать get_context_data(), передавая любые данные, которые он хочет обеспечить, в качестве аргументов ключевых слов. get_context_data() возвращает словарь; в ContextMixin он возвращает свои аргументы ключевых слов, но обычно это переопределяется, чтобы добавить больше членов в словарь. Вы также можете использовать атрибут extra_context.

Построение общих представлений Django на основе классов

Давайте рассмотрим, как два общих представления Django, основанных на классах, построены из миксинов, обеспечивающих отдельные функции. Мы рассмотрим DetailView, который отображает «детальное» представление объекта, и ListView, который отображает список объектов, обычно из набора запросов, и, по желанию, постранично. Это познакомит нас с четырьмя миксинами, которые между собой обеспечивают полезную функциональность при работе как с одним объектом Django, так и с несколькими объектами.

Существуют также миксины, задействованные в общих представлениях редактирования (FormView, и специфических для модели представлениях CreateView, UpdateView и DeleteView), и в общих представлениях, основанных на дате. Они рассматриваются в разделе mixin reference documentation.

DetailView: работа с одним объектом Django

Чтобы показать детали объекта, нам в основном нужно сделать две вещи: найти объект, а затем сделать TemplateResponse с подходящим шаблоном и этим объектом в качестве контекста.

Чтобы получить объект, DetailView полагается на SingleObjectMixin, который предоставляет get_object() метод, который определяет объект на основе URL запроса (он ищет pk и slug аргументы ключевых слов, объявленные в URLConf, и ищет объект либо по model атрибуту представления, либо по queryset атрибуту, если он предоставлен). SingleObjectMixin также переопределяет get_context_data(), который используется во всех встроенных представлениях Django, основанных на классах, для предоставления контекстных данных для рендеринга шаблонов.

Чтобы затем сделать TemplateResponse, DetailView использует SingleObjectTemplateResponseMixin, который расширяет TemplateResponseMixin, переопределяя get_template_names(), как обсуждалось выше. На самом деле он предоставляет довольно сложный набор опций, но основная, которую будет использовать большинство людей, это <app_label>/<model_name>_detail.html. Часть _detail может быть изменена путем установки template_name_suffix в подклассе на что-то другое. (Например, generic edit views использует _form для представлений create и update, и _confirm_delete для представлений delete).

ListView: работа с большим количеством объектов Django

Списки объектов следуют примерно той же схеме: нам нужен (возможно, постраничный) список объектов, обычно QuerySet, а затем нам нужно сделать TemplateResponse с подходящим шаблоном, используя этот список объектов.

Для получения объектов ListView использует MultipleObjectMixin, который предоставляет как get_queryset(), так и paginate_queryset(). В отличие от SingleObjectMixin, здесь не нужно отделять части URL, чтобы определить, с каким кверисетом работать, поэтому по умолчанию используется атрибут queryset или model класса представления. Обычной причиной для переопределения get_queryset() здесь может быть динамическое изменение объектов, например, в зависимости от текущего пользователя или для исключения записей в будущем для блога.

MultipleObjectMixin также переопределяет get_context_data(), чтобы включить соответствующие контекстные переменные для пагинации (предоставляя манекены, если пагинация отключена). Он полагается на то, что object_list будет передан в качестве аргумента ключевого слова, которое ListView организует для него.

Чтобы сделать TemplateResponse, ListView затем использует MultipleObjectTemplateResponseMixin; как и в случае с SingleObjectTemplateResponseMixin выше, это отменяет get_template_names(), чтобы обеспечить a range of options, а наиболее часто используемым является <app_label>/<model_name>_list.html, причем часть _list снова берется из атрибута template_name_suffix. (Общие представления, основанные на дате, используют такие суффиксы, как _archive, _archive_year и т.д., чтобы использовать различные шаблоны для различных специализированных представлений списков, основанных на дате).

Использование миксинов представления Django на основе классов

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

Предупреждение

Не все миксины можно использовать вместе, и не все представления, основанные на общих классах, можно использовать со всеми другими миксинами. Здесь мы приводим несколько примеров, которые работают; если вы хотите объединить другие функциональные возможности, то вам придется рассмотреть взаимодействие между атрибутами и методами, которые пересекаются между различными классами, которые вы используете, и как method resolution order будет влиять на то, какие версии методов будут вызываться в каком порядке.

Справочная документация для Django class-based views и class-based view mixins поможет вам понять, какие атрибуты и методы могут вызвать конфликт между различными классами и миксинами.

Если вы сомневаетесь, часто лучше отступить и основывать свою работу на View или TemplateView, возможно, с SingleObjectMixin и MultipleObjectMixin. Хотя в итоге вы, вероятно, напишете больше кода, он, скорее всего, будет понятен кому-то другому, кто придет к нему позже, и, имея меньше взаимодействий, о которых нужно беспокоиться, вы избавите себя от необходимости думать. (Конечно, вы всегда можете обратиться к реализации общих представлений на основе классов в Django для вдохновения в решении проблем).

Использование SingleObjectMixin с View

Если мы хотим написать представление на основе класса, которое реагирует только на POST, мы подклассифицируем View и напишем метод post() в подклассе. Однако если мы хотим, чтобы наша обработка работала с конкретным объектом, определенным по URL, нам понадобится функциональность, предоставляемая SingleObjectMixin.

Мы продемонстрируем это на примере модели Author, которую мы использовали в generic class-based views introduction.

views.py
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author

class RecordInterestView(SingleObjectMixin, View):
    """Records the current user's interest in an author."""
    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()

        # Look up the author we're interested in.
        self.object = self.get_object()
        # Actually record interest somehow here!

        return HttpResponseRedirect(reverse('author-detail', kwargs={'pk': self.object.pk}))

На практике вы, вероятно, захотите записывать интересующие вас данные в хранилище ключевых значений, а не в реляционной базе данных, поэтому мы оставили эту часть без внимания. Единственная часть представления, которая должна беспокоиться об использовании SingleObjectMixin - это место, где мы хотим найти интересующего нас автора, что делается с помощью вызова self.get_object(). Обо всем остальном за нас позаботится миксин.

Мы можем легко подключить это к нашим URL-адресам:

urls.py
from django.urls import path
from books.views import RecordInterestView

urlpatterns = [
    #...
    path('author/<int:pk>/interest/', RecordInterestView.as_view(), name='author-interest'),
]

Обратите внимание на именованную группу pk, которую get_object() использует для поиска экземпляра Author. Вы также можете использовать slug или любую другую возможность SingleObjectMixin.

Использование SingleObjectMixin с ListView

ListView обеспечивает встроенную пагинацию, но вы можете захотеть просмотреть список объектов, которые все связаны (внешним ключом) с другим объектом. В нашем примере с издательством вы можете захотеть постранично просмотреть все книги определенного издательства.

Один из способов сделать это - объединить ListView с SingleObjectMixin, чтобы кверисет для постраничного списка книг мог зависеть от издателя, найденного как единый объект. Для этого нам нужно иметь два разных кверисета:

Book queryset для использования ListView
Поскольку у нас есть доступ к Publisher, чьи книги мы хотим перечислить, мы переопределяем get_queryset() и используем Publisher’s reverse foreign key manager.
Publisher queryset для использования в get_object()
Мы будем полагаться на реализацию по умолчанию get_object(), чтобы получить правильный объект Publisher. Однако нам необходимо явно передать аргумент queryset, потому что в противном случае реализация по умолчанию get_object() вызовет get_queryset(), который мы переопределили, чтобы вернуть Book объекты вместо Publisher.

Примечание

Мы должны тщательно продумать get_context_data(). Поскольку и SingleObjectMixin, и ListView будут помещать вещи в контекстные данные под значением context_object_name, если оно установлено, мы вместо этого явно убедимся, что Publisher находится в контекстных данных. ListView добавит для нас подходящие page_obj и paginator, если мы не забудем вызвать super().

Теперь мы можем написать новый PublisherDetailView:

from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher

class PublisherDetailView(SingleObjectMixin, ListView):
    paginate_by = 2
    template_name = "books/publisher_detail.html"

    def get(self, request, *args, **kwargs):
        self.object = self.get_object(queryset=Publisher.objects.all())
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['publisher'] = self.object
        return context

    def get_queryset(self):
        return self.object.book_set.all()

Обратите внимание, как мы установили self.object внутри get(), чтобы мы могли использовать его позже в get_context_data() и get_queryset(). Если вы не зададите template_name, шаблон по умолчанию примет обычный выбор ListView, который в данном случае будет "books/book_list.html", потому что это список книг; ListView ничего не знает о SingleObjectMixin, так что у него нет ни малейшего представления о том, что это представление имеет отношение к Publisher.

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

{% extends "base.html" %}

{% block content %}
    <h2>Publisher {{ publisher.name }}</h2>

    <ol>
      {% for book in page_obj %}
        <li>{{ book.title }}</li>
      {% endfor %}
    </ol>

    <div class="pagination">
        <span class="step-links">
            {% if page_obj.has_previous %}
                <a href="?page={{ page_obj.previous_page_number }}">previous</a>
            {% endif %}

            <span class="current">
                Page {{ page_obj.number }} of {{ paginator.num_pages }}.
            </span>

            {% if page_obj.has_next %}
                <a href="?page={{ page_obj.next_page_number }}">next</a>
            {% endif %}
        </span>
    </div>
{% endblock %}

Избегайте чего-то более сложного

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

Подсказка

Каждое из ваших представлений должно использовать только миксины или представления из одной из групп общих представлений, основанных на классах: detail, list, editing и дата. Например, хорошо сочетать TemplateView (встроенное представление) с MultipleObjectMixin (общий список), но у вас, скорее всего, возникнут проблемы с сочетанием SingleObjectMixin (общий детализатор) с MultipleObjectMixin (общий список).

Чтобы показать, что происходит, когда вы пытаетесь стать более сложным, мы покажем пример, который жертвует читабельностью и удобством обслуживания, когда есть более простое решение. Сначала рассмотрим наивную попытку объединить DetailView с FormMixin, чтобы позволить нам POST Django Form на тот же URL, на котором мы отображаем объект с помощью DetailView.

Использование FormMixin с DetailView

Вспомните наш предыдущий пример использования View и SingleObjectMixin вместе. Мы регистрировали интерес пользователя к определенному автору; теперь скажем, что мы хотим позволить ему оставить сообщение о том, почему он ему нравится. Опять же, давайте предположим, что мы не собираемся хранить это в реляционной базе данных, а вместо этого в чем-то более эзотерическом, о чем мы не будем здесь беспокоиться.

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

Мы сохраним обработку GET из DetailView, хотя нам придется добавить Form в контекстные данные, чтобы мы могли отобразить их в шаблоне. Мы также хотим использовать обработку формы из FormMixin, и написать немного кода, чтобы при POST форма вызывалась соответствующим образом.

Примечание

Мы используем FormMixin и реализуем post() сами, а не пытаемся смешивать DetailView с FormView (который уже предоставляет подходящий post()), потому что оба представления реализуют get(), и все стало бы гораздо более запутанным.

Наш новый AuthorDetailView выглядит следующим образом:

# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.

from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDetailView(FormMixin, DetailView):
    model = Author
    form_class = AuthorInterestForm

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        # Here, we would record the user's interest using the message
        # passed in form.cleaned_data['message']
        return super().form_valid(form)

get_success_url() предоставляет место для перенаправления, которое используется в реализации по умолчанию form_valid(). Мы должны предоставить свой собственный post(), как было отмечено ранее.

Лучшее решение

Количество тонких взаимодействий между FormMixin и DetailView уже проверяет нашу способность управлять вещами. Маловероятно, что вы захотите написать такой класс самостоятельно.

В этом случае вы могли бы написать метод post() самостоятельно, сохранив DetailView как единственную общую функциональность, хотя написание кода обработки Form подразумевает много дублирования.

В качестве альтернативы, это было бы менее трудоемко, чем вышеописанный подход, иметь отдельное представление для обработки формы, которое могло бы использовать FormView в отличие от DetailView без проблем.

Альтернативное лучшее решение

На самом деле мы пытаемся использовать два разных представления, основанных на классах, с одного и того же URL. Так почему бы не сделать именно это? Здесь у нас есть очень четкое разделение: GET запросы должны получать DetailViewForm, добавленным к контекстным данным), а POST запросы должны получать FormView. Давайте сначала настроим эти представления.

Представление AuthorDetailView почти такое же, как when we first introduced AuthorDetailView; мы должны написать собственное get_context_data(), чтобы сделать AuthorInterestForm доступным для шаблона. Мы пропустим переопределение get_object() из предыдущего раздела для ясности:

from django import forms
from django.views.generic import DetailView
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDetailView(DetailView):
    model = Author

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = AuthorInterestForm()
        return context

Тогда AuthorInterestForm будет FormView, но мы должны ввести SingleObjectMixin, чтобы мы могли найти автора, о котором мы говорим, и мы должны не забыть установить template_name, чтобы гарантировать, что ошибки формы будут отображать тот же шаблон, который AuthorDetailView использует на GET:

from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin

class AuthorInterestFormView(SingleObjectMixin, FormView):
    template_name = 'books/author_detail.html'
    form_class = AuthorInterestForm
    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

Наконец, мы объединяем все это в новом представлении AuthorView. Мы уже знаем, что вызов as_view() на представлении на основе класса дает нам нечто, что ведет себя точно так же, как представление на основе функции, поэтому мы можем сделать это в момент выбора между двумя вложенными представлениями.

Вы можете передавать аргументы ключевых слов в as_view() таким же образом, как и в URLconf, например, если вы хотите, чтобы поведение AuthorInterestFormView также появлялось в другом URL, но с использованием другого шаблона:

from django.views import View

class AuthorView(View):

    def get(self, request, *args, **kwargs):
        view = AuthorDetailView.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = AuthorInterestFormView.as_view()
        return view(request, *args, **kwargs)

Этот подход также можно использовать с любыми другими общими представлениями на основе классов или вашими собственными представлениями на основе классов, наследующими непосредственно от View или TemplateView, поскольку он позволяет максимально разделить различные представления.

Больше, чем просто HTML

Представления, основанные на классах, эффективны, когда вы хотите делать одно и то же много раз. Предположим, вы пишете API, и каждое представление должно возвращать JSON вместо HTML.

Мы можем создать класс mixin, который будет использоваться во всех наших представлениях, обрабатывая преобразование в JSON один раз.

Например, миксин JSON может выглядеть следующим образом:

from django.http import JsonResponse

class JSONResponseMixin:
    """
    A mixin that can be used to render a JSON response.
    """
    def render_to_json_response(self, context, **response_kwargs):
        """
        Returns a JSON response, transforming 'context' to make the payload.
        """
        return JsonResponse(
            self.get_data(context),
            **response_kwargs
        )

    def get_data(self, context):
        """
        Returns an object that will be serialized as JSON by json.dumps().
        """
        # Note: This is *EXTREMELY* naive; in reality, you'll need
        # to do much more complex handling to ensure that arbitrary
        # objects -- such as Django model instances or querysets
        # -- can be serialized as JSON.
        return context

Примечание

Ознакомьтесь с документацией Сериализация объектов Django для получения дополнительной информации о том, как правильно преобразовывать модели Django и наборы запросов в JSON.

Этот миксин предоставляет метод render_to_json_response() с той же сигнатурой, что и render_to_response(). Чтобы использовать его, нам нужно подмешать его в TemplateView, например, и переопределить render_to_response(), чтобы вместо него вызывать render_to_json_response():

from django.views.generic import TemplateView

class JSONView(JSONResponseMixin, TemplateView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

В равной степени мы можем использовать наш миксин с одним из общих представлений. Мы можем сделать свою собственную версию DetailView, смешав JSONResponseMixin с BaseDetailView – (DetailView до того, как будет подмешано поведение рендеринга шаблона):

from django.views.generic.detail import BaseDetailView

class JSONDetailView(JSONResponseMixin, BaseDetailView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

Затем это представление может быть развернуто так же, как и любое другое DetailView, с точно таким же поведением - за исключением формата ответа.

Если вы хотите быть действительно авантюрным, вы можете даже смешать подкласс DetailView, который способен возвращать и HTML, и JSON контент, в зависимости от некоторого свойства HTTP запроса, такого как аргумент запроса или HTTP заголовок. Смешайте оба подкласса JSONResponseMixin и SingleObjectTemplateResponseMixin, и переопределите реализацию render_to_response(), чтобы отложить соответствующий метод рендеринга в зависимости от типа ответа, который запросил пользователь:

from django.views.generic.detail import SingleObjectTemplateResponseMixin

class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
    def render_to_response(self, context):
        # Look for a 'format=json' GET argument
        if self.request.GET.get('format') == 'json':
            return self.render_to_json_response(context)
        else:
            return super().render_to_response(context)

Из-за того, как Python разрешает перегрузку методов, вызов super().render_to_response(context) заканчивается вызовом render_to_response() реализации TemplateResponseMixin.

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