Spec-Zone.ru › Django 1.9

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

Внимание

Это продвинутая тема. Для изучения этих техник рекомендуется предварительное знание представлений 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 для представлений удаления.)

END_OF_DOCUMENT_MARKER

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 с представлением

Если мы хотим написать простое представление на основе класса, которое отвечает только на POST, мы наследуем View и напишем метод post() в подклассе. Однако если мы хотим, чтобы наша обработка работала с определённым объектом, определённым из URL, мы захотим функциональность, предоставляемую SingleObjectMixin.

Мы продемонстрируем это с моделью Author , которую мы использовали в введении обобщённых представлений на основе класса.

from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author

class RecordInterest(SingleObjectMixin, View):
    """Records the current user's interest in an author."""
    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseForbidden()

        # Look up the author we're interested in.
        self.object = self.get_object()
        # Actually record interest somehow here!

        return HttpResponseRedirect(reverse('author-detail', kwargs={'pk': self.object.pk}))

На практике вам, вероятно, захочется записать интерес в хранилище ключевых значений, а не в реляционную базу данных, поэтому мы опустили этот момент. Единственная часть представления, которая должна беспокоиться об использовании SingleObjectMixin, — это место, где мы хотим найти автора, который нас интересует, что она просто делает с помощью простого вызова self.get_object(). Всё остальное обрабатывается за нас набором миксинов.

Мы можем легко подключить это к нашим URL:

from django.conf.urls import url
from books.views import RecordInterest

urlpatterns = [
    #...
    url(r'^author/(?P<pk>[0-9]+)/interest/$', RecordInterest.as_view(), name='author-interest'),
]

Обратите внимание на pk именованную группу, которую get_object() использует для поиска экземпляра Author . Вы также можете использовать псевдоним или любые другие функции SingleObjectMixin.

Использование SingleObjectMixin с ListView

ListView предоставляет встроенную постраничную навигацию, но вы можете захотеть постраничную навигацию по списку объектов, которые все связаны (с помощью внешнего ключа) с другим объектом. В нашем примере публикации вы можете захотеть постранично просмотреть все книги конкретного издателя.

Один из способов сделать это — объединить ListView с SingleObjectMixin, чтобы набор запросов для постраничного списка книг мог зависеть от издателя, найденного как одиночный объект. Для этого нам нужны два разных набора запросов:

Book queryset for use 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. Однако вещи становятся все более сложными по мере попытки сделать это, и хорошим правилом является:

Подсказка

Каждый из ваших представлений должен использовать только миксины или представления из одной из групп универсальных представлений на основе классов: детали, список, редактирование и дата. Например, сочетание TemplateView (встроенное представление) с MultipleObjectMixin (универсальный список) допустимо, но вы, вероятно, столкнетесь с проблемами при объединении SingleObjectMixin (универсальные детали) с MultipleObjectMixin (универсальный список).

Чтобы продемонстрировать, что происходит, когда вы пытаетесь добиться большей сложности, мы покажем пример, который жертвует удобочитаемостью и поддерживаемостью, когда существует более простое решение. Сначала давайте посмотрим на наивную попытку объединить DetailView с FormMixin, чтобы позволить нам отобразить Django Form на той же URL-адрессе, что и отображение объекта с помощью DetailView.

Использование FormMixin с DetailView

Вернемся к нашему предыдущему примеру использования View и SingleObjectMixin вместе. Мы записывали интерес пользователя к определенному автору; допустим, теперь мы хотим позволить им оставить сообщение, объясняя, почему они их любят. Снова предположим, что мы не будем хранить это в реляционной базе данных, а вместо этого в чем-то более эзотерическом, о чем мы здесь беспокоиться не будем.

На этом этапе естественно обратиться к Form для инкапсуляции информации, отправленной из браузера пользователя в Django. Предположим также, что мы сильно инвестированы в REST, поэтому мы хотим использовать одну и ту же URL-адресу для отображения автора и для захвата сообщения от пользователя. Давайте перепишем наш AuthorDetailView для этого.

Мы сохраним обработку GET из DetailView, хотя нам нужно будет добавить Form в данные контекста, чтобы мы могли отобразить его в шаблоне. Мы также захотим включить обработку формы из FormMixin и написать немного кода, чтобы при POST форма вызывалась должным образом.

Примечание

Мы используем FormMixin и реализуем post() самостоятельно, а не пытаемся смешивать DetailView с FormView (который уже предоставляет подходящую post()), потому что оба представления реализуют get(), и все станет намного сложнее.

Наш новый AuthorDetail выглядит так:

# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.

from django import forms
from django.http import HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDetail(FormMixin, DetailView):
    model = Author
    form_class = AuthorInterestForm

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

    def get_context_data(self, **kwargs):
        context = super(AuthorDetail, self).get_context_data(**kwargs)
        context['form'] = self.get_form()
        return context

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseForbidden()
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        # Here, we would record the user's interest using the message
        # passed in form.cleaned_data['message']
        return super(AuthorDetail, self).form_valid(form)

