Введение в представления-классы (Class-based views, CBV)

Представления, основанные на классах, представляют собой альтернативный путь реализации представлений. Вместо функций здесь используются объекты Python. Этот способ не лучше и не худше, он другой. Вот основные отличия:

  • Организация обработки специфичных для HTTP методов (GET, POST, и т.д.) разнесена по соответствующим методам, вместо того, чтобы писать кучу условий.

  • Объектно-ориентированные технологии, такие как миксины (примеси, множественное наследование) позволяют выделять код в компоненты, которые могут повторно использоваться.

Связь и история обобщённых представлений, представлений, основанных на классах, а также обобщённых представлений, основанных на классах.

В самом начале были только функции: Django передавал ей HttpRequest и ожидал получения HttpResponse. Это всё, что было предусмотрено.

Ранее были найдены общие шаблоны в разработке представлений. Обобщённые представления, основанные на функциях, были выделены в абстрактные шаблоны, подходящие для большинства случаев.

По мере покрытия всё больших возможных случаев выявилась одна проблема - шаблоны становились громоздкими и не расширяемыми. Это ограничивало их использование во многих реальных приложениях.

Представления, основанные на классах, были созданы для решения той же задачи, что и обобщённые функции-представления: упростить разработку. Однако, реализовано это было с помощью примесей: наборов классов, которые делают обобщённые представления-классы более расширяемыми и гибкими в настройке, нежели соответствующие функции.

Если вы ранее успешно использовали функции и их возможностей вам хватало, то не стоит относиться к обобщённым представлениям-классам как к ещё одному эквивалентному способу решения задач. Попробуйте по-новому взглянуть на те случаи, когда этот подход будет более уместен.

Инструментарий базовых классов и примесей Django позволяет создавать обобщённые представления-классы максимально гибкими, имеющими множество хуков, таких как значение атрибутов по умолчанию или базовая реализация методов, которую зачастую не надо переопределять в каждом конкретном случае. Например, вместо того, чтобы использовать атрибут базового класса form_class, реализация вызывает метод get_form, который вызывает get_form_class, который в свою очередь по умолчанию просто возвращает атрибут form_class базового класса. Это даёт вам возможность указать какую форму использовать как в атрибуте так и динамически в переопределённом методе. На первый взгляд такое поведение порождает излишнюю сложность для простых случаев, но это необходимо для покрытия максимально возможных случаев использования.

Использование представлений, основанных на классах

По своей сути представления, основанные на классах, позволяют реагировать на различные методы HTTP запроса различными методами объекта, а не городить ветвление внутри одной функции.

Для примера возьмём обработку GET запроса в функции:

from django.http import HttpResponse

def my_view(request):
    if request.method == 'GET':
        # <view logic>
        return HttpResponse('result')

Теперь та же реализация через классы:

from django.http import HttpResponse
from django.views.generic.base import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse('result')

Посколько роутинг Django отправляет запрос и ассоциированные с ним аргументы в вызываемую функцию, а не класс, все классы имеют статичный метод as_view(), который указан как точка входа при вызове этого класса. В нём создаётся экземпляр вашего класса и вызывается метод dispatch(), в котором определяется тип запроса (GET, POST и др.) и вызывается соответствующий метод. Если такового не нашлось, то происходит исключение HttpResponseNotAllowed:

# urls.py
from django.conf.urls import patterns
from myapp.views import MyView

urlpatterns = patterns('',
    (r'^about/', MyView.as_view()),
)

Стоит отметить, что этот метод возвращает результат, который идентичен результату функции, а именно HttpResponse. Это означает, что http shortcuts или объекты TemplateResponse можно использовать внутри представлений-классов.

В то время как минимальное представление, основанное на классах, не нуждается в каких-либо атрибутах этого класса, они могут быть полезны во многих внутренних методах. Есть два способа их установки.

Первым является стандартный для Python механизм наследования и переопределения атрибутов или методов. Так, если родительский класс имеет атрибут greeting, то выглядеть это будет как-то так:

from django.http import HttpResponse
from django.views.generic.base import View

