naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Django、モデルフォームセットを汎用ビューから使う

プログラミング関連 Django Python 約19日前
2018年1月2日19:22
Djangoでは、CreateViewを使うことでデータの追加機能が作れます。

例えば以下のようなモデルがあったとして...
from django.db import models
from django.utils import timezone


class Day(models.Model):
    """日記の一日となるモデル"""
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateTimeField('日付', default=timezone.now)

    def __str__(self):
        return self.title



CreateViewはシンプルなものだと以下のようになります。
class AddView(generic.CreateView):
    model = Day
    form_class = DayCreateForm
    success_url = reverse_lazy('diary:index')



テンプレートは、例えばシンプルに書くなら以下のように。
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
  <table class="table">
      <tr>
        <th>タイトル</th>
        <td>{{ form.title }}</td>
      </tr>
      <tr>
        <th>本文</th>
        <td>{{ form.text }}</td>
      </tr>
      <tr>
        <th>日付</th>
        <td>{{ form.date }}</td>
      </tr>
  </table>
  <button type="submit" class="btn btn-primary">送信</button>
  {% csrf_token %}
</form>
{% endblock %}




これは1件のデータが追加できるのですが、もし、一度に複数のデータを追加したい場合はどうでしょうか。
その場合、モデルフォームセットが使えます。
forms.pyにて、以下のように書きます。
from django import forms
from .models import Day


class DayCreateForm(forms.ModelForm):

    class Meta:
        model = Day
        fields = '__all__'


# ここが追加!!
DayCreateFormSet = forms.modelformset_factory(
    Day, form=DayCreateForm, extra=3
)




追加されたのは以下です。第一引数はどのモデルか、form引数は基になるフォームがあれば指定、extraは1度に何件追加できるようにするかです。
DayCreateFormSet = forms.modelformset_factory(
    Day, form=DayCreateForm, extra=3
)



特に基となるフォームがないならば、fieldsかexcludeを指定してもOKです。
DayCreateFormSet = forms.modelformset_factory(
    Day, fields='__all__', extra=3
)



では、views.pyです。残念ながらCreateViewそのままでは対応ができません。
FormViewを使い、メソッドを上書きしてみます。FormViewなのでtemplate_nameも指定するよう、忘れないようにしてください。
class AddView(generic.FormView):
    template_name = 'diary/day_formset.html'
    form_class = DayCreateFormSet
    success_url = reverse_lazy('diary:index')

    def form_valid(self, form):
        form.save()
        return super().form_valid(form)



day_formset.htmlは以下のようになります。
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
{{ form.management_form }} 
{% for fm in form %}
{{ fm.id }}
  <table class="table">
      <tr>
        <th>タイトル</th>
        <td>{{ fm.title }}</td>
      </tr>
      <tr>
        <th>本文</th>
        <td>{{ fm.text }}</td>
      </tr>
      <tr>
        <th>日付</th>
        <td>{{ fm.date }}</td>
      </tr>
  </table>
  {% endfor %}

  <button type="submit" class="btn btn-primary">送信</button>
  {% csrf_token %}
</form>
{% endblock %}



{{ management_form }}は、formsetを使う際のおまじないです。フォームセット内には複数のフォームオブジェクトがあり、それらを管理するためのinput type="hidden"なデータが幾つか必要なのです。それらを作成するのが、management_formです。つけておけば間違いありませんから。
{{ form.management_form }} 


今まではformという名前でテンプレートに1フォーム渡されていましたが、今回はformという名前に複数のフォームオブジェクトが紐付いています。それを一つづつ取り出すため、forで回します。
{% for fm in form %}



これもおまじないと思った方が良いかもしれません。実を言うと、フォームセットでは過去に追加したデータも表示されます。ある意味UpdateViewも兼ねていると言えるでしょう。過去に追加したデータを書き換える際に、このidが必要になるのです。fm.pkでは動かないので注意しましょう。
{{ fm.id }}


{{ fm.title }}などは触れなくてもわかるかと思います。この部分は今まで{{ form.title }}のように書けた場所です。{% for field... のようなフォームフィールドの取り出し方も良いのですが、それをする場合は以下のように書きます。visibleは目に見えるといった意味で、上で書いたform.idなんかをこれで無視できます。これは通常のフォームでも使えるので、覚えておくと良いでしょう。
{% for field in form.visible_fields %}


hiddenなフィールドだけを表示することもできますし、field.is_hiddenでのhiddenなフィールドかの確認もできます。
{% for field in form.hidden_fields %}



さて、{{ fm.title }}のような書き方ですが、正直名前がわかりづらいはずです。私は横着なのでこれいっかなーと思いますが、しっかりした方はformsetとformという名前を適切に使いたいはずです。
その場合は、まずviews.pyにて以下のように書きます。djangoの汎用ビューに詳しい人ならば、コメントで内容がわかると思います。formという名前ではなく、formsetという名前でテンプレートに渡すだけですね。
    def get_context_data(self, **kwargs):
        if 'form' in kwargs:
            # form_invalidでのget_context_data呼び出しはこっち
            kwargs['formset'] = kwargs['form']
        else:
            # getからのget_context_data呼び出しはこっち
            kwargs['formset'] = self.get_form()
        return super().get_context_data(**kwargs)




day_formset.htmlは、以下になります。formがformsetに、fmがformという名前に変わっただけです。
こちらは名前的にもちゃんとできてるし、しっくりきますね。
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
{{ formset.management_form }} 
{% for form in formset %}
{{ form.id }}
  <table class="table">
      <tr>
        <th>タイトル</th>
        <td>{{ form.title }}</td>
      </tr>
      <tr>
        <th>本文</th>
        <td>{{ form.text }}</td>
      </tr>
      <tr>
        <th>日付</th>
        <td>{{ form.date }}</td>
      </tr>
  </table>
  {% endfor %}

  <button type="submit" class="btn btn-primary">送信</button>
  {% csrf_token %}
</form>
{% endblock %}



いくつかデータを追加してみましょう。
追加画面に、追加用のフォームが3つあります。extra=3としたからです。


追加すると、ちゃんと同時に追加されます。


もう一度追加画面に行くと、既に追加したデータも表示されていますね。UpdateViewも兼ねていると書きましたが、このように表示されます。


もちろん、追加用の空のフォームも表示されています。extraの数だけ表示されます。



あらかじめ、データを追加できる上限を設定したいときはmax_num引数に指定します。
max_numに5と指定すると、今回ならば既存の3件と追加用の2件が表示されます。
既に5件以上あるときは、追加用のフォームが表示されません。
DayCreateFormSet = forms.modelformset_factory(Day, form=DayCreateForm, extra=3, max_num=5)



モデルフォームセットにはまだまだ機能があります。can_delete引数にTrueと指定し...
DayCreateFormSet = forms.modelformset_factory(Day, form=DayCreateForm, extra=3, can_delete=True)



テンプレートに {% if form.instance.pk %}を足します。これは、そのフォームが追加用の空フォームではない場合に表示されます。
      <tr>
        <th>日付</th>
        <td>{{ form.date }}</td>
      </tr>
      {% if form.instance.pk %}
      <tr>
        <th>削除</th>
        <td>{{ form.DELETE }}</td>
      </tr>
      {% endif %}


すると、削除用のチェックボックスができましたね。