Python
Django

Django: formset を form に埋め込む

More than 1 year has passed since last update.

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


環境

Django 1.11


題材

modelはおなじみのAuthorBook


models


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


forms.py

from django.forms import ModelForm, inlineformset_factory

from .models import Author, Book

class AuthorForm(ModelForm):

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


に加えて


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 に近い:

>>> from books.forms import BookFormSet

>>> inlines = BookFormSet()
>>> inlines.as_table()
'<input type="hidden" name="book_set-TOTAL_(以下省略)

通常はviews.py内でフォームセットをインスタンス化して使う:


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の中に書かなければならず面倒。そこで親フォームに子フォームを埋め込み、これらの処理を一括して行えるようにした。


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を書くときに子フォームを気に掛けずに済む。


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 も通常通り。


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