Spec-Zone.ru › Django 1.11

Использование миксинов с представлениями на основе классов

Внимание

Это продвинутая тема. Перед изучением этих техник рекомендуется хорошее знание представлений 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 «из коробки».

Предупреждение

Не все миксины могут быть использованы вместе, и не все общие представления на основе классов могут быть использованы со всеми миксинами. Здесь мы представим несколько примеров, которые работают; если вы хотите объединить другие функции, то вам нужно будет рассмотреть взаимодействие атрибутов и методов, которые перекрываются между различными используемыми классами, и как порядок разрешения методов методов повлияет на то, какие версии методов будут вызваны в каком порядке.

Документация по общим представлениям на основе классов 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 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 by ListView
Поскольку у нас есть доступ к Publisher, чьи книги мы хотим отобразить, мы просто переопределяем get_queryset() и используем менеджер обратного внешнего ключа Publisher.
Publisher queryset for use in get_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, editing и дата. Например, объединение 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(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.urls 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 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.

Мы можем создать класс-mixin, который будет использоваться во всех наших представлениях, обрабатывая преобразование в JSON один раз.

Например, простой mixin 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.

Этот mixin предоставляет метод render_to_json_response() с той же сигнатурой, что и render_to_response(). Чтобы использовать его, нам просто нужно смешать его в TemplateView (например), и переопределить render_to_response() для вызова render_to_json_response() вместо этого:

from django.views.generic import TemplateView

class JSONView(JSONResponseMixin, TemplateView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

Аналогично, мы могли бы использовать наш mixin с одним из универсальных представлений. Мы можем создать собственную версию DetailView смешав JSONResponseMixin с django.views.generic.detail.BaseDetailView — (поведение DetailView перед рендерингом шаблона было смешано):

from django.views.generic.detail import BaseDetailView

class JSONDetailView(JSONResponseMixin, BaseDetailView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

Это представление можно развернуть так же, как и любое другое DetailView, с абсолютно таким же поведением — за исключением формата ответа.

Если вы хотите быть очень смелым, вы даже можете смешать подкласс DetailView, который может возвращать как HTML, так и JSON-контент, в зависимости от некоторых свойств HTTP-запроса, например, параметра запроса или HTTP-заголовка. Просто смешайте как JSONResponseMixin так и SingleObjectTemplateResponseMixin, и переопределите реализацию render_to_response() для делегирования соответствующему методу рендеринга в зависимости от типа ответа, который пользователь запросил:

from django.views.generic.detail import SingleObjectTemplateResponseMixin

class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
    def render_to_response(self, context):
        # Look for a 'format=json' GET argument
        if self.request.GET.get('format') == 'json':
            return self.render_to_json_response(context)
        else:
            return super(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.11/topics/class-based-views/mixins/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API