В Django версии 1.3 были представлены class-based views — способ описания view в виде классов. Документация, однако, касается только generic views, не описывая общую технику написания «вьюх» в виде классов. Начав использовать generic views и затем пытаясь изменить их поведение, я постепенно дошел до того момента, когда мне пришлось смотреть исходный код, чтобы понять, как что-то сделать. Поэтому я решил написать этот текст. Цель его — объяснить, как использовать class-based view и чем это может быть полезно.
Идея проста — есть некоторый часто используемый функционал (например, отображение объекта/списка объектов из БД, передача контекста в шаблон, и т.п.), который используется очень часто. Чтобы не приходилось писать однообразный код, подобные view описаны прямо в коде фреймворка. Для того, чтобы использовать их, нужно сообщить только специфические параметры, вроде типа объекта или имени шаблона, в который передавать контекст.
Например, у нас есть класс Publisher, и нам хочется отобразить на странице список всех объектов этого класса. В Django 1.2 мы используем generic view так:
Мы просто передаем в словарь параметров интересующий нас queryset, все остальное уже реализовано за нас.
Однако если в процессе разработки обнаруживается необходимость поменять поведение «вьюхи», изначально реализованной как generic (что бывает довольно часто), возникают проблемы:
Хочется иметь технологию, позволяющую использовать готовый фунционал Django для написания view, при этом имея возможность изменять его поведение в любой части.
Для того, чтобы в generic views можно было менять все, что угодно, разработчики решили представлять их в виде классов, в которых основные параметры представлены полями, а фунционал наглядно разбит на функции — отдельно диспетчеризация, отдельно формирование контекста, отдельно рендеринг шаблона (подробнее об этом см. ниже). От такого класса можно наследовать свои view, и перекрывать в них переменные и функции вместо передачи их в параметрах. Так выгладит реализация примера с Publisher на классах:
В urls.py же вместо названия «вьюхи» теперь передается метод as_view() описанного класса:
В качестве параметров этой фунции могут быть переданы переменные типа model или template_name, аналогично тому, как это делалось с function-based views.
Теперь, если нам нужно будет, например, поменять контекст, передаваемый в шаблон, можно просто переопределить в этом классе фунцию get_context_data(), которая занимается формированием контекста. И код будет жить там, где ему полагается — во views.py. Осталось только разобраться с тем, какие функции в каких generic view есть, и что они делают.
Ответы на вопросы о том, как вы хотите обработать параметры входящего запроса, как будете выбирать данные из БД и как будете формировать ответ часто не зависят друг от друга. Иногда, вам хочется выдать в качестве ответа HTML страницу, иногда — JSON-объект. Если вы хотите написать общий код для обработки запроса и работы с БД, который будет использоваться с обоих случаях, нужно как-то выделить группу всех методов, отвечающих за формирование ответа из контекста. Такую проблему можно решить с помощью паттерна примесей (mixins).
Каждая class-based generic view:
Итак, для расширения фунционала generic view нужно:
Использование class-based generic views мотивирует писать все «вьюхи» в виде классов, все из тех же соображений снижения повторяемости и структуризации. Если из function-based generic views вам часто не подходила ни одна — по причинам, описанным выше, то из class-based generic views вам всегда будет полезен View.
Все generic views наследуются от этого класса. В нем реализована функция dispatch(), принимающая request и возвращающая HttpReponse, собственно то, что и делала всегда view, будучи фунцией. Стоит заметить, что функция as_view() как раз возвращает указатель на dispatch(), что делает ясной аналогию с function-based views — в своем минималистичном варианте класс View — класс, содержащий всего одну функцию, которую и вызывает контроллер из URLCONF.
Класс View умеет вызывать свои фунции get(), post() и т.п. в зависимости от типа запроса, передавая request туда.
Пример простой реализации «вьюхи» в виде класса можно посмотреть, например, здесь. Ниже приведен сложный пример — моя переделка примера из документации — гораздо более полно иллюстрирующий удобство написания «вьюх» в виде классов.
Проект требует, чтобы любые данные, запрашиваемые клиентом, могли быть возвращены как в виде обычной HTML-страницы, так и в виде JSON-объекта — в зависимости от GET-параметра запроса.
Нужно написать общий класс «вьюхи», умеющий представлять ответ в двух видах, и от него наследовать все последующие «вьюхи» в проекте.
В django реализован TemplateResponseMixin, функция render_to_response() которого умеет рендерить указанный в template_name шаблон с переданным ей в качестве параметра контекстом.
Напишем свою примесь, которая будет вместо HTML возвращать JSON:
Функцию конвертации контекста в JSON нужно писать уже понимая, что этот контекст из себя представляет, поэтому она не реализована.
Теперь напишем класс, который будет уметь возвращать и HTML, и JSON:
Функция get переопределяет ту, что объявлена во View. В зависимости от GET-параметра, контекст отдается на рендеринг в одну из двух примесей, и возвращает JSON или HTML. В первом случае понадобится функция, определяющая перевод данного контекста в json. Во втором случае понадобится название шаблона, в котором этот контекст рендерить. Любая «вьюха»-наследник этой, должна иметь имплементации двух методов: get_context(request) — формирующего контекст, и convert_context_to_json(context) — переводящего его в JSON, и определенный template_name.
Реализация конечного view могла быть, например, такой:
В этой статье я постарался прояснить то, чего мне нехватало в документации Django — идеи, стоящие за class-based views, и способ их использования. Надеюсь, практика такого повсеместного использования generic views и их последующего расширения в тех случаях, когда это необходимо, покажется кому-то полезной.
Спасибо за внимание, буду рад любой критике.
Мотивация
Идея проста — есть некоторый часто используемый функционал (например, отображение объекта/списка объектов из БД, передача контекста в шаблон, и т.п.), который используется очень часто. Чтобы не приходилось писать однообразный код, подобные view описаны прямо в коде фреймворка. Для того, чтобы использовать их, нужно сообщить только специфические параметры, вроде типа объекта или имени шаблона, в который передавать контекст.
Например, у нас есть класс Publisher, и нам хочется отобразить на странице список всех объектов этого класса. В Django 1.2 мы используем generic view так:
# urls.py
from django.views.generic import list_detail
from books.models import Publisher
urlpatterns = patterns('',
(r'^publishers/$', list_detail.object_list, {
"queryset" : Publisher.objects.all(),
})
)
Мы просто передаем в словарь параметров интересующий нас queryset, все остальное уже реализовано за нас.
Однако если в процессе разработки обнаруживается необходимость поменять поведение «вьюхи», изначально реализованной как generic (что бывает довольно часто), возникают проблемы:
- В generic view не предусмотрены параметры, меняющие интересующее поведение.
- Код, формирующий параметры для generic view, сравним по объему с кодом отдельно написанной «вьюхи», и лежит в URLCONF, где ему явно не место.
Хочется иметь технологию, позволяющую использовать готовый фунционал Django для написания view, при этом имея возможность изменять его поведение в любой части.
Generic views на классах
Для того, чтобы в generic views можно было менять все, что угодно, разработчики решили представлять их в виде классов, в которых основные параметры представлены полями, а фунционал наглядно разбит на функции — отдельно диспетчеризация, отдельно формирование контекста, отдельно рендеринг шаблона (подробнее об этом см. ниже). От такого класса можно наследовать свои view, и перекрывать в них переменные и функции вместо передачи их в параметрах. Так выгладит реализация примера с Publisher на классах:
# views.py
from django.views.generic import DetailView
from books.models import Publisher
class PublisherDetailView(DetailView):
model = Publisher
В urls.py же вместо названия «вьюхи» теперь передается метод as_view() описанного класса:
urlpatterns = patterns('',
(r'^publishers/$', PublisherDetailView.as_view()),
)
В качестве параметров этой фунции могут быть переданы переменные типа model или template_name, аналогично тому, как это делалось с function-based views.
Теперь, если нам нужно будет, например, поменять контекст, передаваемый в шаблон, можно просто переопределить в этом классе фунцию get_context_data(), которая занимается формированием контекста. И код будет жить там, где ему полагается — во views.py. Осталось только разобраться с тем, какие функции в каких generic view есть, и что они делают.
Примеси
Ответы на вопросы о том, как вы хотите обработать параметры входящего запроса, как будете выбирать данные из БД и как будете формировать ответ часто не зависят друг от друга. Иногда, вам хочется выдать в качестве ответа HTML страницу, иногда — JSON-объект. Если вы хотите написать общий код для обработки запроса и работы с БД, который будет использоваться с обоих случаях, нужно как-то выделить группу всех методов, отвечающих за формирование ответа из контекста. Такую проблему можно решить с помощью паттерна примесей (mixins).
Каждая class-based generic view:
Итак, для расширения фунционала generic view нужно:
- Посмотреть в документации, какие примеси она содержит;
- Найти в этих примесях ту фунцию, в которой реализовано интересующее вас поведение;
- Переопределить эту фунцию.
Все «вьюхи» как классы
Использование class-based generic views мотивирует писать все «вьюхи» в виде классов, все из тех же соображений снижения повторяемости и структуризации. Если из function-based generic views вам часто не подходила ни одна — по причинам, описанным выше, то из class-based generic views вам всегда будет полезен View.
View
Все generic views наследуются от этого класса. В нем реализована функция dispatch(), принимающая request и возвращающая HttpReponse, собственно то, что и делала всегда view, будучи фунцией. Стоит заметить, что функция as_view() как раз возвращает указатель на dispatch(), что делает ясной аналогию с function-based views — в своем минималистичном варианте класс View — класс, содержащий всего одну функцию, которую и вызывает контроллер из URLCONF.
Класс View умеет вызывать свои фунции get(), post() и т.п. в зависимости от типа запроса, передавая request туда.
Пример
Пример простой реализации «вьюхи» в виде класса можно посмотреть, например, здесь. Ниже приведен сложный пример — моя переделка примера из документации — гораздо более полно иллюстрирующий удобство написания «вьюх» в виде классов.
Задача
Проект требует, чтобы любые данные, запрашиваемые клиентом, могли быть возвращены как в виде обычной HTML-страницы, так и в виде JSON-объекта — в зависимости от GET-параметра запроса.
Решение
Нужно написать общий класс «вьюхи», умеющий представлять ответ в двух видах, и от него наследовать все последующие «вьюхи» в проекте.
В django реализован TemplateResponseMixin, функция render_to_response() которого умеет рендерить указанный в template_name шаблон с переданным ей в качестве параметра контекстом.
Напишем свою примесь, которая будет вместо HTML возвращать JSON:
class JsonResponseMixin(object):
def render_to_reponse(self, context):
return http.HttpResponse(self.convert_context_to_json(context),
content_type='application/json')
def convert_context_to_json(self, context, extract_from_queryset=None):
pass
Функцию конвертации контекста в JSON нужно писать уже понимая, что этот контекст из себя представляет, поэтому она не реализована.
Теперь напишем класс, который будет уметь возвращать и HTML, и JSON:
class MixedView(View, JsonResponseMixin, TemplateResponseMixin):
def get_context(self, request):
pass
def get(self, request, *args, **kwargs):
context = self.get_context(request)
if request.GET.get('format', 'html') == 'json' or self.template_name is None:
return JsonResponseMixin.render_to_reponse(self, context)
else:
return TemplateResponseMixin.render_to_response(self, context)
Функция get переопределяет ту, что объявлена во View. В зависимости от GET-параметра, контекст отдается на рендеринг в одну из двух примесей, и возвращает JSON или HTML. В первом случае понадобится функция, определяющая перевод данного контекста в json. Во втором случае понадобится название шаблона, в котором этот контекст рендерить. Любая «вьюха»-наследник этой, должна иметь имплементации двух методов: get_context(request) — формирующего контекст, и convert_context_to_json(context) — переводящего его в JSON, и определенный template_name.
Реализация конечного view могла быть, например, такой:
class PublisherView(MixedView):
def get_context(self, request):
context = dict()
context['publishers'] = Publisher.objects.all()
return context
template_name = 'publisher_list.html'
def convert_context_to_json(self, context):
json_context = dict()
json_context['publisher_names'] = [p.name for p in context['publishers']]
return json.dumps(json_context, encoding='utf-8', ensure_ascii=False)
Заключение
В этой статье я постарался прояснить то, чего мне нехватало в документации Django — идеи, стоящие за class-based views, и способ их использования. Надеюсь, практика такого повсеместного использования generic views и их последующего расширения в тех случаях, когда это необходимо, покажется кому-то полезной.
Спасибо за внимание, буду рад любой критике.