前回に引き続き Django のお話です。

前回と同じプロジェクトのある画面で「サブミットされた複数のフォームに対してバリデーションをかけて、それぞれのフィールドにエラーメッセージを表示する」という内容がありました。
そのプロジェクトはフロントに 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 サイトを作ることが増えてきた今、サーバサイドでここまですることもだいぶ減りましたが、まだ需要は少なからずあると思っています。
何かのお役に立てば幸いです。