Использование миксинов с представлениями на основе классов¶
Осторожно
Это продвинутая тема. Перед изучением этих техник рекомендуется иметь рабочие знания 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 работать, поэтому по умолчанию просто используется атрибут 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 RecordInterest(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 RecordInterest
urlpatterns = [
#...
path('author/<int:pk>/interest/', RecordInterest.as_view(), name='author-interest'),
]
Обратите внимание на именованную группу pk
, которую get_object()
использует для поиска экземпляра Author
. Вы также можете использовать slug или любую другую возможность SingleObjectMixin
.
Использование SingleObjectMixin
с ListView
¶
ListView
обеспечивает встроенную пагинацию, но вы можете захотеть просмотреть список объектов, которые все связаны (внешним ключом) с другим объектом. В нашем примере с издательством вы можете захотеть постранично просмотреть все книги определенного издательства.
Один из способов сделать это - объединить ListView
с SingleObjectMixin
, чтобы кверисет для постраничного списка книг мог зависеть от издателя, найденного как единый объект. Для этого нам нужно иметь два разных кверисета:
Book
queryset для использованияListView
- Поскольку у нас есть доступ к
Publisher
, чьи книги мы хотим перечислить, мы просто переопределяемget_queryset()
и используемPublisher
’s 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()
.
Теперь мы можем написать новый PublisherDetail
:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetail(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()
, и все стало бы гораздо более запутанным.
Наш новый AuthorDetail
выглядит следующим образом:
# 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 AuthorDetail(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
. Давайте сначала настроим эти представления.
Представление AuthorDisplay
почти такое же, как when we first introduced AuthorDetail; мы должны написать собственное 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 AuthorDisplay(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = AuthorInterestForm()
return context
Тогда AuthorInterest
является простым FormView
, но мы должны ввести SingleObjectMixin
, чтобы мы могли найти автора, о котором идет речь, и мы должны не забыть установить template_name
, чтобы гарантировать, что ошибки формы будут отображать тот же шаблон, который AuthorDisplay
использует на 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 AuthorInterest(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})
Наконец, мы объединяем все это в новом представлении AuthorDetail
. Мы уже знаем, что вызов as_view()
на представлении на основе класса дает нам нечто, что ведет себя точно так же, как представление на основе функции, поэтому мы можем сделать это в момент выбора между двумя вложенными представлениями.
Конечно, вы можете передавать аргументы ключевых слов в as_view()
таким же образом, как и в URLconf, например, если вы хотите, чтобы поведение AuthorInterest
также появлялось в другом URL, но с использованием другого шаблона:
from django.views import View
class AuthorDetail(View):
def get(self, request, *args, **kwargs):
view = AuthorDisplay.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterest.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
с django.views.generic.detail.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
.