get_success_url() просто предоставляет место для перенаправления, которое используется в реализации по умолчанию form_valid(). Мы должны предоставить собственную post(), как отмечалось ранее, и переопределить get_context_data() , чтобы сделать Form доступным в данных контекста.

Лучшее решение

Должно быть очевидно, что количество тонких взаимодействий между FormMixin и DetailView уже проверяет нашу способность управлять вещами. Вряд ли вы захотите написать такой класс сами.

В этом случае было бы довольно легко просто написать метод post() самостоятельно, сохраняя DetailView единственной универсальной функциональностью, хотя написание кода обработки Form предполагает значительную дублирование.

В качестве альтернативы, было бы проще, чем вышеупомянутый подход, иметь отдельное представление для обработки формы, которое могло бы использовать FormView, отличное от DetailView, без проблем.

Альтернативное лучшее решение

Мы пытаемся использовать два разных представления на основе классов из одного URL. Так почему бы не сделать именно так? У нас есть очень четкое разделение: запросы GET должны получать DetailView (с Form, добавленным в данные контекста), а запросы POST должны получать FormView. Давайте сначала настроим эти представления.

Представление AuthorDisplay почти такое же, как когда мы впервые представили AuthorDetail; нам нужно написать собственное get_context_data() для того, чтобы AuthorInterestForm были доступны шаблону. Мы пропустим переопределение get_object() из предыдущего примера для большей ясности:

from django.views.generic import DetailView
from django import forms
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDisplay(DetailView):
    model = Author

    def get_context_data(self, **kwargs):
        context = super(AuthorDisplay, self).get_context_data(**kwargs)
        context['form'] = AuthorInterestForm()
        return context

Затем AuthorInterest — это простое FormView, но нам нужно добавить SingleObjectMixin, чтобы мы могли найти автора, о котором говорим, и нам нужно запомнить установку template_name для того, чтобы ошибки формы отображались в том же шаблоне, что и AuthorDisplay использует для GET:

from django.core.urlresolvers import reverse
from django.http import HttpResponseForbidden
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin

class AuthorInterest(SingleObjectMixin, FormView):
    template_name = 'books/author_detail.html'
    form_class = AuthorInterestForm
    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseForbidden()
        self.object = self.get_object()
        return super(AuthorInterest, self).post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

Наконец, мы объединяем всё это в новом представлении AuthorDetail. Мы уже знаем, что вызов as_view() для представления на основе класса даёт нам что-то, что ведет себя точно так же, как представление на основе функции, поэтому мы можем сделать это в момент выбора между двумя подпредставлениями.

Конечно, вы можете передавать ключевые аргументы в as_view() так же, как и в вашем URLconf, например, если вы хотите, чтобы поведение AuthorInterest также появлялось на другом URL, но с использованием другого шаблона:

from django.views.generic import View

class AuthorDetail(View):

    def get(self, request, *args, **kwargs):
        view = AuthorDisplay.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = AuthorInterest.as_view()
        return view(request, *args, **kwargs)

Этот подход также может быть использован с любыми другими общими представлениями на основе класса или с вашими собственными представлениями на основе класса, наследующими непосредственно от View или TemplateView, поскольку он держит разные представления максимально независимыми.

Больше, чем просто HTML

Преимущества представлений на основе классов проявляются, когда вам нужно сделать одно и то же много раз. Предположим, вы пишете API, и каждое представление должно возвращать JSON вместо рендеринга HTML.

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

Например, простой миксин JSON может выглядеть примерно так:

from django.http import JsonResponse

class JSONResponseMixin(object):
    """
    A mixin that can be used to render a JSON response.
    """
    def render_to_json_response(self, context, **response_kwargs):
        """
        Returns a JSON response, transforming 'context' to make the payload.
        """
        return JsonResponse(
            self.get_data(context),
            **response_kwargs
        )

    def get_data(self, context):
        """
        Returns an object that will be serialized as JSON by json.dumps().
        """
        # Note: This is *EXTREMELY* naive; in reality, you'll need
        # to do much more complex handling to ensure that arbitrary
        # objects -- such as Django model instances or querysets
        # -- can be serialized as JSON.
        return context

Примечание

Обратитесь к документации по сериализации объектов Django за дополнительной информацией о том, как правильно преобразовать модели Django и запросы в JSON.

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

from django.views.generic import TemplateView

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

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

from django.views.generic.detail import BaseDetailView

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

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

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

from django.views.generic.detail import SingleObjectTemplateResponseMixin

class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
    def render_to_response(self, context):
        # Look for a 'format=json' GET argument
        if self.request.GET.get('format') == 'json':
            return self.render_to_json_response(context)
        else:
            return super(HybridDetailView, self).render_to_response(context)

Из-за того, как Python разрешает перегрузку методов, вызов super(HybridDetailView, self).render_to_response(context) в конечном итоге вызывает реализацию render_to_response() у TemplateResponseMixin.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.9/topics/class-based-views/mixins/

Spec-Zone.ru

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