Django: formset を form に埋め込む


Foreign key の子モデルのフォームを親モデルのフォームに埋め込み、取り回しをよくする[^1]。

# 環境
Django 1.11

# 題材

modelはおなじみの`Author`と`Book`。

### models

```py3:models.py

from django.db import models

# Create your models here.

class Author(models.Model):
    name = models.CharField(max_length=100)

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

# inline formset
複数のモデルインスタンスを作成/編集するフォームを作るには inline formset を使う[^2]。この題材の例では`forms.py`に

```py3:forms.py
from django.forms import ModelForm, inlineformset_factory
from .models import Author, Book

class AuthorForm(ModelForm):

    class Meta:
        model = Author
        fields = ('name',)
```

に加えて

```py3:forms.py(つづき)
class BookForm(ModelForm):

    class Meta:
        model = Book
        fields = ('title',)

BookFormSet = inlineformset_factory(
    parent_model=Author,
    model=Book,
    form=BookForm,
    extra=3
)
```
のようにFormSetを定義して使う。

FormSet の API はこんな感じで、ふつうの Form に近い:

```py3
>>> from books.forms import BookFormSet
>>> inlines = BookFormSet()
>>> inlines.as_table()
'<input type="hidden" name="book_set-TOTAL_(以下省略)
```
通常は`views.py`内でフォームセットをインスタンス化して使う:

```py3:views.py
class CreateAuthorView(CreateView):

    model = Author
    form_class = AuthorForm
    template_name = 'books/create-Author.html'
    success_url = reverse_lazy('books:create_Author')

    def get_context_data(self, **kwargs):
        context = super(CreateAuthorView, self).get_context_data(**kwargs)

        # 子フォームをつくる
        context['inlines'] = BookFormSet()
        return context
```

これでもいいけど、子モデルのvalidationや保存処理のためのコードをviewsの中に書かなければならず面倒。そこで親フォームに子フォームを埋め込み、これらの処理を一括して行えるようにした。

```py3:forms.py
from django.forms import ModelForm, inlineformset_factory
from .models import Author, Book

# 追加
class ModelFormWithFormSetMixin:

    def __init__(self, *args, **kwargs):
        super(ModelFormWithFormSetMixin, self).__init__(*args, **kwargs)
        self.formset = self.formset_class(
            instance=self.instance,
            data=self.data if self.is_bound else None,
        )

    def is_valid(self):
        return super(ModelFormWithFormSetMixin, self).is_valid() and self.formset.is_valid()

    def save(self, commit=True):
        saved_instance = super(ModelFormWithFormSetMixin, self).save(commit)
        self.formset.save(commit)
        return saved_instance


class BookForm(ModelForm):

    class Meta:
        model = Book
        fields = ('title',)

BookFormSet = inlineformset_factory(
    parent_model=Author,
    model=Book,
    form=BookForm,
    extra=3
)


class AuthorForm(ModelFormWithFormSetMixin, ModelForm):

    # 追加
    formset_class = BookFormSet

    class Meta:
        model = Author
        fields = ('name',)

```
埋め込まれる側の formset を処理するためのコードを ModelFormWithFormSetMixin にまとめ、親 form の定義時に継承する。親 form の is_valid()、save() が呼ばれたときに子フォームの同じメソッドを呼び出す単純な仕組み。

注意点として、`ModelFormWithFormSetMixin.save()` 内では、子よりも先に親フォームの `save()` を呼び出す必要がある。子は親をForeignKeyで参照しているので、子を親よりも先に insert することはできない。

viewsは通常通りでOK。子フォームは AuthorForm の中に収まっているので、viewsを書くときに子フォームを気に掛けずに済む。

```py3:views.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy

from .models import Author
from .forms import AuthorForm

# Create your views here.

class CreateAuthorView(CreateView):

    model = Author
    form_class = AuthorForm
    template_name = 'books/create-Author.html'
    success_url = reverse_lazy('books:create_Author')
```

Template も通常通り。

```html+django:create-Author.html
<html>
    <head>
        <title>Create Author</title>
    </head>
    <body>
        <h1>Author</h1>
        <form method="POST">
            {% csrf_token %}
            <table>
                {{ form.as_table }}
                {{ form.formset.as_table}}
            </table>
            <button type="submit">Submit</button>
        </form>
    </body>
</html>
```

### 完成形
![form](https://qiita-image-store.s3.amazonaws.com/0/207790/8966073e-dbd9-1e2f-02a0-a39ec8a3f736.png)


***


[^1]:  [HOW TO USE NESTED FORMSETS IN DJANGO](https://micropyramid.com/blog/how-to-use-nested-formsets-in-django/) が参考になった。

[^2]: [Inline formset に関する Django のドキュメンテーション](https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#inline-formsets)