Использование миксинов с представлениями на основе классов¶
Осторожно
Это продвинутая тема. Перед изучением этих техник рекомендуется иметь рабочие знания Django’s class-based views.
Встроенные в Django представления на основе классов предоставляют много функциональных возможностей, но некоторые из них вы можете захотеть использовать отдельно. Например, вы можете захотеть написать представление, которое отображает шаблон для создания HTTP ответа, но вы не можете использовать TemplateView
; возможно, вам нужно отобразить шаблон только на POST
, а GET
будет делать что-то совсем другое. Хотя вы можете использовать TemplateResponse
напрямую, это, скорее всего, приведет к дублированию кода.
По этой причине Django также предоставляет ряд миксинов, которые обеспечивают более дискретную функциональность. Рендеринг шаблонов, например, инкапсулирован в TemplateResponseMixin
. Справочная документация Django содержит full documentation of all the mixins.
Контекст и шаблонные ответы¶
Предоставляются два центральных миксина, которые помогают обеспечить согласованный интерфейс для работы с шаблонами в представлениях, основанных на классах.
TemplateResponseMixin
Каждое встроенное представление, возвращающее значение
TemplateResponse
, будет вызывать методrender_to_response()
, который предоставляетTemplateResponseMixin
. Большую часть времени этот метод будет вызываться за вас (например, он вызывается методомget()
, реализованным какTemplateView
, так иDetailView
); также маловероятно, что вам понадобится переопределить его, хотя если вы хотите, чтобы ваш ответ возвращал что-то не отображаемое через шаблон Django, то вы захотите это сделать. В качестве примера можно привести JSONResponseMixin example.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
), и в общих представлениях, основанных на дате. Они рассматриваются в разделе mixin reference documentation.
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
в подклассе на что-то другое. (Например, generic edit views использует _form
для представлений create и update, и _confirm_delete
для представлений 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 class-based views и class-based view mixins поможет вам понять, какие атрибуты и методы могут вызвать конфликт между различными классами и миксинами.
Если вы сомневаетесь, часто лучше отступить и основывать свою работу на View
или TemplateView
, возможно, с SingleObjectMixin
и MultipleObjectMixin
. Хотя в итоге вы, вероятно, напишете больше кода, он, скорее всего, будет понятен кому-то другому, кто придет к нему позже, и, имея меньше взаимодействий, о которых нужно беспокоиться, вы избавите себя от необходимости думать. (Конечно, вы всегда можете обратиться к реализации общих представлений на основе классов в Django для вдохновения в решении проблем).
Использование SingleObjectMixin
с View¶
Если мы хотим написать представление на основе класса, которое отвечает только на POST
, мы подклассифицируем View
и напишем метод post()
в подклассе. Однако если мы хотим, чтобы наша обработка работала с конкретным объектом, определенным по URL, нам понадобится функциональность, предоставляемая SingleObjectMixin
.
Мы продемонстрируем это на примере модели Author
, которую мы использовали в generic class-based views introduction.
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-адресам:
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
. Вы также можете использовать slug или любую другую возможность SingleObjectMixin
.
Использование SingleObjectMixin
с ListView
¶
ListView
обеспечивает встроенную пагинацию, но вы можете захотеть просмотреть список объектов, которые все связаны (внешним ключом) с другим объектом. В нашем примере с издательством вы можете захотеть постранично просмотреть все книги определенного издательства.
Один из способов сделать это - объединить ListView
с SingleObjectMixin
, чтобы кверисет для постраничного списка книг мог зависеть от издателя, найденного как единый объект. Для этого нам нужно иметь два разных кверисета:
Book
queryset для использованияListView
- Поскольку у нас есть доступ к
Publisher
, чьи книги мы хотим перечислить, мы переопределяемget_queryset()
и используемPublisher
из reverse foreign key manager. Publisher
queryset для использования вget_object()
- Мы будем полагаться на реализацию по умолчанию
get_object()
, чтобы получить правильный объектPublisher
. Однако нам необходимо явно передать аргументqueryset
, потому что в противном случае реализация по умолчаниюget_object()
вызоветget_queryset()
, который мы переопределили, чтобы вернутьBook
объекты вместоPublisher
.
Примечание
Мы должны тщательно продумать get_context_data()
. Поскольку и SingleObjectMixin
, и ListView
будут помещать вещи в контекстные данные под значением context_object_name
, если оно установлено, мы вместо этого явно убедимся, что Publisher
находится в контекстных данных. ListView
добавит для нас подходящие page_obj
и paginator
, если мы не забудем вызвать super()
.
Теперь мы можем написать новый PublisherDetailView
:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(SingleObjectMixin, ListView):
paginate_by = 2
template_name = "books/publisher_detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Publisher.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["publisher"] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
Обратите внимание, как мы установили self.object
внутри get()
, чтобы мы могли использовать его позже в get_context_data()
и get_queryset()
. Если вы не зададите template_name
, шаблон по умолчанию примет обычный выбор ListView
, который в данном случае будет "books/book_list.html"
, потому что это список книг; ListView
ничего не знает о SingleObjectMixin
, так что у него нет ни малейшего представления о том, что это представление имеет отношение к Publisher
.
В примере paginate_by
намеренно сделан маленьким, чтобы вам не пришлось создавать много книг, чтобы увидеть, что пагинация работает! Вот шаблон, который вы хотите использовать:
{% extends "base.html" %}
{% block content %}
<h2>Publisher {{ publisher.name }}</h2>
<ol>
{% for book in page_obj %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endblock %}
Избегайте чего-то более сложного¶
Как правило, вы можете использовать TemplateResponseMixin
и SingleObjectMixin
, когда вам нужна их функциональность. Как показано выше, при некоторой осторожности можно даже комбинировать SingleObjectMixin
с ListView
. Однако все усложняется по мере того, как вы пытаетесь это сделать, и хорошим эмпирическим правилом является следующее:
Подсказка
Каждое из ваших представлений должно использовать только миксины или представления из одной из групп общих представлений, основанных на классах: detail, list, editing и дата. Например, хорошо сочетать 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
почти такой же, как when we first introduced 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.
Мы можем создать класс mixin, который будет использоваться во всех наших представлениях, обрабатывая преобразование в 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
.