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

    Здравствуйте! В продолжении серии статей про Class Based Views (далее CBV) переходим к разделу, посвященному редактированию объектов. В данной статье мы рассмотрим четыре класса с говорящими названиями: FormView, CreateView, UpdateView, DeleteView.

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


    Создание и обработка формы с помощью CBV

    Для ряда действий, будь то регистрация или авторизация на сайте, публикация новости, комментария или добавление товара в магазине, невозможно обойтись без форм. В качестве универсального инструмента создания форм в Django выступает класс FormView. В самом просто случае для создания работоспособной формы достаточно лишь передать ему лишь ссылку на класс, описывающий необходимую форму:
    from django.views.generic.edit import FormView
    
    class RegisterForm(FormView):
        form_class = Register
        success_url = '/thanks/'
    


    или передать нужные данные непосредственно экземпляру класса FormView в нашем urlconf:

    url(r'^register/$', FormView.as_view(form_class=Register, success_url='/thanks/')

    Note: Пример синтетический и, разумеется, в таком виде использовать для страницы регистрации не получится.

    Класс формы, который необходимо обработать, возвращается методом get_form_class. По умолчанию данный метод возвращает атрибут form_class, которому мы присваивали класс нашей формы в примере выше. Мы можем переопределить данный метод, если нам требуется более сложная логика в определении класса формы.
    Метод get_success_url возвращает url ссылку, на которую будет осуществляться переход после успешной обработки формы. По умолчанию данный метод возвращает атрибут success_url.
    Для указания полям формы значений по умолчанию мы можем передать их непосредственно в атрибут initial, представляющий из себя словарь, ключи которого должны иметь имена требуемых полей формы. Значение данного атрибута возвращается по умолчанию методом get_initial.
    Часто возникает необходимость передать в форму определенные данные, например объект пользователя или заранее определенный список разделов. Для данного действия подходит метод get_form_kwargs. При переопределении данного метода необходимо соблюдать осторожность и не переписать случайно данные, передаваемые в форму по умолчанию. Среди них:

    def get_form_kwargs(self):
            """
            Возвращает словарь аргументов для экземпляра формы
            """
            kwargs = {'initial': self.get_initial()}
            if self.request.method in ('POST', 'PUT'):
                kwargs.update({
                    'data': self.request.POST,
                    'files': self.request.FILES,
                })
            return kwargs
    


    Чтобы избежать потери этих данных мы должны сначала получить словарь из родительского класса, затем добавить в него требуемые данные:

    class ProductForm(FormView):
    
        def get_form_kwargs(self):
            kwargs = super(ProductForm, self).get_form_kwargs()
            kwargs.update({
                'sections': Section.objects.filter(is_active=True)
            })
            return kwargs
    


    Для получения класса нашей формы мы можем использовать метод get_form, который по умолчанию возвращает класс формы, указанный через метод form_class, с переданным ему словарем метода get_form_kwargs:

    def get_form(self, form_class):
            return form_class(**self.get_form_kwargs())
    


    При обработке формы, в случае успеха, Django вызывает метод form_valid нашего отображения. По умолчанию данный метод осуществляет редирект по ссылке, возвращаемой методом get_success_url.
    В случае, когда форме переданы некорректные данные вызывается метод form_invalid, который по умолчанию возвращает пользователя обратно на страницу формы, передавая ей объект со списком ошибок валидации.

    class CreatePost(FormView):
        form_class = PostForm
        template_name = 'create_post.html'
        success_url = '/success/'
    
        @method_decorator(login_required)
        def dispatch(self, request, *args, **kwargs):
            super(CreatePost, self).dispatch(request, *args, **kwargs)
    
        def form_valid(self, form):
            Post.objects.create(**form.cleaned_data)
            return redirect(self.get_success_url())
    


    Теперь рассмотрим разные сферы применения форм более подробно.

    Создание нового экземпляра объекта

    С помощью примера выше мы можем с легкостью создать новый объект статьи, однако в Django есть средства, позволяющие создать объект с еще большей легкостью, это класс CreateView. Для применения данного класса, передаваемая ему форма должна наследоваться от ModelForm:

    from django import forms
    
    class NewsForm(forms.ModelForm):
        class Meta(object):
            model = News
            exclude = ('status',)
    


    Теперь мы можем передать объект данной формы в наше отображение. Пример отображения почти аналогичен предыдущему:

    from django.views.generic.edit import CreateView
    
    class CreateNews(CreateView):
        form_class = NewsForm
        template_name = 'create_news.html'
        succes_url = '/success/'
    
        def form_valid(self, form):
            Post.objects.create(**form.cleaned_data)
            return redirect(self.get_success_url())
    


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

    
    def form_valid(self, form):
            # Мы используем ModelForm, а его метод save() возвращает инстанс
            # модели, связанный с формой. Аргумент commit=False говорит о том, что
            # записывать модель в базу рановато.
            instance = form.save(commit=False)
    
            # Теперь, когда у нас есть несохранённая модель, можно ей чего-нибудь
            # накрутить. Например, заполнить внешний ключ на auth.User. У нас же
            # блог, а не анонимный имижборд, правда?
            instance.user = request.user
    
            # А теперь можно сохранить в базу
            instance.save() 
    
            return redirect(self.get_success_url())
    


    Note: Определение метода form_valid может быть излишне, так как CreateView наследует ModelFormMixin, который сохраняет экземпляр объекта из формы автоматически.

    Для обработки ошибок также используется метод form_invalid, функциональность которого аналогична оной класса FormView.

    Обновление экземпляра объекта

    Основное отличие класса UpdateView от CreateView, это передача экземпляра изменяемого объекта атрибуту object данного класса, в остальном данные классы идентичны. Для редактирования нам достаточно передать в url первичный ключ или slug изменяемого объекта:

    # Маршрут в urlconf
    url(r'^item/(?P<pk>\d+)/edit/$', ItemUpdate.as_view()),


    # Отображение в views.py
    
    from django.views.generic.edit import UpdateView
    
    class ItemUpdate(UpdateView):
        form_class = ItemForm
        model = Item
        template_name = 'create_item.html'
        success_url = '/success/'
    


    # Форма
    
    form django import forms
    
    class NewsForm(forms.ModelForm):
        class Meta(object):
            model = Item
            exclude = ('status',)
    


    # Модель
    
    from django.db import models
    
    class Item(models.Model):
        name = models.CharField(max_length=32, verbose_name=u'Название')
        description = models.TextField(verbose_name=u'Описание')
        status = models.BooleanField(default=True)
    
        def __unicode__(self):
            return self.name
    


    Описание формы и модели для класса CreateView выглядит аналогичным образом.

    Удаление экземпляра объекта

    Логика отображения для удаления объекта несколько отличается от рассмотренных предыдущих классов в данной статье. Удаление объекта, в целях безопасности, доступно лишь после передачи post или delete запроса. В случае get запроса будет отображена страница с подтверждением удаления объекта. В самом минималистичном варианте она может представлять из себя форму с кнопкой отправки и csrf токеном:

    <!-- item_confirm_delete.html -->
    <form action="" method="post">
        {% csrf_token %}
        <button>Удалить</button>
    </form>
    


    Отображение может выглядеть следующим образом:
    from django.views.generic.edit import DeleteView
    
    class ItemDelete(DeleteView):
        model = Item
        template_name = 'item_confirm_delete.html'
        success_url = '/success/'
    


    Разумеется если вам потребуются соответсвующие проверки на наличие авторизации у пользователей или соответствующих прав, то вы можете сделать это используя декораторы для метода dispatch или добавить свою логику проверки:

    from django.views.generic.edit import DeleteView
    
    class ItemDelete(DeleteView):
        model = Item
        template_name = 'item_confirm_delete.html'
        success_url = '/success/'
    
        @method_decorator(login_required)
        def dispatch(self, request, *args, **kwargs):
            super(ItemDelete, self).dispatch(request, *args, **kwargs)
    


    В данной статье мы рассмотрели работу с формами и управлением объектами, если у вас возникнут какие-либо вопросы, то я или другие читатели, надеюсь, смогут на них ответить. Также прошу сообщить обо всех найденных ошибках или неточностях и предложения по добавлению информации в статью, если я что-то пропустил. Спасибо, что прочитали статью и желаю счастливых выходных =)
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      +1
      def form_valid(self, form):
              Post.objects.create(**form.cleaned_data)
              return redirect(self.get_success_url())
      

      Это жесть и чревато ошибками. Лучше так:
      def form_valid(self, form):
              # Мы используем ModelForm, а его метод save() возвращает инстанс
              # модели, связанный с формой. Аргумент commit=False говорит о том, что
              # записывать модель в базу рановато.
              instance = form.save(commit=False)
      
              # Теперь, когда у нас есть несохранённая модель, можно ей чего-нибудь
              # накрутить. Например, заполнить внешний ключ на auth.User. У нас же
              # блог, а не анонимный имижборд, правда?
              instance.user = request.user
      
              # А теперь можно сохранить в базу
              instance.save() 
      
              return redirect(self.get_success_url())
      


      И ещё: не надоело в каждой вьюхе писать что-то такое?
          @method_decorator(login_required)
          def dispatch(self, request, *args, **kwargs):
              super(CreatePost, self).dispatch(request, *args, **kwargs)
      


      Особенно нелепо это выглядит на фоне class based, основная идея которого — избавить нас от рутины. Почему бы не сделать миксин, в котором все лишь раз прописать эту магическую формулу, а потом уже наследовать все вьюхи от него?
        +1
        Это жесть и чревато ошибками. Лучше так

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

        И ещё: не надоело в каждой вьюхе писать что-то такое?

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

        Разумеется если вам потребуются соответсвующие проверки на наличие авторизации у пользователей или соответствующих прав, то вы можете сделать это используя декораторы для метода dispatch или добавить свою логику проверки:


        Спасибо еще раз за объективную критику, на мой взгляд многие из ваших комментариев были полезнее самой статьи)
          0
          Ну как минимум миксинов будет не один, а пачка.
          В реальном проекте часто бывает нужен не один, а два-три декоратора в разном сочетании.
          Ну и для передачи параметров для миксинов придется писать код в любом случае.
            +4
            Давайте всё же попробуем обойтись одним миксином безо всяких пачек? =)

            class DecoratorChainingMixin(object):
                def dispatch(self, *args, **kwargs):
                    decorators = getattr(self, 'decorators', [])
                    base = super(DecoratorChainingMixin, self).dispatch
            
                    for decorator in decorators:
                        base = decorator(base)
                    return base(*args, **kwargs)
            

            А потом нам потребуется всего лишь перечислить требуемые декораторы в нужном порядке:

            class MyView(DecoratorChainingMixin, UpdateItem):
                form_class = ItemForm
                model = Item
                decorators = [login_required, never_cache, i_cherta_lysogo_v_stupe(foo='bar')]
            


            Но это, что называется, низкий уровень. Если хочется абстракций, можно пытаться писать микшины с изменяемым поведением:

            class UserPassesTestMixin(object):
                def user_passes_test(self, user):
                    return user.is_authenticated()
            
                def user_failed_test(self):
                    return redirect(LOGIN_URL)
            
                def dispatch(self, request, *args, **kwargs):
                    if not self.user_passes_test(request.user):
                        return self.user_failed_test()
                    return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)
            

            Наследуемся и когда потребуется сменить логику проверки доступа пользователя, просто перекрываем user_passes_test. В том маловероятном случае, если не устраивает редирект на страницу логина, можно перекрыть и user_failed_test.
              0
              Это уже интереснее. Спасибо!
              Попробую первый вариант использовать в текущем проекте :)
              Нужно пнуть разработчиков django чтобы улучшили работу с декораторами.
              Да и вообще текущая документация по CBV очень куцая…
                0
                В стиле django это должно называться иначе:
                -class DecoratorChainingMixin(object):
                +class ClassBasedViewMiddleware(object):
                
                  0
                  Почему? Это же не миддльварь всё-таки, а именно микшин, хоть и странный.
            0
            login_required можно оборачивать в urls.py.

            url(r'^/register$', login_required(AnyView.as_view())),
              0
              Это не всегда подходит. А так да, можно.
                0
                Оно может не подходить если в хендлере необходима кастомная проверка (но в этом случае декоратор вообще не подходит), а в целом оборачивание метода dispatch делает то же самое.
              0
              а это конец или будет еще? Можно еще тему миксинов разжувать :)
                0
                Ну если кто-нибудь не возьмется написать, то возможно попробую собрать список используемых миксинов. Хотя тут все уже зависит от конкретных задач и универсальных рецептов нет.
                0
                В наборе стандартных CBV не хватает вьюх для обработки формсетов и работы с дочерними объектами (inline). В нескольких проектах требовался такой функционал. Еще пара подобных проектов и можно будет куда-нибудь выложить боле-менее универсальное решение.
                  0
                  Спасибо за цикл статей. Как-то потратил много времени, чтобы разобраться.

                  Only users with full accounts can post comments. Log in, please.