Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих техник рекомендуется иметь практические знания о представлениях 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() для включения соответствующих переменных контекста для постраничности (предоставляя dummy, если постраничность отключена). Он полагается на то, что 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 с View
Если мы хотим написать простое представление на основе класса, которое реагирует только на POST, мы будем наследовать от View и написать метод post() в подклассе. Однако если мы хотим, чтобы наша обработка работала с конкретным объектом, определенным из URL, нам понадобится функциональность, предоставляемая SingleObjectMixin.
Мы продемонстрируем это с моделью Author, которую мы использовали в введении в обобщенные представления на основе классов.
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. Вы также можете использовать слаг или любые другие функции 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().
Теперь мы можем написать новый 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. Однако вещи становятся все более сложными, когда вы пытаетесь это сделать, и хорошее правило — следующее:
Подсказка
Каждое из ваших представлений должно использовать только миксины или представления из одной из групп представлений на основе классов: деталь, список, редактирование и дата. Например, нормально объединять 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 почти такой же, как когда мы впервые представили 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.
Мы можем создать класс-миксин для использования во всех наших представлениях, обрабатывая преобразование в 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.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.2/topics/class-based-views/mixins/