複数の単体formを共存させるやり方はネット上にいろいろありますが、複数のformsetを共存させる情報がなかったので、書いてみます。
少し冗長ですが、今回もviews.pyの中でやります。汎用性があれば、forms.pyにまとめたり、クラス化したりするのが良いかと思います。
まず、formsetの基となる2種類のフォーム情報を、forms.pyに書きます。ModelFormを使います。(models.pyは割愛します。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
from django import forms from .models import MyModel_1, MyModel_2 class MyForm_1(forms.ModelForm): class Meta: model = MyModel_1 fields = ( 'field_1', 'field_2', 'field_3', ) widgets = { 'field_1': forms.Select(), 'field_2':forms.Select(), 'field_3':forms.NumberInput(), } def __init__(self, *args, **kwargs): # Classの指定など for field in self.fields.values(): field.widget.attrs['class'] = 'form-control' class MyForm_2(forms.ModelForm): class Meta: model = MyModel_2 fields = ( 'field_1', 'field_2', 'field_3', ) widgets = { 'field_1': forms.Select(), 'field_2':forms.Select(), 'field_3':forms.NumberInput(), } def __init__(self, *args, **kwargs): # Classの指定など for field in self.fields.values(): field.widget.attrs['class'] = 'form-control' |
views.pyの中で、フォームセットを定義します。処理が複雑になるので、今回もクラスviewは使わず、関数viewで書きます。
ポイントは、保存時にフォームセット数を調整してあげることです。
なぜかというと、複数のフォームセットをテンプレートに出力すると”form-TOTAL_FORMS”というhiddenフィールドがセットの数だけ複数出力されるため、単純にPOSTで受け取っただけだと、いずれかが無効になってしまいます。
その結果、form-TOTAL_FORMSの数と実際にPOSTしたフォームの数が合わずに、バリデーションエラーとなります。(※すべてのフォームセットへの入力数が同じ数であれば、この心配はいりません。)
この対策として、POSTされたデータを一旦コピー&form-TOTAL_FORMSフィールドの値を上書きし、フォームセットオブジェクト生成時に上書きしたPOSTデータを引数として渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
from django.forms.models import modelformset_factory from django.db import transaction def HogeCreateView(request): template_name = 'myapp/hoge_form.html' # フォームセット定義 MyFormSet_1= modelformset_factory( model=MyModel_1, form=MyForm_1, extra=3, # セットの表示数 defaultは1 max_num=3 # 最大表示数 defaultは1 ) MyFormSet_2= modelformset_factory( model=MyModel_2, form=MyForm_2, extra=5, # セットの表示数 defaultは1 max_num=5 # 最大表示数 defaultは1 ) if request.method == 'GET' : # フォームの初期値を指定する場合 form_1_initial = [{ 'field_1' : 'initial_value_1', 'field_2' : 'initial_value_2', }] form_2_initial = [{ 'field_1' : 'initial_value_1', 'field_2' : 'initial_value_2', }] # フォームセットのオブジェクト生成 form_set_1 = MyFormSet_1( initial=form_1_initial, # 新規作成フォームのみ表示(既存レコードは表示しない) queryset=MyModel_1.objects.none() ) form_set_2 = MyFormSet_2( initial=form_2_initial, queryset=MyModel_2.objects.none() ) else : # POST # POSTされたデータをコピー(直接編集しようとすると怒られる) post_form_set_1 = request.POST.copy() post_form_set_2 = request.POST.copy() # 'form-TOTAL_FORMS'をもともとのextraの値に上書き post_form_set_1['form-TOTAL_FORMS'] = 3 post_form_set_2['form-TOTAL_FORMS'] = 5 # 改めてオブジェクト生成 form_set_1 = MyFormSet_1(post_form_set_1 ) form_set_2 = MyFormSet_2(post_form_set_2 ) if form_set_1.is_valid() and form_set_2.is_valid(): post_form_set_1 = form_set_1.save(commit=False) post_form_set_2 = form_set_2.save(commit=False) # 保存処理 with transaction.atomic() : # 各々save for p in post_form_set_1 : # さらに他のフォームを併設している場合、そこでsaveしたレコードのPKを使う場合はobject.pkでとれる #p.parent_pk = other_post.pk p.save() for p in post_form_set_2 : p.save() messages.info(request, f'保存しました。') return redirect('myapp:top') # レンダリング context = { 'form_set_1': form_set_1, 'form_set_2': form_set_2, } return render(request, template_name, context) |
テンプレートは、ただフォームセットを埋め込むだけです。なお、crispyを使っていると、formタグが勝手に生成されてしまうので、この場合は使わない方が得策です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{{ form.certifications.errors }} <div class="container"> <div class="row"> <form method="post"> {% csrf_token %} <div class="clearfix"> {{ form_set_1 }} </div> <div class="clearfix"> {{ form_set_2 }} </div> <div> <input type="submit" class="btn" value="登録" /> <a class="btn btn-secondary" href="{% url 'myapp:top' %}">戻る</a> </div> </form> </div> </div> |
参考サイト)