Date Modified Tags python / django

2021-01-25: 追記

ModelFormでModelの1つのカラムを複数のフォーム要素(inputとか)で表現したかったので調べてみた。フォーム要素を適当に作って、clean_xxxの時にくっつけるとかそんな実装の仕方も出来るみたいだけど、MultiWidgetを使うと、Modelの1つのカラムに対して複数のinput要素を使うことが出来るみたいなので、使ってみた。名前のカラムに対して姓、名の入力要素を作るとかそんなのが出来るみたい。

まずはMultiWidgetを継承したクラスを作る

# models.py
from django.forms.widgets import MultiWidget

class NameWidget(MultiWidget):

次にクラスに対して、__init__をオーバーライドし、widgetとしてTextInputのフォームを2つ登録してみた。コードはこんな感じ。

# models.py
class NameWidget(MultiWidget):
    def __init__(self,attrs=None):
        widgets = (
                forms.TextInput(attrs=attrs.update({"placeholder":"姓"})),
                forms.TextInput(attrs=attrs.update({"placeholder":"名"}))
                )
        super(NameWidget,self).__init__(widgets,attrs)

widgetsはリストでもタプルでも良さげ?ともかくやることはwidgetのリストかタプルを作って、親クラス(MultiWidget)のinitを呼び出すこと。

次に、decompress、value_from_datadictメソッドを書く。decompressはカラムの値を複数のフォームに振り分ける手順、value_from_datadictはModelFormの入力データからModelのカラムに入れる値を作る手順を記述する。姓名を半角スペースで繋げて、半角スペースで分割するように作ってみた。

# models.py
class NameWidget(MultiWidget):
    def __init__(self,attrs={}):
        attrsSei = attrs.copy()
        attrsMei = attrs.copy()
        widgets = (
                forms.TextInput(attrs=attrsSei.update({"placeholder":"姓"})),
                forms.TextInput(attrs=attrsMei.update({"placeholder":"名"}))
                )
        super(NameWidget,self).__init__(widgets,attrs)
    def decompress(self,value):
        if value:
            names = value.split(' ')
            return (names[0],names[1])
        return (None,None)
    def value_from_datadict(self,data,files,name):
        ulist = [widget.value_from_datadict(data,files,name+'_{0}'.format(i)) for i, widget in enumerate(self.widgets)]
        return u"{0} {1}".format(ulist[0].replace(u' ',''),ulist[1].replace(u' ',''))

あとはこのWidgetを使いたい要素にwidgetとして指定すればおk

# models.py
from django import forms
...(上のコードなど)...
class HogeForm(forms.ModelForm):
    name = forms.CharField(max_length=100,label=u'名前',widget=NameWidget())

このコードだと姓、名のフォームが2行に表示されている。1行にしたい場合はインライン指定とかそんなのを付けたクラスをattrsで指定して後はcssでやっちゃえばいいんじゃないかなと

# models.py
    name = forms.CharField(max_length=100,label=u'名前',widget=NameWidget(attrs={"class":"inline"}))

とかそんな感じ

他にももっと細かくhtmlを記述出来るformat_outputとかあるみたいだけど、試してないので省略。なくても取りあえず出来る。

参考:Django英語ドキュメント

2021-01-25追記

上のコードではplaceholderを姓、名で分けることが出来なかった。下のコードのように一旦superで親クラスをinitしておいて、その後、widgetに設定する必要がある。修正したコードはこんな感じ

# models.py
class NameWidget(MultiWidget):
    def __init__(self,attrs={}):
        widgets = (
                forms.TextInput(),
                forms.TextInput()
                )
        super(NameWidget,self).__init__(widgets,attrs)
        self.widgets[0].attrs.update({"placeholder":u"姓"})
        self.widgets[1].attrs.update({"placeholder":u"名"})
    def decompress(self,value):
        if value:
            names = value.split(' ')
            return (names[0],names[1])
        return (None,None)
    def value_from_datadict(self,data,files,name):
        ulist = [widget.value_from_datadict(data,files,name+'_{0}'.format(i)) for i, widget in enumerate(self.widgets)]
        return u"{0} {1}".format(ulist[0].replace(u' ',''),ulist[1].replace(u' ',''))

参考:How to set different placeholders for DateFromToRangeFilter? Ask Question

またvalue_from_datadictは「入力」→「確認」→「完了」みたいな遷移をさせる場合に、確認から完了への遷移時にデータが入らずにexceptionを吐くので注意。try catchしてcatch側で通常処理

return self.data.get(name)

とかしておけば良いと思う。