🍛

Django ひとつの view に複数の form をもたせる方法と拡張

2020/10/22に公開1

背景

Recently, I had to implement the handling of two different forms on
the same page in Django. Though this use case is surprisingly common, I
couldn't find many examples on how to do it the "Django" way.
-- Handling Multiple Forms on the Same Page in Django

最近、Djangoで2つの異なるフォームをひとつのページに表示させなきゃいけないことがあった。びっくりするほどよくあることなのに、"Django的"に書く方法がほとんど見当たらなかった。

激しく同意。
そしてしっかり"Django的"なソリューションを残してくれたインド人に感謝。
GitHub Gist badri/cbv_multiple_forms.html

使ってみて思うこと

view.py
class MultipleFormsDemoView(MultiFormsView):
    template_name = "pages/cbv_multiple_forms.html"
    form_classes = {
        'contact': ContactForm,
        'subscription': SubscriptionForm,
    }

    success_urls = {
        'contact': reverse_lazy('form-redirect'),
        'subscription': reverse_lazy('form-redirect'),
    }

    def contact_form_valid(self, form):
        title = form.cleaned_data.get('title')
        form_name = form.cleaned_data.get('action')
        print(title)
        return HttpResponseRedirect(self.get_success_url(form_name))
    
    def subscription_form_valid(self, form):
        email = form.cleaned_data.get('email')
        form_name = form.cleaned_data.get('action')
        print(email)
        return HttpResponseRedirect(self.get_success_url(form_name))

複数のformをひとつのviewで扱えて感激。ただ、[form_name]_form_valid(self, form)を自分で実装しなきゃいけなくてフォームクラスが長くなりがち。この例はフォーム2個だからいいけど、今作ろうとしてるアプリは1ページにフォームが7個もある。
Lakshmiさんが作ってくれたMultiFormsViewの良さを残しつつ、generic.UpdateViewみたいにできないだろうか?

やってみた

Lakshmiさんの作ったMultiFormMixinProcessMultipleFormsViewと、DjangoのModelFormMixinSingleObjectTemplateResponseMixinをうまいこと使って、MultiFormsUpdateViewを作ってみました。

multiforms.py
# Lakshmi さんの multiforms.py のつづき

class ModelMultiFormMixin(ModelFormMixin, MultiFormMixin):
   def get_form_kwargs(self, form_name):
       kwargs = {}
       kwargs.update({'initial': self.get_initial(form_name)})
       kwargs.update({'prefix': self.get_prefix(form_name)})
       if self.request.method in ('POST', 'PUT'):
           kwargs.update({
               'data': self.request.POST,
               'files': self.request.FILES,
           })
       if hasattr(self, 'object'):
           kwargs.update({'instance': self.object})
       return kwargs

   def get_initial(self, form_name):
       initial_method = 'get_%s_initial' % form_name
       if hasattr(self, initial_method):
           return getattr(self, initial_method)()
       else:
           return {'action': form_name}

   def get_prefix(self, form_name):
       return self.prefixes.get(form_name, self.prefix)

   def get_context_data(self, **kwargs):
       kwargs.setdefault('view', self)
       if self.extra_context is not None:
           kwargs.update(self.extra_context)
       return kwargs

   def get_success_url(self, form_name=None):
       return self.success_urls.get(form_name, self.success_url)

   def forms_valid(self, forms, form_name):
       """If the forms are valid, save the associated model."""
       obj = forms.get(form_name)
       obj.save()
       return HttpResponseRedirect(self.get_success_url(form_name))


class BaseMultipleFormsUpdateView(ModelMultiFormMixin, ProcessMultipleFormsView):
   """
   Base view for updating an existing object.

   Using this base class requires subclassing to provide a response mixin.
   """
   def get(self, request, *args, **kwargs):
       self.object = self.get_object()
       return super().get(request, *args, **kwargs)

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


class MultiFormsUpdateView(SingleObjectTemplateResponseMixin, BaseMultipleFormsUpdateView):
   pass

なお、FormクラスはMultipleFormクラスのかわりに、そのModelForm版であるModelMultipleFormクラスを継承して作る必要がある。

forms.py
class ModelMultipleForm(forms.ModelForm):
    action = forms.CharField(max_length=60, widget=forms.HiddenInput())

そうすると、viewがかなりスッキリする。

view.py
class MultipleFormsDemoView(MultiFormsUpdateView):
    template_name = "pages/cbv_multiple_forms.html"
    form_classes = {
        'contact': ContactForm,
        'subscription': SubscriptionForm,
    }

    success_urls = {
        'contact': reverse_lazy('form-redirect'),
        'subscription': reverse_lazy('form-redirect'),
    }

作り方(良い子はマネしないでね)

まずはFormViewの構造を知るところから。
FormView
ふむふむ。

次はUpdateViewの構造。
UpdateView
FormViewに比べて、BaseFormViewBaseUpdateViewに変わったことで、FormMixinを継承する前にModelFormMixinがはさまった。
ModelFormMixinSingleObjectMixinも継承している。また、TemplateResponseMixinの前にSingleObjectTemplateResponseMixinがはさまったが、こっちはこのまま使えそうだ。

ここでMultiFormsViewの構造を勉強。
MultiFormsView
FormViewに比べて、FormMixinMultiFormMixinに変わっている。また、ProcessFormViewの前にProcessMultipleFormsViewがはさまっている。
UpdateViewと比べると、ModelFormMixinMultiFormMixinに変わっていることがわかる。SingleObjectTemplateResponseMixinは言わずもがな。

つまり、目指す姿はこうや!(ドン!
ModelMultiFormsView

あとは試行錯誤して作りましたとさ。
めでたしめでたし!

つぶやき

Django公式のgenericビューもLakshmi方式を採用してほしいなー。

Discussion

taungyeontaungyeon

汎用UpdateViewの作成ありがとうございます!
model or querysetをどこにも指定していないので、self.get_object()時に落ちるのですが何か抜け漏れ等ありませんでしょうか?
検討外れなことを言っていたら申し訳ないです;;