Django Framework

индекс
177,76

Немного подробностей про Class Based View, ч.1

Вступление

Здравствуйте, уважаемые читатели! Для многих джанго-разработчиков не секрет, что с версии 1.3+ вместо используемых ранее generic views нам необходимо начинать использовать class based views. А с 1.4+ версии generic views вообще могут стать deprecated. Информация про class based views (далее CBV) в интернете довольно скупа (а в рунете и подавно). Нет, конечно есть превосходные статьи, в том числе тут на хабре. Одна из статей была опубликована буквально на днях. Но я отношусь к той категории людей, что привыкли все изучать на практике и данный топик написан для таких же людей.
В качестве практического задания мной был выбран классический вариант — создание блога. Для того, чтобы использовать возможности CBV по максимуму мы немного усложним задачу. Пусть это будет не просто блог, а блог, с возможностью публиковать приватные статьи, недоступные неавторизованным пользователям.
Дальнейшие рассуждения подразумевают, что читатель ознакомлен с фреймворком Django и имеет навыки в создании проектов.


Часть 1, часть 2, часть 3, часть 4

Собственно с этого мы и начнем — с создания нового проекта. Хм… Пусть проект будет называться habratest.

Работа с проектом

Создание шаблонов и настройку проекта я оставляю на ваших плечах, статья не о том. Я же сразу перейду к нашим моделям и отображениям. Для начала определимся со структурой моделей. Вот на каком варианте остановился я:

# coding: utf-8
# author: damirazo

from django.contrib.auth.models import User
from django.db import models
from django.contrib import admin

class Post(models.Model):
    author = models.ForeignKey(to=User, verbose_name=u'Автор')
    name = models.CharField(max_length=128, verbose_name=u'Название')
    text = models.TextField(verbose_name=u'Текст')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name=u'Дата публикации')
    rating = models.IntegerField(default=0, verbose_name=u'Рейтинг')
    is_private = models.BooleanField(default=False, verbose_name=u'Приватная статья?')
    is_delete = models.BooleanField(default=False, verbose_name=u'Удаленная статья?')

    def __unicode__(self):
        return self.name

    class Meta(object):
        db_table = 'habraposts'
        ordering = ['-created_at',]

class PostAdmin(admin.ModelAdmin):
    list_display = ('name', 'author', 'created_at')
admin.site.register(Post, PostAdmin)


Собственно назначение каждого из полей понятно уже из атрибута verbose_name, поэтому я не буду здесь задерживаться.
Далее необходимо приступить к самому важному из пунктов — создание отображений, которые мы реализуем посредством CBV. Наш файл views.py будет выглядеть следующим образом:

# coding: utf-8
# author: damirazo

from django.views.generic import ListView
from models import Post

class Posts(ListView):
    """
    Список всех доступных статей
    """

    # Нижеуказанные параметры можно также передать данному отображению через метод as_view()
    # url(r'^$', Posts.as_view(context_object_name='posts', template_name='posts.html))
    model = Post
    # Под данным именем наш список статей будет доступен в шаблоне
    context_object_name = 'posts'
    # Название шаблона
    template_name = 'posts.html'
    # Количество объектов на 1 страницу
    paginate_by = 10

    def get_queryset(self):
        qs = Post.objects.filter(is_delete=False).order_by('-created_at')
        if not self.request.user.is_authenticated():
            return qs.exclude(is_private=True)
        return qs

class PostsIndex(Posts):
    """
    Список статей для главной страницы
    """
    template_name = 'index.html'

    def get_queryset(self):
        return super(PostsIndex, self).get_queryset().exclude(rating__lt=10)


Здесь, несмотря на комментарии в коде, я остановлюсь подробнее.
Класс Posts наследуется непосредственно от ListView, в котором реализована поддержка списков, разбитых на страницы. Мы переопределяем метод get_queryset, в котором осуществляем проверку на то авторизован ли пользователь на сайте. Для неавторизованных пользователей (гостей) мы исключаем из исходного queryset все объекты, значение атрибута is_private которых равняется True.
Следующий класс, который используется в данном примере носит имя PostsIndex. Он будет использоваться для отображения статей на главной странице. По аналогии с хабром там будут отображаться статьи, которые имеют рейтинг равный или больший, чем 10. В данном классе мы вновь переопределяем метод get_queryset. На этот раз из объекта Queryset (списка наших статей) исключаем также статьи, рейтинг которых менее 10. Так, как в родительском классе Posts прошла фильтрация объекта Queryset для пользователей и неавторизованных пользователей, то в классе PostIndex мы сразу получаем объект, доступный данному пользователю (или гостю). Поэтому одно из основных преимуществ CBV в возможности использовать все средства ООП при работе с отображениями.
Нам осталось лишь рассмотреть, как будут выглядеть маршруты в нашем проекте:

