DjangoのModelFormで、ManyToMany/ForeignKeyのフィールドをCharFieldで入力する
N:Mのフィールドを文字列で入力したい
DjangoのModelでForeignKeyやManyToManyのフィールドを定義したとき、それをそのまま継承してModelFormを作成するとModelChoiceFieldだったりModelMultipleChoiceFieldが設定されます。
が、例えば「ユーザーに任意の値を入力させて、その値が既にある場合はリレーションを設定、その値がない場合はリレーション先にデータを登録してからリレーションを設定」というような処理をやりたい場合、選択式のフィールドだと何かと不便です。
この点でしばらく悩んだので、一応の解決策をメモしておきます。もしかしたらもっと良い解決策があるかもしれないので、ご存知の方はコメント等で教えていただけると幸いです。
結論
先に成功したコードだけ書きます。例として書籍情報を管理するアプリを作ります。
大まかな流れとしては
- リレーションを持つフィールドをModelから引き継がずFormで新たに定義
- (ManyToManyの場合)Formのバリデーションで入力値を整形
- Viewでのバリデーション成功時にget_or_create()して紐づけ
となっています。
class Author(models.Model):
"""著者情報モデル"""
name = models.CharField(verbose_name="著者名", max_length=255)
class Publisher(models.Model):
"""出版社情報モデル"""
name = models.CharField(verbose_name="出版社名", max_length=255)
class Book(models.Model):
"""書籍情報モデル"""
title = models.CharField(verbose_name="書名", max_length=255, blank=False)
author = models.ManyToManyField(Author, verbose_name="著者", blank=True, null=True)
publisher = models.ForeignKey(
Publisher, verbose_name="出版社", on_delete=models.PROTECT, blank=True, null=True
)
class BookCreateForm(forms.ModelForm):
author = forms.CharField(label="著者", max_length=255, required=False)
publisher = forms.CharField(label="出版社", max_length=255, required=False)
class Meta:
model = Book
fields = (
"title",
)
# カンマ区切りのauthorを受け取ってリストを返す
def clean_author(self):
author = self.cleaned_data.get("author")
authors_list = author.split(",")
return authors_list
class BookCreateView(generic.CreateView):
"""書籍登録画面のビュー"""
model = Book
template_name = "create.html"
form_class = BookCreateForm
def form_valid(self, form: ModelForm) -> HttpResponse:
book = form.save(commit=False)
authors_list = form.cleaned_data.get("author")
publisher = form.cleaned_data.get("publisher")
# publisherをBookに紐づけ
if publisher:
book.publisher, created = Publisher.objects.get_or_create(name=publisher)
book.save() # 一旦save
# authorをBookに紐づけ
# ManyToManyのFieldにaddするにはidが振られている必要があるため、
# save()した後に登録する
if authors_list:
for author in authors_list:
book.author.add(Author.objects.get_or_create(name=author)[0])
form.save_m2m()
return super().form_valid(form)
解説
まずはModelを定義します。
今回例に挙げるアプリは以下の3つのModelを持っています。
class Author(models.Model):
name = models.CharField(verbose_name="著者名", max_length=255)
class Publisher(models.Model):
name = models.CharField(verbose_name="出版社名", max_length=255)
class Book(models.Model):
title = models.CharField(verbose_name="書名", max_length=255, blank=False)
author = models.ManyToManyField(Author, verbose_name="著者", blank=True, null=True)
publisher = models.ForeignKey(
Publisher, verbose_name="出版社", on_delete=models.PROTECT, blank=True, null=True
)
このModelを元にModelFormを作りますが、この時、authorとpublisherはBookから引き継がず、新たにFieldを定義します[1]。今回は普通の文字列を入力させたいので、author、publisherともにCharFieldとしています。
ManyToManyのauthorは複数の値が入力される可能性があるため、authorのバリデーション処理をオーバーライドし、入力値をカンマ区切りのリストにして返します。
class BookCreateForm(forms.ModelForm):
# authorとpublisherのFieldを新たに定義
author = forms.CharField(label="著者", max_length=255, required=False)
publisher = forms.CharField(label="出版社", max_length=255, required=False)
class Meta:
model = Book
# authorとpublisherはfieldsに含めない
fields = (
"title",
)
# カンマ区切りのauthorを受け取ってリストを返す
def clean_author(self):
author = self.cleaned_data.get("author")
authors_list = author.split(",")
return authors_list
最後に、作成したフォームを使って登録処理を行うViewです。このViewではform_validメソッド内でget_or_create関数を用いて、該当するデータがあった場合はそのオブジェクトを、無ければ新規登録したオブジェクトを受け取り、Bookモデルに紐づけています。注意点として、ManyToManyFieldにaddする際にBookモデルにidが振られている必要があるため、save()を行った後に改めて紐づけを行い、save_m2m()する必要があります。
class BookCreateView(generic.CreateView):
"""書籍登録画面のビュー"""
model = Book
template_name = "create.html"
form_class = BookCreateForm
def form_valid(self, form: ModelForm) -> HttpResponse:
book = form.save(commit=False)
authors_list = form.cleaned_data.get("author")
publisher = form.cleaned_data.get("publisher")
# publisherをBookに紐づけ
if publisher:
book.publisher, created = Publisher.objects.get_or_create(name=publisher)
book.save() # 一旦save
# authorをBookに紐づけ
# ManyToManyのFieldにaddするにはidが振られている必要があるため、
# save()した後に登録する
if authors_list:
for author in authors_list:
book.author.add(Author.objects.get_or_create(name=author)[0])
form.save_m2m()
return super().form_valid(form)
まとめ
ForeignKeyやManyToManyといったリレーションを持つフィールドのformで任意の入力値を入れるには、form側でModelからの継承ではない新たなフィールドを定義、Viewでget_or_createを使用してオブジェクトを受け取り、Modelに登録します。今回はCharFieldでやってみましたが、別のフィールドでも応用できると思います。
-
Modelから引き継ぐフィールドが1つだけならそもそもModelFormを使わなくてもよさそうですが、実際のアプリはもっと多くのフィールドがあるのでModelFormを利用しています ↩︎
Discussion