前回と同じプロジェクトのある画面で「サブミットされた複数のフォームに対してバリデーションをかけて、それぞれのフィールドにエラーメッセージを表示する」という内容がありました。
そのプロジェクトはフロントに Vue を利用しており、この画面でも担当者は Vue を使うような実装で対応しようとしていたのですが、以下理由から却下になりました。
- 原則 Django 側のテンプレートで描画するような方針であること
- テスト期間中の修正対応であること
- その画面は原則 Django で作成されており、ここで処理をごっそり Vue に変更するのはリスクが大きいこと
その結果、私が Django の FormSet を使うことで対応しました。
今回は FormSet を使った複数フォームの表示・更新処理について記載します。
なお、Django プロジェクトの基本的なセットアップ手順は省略しています(必要に応じてこちらをご覧いただければよいかと思います)。
モデル
# app/models.py
from django.db import models
from django.utils.translation import gettext as _
class Player(models.Model):
name = models.CharField(
label=_('名前'),
)
password = models.CharField(
label=_('パスワード'),
max_length=8,
)
checked = models.BooleanField(
label=_('チェック'),
)
class Meta:
""" モデルのメタ情報 """
db_table = 'players'
verbose_name = _('プレイヤー')
verbose_name_plural = _('プレイヤーたち')
フォーム
# app/forms.py
from django import forms
from django.utils.translation import gettext as _
from app.models import Player
class PlayerForm(forms.ModelForm):
email = forms.EmailField(
label=_('メールアドレス'),
widget=forms.TextInput(attrs={'class': 'form-control'}),
required=True,
error_messages={
'unique': _('既に同じメールアドレスが存在します'),
},
)
password = forms.CharField(
label=_('パスワード'),
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
min_length=4,
max_length=8,
error_messages={
'min_length': _('4文字以上8文字以下で入力してください'),
'max_length': _('4文字以上8文字以下で入力してください'),
},
)
checked = forms.CharField(
label=_('チェック'),
widget=forms.CheckboxInput,
)
class Meta:
""" フォームのメタ情報 """
model = Player
fields = [
'email', 'password', 'checked',
]
クラスベースビュー
モデルおよびフォームを利用するクラスベースビューを用意します。
# app/views.py
from django.forms import modelformset_factory
from django.http import HttpRequest
from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView
from app.forms import PlayerForm
from app.models import Player
class PlayersView(FileMixin, ListView):
""" 複数のプレイヤーを編集するビュー """
model = Player
http_method_names = ['get', 'post']
template_name = 'players.html'
form_class = PlayerForm
def get_formset(self, *args, **kwargs):
""" 自身に設定されたモデルとフォームからフォームセットを作成する """
formset = modelformset_factory(self.model, form=self.form_class, extra=0)
return formset(*args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs):
return super(PlayersView, self).get(request, *args, **kwargs)
def post(self, request: HttpRequest, *args, **kwargs):
# self.object_list は ListView の get_context_data で参照されるため、取得しておく必要がある
self.object_list = self.get_queryset()
# リクエストデータから FormSet を作成
formset = self.get_formset(request.POST or None)
# FormSet 内のフォームに1件でもエラーがあった場合は更新失敗
if formset.is_valid():
# get_context_data の引数に formset を渡すことで、エラー時にフォームの状態を維持できるようにする
return self.render_to_response(self.get_context_data(formset=formset))
# FormSet 内のフォーム1件ずつ処理していく
for form in formset:
# フォームの情報を反映する前のモデルのデータが欲しい時
# print(form.initial['checked'])
# フォームの情報を反映した後のモデルのデータが欲しい時
# print(form.instance.checked)
# ListView では保存まではやってくれないのでここで保存
# form.save() で関連するモデルの保存をやってくれる
form.save()
# 更新成功
return self.render_to_response(self.get_context_data())
def get_context_data(self, **kwargs):
"""
コンテキストに設定するデータを取得する
この関数は親の get, post 関数でコールされるため、このビューの get, post 関数では親要素を呼び出しておく
ここで kwargs に設定しておけば、親クラスの get_context_data で context に設定される
"""
# フォームセットが設定されていなければ作成しておく
if 'formset' not in kwargs:
kwargs['formset'] = self.get_formset(queryset=self.get_queryset())
return super(PlayersView, self).get_context_data(**kwargs)
ルーティング
# app/urls.py
from django.urls import path
from app.views import PlayersView
app_name = 'app'
urlpatterns = [
path('', PlayersView.as_view(), name='players'),
]
テンプレート
ビューで設定した formset
を利用するよう修正します(以下コードは一覧表示部のみ)。
{% csrf_token %}{# フォームを POST する場合は必要 #}
{{ formset.management_form }}{# フォームセットを利用する場合は必要 #}
{% for form in formset %}
{{ form.checked }}
{{ form.email }}
{{ form.email.errors }}
{{ form.password }}
{{ form.password.errors }}
{% endfor %}
更新
これで表示・更新ができるはずです。
いかがでしたでしょうか。
SPA で Web サイトを作ることが増えてきた今、サーバサイドでここまですることもだいぶ減りましたが、まだ需要は少なからずあると思っています。
何かのお役に立てば幸いです。