Использование миксинов с представлениями на основе классов
Внимание
Это продвинутая тема. Для изучения этих техник рекомендуется знание представлений 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 с View
Если мы хотим написать представление на основе класса, которое реагирует только на POST, мы создадим подкласс View и напишем метод post() в подклассе. Однако, если мы хотим, чтобы наша обработка работала с конкретным объектом, определённым из URL-адреса, нам понадобится функциональность, предоставляемая SingleObjectMixin.
Мы продемонстрируем это с моделью Author, которую мы использовали в введении в обобщённые представления на основе классов.
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
class RecordInterestView(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 RecordInterestView
urlpatterns = [
#...
path('author/<int:pk>/interest/', RecordInterestView.as_view(), name='author-interest'),
]
Обратите внимание на pk именованную группу, которую get_object() использует для поиска экземпляра Author. Вы также можете использовать слаг или любые другие возможности SingleObjectMixin.
Использование SingleObjectMixin с ListView
ListView предоставляет встроенную пагинацию, но вы, возможно, захотите постранично отобразить список объектов, которые все связаны (с помощью внешнего ключа) с другим объектом. В нашем примере публикации вы можете захотеть просмотреть все книги конкретного издателя.
Один из способов сделать это — объединить ListView с SingleObjectMixin, так что набор запросов для постраничного списка книг может опираться на издателя, найденного как единственный объект. Для этого нам нужны два разных набора запросов:
-
Book queryset for use byListView - Так как у нас есть доступ к
Publisher, чьи книги мы хотим отобразить, мы переопределяемget_queryset()и используем менеджер обратного внешнего ключаPublisher. -
Publisher queryset for use inget_object() - Мы будем полагаться на стандартную реализацию
get_object()для получения нужного объектаPublisher. Однако нам нужно явно передать аргументqueryset, так как в противном случае стандартная реализацияget_object()вызоветget_queryset(), что мы переопределили для возврата объектовBookвместо объектовPublisher.
Примечание
Мы должны тщательно продумать get_context_data(). Так как и SingleObjectMixin, и ListView поместят элементы в данные контекста под значением context_object_name, если оно задано, мы вместо этого будем явно гарантировать, что Publisher находится в данных контекста. ListView добавит соответствующие page_obj и paginator за нас, при условии, что мы помним вызвать super().
Теперь мы можем написать новую PublisherDetailView:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(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(), и это приведет к значительно большей путанице.
Наш новый AuthorDetailView выглядит так:
# 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 AuthorDetailView(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. Сначала давайте настроим эти виды.
Вид AuthorDetailView почти такой же, как когда мы впервые представили AuthorDetailView; нам нужно написать собственное 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 AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = AuthorInterestForm()
return context
Затем AuthorInterestForm является FormView, но мы должны включить SingleObjectMixin, чтобы мы могли найти автора, о котором идет речь, и мы должны помнить, чтобы установить template_name для того, чтобы ошибки формы отображались в том же шаблоне, что и AuthorDetailView использует для 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 AuthorInterestFormView(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})
Наконец, мы объединяем это в новый вид AuthorView. Мы уже знаем, что вызов as_view() для представления с классом дает нам нечто, что ведет себя точно так же, как функция вида, поэтому мы можем сделать это в тот момент, когда мы выбираем между двумя подпредставлениями.
Вы можете передавать ключевые аргументы в as_view() так же, как вы бы сделали в вашем URLconf, например, если вы хотите, чтобы поведение AuthorInterestFormView также появилось в другом URL, но с использованием другого шаблона:
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.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 с 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.2/topics/class-based-views/mixins/