Django: список с формой добавления

May 18, 2014 15:13 · 525 words · 3 minute read django

Для одного из своих pet-проектов понадобилось реализовать список с формой добавления туда элемента. Как оказалось, в Django это несколько нетривиальная задача. Все, кто работал с этим фреймворком, знают про class-view (CreateView, ListView…): вызываешь as_view с нужными параметрами в urlconf и всё готово :) Но класса типа ListAndCreateView я не нашёл. Хотя, как мне кажется, это довольно распространённая задача. Немного погуглив, я нашёл два ответа на SO (1, 2) на подобный вопрос. Первый сводится к тому, чтобы подмешать форму в контекст вывода, второй - сделать свой ListAndCreateView через множественное наследование. Рассмотрим сначала последний вариант.

Решение через множественное наследование

Предлагается перекрыть всего один метод get, который отвечает за формирование страницы, получив там объекты (форма добавления и список) для контекста:

class FormAndListView(BaseCreateView, BaseListView, TemplateResponseMixin):
    def get(self, request, *args, **kwargs):
        formView = BaseCreateView.get(self, request, *args, **kwargs)
        listView = BaseListView.get(self, request, *args, **kwargs)
        formData = formView.context_data['form']
        listData = listView.context_data['object_list']
        return render_to_response('textfrompdf/index.html', {'form' : formData, 'all_PDF' : listData},
                           context_instance=RequestContext(request))

Замечу, что в BaseView.get тоже происходит render_to_response, так что шаблон будет обработан трижды. Как-то нехорошо, ну да ладно, всё равно этот способ нерабочий :) Мне не удалось его запустить из-за множественного наследования. В BaseCreateView.get должен вызываться метод ProcessFormView.get, а вызывается предок BaseListView. В общем, из-за этого, а также остальных недостатков, я решил отказаться от этого решения.

Решение через простое наследование

Сразу скажу, что это решение заработало :) Идея в том, чтобы перекрыть метод заполнения контекста (get_context_data) и подмешать туда нужные данные. В итоге у меня получился такой класс:

class ListAndCreateView(ListView):

    form_class = None
    success_url = None

    def get_context_data(self, **kwargs):
        context = super(self.__class__, self).get_context_data(**kwargs)
        context['form'] = self.form_class()
        return context

    def post(self, request):
        form = self.form_class(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(self.success_url)
        else:
            return self.get(request)

Вызвать же можно как-то так (urls.py):

    url(r'^$', ListAndCreateView.as_view(
        queryset=Items.objects.all(),
        context_object_name='itemsProd',
        form_class=ItemsForm,
        success_url = reverse('items:index'),
        template_name='items/items.html',
    ), name="index"),

Как видите, появились новые параметры - название класса формы (form_class) и адрес куда переходить в случае успеха (success_url). В методе post содержится код, который взят из ModelFormMixin.form_valid.

Для тех, кто дочитал до конца

Есть ещё один способ решить эту проблему, но он основывается на знании внутренней иерархии классов-представлений. Взят он с github:

from django.views.generic.list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin, ProcessFormView

class ListAppendView(MultipleObjectMixin,
        MultipleObjectTemplateResponseMixin,
        ModelFormMixin,
        ProcessFormView):
    """ A View that displays a list of objects and a form to create a new object.
    The View processes this form. """
    template_name_suffix = '_append'
    allow_empty = True

    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()
        if not allow_empty and len(self.object_list) == 0:
            raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.")
                          % {'class_name': self.__class__.__name__})
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        context = self.get_context_data(object_list=self.object_list, form=form)
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        self.object = None
        return super(ListAppendView, self).post(request, *args, **kwargs)

    def form_invalid(self, form):
        self.object_list = self.get_queryset()
        return self.render_to_response(self.get_context_data(object_list=self.object_list, form=form))

Здесь методы get и post взяты из ListView и CreateView соответственно и творчески переработаны :) Отдельно стоит обратить внимание на кучу миксинов, которые так популярны в Django. Не скажу, что этот способ идеален, т.к. здесь переопределяются методы без вызова super, то есть фактически нарушен как минимум один из принципов SOLID. В последующих версиях Django авторы могут изменить поведение (добавить аннотации, валидаторов или чего ещё), а этот код так и не будет проадаптирован.