Django: список с формой добавления
May 18, 2014 15:13 · 525 words · 3 minute read
Для одного из своих 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 авторы могут изменить поведение (добавить аннотации, валидаторов или чего ещё), а этот код так и не будет проадаптирован.