1-15. FormとModelForm

今回のテーマは「FormとModelForm」です。前回までフォームはModelFormを扱ってきました。今回は次の説明のためにもFormとModelFormについて触れておきます。

※本ページは簡単なトピック投稿画面の作成するまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。

Formを使う

これまでDjangoにおけるフォームの役割を説明してきました。このフォーム機能を具現化するクラスがFormクラスです。Djangoの便利さを体感するためModelFormから見てきましたが、ModelFormはFormを継承したクラスです。ではTopicModelFormをFormを継承したクラスで書き直して見ましょう。

thread/forms.py


from django import forms
from . models import Topic, Category

class TopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'user_name',
            'category',
            'message',
        ]

    def __init__(self, *args, **kwargs):
        # kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = '匿名'
        # self.fields['title'].widget.attrs['class'] = 'huga'

class TopicForm(forms.Form):
    title = forms.CharField(
        label='タイトル',
        max_length=255,
        required=True,
    )
    user_name = forms.CharField(
        label='お名前',
        max_length=30,
        required=True,
        widget=forms.TextInput(attrs={'value': '名無し'}),
    )
    category = forms.ModelChoiceField(
        label='カテゴリー',
        queryset=Category.objects.all(),
        required=True,
        empty_label='選択して下さい',
    )
    message = forms.CharField(
        label='本文',
        widget=forms.Textarea,
        required=True,
    )

次に先程作成したTopicFormを使ってトピックを作成する関数を見てみましょう。処理をわかりやすくするためクラスベースビューでなく敢えてtopic_create関数に変更を加えています。TopicFormのインポートを忘れないようにしてくださいね。

thread/views.py(一部抜粋)


from . forms import TopicModelForm, TopicForm

def topic_create(request):
    template_name = 'thread/create_topic.html'
    ctx = {}
    if request.method == 'GET':
        form = TopicForm()
        ctx['form'] = form
        return render(request, template_name, ctx)
    
    if request.method == 'POST':
        topic_form = TopicForm(request.POST)
        if topic_form.is_valid():
            # topic_form.save()
            topic = Topic()
            cleaned_data = topic_form.cleaned_data
            topic.title = cleaned_data['title']
            topic.message = cleaned_data['message']
            topic.user_name = cleaned_data['user_name']
            topic.category = cleaned_data['category']
            topic.save()            
            return redirect(reverse_lazy('base:top'))
        else:
            ctx['form'] = topic_form
            return render(request, template_name, ctx)

views.pyのcreate_topic関数を使うようにurls.pyも変更しておきましょう

thread/urls.py


from django.urls import path

from . import views
app_name = 'thread'

urlpatterns = [
    # path('create_topic/', views.TopicFormView.as_view(), name='create_topic'),
    path('create_topic/', views.topic_create, name='create_topic'),
]

前回説明を省きましたが、formのコンストラクタにrequest.POSTを入れることでデータと紐付けられたフォームが出来ます。このフォームはis_valid()関数を呼ぶことでデータの検証を行えます。

Topicオブジェクトの保存の仕方が変更されたことにお気づきでしょうか?Formの場合はis_valid()関数が呼ばれた後にcleaned_dataから検証済みのデータを取り出し、Modelのオブジェクトに値をセットしてオブジェクトのsave()関数を呼び保存します。尚、データの検証はFormに書かれた各フィールドの内容に応じてチェックされます。

一方、ModelFormはis_valid()関数が呼ばれるとModelに記載されたルールでデータ検証が行われます。そして保存する場合はModelForm自体が備えているsave()を呼び出して保存をします。ここが大きな違いですね。

セレクトタグの未選択の文言やValueの初期値を与える

カテゴリーはセレクトタグを使用していますが、(正確にはデフォルトのwidgetがselectという意味)このセレクトタグの未選択状態では「——–」というように点線で表現されています。この文言を変更したいという需要は多いと思います。Formの場合は以下のようにempty_labelを指定するだけです。また、入力値の初期値を与えるなどタグの要素を変更する場合は以下のようにします。

thread/forms.py


    user_name = forms.CharField(
        label='お名前',
        max_length=50,
        required=True,
+       widget=forms.TextInput(attrs={'value': '名無し'}),
    )
    category = forms.ModelChoiceField(
        label='カテゴリー',
        queryset=Category.objects.all(),
        required=True,
+       empty_label='選択して下さい',

確認してみましょう。以下のように変更されていればOKです。

ではModelFormの場合はどうすれば良いのでしょうか?ModelFormの場合は以下のようにします。

thread/forms.py


class TopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'user_name',
            'category',
            'message',
        ]  

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = '名無し'

ModelFormの場合、プロパティを直接変更できないため、__init__関数をオーバライドして対応します。この方法はインプットタグのclass要素を修正したい場合にも使えます。また、ModelFormでwidgetを変更する場合には以下のように変更することが出来ます。widgetの指定をしながらタグの要素を指定するのです。

thread/forms.py


class TopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'user_name',
            'category',
            'message',
        ]
+      widgets = {
+          'title' : forms.TextInput(attrs={'class': 'hoge'}),
+           'user_name' : forms.TextInput(attrs={'value': '名無し'}),
+       }

    def __init__(self, *args, **kwargs):
        # kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'

これでレンダリングされた’title’のinputタグには’hoge’クラスが付与されています。クラスだけではなくattrsを用いるとタグの様々な属性について操作をすることが出来ます。

label_suffixを変更する

label_suffixとはラベルの末尾につく記号でデフォルトではコロンがついています。たとえば「タイトル:」や「お名前:」のように全てコロンが付いていますよね。場合によってはこれを変更したい、或いは削除したいということもあると思います。そういう場合にlabel_suffixを設定します。フォーム生成時にコンストラクタに渡す方法もあるのですが、__init__関数をオーバライドしてlabel_suffixを設定する方法が簡単で良いと思います。
thread/forms.py(一部抜粋)


class TopicModelForm(forms.ModelForm):
    class Meta:
        model=Topic
        fields=[
            'title',
            'user_name',
            'category',
            'message',
        ]  

    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
        self.fields['category'].empty_label = '選択して下さい'
        self.fields['user_name'].widget.attrs['value'] = '名無し'

class TopicForm(forms.Form):
    label_suffix = ''
    title = forms.CharField(
        label='タイトル',
        max_length=255,
        required=True,
    )
    user_name = forms.CharField(
        label='お名前',
        max_length=30,
        required=True,
        widget=forms.TextInput(attrs={'value': '名無し'}),
    )
    category = forms.ModelChoiceField(
        label='カテゴリー',
        queryset=Category.objects.all(),
        required=True,
        empty_label='選択して下さい',
    )
    message = forms.CharField(
        label='本文',
        widget=forms.Textarea,
        required=True,
    )

    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

これでラベルのコロンが消えました。もちろん、矢印等にカスタムすることも可能です。公式ドキュメントではフォームAPIで扱っています。

最後に

ModelFormとFormどちらも似たようなことが出来るので使い方に迷うかも知れません。モデルドリブンなWebアプリの場合にはModelFormの方が望ましいのではないかと考えています。さて、次回はフォームによるレンダリングについて少し詳しく見ていこうと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です