Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих техник рекомендуется хорошее знание представлений на основе классов 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 на основе классов и миксинам представлений Django на основе классов поможет вам понять, какие атрибуты и методы, скорее всего, вызовут конфликт между различными классами и миксинами.
В случае сомнений лучше вернуться к View или TemplateView, возможно, с SingleObjectMixin и MultipleObjectMixin. Хотя вам, вероятно, придется написать больше кода, он, скорее всего, будет более понятен кому-то другому, кто к нему обращается позже, и с меньшим количеством взаимодействий вам нужно будет беспокоиться, что сэкономит вам время размышлений. (Конечно, вы всегда можете обратиться к реализации Django общих представлений на основе классов, чтобы получить вдохновение о том, как справиться с проблемами.)
Использование SingleObjectMixin с представлением
Если мы хотим написать представление на основе класса, которое отвечает только на POST, мы будем наследовать от View и написать метод post() в подклассе. Однако, если мы хотим, чтобы наша обработка работала с конкретным объектом, определенным из URL, нам понадобится функциональность, предоставляемая SingleObjectMixin.
Мы продемонстрируем это с моделью Author, которую мы использовали в введении в общие представления на основе классов.
views.pyfrom 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.pyfrom 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 byListView -
Поскольку у нас есть доступ к
Publisher, книги которого мы хотим перечислить, мы переопределяемget_queryset()и используем менеджер обратного внешнего ключаPublisher. -
Publisher queryset for use inget_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, поэтому он не имеет ни малейшего представления, что это представление связано с издателем.
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
Затем 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, так как он сохраняет различные представления максимально независимыми.
Больше, чем просто 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/5.2/topics/class-based-views/mixins/