Использование примесей с представлениями на основе классов
Предупреждение
Это продвинутая тема. Прежде чем изучать эти методы, рекомендуется ознакомиться с представлениями 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, которое отображает список объектов, обычно полученных из queryset, и при необходимости разбивает его на страницы. Это позволит познакомиться с четырьмя примесями, которые вместе предоставляют полезные возможности при работе как с одним объектом 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, поэтому по умолчанию берётся атрибут 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.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. Можно также использовать slug или любую другую возможность SingleObjectMixin.
Использование SingleObjectMixin с ListView
ListView поддерживает встроенное разбиение на страницы, но иногда требуется разбить на страницы список объектов, связанных (внешним ключом) с другим объектом. В нашем примере с издательством можно разбить на страницы все книги определённого издателя.
Один из способов — объединить ListView с SingleObjectMixin, чтобы queryset списка книг, разбитого на страницы, формировался на основе издателя, найденного как единственный объект. Для этого нам понадобятся два разных queryset:
-
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 и поэтому не может определить, что это представление связано с 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
Затем 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 в JSON, см. в документации Сериализация объектов Django.
Эта примесь предоставляет метод render_to_json_response() с той же сигнатурой, что и у render_to_response(). Чтобы использовать её, нужно, например, включить её в TemplateView и переопределить render_to_response(), чтобы вместо него вызывать render_to_json_response():
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Также можно использовать эту примесь с одним из обобщённых представлений. Мы можем создать собственную версию DetailView, включив JSONResponseMixin в BaseDetailView (поведение DetailView, связанное с рендерингом шаблона, ещё не добавлено):
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Затем это представление можно использовать так же, как любое другое DetailView, с точно таким же поведением — за исключением формата ответа.
Если вы готовы к экспериментам, можно даже объединить подкласс 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/6.0/topics/class-based-views/mixins/