Spec-Zone.ru › Django 1.10

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

Внимание

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

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

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

Документация по ссылкам на представления классов 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.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, чтобы иметь возможность 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.

Мы можем создать класс-миксин, чтобы использовать его во всех наших представлениях, обрабатывая преобразование в 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.10/topics/class-based-views/mixins/

Spec-Zone.ru

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