Spec-Zone.ru › Django 5.1

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

Внимание

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

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

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

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

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

Документация по представлениям 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 queryset for use by ListView
Поскольку у нас есть доступ к Publisher, чьи книги мы хотим перечислить, мы переопределяем get_queryset() и используем менеджер обратных внешних ключей Publisher.
Publisher queryset for use in 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

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

Пример JSONResponseMixin

Превосходство представлений на основе классов заключается в том, что вы можете многократно выполнять одно и то же действие. Допустим, вы пишете 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 для получения дополнительной информации о правильном преобразовании моделей 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.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/topics/class-based-views/mixins/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API