# Укажем дополнительный маршрут, чтобы главная страница была доступна без указания номера страницы
    # Отдельный список статей для главной страницы (рейтинг которых выше, либо равен, 10)
    url(r'^$', PostsIndex.as_view()),
    url(r'^page(?P<page>\d+)/$', PostsIndex.as_view()),

    # Список всех активных статей
    url(r'^posts/$', Posts.as_view()),
    url(r'^posts/page(?P<page>\d+)/$', Posts.as_view())


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

P.S. Если вы обнаружите ошибки или неточности в статье, то добро пожаловать в личные сообщения или комментарии. Также буду очень рад советам профессионалов, которые смогут расставить все точки над «i» и рассказать о моих ошибках.

Желаю всем хабрачитателям счастливых выходных и удачи :).

UPD. Обновил пример в статье (спасибо пользователю marazmiki). А также выложил тестовый проект, включающий данный пример. Не забудьте отредактировать файл settings.py под свои нужды. Проект можно найти здесь. Если вы обнаружите какие-либо ошибки или недоработки, то прошу сообщить.
+18
28 января 2012, 01:21
62

комментарии (19)

+1
Progrik #
Продолжайте дальше :)
Хотелось бы увидеть что-нибудь с реализациями CreateView, и что-нибудь интересное с BaseDateListView.
+2
damirazo #
Я лишь за :) В моих планах описать работу с CBV со всех сторон, причем на личном опыте. Уже готов пример статье работы с FormView (и будет опубликован в ближайшие дни).
Хотелось бы рассказать про основные SOAP модули (post/get/put/delete), и про остальные методы CBV. Тема достаточно интересная.
0
Progrik #
Давайте, публикуйте)
Хоть у этого блога и прилично читателей, но вот, к сожалению, реакции к статьям тут намного спокойнее, чем в других блогах. То есть, если «не выстрелит» — не отчаивайтесь.
+2
damirazo #
Поправка, не SOAP конечно, а REST :)
+5
marazmiki #
Мне кажется, что за формирование кверисета get_queryset() должен отвечать. А он тут статичен, зато вся логика по его созданию и изменению в зависимости от условий размазывается в dispatch(), который суть точка входа и желательно его по возможности вообще не трогать.

По-моему, рабочая вьюха с минимальной функциональностью делается так:

class PostView(ListView):
    model = Post

Обратите внимание, никаких перекрытий dispatch() и прочего. Пагинацию тоже не надо использовать низкоуровнево: в ListView, примесью которого является MultipleObjectMixin, она (пагинация) работает по умолчанию (пруф и примеры)

Собственно, весь Ваш код можно переписать компактнее

class PostView(ListView):
    """
    Просмотр списка статей, доступных текущему пользователю
    """
    model = Post
    context_object_name = 'posts'
    template_name = 'posts.html'

    def queryset(self):
        qs = Post.objects.filter(is_delete=False).order_by('-created_at')

        if not request.user.is_authenticated():
            qs = qs.exclude(is_private=True)
        return qs

class PostsIndex(PaginatedList):
    """
    Список статей для главной страницы
    """
    def get_queryset(self):
        return super(PostsIndex, self).get_queryset().exclude(rating__lt=10)


Я не проверял, но мне кажется, что должно быть примерно так :)

А вообще, конечно, погорячились разработчики Джанги с обязательным введением CBV. Начнут ведь теперь лепить классы везде где надо и не надо
0
marazmiki #
упс, сразу же работа над ошибками:

  1. в PostView следует читать queryset, а get_queryset
  2. в нём же: не request, а self.request
0
damirazo #
Ну вообще вы правы. Можно вообще все данные отображения свести до одного класса и реализовать классический RESTful. И насчет использования CBV это тоже холивар не для хабра) Просто в данной статье хотелось подчеркнуть особенности использования, показания лучшие стороны в ООП подобных отображений. То есть, что можно отнаследоваться от базового ListView и сделать свой класс с пагинацией, затем уже отнаследоваться от него и возвращать уже пагинованные списки объектов. Если делать все «по-человечески», то это можно реализовать в том числе «примесями (mixens)». ООП предлагает множество путей решения проблемы.
И да, везде использовать это не стоит, но иногда оправдано. Например, недавно решил переписать проект полностью на CBV, в итоге на логику отображений ушло намного меньше кода.
+1
marazmiki #
Можно вообще все данные отображения свести до одного класса и реализовать классический RESTful.

