Spec-Zone.ru › Django 4.1

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

Внимание

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

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

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

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

Документация по общим представлениям 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 (обобщенное представление detail) с 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

Затем 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.

Мы можем создать класс-микшин для использования во всех наших представлениях, обрабатывая преобразование в 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/4.1/topics/class-based-views/mixins/

Spec-Zone.ru

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