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