django.forms.ModelChoiceFieldをカスタマイズ

newformsは奥が深い。(1.0final前提。1.0rc1だとうまく動かない)

たとえば、django.contrib.auth.models.Userを拡張するこんなモデルがあったとして。(user.get_profile() で UserProfile が取れるように設定しておく)

class Department(models.Model):
    name = models.CharField(u'名称', max_length=100, unique=True)

class UserProfile(models.Model):
    user = models.ForeignKey(User, verbose_name=u'ユーザー', unique=True)
    dept = models.ForeignKey(Department, verbose_name=u'部署')

class Hoge(models.Model):
    user = models.ForeignKey(User, verbose_name=u'ユーザー', unique=True)
    remark = models.CharField(u'備考', max_length=100)

Userを条件にしてHogeを検索したい場合, フォームはこう書く。レンダリング結果はSelectタグになる。

class HogeSearchForm(forms.Form):
    user = ModelChoiceField(label='ユーザー', queryset=User.objects.all(), required=False)

この検索フォームで、ある特定の部署のユーザーだけを表示したい場合や、ユーザーの部署もレンダリング結果に反映したい場合、1.0未満ではどうしようもなかったと思う。

1.0ではちょっとしたカスタマイズで両方実現できてしまう。
肝は__init__内でself.querysetを初期化することと、label_from_instanceをオーバーライドすること。

class CustomChoiceField(forms.ModelChoiceField):
    def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
                 required=True, widget=None, label=None, initial=None,
                 help_text=None, to_field_name=None, *args, **kwargs):
        super(CustomChoiceField, self).__init__(queryset, empty_label, cache_choices,
                 required, widget, label, initial,
                 help_text, to_field_name, *args, **kwargs)

        # 選択候補を絞り込み
        self.queryset = User.objects.filter(
            id__in=UserProfile.objects.filter(dept__id=1).values('user__pk').query)

    def label_from_instance(self, obj):
        """ 表示をカスタマイズ。
        """
        return u'%s:%s' % (obj.get_profile().dept.name, obj.name)

class HogeSearchForm(forms.Form):
    user = CustomChoiceField(label='test', queryset=None, required=False)

ちなみに、optionタグのvalueをカスタマイズしたい場合は、CustomChoiceFieldの__init__でto_field_nameにフィールド名を指定する。
こいつが使われるのが、django.forms.ModelChoiceIteratorのchoiceメソッド。

    def choice(self, obj):
        if self.field.to_field_name:
            try:
                key = getattr(obj, self.field.to_field_name).pk
            except AttributeError:
                key = getattr(obj, self.field.to_field_name)
        else:
            key = obj.pk
        return (key, self.field.label_from_instance(obj))

to_field_nameが未指定の場合、モデルのIDがvalueに設定される。指定した場合は、getattrで属性を取得して、PK属性があればそれをvalueに設定するようになっている。

表示はUserProfileを使いたいけど、form.cleaned_data['user']ではUserを取得したい場合とかに便利。