Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих техник рекомендуется знание представлений 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. Вы также можете использовать slug или любые другие функции 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. Однако вещи становятся все более сложными по мере попытки сделать это, и хорошее правило — это:
Подсказка
Каждый из ваших представлений должен использовать только миксины или представления из одной из групп представлений с использованием класса: detail, list, editing и date. Например, сочетание 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 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.
Мы можем создать класс-миксин для использования во всех наших представлениях, обрабатывая преобразование в 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.0/topics/class-based-views/mixins/