Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих техник рекомендуется предварительное знание представлений Django на основе классов.
Встроенные представления Django на основе классов предоставляют множество функций, некоторые из которых вы можете использовать по отдельности. Например, вы можете написать представление, которое отображает шаблон для создания HTTP-ответа, но не можете использовать TemplateView; возможно, вам нужно отобразить шаблон только в POST, при этом GET выполняет что-то совершенно другое. Хотя вы можете использовать TemplateResponse напрямую, это, вероятно, приведет к дублированию кода.
По этой причине Django также предоставляет ряд миксинов, которые обеспечивают более изолированную функциональность. Например, отображение шаблонов инкапсулировано в TemplateResponseMixin. Документация Django содержит полную документацию по всем миксинам.
Контекст и ответы шаблонов
Предоставляются два ключевых миксина, которые помогают обеспечить согласованный интерфейс работы с шаблонами в представлениях на основе классов.
-
TemplateResponseMixin - Всякое встроенное представление, возвращающее
TemplateResponse, вызовет методrender_to_response(), которыйTemplateResponseMixinобеспечивает. В большинстве случаев это будет происходить автоматически (например, его вызывает методget(), реализованный как вTemplateView, так и вDetailView); вряд ли вам понадобится его переопределять, хотя если вы хотите, чтобы ваш ответ возвращал что-то, не отображаемое с помощью шаблона Django, то вам нужно будет это сделать. Пример этого приведен в примере JSONResponseMixin.render_to_response()сам вызываетget_template_names(), который по умолчанию будет искатьtemplate_nameв представлении на основе класса; два других миксина (SingleObjectTemplateResponseMixinиMultipleObjectTemplateResponseMixin) переопределяют его, чтобы обеспечить более гибкие значения по умолчанию при работе с реальными объектами. -
ContextMixin - Всякое встроенное представление, которому требуется данные контекста, такие как для отображения шаблона (включая
TemplateResponseMixinвыше), должно вызыватьget_context_data(), передавая любые данные, которые должны быть в нем, в качестве именованных аргументов.get_context_data()возвращает словарь; вContextMixinон возвращает свои именованные аргументы, но часто переопределяется для добавления дополнительных элементов в словарь. Также можно использовать атрибутextra_context.
Создание представлений Django на основе классов
Рассмотрим, как два представления Django на основе классов генерируются из миксинов, предоставляющих изолированную функциональность. Мы рассмотрим DetailView, которое отображает представление «детали» объекта, и ListView, которое отображает список объектов, обычно из набора запросов, и необязательно страницирует их. Это познакомит нас с четырьмя миксинами, которые вместе обеспечивают полезную функциональность при работе как с одним объектом Django, так и с несколькими.
Также существуют миксины, участвующие в представлениях на основе классов для редактирования (FormView, и представления для конкретных моделей CreateView, UpdateView и DeleteView), а также в представлениях на основе дат. Они описаны в документации по миксинам.
DetailView: работа с одним объектом Django
Для отображения деталей объекта нам нужно сделать два шага: найти объект, а затем создать TemplateResponse с подходящим шаблоном и этим объектом в качестве контекста.
Для получения объекта, DetailView использует SingleObjectMixin, который предоставляет метод get_object() для определения объекта на основе URL-адреса запроса (он ищет pk и slug именованные аргументы, объявленные в URLConf, и ищет объект либо из атрибута model представления, либо из атрибута queryset, если он предоставлен). SingleObjectMixin также переопределяет get_context_data(), используемый во всех встроенных представлениях Django на основе классов для предоставления данных контекста для отображения шаблонов.
Чтобы затем создать TemplateResponse, DetailView использует SingleObjectTemplateResponseMixin, который расширяет TemplateResponseMixin, перезаписывая get_template_names(), как обсуждалось выше. Он фактически предоставляет довольно изощрённый набор опций, но основной, которую большинство людей будут использовать, это <app_label>/<model_name>_detail.html. Часть _detail может быть изменена, установив template_name_suffix в подклассе на что-то другое. (Например, общие представления редактирования используют _form для представлений создания и обновления, и _confirm_delete для представлений удаления.)
ListView: работа с множеством объектов Django
Списки объектов следуют примерно той же схеме: нам нужен (возможно, постраничный) список объектов, обычно QuerySet, а затем нам нужно создать TemplateResponse с подходящим шаблоном, используя этот список объектов.
Чтобы получить объекты, ListView использует MultipleObjectMixin, который предоставляет как get_queryset(), так и paginate_queryset(). В отличие от SingleObjectMixin, нет необходимости использовать части URL для определения набора запросов для работы, поэтому по умолчанию используется атрибут queryset или model в классе представления. Распространённая причина перезаписи get_queryset() — динамически изменять объекты, например, в зависимости от текущего пользователя или исключать будущие записи в блоге.
MultipleObjectMixin также переопределяет get_context_data() для включения соответствующих переменных контекста для постраничной навигации (предоставляя заглушки, если постраничная навигация отключена). Она полагается на object_list, переданную в качестве ключевого аргумента, что ListView организует.
Чтобы создать TemplateResponse, ListView затем использует MultipleObjectTemplateResponseMixin; как и SingleObjectTemplateResponseMixin выше, это переопределяет get_template_names() для предоставления a range of
options, причём наиболее часто используемым является <app_label>/<model_name>_list.html, а часть _list снова взята из атрибута template_name_suffix. (Дата-зависимые общие представления используют суффиксы, такие как _archive, _archive_year и так далее, чтобы использовать различные шаблоны для различных специализированных дата-зависимых представлений списков.)
Использование миксинов Django для представлений на основе классов
Теперь, когда мы увидели, как общие представления Django на основе классов используют предоставленные миксины, давайте рассмотрим другие способы их комбинирования. Мы всё ещё будем комбинировать их либо с встроенными представлениями на основе классов, либо с другими общими представлениями на основе классов, но есть ряд менее распространённых проблем, которые можно решить, чем те, которые предоставляются Django по умолчанию.
Предупреждение
Не все миксины можно использовать вместе, и не все представления на основе классов можно использовать со всеми миксинами. Здесь мы представляем несколько примеров, которые работают; если вы хотите объединить другую функциональность, то вам нужно рассмотреть взаимодействие между атрибутами и методами, которые пересекаются между различными классами, которые вы используете, и как порядок разрешения методов method resolution order повлияет на то, какие версии методов будут вызваны в каком порядке.
Документация по представлениям Django на основе классов и миксинам представлений на основе классов поможет вам понять, какие атрибуты и методы, скорее всего, вызовут конфликт между различными классами и миксинами.
Если вы сомневаетесь, лучше использовать View или TemplateView, возможно, с SingleObjectMixin и MultipleObjectMixin. Хотя вам, вероятно, придётся написать больше кода, он, скорее всего, будет более понятен для кого-то, кто будет работать с ним позже, и с меньшим количеством взаимодействий, о которых нужно беспокоиться, вы сэкономите себе время размышлений. (Конечно, вы всегда можете обратиться к реализации Django общих представлений на основе классов, чтобы получить вдохновение для решения проблем.)
Использование SingleObjectMixin с представлением
Если мы хотим написать представление на основе класса, которое реагирует только на 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, чтобы набор запросов для постраничного списка книг мог опираться на издателя, найденного как единственный объект. Для этого нам необходимо иметь два разных набора запросов:
-
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. Однако вещи становятся все более сложными по мере попыток сделать это, и хорошее правило — следующее:
Подсказка
Каждый из ваших представлений должен использовать только миксины или представления из одной из групп представлений на основе класса общего назначения: detail, list, editing и date. Например, объединение TemplateView (встроенное представление) с MultipleObjectMixin (обобщенное представление списка) допустимо, но у вас, вероятно, возникнут проблемы при объединении SingleObjectMixin (обобщенное представление детали) с MultipleObjectMixin (обобщенное представление списка).
Чтобы показать, что происходит при попытке сделать это более сложным, мы покажем пример, который жертвует удобочитаемостью и поддерживаемостью, когда существует более простое решение. Сначала давайте рассмотрим неудачную попытку объединить DetailView с FormMixin, чтобы нам удалось POST Django Form по тому же URL, что и при отображении объекта с помощью DetailView.
Использование FormMixin с DetailView
Вспомните наш предыдущий пример использования View и SingleObjectMixin вместе. Мы записывали интерес пользователя к определенному автору; скажем теперь, что мы хотим дать им возможность оставить сообщение о том, почему они их любят. Снова предположим, что мы не будем хранить это в реляционной базе данных, а в чем-то более эзотеричном, о чем мы здесь беспокоиться не будем.
На этом этапе естественно обратиться к Form, чтобы инкапсулировать информацию, отправленную из браузера пользователя в Django. Предположим также, что мы сильно заинтересованы в REST, поэтому хотим использовать тот же URL для отображения автора, что и для сбора сообщения от пользователя. Перепишем наше AuthorDetailView для этого.
Мы сохраним обработку GET от DetailView, хотя нам придется добавить Form в данные контекста, чтобы мы могли его отобразить в шаблоне. Мы также захотим включить обработку формы из FormMixin и написать немного кода, чтобы при POST форма вызывалась должным образом.
Примечание
Мы используем FormMixin и сами реализуем post(), а не пытаемся смешивать DetailView с FormView (который уже предоставляет подходящий post()), потому что оба представления реализуют get(), и вещи станут намного сложнее.
Наш новый AuthorDetailView выглядит так:
# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.
from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
# Here, we would record the user's interest using the message
# passed in form.cleaned_data['message']
return super().form_valid(form)
get_success_url() предоставляет место для перенаправления, которое используется в стандартной реализации form_valid(). Нам нужно предоставить собственную post(), как отмечалось ранее.
Лучшее решение
Количество тонких взаимодействий между FormMixin и DetailView уже проверяет нашу способность управлять вещами. Вряд ли вам захочется писать такой класс самостоятельно.
В этом случае вы могли бы сами написать метод post(), сохранив DetailView в качестве единственной общей функциональности, хотя написание кода обработки Form предполагает много дублирования.
В качестве альтернативы, это по-прежнему будет менее трудоёмким, чем вышеописанный подход, если иметь отдельный вид для обработки формы, который может использовать FormView, отличного от DetailView без проблем.
Альтернативное лучшее решение
На самом деле, мы пытаемся использовать два разных класса-представления из одного URL. Так почему бы не сделать именно это? Здесь у нас очень чёткое разделение: запросы GET должны получить DetailView (с добавленным Form в данные контекста), а запросы POST должны получить FormView. Давайте сначала настроим эти представления.
Представление AuthorDetailView почти такое же, как когда мы впервые представили AuthorDetailView; нам нужно написать свой get_context_data(), чтобы сделать AuthorInterestForm доступным для шаблона. Мы пропустим переопределение get_object() из предыдущего примера для ясности:
from django import forms
from django.views.generic import DetailView
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = AuthorInterestForm()
return context
Затем AuthorInterestFormView является FormView, но нам нужно добавить SingleObjectMixin, чтобы мы могли найти автора, о котором говорим, и нам нужно помнить о настройке template_name, чтобы ошибки формы отображались в том же шаблоне, что и AuthorDetailView, используемый в GET.
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterestFormView(SingleObjectMixin, FormView):
template_name = "books/author_detail.html"
form_class = AuthorInterestForm
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
Наконец, мы объединяем это в новое представление AuthorView. Мы уже знаем, что вызов as_view() на представлении на основе классов даёт нам нечто, что ведёт себя точно так же, как представление на основе функций, поэтому мы можем сделать это в тот момент, когда выбираем между двумя подпредставлениями.
Вы можете передать ключевые аргументы в as_view() так же, как и в вашем URLconf, например, если вы хотите, чтобы поведение AuthorInterestFormView также появилось в другом URL, но с использованием другого шаблона:
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.as_view()
return view(request, *args, **kwargs)
Этот подход также может быть использован с любыми другими представлениями на основе классов или вашими собственными представлениями на основе классов, наследующими непосредственно от View или TemplateView, так как он сохраняет разные представления максимально независимыми.
Пример JSONResponseMixin
Преимущество представлений на основе классов заключается в том, что вы можете делать одно и то же много раз. Предположим, что вы пишете API, и каждое представление должно возвращать JSON вместо рендерного HTML.
Мы можем создать класс-миксин, чтобы использовать его во всех представлениях, обрабатывая преобразование в JSON один раз.
Например, миксин JSON может выглядеть следующим образом:
from django.http import JsonResponse
class JSONResponseMixin:
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(self.get_data(context), **response_kwargs)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
Примечание
Обратитесь к документации Сериализация объектов Django для получения дополнительной информации о том, как правильно преобразовать модели и запросы Django в JSON.
Этот миксин предоставляет метод render_to_json_response(), имеющий такую же сигнатуру, как render_to_response(). Чтобы использовать его, нам нужно смешать его с TemplateView, например, и переопределить render_to_response(), чтобы вызвать render_to_json_response() вместо него.
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Аналогично, мы можем использовать наш миксин с одним из общих представлений. Мы можем создать нашу собственную версию DetailView, смешав JSONResponseMixin с BaseDetailView – (поведение DetailView перед рендерингом шаблона было смешано):
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Это представление можно развернуть так же, как и любое другое DetailView, с абсолютно тем же поведением – за исключением формата ответа.
Если вы хотите быть очень смелыми, вы можете даже смешать подкласс DetailView, способный возвращать оба HTML и JSON-содержимое, в зависимости от какого-то свойства HTTP-запроса, например, аргумента запроса или HTTP-заголовка. Смешайте как JSONResponseMixin, так и SingleObjectTemplateResponseMixin, и переопределите реализацию render_to_response(), чтобы отложить вызов соответствующему методу рендеринга в зависимости от типа ответа, который запросил пользователь:
from django.views.generic.detail import SingleObjectTemplateResponseMixin
class HybridDetailView(
JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get("format") == "json":
return self.render_to_json_response(context)
else:
return super().render_to_response(context)
Из-за того, как Python разрешает перегрузку методов, вызов super().render_to_response(context) в конечном итоге вызывает реализацию render_to_response() TemplateResponseMixin.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.2/topics/class-based-views/mixins/