Django Formsetを使ってFormをたくさん並べよう!

## 環境
Django2.2

## まえがき
[Formsets \| Django documentation | Django](https://docs.djangoproject.com/en/2.2/topics/forms/formsets/)の内容から、formsetを使用する上で最低限必要な内容+実際に使うときの注意点をまとめた記事です。

またmodelformsetの公式リンクは[Creating forms from models \| Django documentation | Django](https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#model-formsets)です。本記事で使用しているコードの一部は、上記リンクを参考に、変更を加えたものです。

私自身初学者ですので、対象となる読者も初心者となります。**Djangoのチュートリアルは理解していて、通常のformの挙動は理解しているくらいの読者を想定しています。** 中級者には回りくどく感じると思います。また、省略可能な引数名をなるべく省略せずに書いています。可読性と説明の都合です。

[GitHubに、実際に動くProjectの形でコードを上げたので](https://github.com/Kyutatsu/qiitaFormSet)、よろしければパラメータを変えて遊んでみてください。SECRET_KYEは抜いてあるのでご注意ください。

筆者はWeb開発初心者ですので、間違いや不足点などあればご指摘いただけると幸いです。


## Formsetとは
**同じ種類**のFormが複数並んでいるものです。全てのフォームに入力してから一気にsubmitすることができます。下の図では、データベース上の画像に対してラジオボタンを表示し、ラベルを振る例を示しています。

![formset1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480802/b2e025a9-4c1e-4454-c9f3-fbb1cd151595.png)


並べるformが2~3個ならば,prefixを利用(後述)しても可能ですが、formsetを利用すればコードも綺麗になり、選択肢も増えるので推奨します。

## Formset: 実際の使い方

通常のformの使い方を理解しているならば、同じようにformsetも利用できます。通常のformクラスから、formset_factory関数を使ってformsetクラスを作成できます。formsetはformと同じように扱うことができます。

以下、ソースコードの後に解説を載せています。

### formset_factory
単一種類のformを複数個表示する際に利用します。

```python:models.py
class TestForm(forms.Form):
    title = forms.CharField(max_length=20)
    date = forms.DateField()
```

- 上記が使用するFormです。

<br>

```django:first/form1.html
<form method="post">
	{% csrf_token %}
	{{ formset }}
	<button type="submit">submit</button>
</form>
```

- 次に示すviews.pyで使っているtemplateの該当部分です。

<br>

```python:views.py
from django import forms
from first.forms import TestForm
from django.http import HttpResponse


def make_test_formset(request):
    TestFormSet = forms.formset_factory(
            form=TestForm,
            extra=3,     # default-> 1
            max_num=4    # initial含めformは最大4となる
    )
    # 通常のformと同様に処理できる。
    if request.method == 'POST':
        formset = TestFormSet(request.POST)
        if formset.is_valid():
			# 参考として,cleaned_dataの中身を表示してみます。			
            data = repr(formset.cleaned_data)
            return HttpResponse(data) 
    else:
        formset = TestFormSet() # initialを渡すことができます。
		# formset = TestFormSet(initial=[{'title':'abc', 'date': '2019-01-01'},])
    return render(request, 'first/form1.html', {'formset': formset})
```

上記views.pyのポイントは以下の通りです。

- 通常のformとviewsの書き方はほとんど変わりません。
- formset.cleaned_dataに入力された値が入ります。本コードでの実際の結果を示すと以下のように,listに各Formのデータがmappingの形で順番に入ります。イテレーションするなりして利用してください。

```
[{'title': 'No1', 'date': datetime.date(2019, 1, 1)},
{'title': 'No2', 'date': datetime.date(2019, 1, 2)}, 
{'title': 'asdf', 'date': datetime.date(2019, 2, 2)}, 
{'title': 'asdfdsfadsfaddsf', 'date': datetime.date(2000, 2, 2)}]
```


- 上記のviews.pyで`forms.formset_factory`関数は、TestFormと名付けたformクラスからformsetを作成しています。この際重要になるのは**extraとmax_num**です。これらは表示されるformの個数を指定するために使います。
- extraは「空のformの個数」で、max_numは「トータルのformの個数」です。formにはinitial引数により、あらかじめ値が入力されたformを指定することができます(データのformへのbindについては[The Forms API \| Django documentation | Django](https://docs.djangoproject.com/en/2.2/ref/forms/api/#bound-and-unbound-forms)を確認してください。簡単に言うと、formに初期値として表示したい値はinitialで渡す必要があり、またGET時だけでなくPOST時にも渡してください)。その際の挙動を制御するため、extraとmax_numによる指定が必要です。

例えば

```python
TestFormSet = formset_factory(form=TestForm, extra=3, max_num=10)
formset = TestFormSet(initial=[
                {'title': 'No1', 'date': '2019-01-01'},
                {'title': 'No2', 'date': '2019-01-02'},
])
```
上記のような場合、extra=3なので、initialで指定された入力済みのformが2つ、空のformが3つ、合計5つのformが 表示されます。 

<br>

```python
TestFormSet = formset_factory(form=TestForm, extra=3, max_num=10)
formset = TestFormSet()
```
上記の場合、単に空のformだけが3つ表示されます。

<br>

```python
TestFormSet = formset_factory(form=TestForm, extra=3, max_num=3)
formset = TestFormSet(initial=[
                {'title': 'No1', 'date': '2019-01-01'},
                {'title': 'No2', 'date': '2019-01-02'},
])
```
上記の場合、initialで指定されたformが2つ、**空のformは一つ**表示され、合計3つのformが表示されます。max_num=3としてあるので、最大3つまでしか空のformを増やすことができないからです。

<br>

```python
TestFormSet = formset_factory(form=TestForm, extra=3, max_num=1)
formset = TestFormSet(initial=[
                {'title': 'No1', 'date': '2019-01-01'},
                {'title': 'No2', 'date': '2019-01-02'},
])
```
上記の場合、max_num=1ですが、**initialで指定した入力済みformが2つ表示されます**。空のformは表示されません。max_numはあくまで「空のform」の数を指定するので、**initialに指定した分は必ず表示されます**。最上部の公式Documentのリンク中にある、shellの例も試してみてください。

また、**max_numのdefault値は1000ですので、特に指定しない場合は実質、無制限となります。**

また、initialを利用する際にはGETとPOSTのブロックでも同じinitialを指定しなくてはいけません。以下は公式Documentからの引用です。

>If you use an initial for displaying a formset, you should pass the same initial when processing that formset’s submission so that the formset can detect which forms were changed by the user. For example, you might have something **like: ArticleFormSet(request.POST, initial=[...]).**


	
	

### modelformset_factory
- Modelからformsetを作る際に利用できます。save()メソッドが便利ですので、ほとんどこちらを使うことになると思います。modelformとほとんど同じ感覚で使用できます。
- 下の例では、**空のformを3つ表示し、データベースに保存する**という例を示しています。

```python:models.py
class TestModel(models.Model):
    text = models.CharField(max_length=20)
```

- モデルは上のようにしました。テンプレートは最初のformset_factoryの解説で使用したものと同じです。

<br>

```python:views.py
def make_test_modelformset(request):
    TestModelFormSet = forms.modelformset_factory(
            model=TestModel,
            fields=('text',),
            extra=3,
    )
    if request.method == 'POST':
        formset = TestModelFormSet(request.POST,queryset=TestModel.objects.none()) 
        if formset.is_valid():
            formset.save()  # データベースに保存します。
			# チェック用にデータベース上の全データを表示してみます。
            data = [x.text for x in TestModel.objects.all()]
            return HttpResponse(repr(data))
    else:
        formset = TestModelFormSet(queryset=TestModel.objects.none())
    return render(request, 'first/form1.html', {'formset': formset})
```

- 先のformset_factoryとの違いは、modelformset_factoryではModelを元にformsetを作成できる点です。`fields=('text',)`として、formで表示したいfieldを指定しています。
- また**queryset**引数としてquerysetオブジェクトを指定でき、データベース上のデータを取得して変更することができます。上記の例では新しいデータをデータベースに追加するので、既存のデータは使いません。なので、`Model.objects.none()`を指定します。
- modelformと同様に、`formset.save()`メソッドでデータベースに保存します。commit=falseも利用できます。
- ここでは示しませんが、**modelfromset_facotryはFormやFormSetオブジェクトを引数にとることもできます。** fieldの指定だけで良いなら上述のように書けばいいと思いますが、複雑になるならばforms.pyにmodelに対応したform(もしくはformset)を書き、viewsにimportした方が、読みやすさからもロジックの分離の観点からもいいと思います。

#### querysetの指定

- querysetはdefalutでは`Model.objects.all()`となっています。つまり、**データベース上の全てのデータがformに入力された状態で表示されます。**
- querysetはPOST, GET両方で同じ表記にします。
- querysetはqueryset objectをとるので、filterを利用することができます。

```
formset = TestModelFormSet(
	queryset=TestModel.objects.filter(text__startswith='a')
)
```

- querysetを指定する場合、modelformset_factoryを使う際のextraに注意します。空白のformを全く表示したくない場合は0にします。

#### 注意：querysetにスライスは利用できない

例えばデータベース上のデータに対して、10個ずつ抽出し、順番にラベルなどを振ることを想定します。

**modelformset_factory関数のqueryset引数に指定するquerysetにはスライスを利用できないようです。**

```python
>>>FormSet = forms.modelformset_factory(model=TestModel, fields=('text',))
>>>formset = FormSet(queryset=TestModel.objects.all()[0:10])
>>>print(fs)
......
AssertionError: Cannot reorder a query once a slice has been taken.
```

- そもそもdjangoのquerysetオブジェクトは、一度スライスをかけるとその後スライスやfilterなどの操作を禁止する仕様になっています。:[公式Document](https://docs.djangoproject.com/en/2.2/topics/db/queries/#limiting-querysets)
- 例えば以下のような例です。

```python
>>>querys = TestModel.objects.all()
>>>querys.filter(text__startswith='a')
	# ↑これは可能
>>>querys[0:2].filter(text__startswith='a')
	# ↑これはできない!!
```

- このような場合、SQLでのLIMIT clauseのような指定ができないと思います。
- **この記事をご覧の方で良い方法を知っていらっしゃる方がいれば、ご教示願いたいです。**
- 良い方法なのかわからないですが、私が使っている方法を下に示しました。idが小さいデータから順番に3つ取得する例です。
	- データベースのobjectは(deleteなどで値が飛んでいても)大きさ順のidを持つので、それを利用して一度自分がスライスしたいところまでのidを取得しています。
	- 変更済みのデータには何かしらのフラグをつけます(下には示していない).例としてデータに対してラベルを追加するならば、例えばModelに`labeler`というfiledを作り、これに現在ログインしているUserを入れるなどします。max_idを求める際にfileterでlabelerがNoneのものだけを取得する、などしています。

```python
max_id = TestModel.objects.all()[2].pk  # 2番目のobjectのpk
......
formset = TestModelFormset(
	queryset=TestModel.objects.filter(id__lt=max_id)
)
```


### inlineformset_factory
- ForeignKeyでの親を指定して、子のmodelformsetを作成する際に使います。言い換えると、ForeignKeyをfieldsにもつModelにおいて、その親を指定してformsetを作ることができるということです。
- 例: [Django公式Document](https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#using-an-inline-formset-in-a-view)の例を使って説明します。(若干変えてあります)


```python:models.py
class Author(models.Model):
    name = models.CharField(max_length=20)
	
    def get_absolute_url(self):
        return reverse('first:author', args=[str(self.pk),])

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
```

- 上記が使用するmodelです。adminサイトから適当に複数インスタンスを作成しておきます。
- absoluteurlは,DetailViewなどでauthorの持つbookを表示するように書いておきます。Django公式のtutorialと同じように書いたので、省略します。


```python:views.py
def make_inline_formset(request, author_id):
    author = Author.objects.get(pk=author_id)
    BookFormSet = forms.inlineformset_factory(
            parent_model=Author,
            model=Book,
            fields=('title',),
            extra=2,
    )
    if request.method == 'POST':
        formset = BookFormSet(
			data=request.POST, 
			instance=author,
			queryset=Book.objects.none()
		)
        if formset.is_valid():
            formset.save()
            return HttpResponseRedirect(author.get_absolute_url())
    else:
        formset = BookFormSet(
                instance=author,
                queryset=Book.objects.none(),
        )
    return render(request, 'first/inline_formset.html', {'formset': formset})
```

- `author = Author.objects.get(pk=author_id)`で取得した親オブジェクトを、inlineformset_factoryの`parent_model`引数に渡します。
- またBookFormSetをインスタンス化する際に、`instance=author`としてauthorオブジェクトを渡します。
- POST時にもGET時にも同じauthorを渡す点に注意します。



### prefix
- 複数種類のformsetを同時に使う際に使います。下のようなイメージです。

![text.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480802/4cac4803-6799-de0e-bcd1-4b94623984cf.png)


##### prefixの前置き
そもそもformsetを利用した場合、HTMLは以下のようになります。

```python
>>>FormSet = forms.formset_factory(TestForm)
>>>formset = FormSet()
>>>print(formset)

<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">

<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" maxlength="20" id="id_form-0-title"></td></tr>

<tr><th><label for="id_form-0-date">Date:</label></th><td><input type="text" name="form-0-date" id="id_form-0-date"></td></tr>
```

formsetの各formのコントロールは、`form-0-title`のようなnameを持ちます。つまりFormの名前(この場合`TestForm`)が入ってません。ですので、複数"種類"のformからそれぞれformsetを作るには、名前が被らないように管理しないといけません。

ところで、通常のformは**prefix**が指定できます。[The Forms API \| Django documentation | Django](https://docs.djangoproject.com/en/2.2/ref/forms/api/#prefixes-for-forms)に解説されています。別々のprefixを指定することで、htmlがレンダリングされる際、formのコントロールのnameを区別することができます。

ちなみにprefixを利用すれば、formsetでやっていること(同じ種類のformを複数同時に表示する)がある程度可能です。数が多い時や、modelformを利用したいならformsetを使う方が素直で簡単だと思います。

#### prefixを利用して異なる種類のformsetを使う
本題です。prefixとして好きな文字列を与えることで、`form-0-title`のようなnameが`prefix-form-0-title`のようになり、異なるformsetを区別することができます。


```python:forms.py
class FirstForm(forms.Form):
    name = forms.CharField(max_length=10)

class SecondForm(forms.Form):
    name = forms.CharField(max_length=10)
```


```python:views.py
def make_multiple_formsets(request):
    FirstSet = forms.formset_factory(form=FirstForm, extra=2)
    SecondSet = forms.formset_factory(form=SecondForm, extra=2)
    if request.method == 'POST':
        first_set = FirstSet(request.POST, prefix='first')
        second_set = SecondSet(request.POST, prefix='second')
        if first_set.is_valid() and second_set.is_valid():
            text = 'first:{}<br><br>second:{}'.format(
                    repr(first_set.cleaned_data),
                    repr(second_set.cleaned_data),
            )
            return HttpResponse(text)
    else:
        first_set = FirstSet(prefix='first')
        second_set = SecondSet(prefix='second')
    return render(
            request,
            'first/multiple_formsets.html',
            {
                'firstform': first_set,
                'secondform': second_set
            }
    )
```

- 上述のように、prefix引数に適当な文字列を渡すだけです。
- POST時もGET時も同じ引数を渡すように注意します。
- また、`formset.is_valid()`でチェックする際、両方のformをチェックするようにしてください。


### テンプレートへのレンダリング

- 上述の例では`{{ formset }}`とだけ書いてきましたが、通常のformと同じようにfor文で回せば、細かく指定することができます。
- 注意点として、`{{ formset.management_form }}`を指定する必要があります。表示はされませんが、これ自身もformです。Totalのform数などの情報が入っていて、formsetのハンドリングに必要です。
- [Formsets \| Django documentation | Django](https://docs.djangoproject.com/en/2.2/topics/forms/formsets/#using-a-formset-in-views-and-templates)を参照してください。特に通常のformと変わりません。