• 3.1
  • 5.0
  • 6.1
  • Версия документации: 3.2

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

Осторожно

Это продвинутая тема. Перед изучением этих методов рекомендуется иметь практические знания представлений на основе классов в Django.

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

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

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

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

TemplateResponseMixin

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

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), а также в общие представления на основе даты. Они описаны в Справочной документации по миксинам.

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 в подклассе на что-то другое. (Например, обобщенные представления редактирования используют _form для создания и обновления представлений, а _confirm_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() для предоставления диапазона опций, наиболее часто используемым из которых является <app_label>/<model_name>_list.html, причем часть _list снова берется из атрибута template_name_suffix. (Общие представления на основе даты используют суффиксы, такие как _archive, _archive_year и т. д., чтобы использовать разные шаблоны для различных специализированных списковых представлений на основе даты.)

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

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

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

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

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

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

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

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

Мы продемонстрируем это с помощью модели Author, которую мы использовали в общем представлении на основе классов.

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. Вы также можете использовать слаг или любую другую функцию SingleObjectMixin.

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

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

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

Набор запросов Book для использования ListView

Поскольку у нас есть доступ к Publisher, книги которого мы хотим перечислить, мы переопределяем get_queryset() и используем Publisherобратный менеджер внешних ключей.

Набор запросов Publisher для использования в 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 и date. Например, можно комбинировать 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 должны получать DetailView (с добавлением Form к данным контекста), а запросы POST должны получать FormView. Давайте сначала настроим эти представления.

Представление AuthorDetailView почти такое же, как когда мы впервые представили 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

Then the AuthorInterestForm is a FormView, but we have to bring in SingleObjectMixin so we can find the author we’re talking about, and we have to remember to set template_name to ensure that form errors will render the same template as AuthorDetailView is using on 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.

Мы можем создать класс примеси для использования во всех наших представлениях, однократно обрабатывая преобразование в 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 в JSON, можно найти в документации Сериализация объектов Django.

Этот миксин предоставляет метод 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, с точно таким же поведением, за исключением формата ответа.

If you want to be really adventurous, you could even mix a DetailView subclass that is able to return both HTML and JSON content, depending on some property of the HTTP request, such as a query argument or a HTTP header. Mix in both the JSONResponseMixin and a SingleObjectTemplateResponseMixin, and override the implementation of render_to_response() to defer to the appropriate rendering method depending on the type of response that the user requested:

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.

Back to Top