Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих методов рекомендуется предварительное знакомство с представлениями 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, которую мы использовали в введении в общие представления на основе классов.
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 - Поскольку у нас есть доступ к издателю, чьи книги мы хотим отобразить, мы просто переопределяем
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, чтобы добавить возможность обработки 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 get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = self.get_form()
return context
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(), как отмечалось ранее, и переопределить get_context_data() для того, чтобы Form был доступен в данных контекста.
Лучшее решение
Должно быть очевидно, что количество тонких взаимодействий между 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, так как он максимально разделяет разные представления.
Пример JSONResponseMixin
Преимущество представлений на основе классов заключается в том, что вы можете делать одно и то же много раз. Предположим, вы пишете API, и каждое представление должно возвращать JSON вместо HTML.
Мы можем создать класс-mixin для использования во всех наших представлениях, обрабатывая преобразование в JSON один раз.
Например, простой mixin 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.
Этот mixin предоставляет метод 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)
Аналогично, мы можем использовать наш mixin с одним из представлений на основе шаблонов. Мы можем создать свою версию 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.1/topics/class-based-views/mixins/