Django、モデルフォームのウィジェットを変更する幾つかの方法

Python - Django
2018年11月19日2:44に更新(約9時間前)
2018年11月5日20:46に作成(約13日前)

旧ブログ移行記事です。

概要

モデルフォームを使うと、モデルのフィールドによって利用されるウィジェットが決定します。 ManyToManyFieldは、select要素としてhtmlに出力されます。 しかし、input type="checkbox" に変更したいということも多々あります。 つまり、以下のようなデフォルトの見た目...select要素を
select要素の見た目

input type="checkbox"へと変更したい。
checkboxの見た目

いくつかの方法があるので、それを紹介していきます。

今回使うモデルとフォーム

以下のようなモデルとフォームを例に説明します。

from django.db import models


class Tag(models.Model):
    name = models.CharField('タグ名', max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    tag = models.ManyToManyField(Tag, verbose_name='紐づくタグ')

    def __str__(self):
        return self.title
from django import forms
from .models import Post, Tag


class PostCreateForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = '__all__'

1. フォームフィールドとして設定

方法1です。 単純にフォームフィールドを上書きし、widget引数に使いたいウィジェットを指定します。 もともとのフィールドを上書きしたいので、フィールド名はモデルのフィールド名と同じにします。

from django import forms
from .models import Post, Tag


class PostCreateForm(forms.ModelForm):
    tag = forms.ModelMultipleChoiceField(
        queryset=Tag.objects,
        widget=forms.CheckboxSelectMultiple
    )

    class Meta:
        model = Post
        fields = '__all__'

queryset引数は、選択肢としてどのクエリセットを使うかの指定です。 モデルフィールドで設定した他のオプション引数があれば、それも設定すると良いでしょう。具体的にはモデルでdefault=...としていれば、フォームではinitial=...と設定する等です。

ちなみにですが、htmlのclass属性等を変更する場合は以下のようにします。attrs引数で、classをはじめ色々設定できます。

    tag = forms.ModelMultipleChoiceField(
        queryset=Tag.objects,
        widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-control'})
    )

モデルフォームではない通常のフォームであれば、この方法でしかフォームフィールドを定義できないので、これでウィジェットを変更するのが一般的な方法になります。 モデルフォームにおいては他の方法を使うことが多いです。

2. __init__内で変更する

通常のフォームでも使え、そして覚えておくと結構便利な方法です。

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['tag'].widget = forms.CheckboxSelectMultiple()  # 引数にattrs={'class': 'form-control'}も勿論できる。
        self.fields['tag'].queryset = Tag.objects

self.fields['フィールド名'].widget = 任意のwidget とします。 ManyToManyForeignKey等では、querysetも指定する必要がありちょっと面倒です。

フォームの__init__メソッドで処理すると楽なケースはたまにあります。 例えば、すべてのclass属性に'form-control'と指定したい場合は、一つずつ指定するよりも、__init__内で以下のようにする方が簡単です。

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs["class"] = "form-control"

他のケースでは、モデルフォームを使っていて、ラベルとして設定された値(モデルフィールドのverbose_name)をhtmlのプレースホルダ属性に入れたい、なんてケースですね。 1つずつ設定するのが面倒な場合は、以下のように__init__内で一括設定することもあります。

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            # placeholderにフィールドのラベルを入れる
            field.widget.attrs['placeholder'] = field.label

3. ビューで変更する

フォームのインスタンス化時や、その後に何らかの変更を加えるのが最もなケースもあります。 例えば フォームに動的な初期値を与えたい場合は、ビュー側でform = YourForm(initial=...) のように直接設定することになります。 ウィジェットの変更をビューから行うのが適切なケースはそうそうないはずですが、覚えておくとフォームの理解に繋がるかもしれません。

CreateViewやUpdateView、FormViewならば以下のような感じになります。

from django import forms  # このimportをわすれないように。
...
...

    def get_form(self, form_class=None):
        form = super().get_form(form_class=form_class)
        form.fields['tag'].widget = forms.CheckboxSelectMultiple()
        form.fields['tag'].queryset = Tag.objects
        return form

通常の関数ビューなら、もっと単純ですね。

form = PostCreateForm(request.POST or None)
form.fields['tag'].widget = forms.CheckboxSelectMultiple()
form.fields['tag'].queryset = Tag.objects

他によくあるのは、選択肢を動的に変更したいケースです。これはビューから行うことになるでしょう。

form.fields['tag'].queryset = Tag.objects.filter(.....

4. class Meta内で行う

モデルフォーム限定ですが、モデルフォームならばこの方法が一番オーソドックスでしょう。 widgetsの変更だけでなく、labels等の属性で他の変更もできます。

class PostCreateForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'tag': forms.CheckboxSelectMultiple
        }

classの変更も例によって可能です。

        widgets = {
            'tag': forms.CheckboxSelectMultiple(attrs={'class': 'form-control'})
        }

使い分けのまとめ

どうやって使い分けるかをまとめます。

  • モデルフォームならば4. class Meta内で行う
  • 通常のフォームならば1. フォームフィールドとして設定
  • 一括で変更・設定する必要があり、1つずつ書くのが面倒な場合は2. __init__内で変更する
  • 動的に変更され、そもそもフォーム内では対応できなければ3. ビューで変更する

としておくと間違いないでしょう。

おまけ HiddenInputへの変更

稀なケースですが、HiddenInputに変更したい場合は、上記の方法に加えて別の方法があります。 テンプレートにて、以下のようにするだけです。

    {{ form.title.as_hidden }}
    {{ form.tag.as_hidden }}

テンプレートで上記のように書けない場合は、今まで通り書きます。

    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            # ManyToManyは、MultipleHiddenInputになる。
            'title': forms.HiddenInput,
            'tag': forms.MultipleHiddenInput,
        }

記事にコメントする