Foreign key の子モデルのフォームを親モデルのフォームに埋め込み、取り回しをよくする1。
環境
Django 1.11
題材
modelはおなじみのAuthor
とBook
。
models
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
に
from django.forms import ModelForm, inlineformset_factory
from .models import Author, Book
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name',)
に加えて
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
内でフォームセットをインスタンス化して使う:
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の中に書かなければならず面倒。そこで親フォームに子フォームを埋め込み、これらの処理を一括して行えるようにした。
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を書くときに子フォームを気に掛けずに済む。
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>
<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>