class GreetingView(View):
    greeting = "Good Day"

    def get(self, request):
        return HttpResponse(self.greeting)

Вы можете переопределить это в наследнике:

class MorningGreetingView(GreetingView):
    greeting = "Morning to ya"

Другим способом является указание параметра с соответствующим именем при вызове as_view() в URLconf:

urlpatterns = patterns('',
    (r'^about/', GreetingView.as_view(greeting="G'day")),
)

Примечание

Хотя ваш класс инициализируется для обработки каждого запроса, атрибуты в методе as_view() задаются только раз при импорте URL`ов.

Использование примесей (mixins)

Примеси представляют собой форму множественного наследования, в котором поведение и атрибуты всех родителей соединены в один класс.

Например, в Django есть примесь под названием TemplateResponseMixin, основной задачей которого является определение метода render_to_response(). Когда она будет объединена с поведением базового класса View, получится класс TemplateView, который обрабатывает запрос соответствующим методом (реализация находится в классе View), а затем передаётся в render_to_response(), который, используя атрибут template_name, возвращает объект TemplateResponse (реализовано в TemplateResponseMixin).

Примеси - великолепный путь для повторного использования общего кода, однако у них есть некоторые минусы. Чем больше реализация методов будет размазана по примесям, тем сложнее будет читать дочерний класс и понимать что же он всё-таки делает, какие методы откуда берутся и какие методы уже перекрыты в родительских классах.

Стоит отметить, что можно унаследоваться только от одного обобщённого класса, т.к. каждый из них наследуется от View; остальные должны быть примеси. Попытка скрестить более чем один обобщённый класс, например, ProcessFormView и ListView, не будет работать так, как вы ожидаете.

Обработка форм с помощью представлений-классов

Обработка формы через представление-функцию выглядит как-то так:

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import MyForm

def myview(request):
    if request.method == "POST":
        form = MyForm(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')
    else:
        form = MyForm(initial={'key': 'value'})

    return render(request, 'form_template.html', {'form': form})

Код, выполняющий тоже самое, но реализованный через классы:

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views.generic.base import View

from .forms import MyForm

class MyFormView(View):
    form_class = MyForm
    initial = {'key': 'value'}
    template_name = 'form_template.html'

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {'form': form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')

        return render(request, self.template_name, {'form': form})

Это очень простой пример, однако он демонстрирует возможности настройки атрибутов класса, таких как form_class, через конфигурацию URLconf или же переопределение методов в дочернем классе.

Декорирование представлений-классов

Расширение представлений-классов не ограничивается использованием миксинов, также можно подключить декораторы. Так как представления-классы не являются функциями, то работа с ними несколько отличается в зависимости от того вызываете ли вы as_view или создаёте дочерний класс.

Декорирование в URLconf

Простейшим вариантом является передача результата вызова метода as_view() в нужный декоратор. Лучшее место для этого - URLconf, в котором используется представление:

from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView

from .views import VoteView

urlpatterns = patterns('',
    (r'^about/', login_required(TemplateView.as_view(template_name="secret.html"))),
    (r'^vote/', permission_required('polls.can_vote')(VoteView.as_view())),
)

Этот подход работает только когда нужно обернуть только один вызов, в противном случае следует действовать по-другому.

Декорирование классов

Для декорирования каждого метода представления-класса, вам нужно было бы обернуть каждый метод. В Django это сделано проще: надо всего лишь декорировать метод dispatch().

Метод класса не то же самое, что и обычная функция, к нему просто так применить декоратор не получится – нужно сначала преобразовать его в метод-декоратор. Для этого существует декоратор method_decorator, которым можно воспользоваться как-то так:

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

class ProtectedView(TemplateView):
    template_name = 'secret.html'

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(ProtectedView, self).dispatch(*args, **kwargs)

В этом примере каждый вызов метода из ProtectedView будет требовать авторизацию.

Примечание

method_decorator передаёт *args и **kwargs как параметры в декорированный метод. Если ваш метод не сможет их принять, то будет сгенерировано исключение TypeError.