Spec-Zone.ru › Django 4.0

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

Внимание

Это продвинутая тема. Для изучения этих техник рекомендуется знание представлений 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(), чтобы включить соответствующие переменные контекста для постраничной навигации (предоставляя dummies, если постраничная навигация отключена). Он полагается на 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 с представлением

Если мы хотим написать представление на основе класса, которое отвечает только на 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. Однако вещи становятся все более сложными по мере попыток сделать это, и хорошим правилом является:

Подсказка

Каждое из ваших представлений должно использовать только миксины или представления из одной из групп универсальных представлений на основе классов: детали, списки, редактирование и даты. Например, объединение 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

Затем 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.0/topics/class-based-views/mixins/

Spec-Zone.ru

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