Мне кажется, вместо «можно» нужно употрелять слово «нужно». Собственно, они для того и писались.

Просто в данной статье хотелось подчеркнуть особенности использования, показания лучшие стороны в ООП подобных отображений

Самое грустное в том, что это неправильный пример. Я бы даже сказал — антипаттерн. Потому что (возможно, повторюсь):

  1. много буквстрок, следовательно «одним взглядом» весь код не охватить и не понять.
  2. написание собственного велосипеда вместо использования готового решения — это я в сторону пагинации. Вы на коленке реализовали то, что уже работает (и лучше :-)) из коробки.
  3. логика разбора кверисета находится совсем не там, где ей надлежит быть
  4. и таки да — перекрытие dispatch. Этого лучше по возможности избегать. Понятно, конечно, что далеко не всегда это пожелание осуществимо, но в данном случае — да.


Практическая полезность этого кода в том, что вы открыли dispatch — точку входа во вьюху. Молодым исследователям будет понятно, с чего начинать. Но я бы сделал приписку: «в чистом виде такой код к применению не рекомендуется» :-)
0
damirazo #
Насчет dispatch и get_queryset, в принципе, полностью согласен. Но пример не совсем удачный скорее не потому, что его можно сделать меньше, а потому, что он охватывает всего пару методов, имеющихся в CBV. В идеале, для лучшего восприятия материала, надо было показать работу и с остальными методами. Вот только не знаю как это лучше реализовать, чтобы все и сразу. А делать для этого отдельную статью было бы излишне с моей стороны.
Спасибо вам за конструктивные комментарии. Как я уже написал в статье — информации по CBV и правильным практикам их применения очень мало. Большинство их сходится лишь к холиварам на темы плохо это или хорошо. Поэтому все приходится изучать путем проб, ошибок и просмотра исходного кода. Пример в статье перепишу с учетом вашего комментария.
+2
phasma #
А в чем преимущество использования ООП во View?
0
damirazo #
Думаю то же, что и использование в других местах. Это не совсем преимущество, просто другой угол зрения, но для меня более удобным является возможность сделать базовый класс, определить функциональность, которая требуется в других отображениях. Затем данный класс можно наследовать другими отображениями как «примеси», используя на порядок меньше кода. Множественное наследование позволяет использовать несколько «примесей» для одного отображения, главное не забыть проконтролировать их порядок применения и конфликты.
0
grafmishurov #
Базовый класс т.н. абстрактный класс, я их так называю. Спасибо за статью, с нетерпением жду продолжения.
+1
kamiram #
Базовый класс, абстрактный класс и примесь это понятия из разных граней ООП :)
0
damirazo #
UPD. Обновил пример в статье (спасибо пользователю marazmiki). А также выложил тестовый проект, включающий данный пример. Не забудьте отредактировать файл settings.py под свои нужды. Проект можно найти здесь. Если вы обнаружите какие-либо ошибки или недоработки, то прошу сообщить.
+2
marazmiki #
*(а я кланяюсь, кланяюсь)*

class Posts(ListView, MultipleObjectMixin):


Не надо наследоваться от MultipleObjectMixin: ListView уже сделал это за Вас.
+1
damirazo #
И снова от меня тысяча благодарностей :) Действительно, я даже не удосужился посмотреть в исходный код. Радует, что разработчики django предусмотрели в списке объектов пагинацию «из коробки». Однако огорчает обилие лишних переменных. Все эти object_list и page_obj стоило, наверное, как-то собрать в 1 объект, чтобы было проще вызывать в шаблоне.
+1
alb #
После обновления примера объяснение к нему больше не соответствует коду, надо бы тоже поправить. Ну и import MultipleObjectMixin
теперь можно убрать.
+1
magic4x #
Да уж скорее бы :) Чувствовал себя идиотом, пока не дошло что код не тот. Ну или по крайней мере UPD подвиньте повыше, читатель пока до него не дойдет будет сидеть с круглыми глазами.
0
damirazo #
Вроде исправил) Прошу прощения, что сразу не заметил новые комментарии, по какой-то причине слетела подписка на статью, обнаружил чисто случайно.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.