Использование миксинов с представлениями на основе классов
Внимание
Это сложная тема. Для изучения этих техник рекомендуется предварительное знакомство с представлениями 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он просто возвращает свои ключевые аргументы, но обычно его переопределяют, чтобы добавить в словарь дополнительные элементы.
Создание представлений 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 на основе классов и миксинам представлений Django на основе классов поможет вам понять, какие атрибуты и методы могут привести к конфликту между различными классами и миксинами.
В случае сомнений, лучше всего отступить и основывать свою работу на View или TemplateView, возможно, с SingleObjectMixin и MultipleObjectMixin. Хотя вам, вероятно, придётся написать больше кода, он будет более понятен для других, кто будет его читать позже, а с меньшим количеством взаимодействий, о которых нужно беспокоиться, вы сэкономите время на размышления. (Конечно, вы всегда можете обратиться к реализации обобщенных представлений Django на основе классов для вдохновения по решению проблем.)
Использование SingleObjectMixin с View
Если мы хотим написать простое представление на основе класса, которое реагирует только на POST, мы будем наследоваться от View и напишем метод post() в подклассе. Однако если мы хотим, чтобы наша обработка работала с определённым объектом, идентифицированным из URL, нам потребуется функциональность, предоставляемая SingleObjectMixin.
Мы продемонстрируем это с моделью Author, которую мы использовали в вводном руководстве по обобщенным представлениям на основе классов.
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views.generic 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.conf.urls import url
from books.views import RecordInterest
urlpatterns = [
#...
url(r'^author/(?P<pk>[0-9]+)/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\'s обратный менеджер внешнего ключа. -
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(PublisherDetail, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(PublisherDetail, self).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, редактирование и дата. Например, комбинировать 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.core.urlresolvers 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(AuthorDetail, self).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(AuthorDetail, self).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.views.generic import DetailView
from django import forms
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(AuthorDisplay, self).get_context_data(**kwargs)
context['form'] = AuthorInterestForm()
return context
Затем AuthorInterest представляет собой простое FormView, но нам нужно добавить SingleObjectMixin, чтобы мы могли найти автора, о котором идёт речь, и нам нужно запомнить установить template_name , чтобы ошибки формы отображались в том же шаблоне, что и AuthorDisplay использует в GET:
from django.core.urlresolvers import reverse
from django.http import HttpResponseForbidden
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(AuthorInterest, self).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.generic 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(object):
"""
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(HybridDetailView, self).render_to_response(context)
Из-за того, как Python разрешает перегрузку методов, вызов super(HybridDetailView, self).render_to_response(context) в конечном итоге вызывает реализацию render_to_response() реализации TemplateResponseMixin.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.8/topics/class-based-views/mixins/