Django で画像を選択するための Widget 書いてみた

今作っているサイトでラジオボタンの選択肢にアバターの画像を指定するようなフォームが必要になったもんで、なんかよい Widget はないものかと公式のドキュメントとか見てみたけど、Django には label に普通のテキストを表示させる Widget はあっても label に画像を表示するような Widget はないみたい(たぶん。もしかしたら見落としてる?!)。
なので頑張って既存の Widget をオーバーライドして書いてみることにした。んでとりあえずはラジオボタンの選択肢部分を画像に変えたいだけだったので一番近そうな RadioSelect を改造して ImageSelect って Widget を作ってみた。choices に value と label になるタプルのリストを指定して、Widget に渡す引数には、画像のモデル、レンダリングされたときに label 内に出力されるフィールド名と value になるフィールド名、画像のフィールド名を指定するような感じ。(んーわかりにくいなぁ。。説明ベタだよなぁ。。)
でも実際、全然これであってるかどうかわからんし、なんか色々無駄なことしてそうな気がする。。そもそもわざわざ Widget 作る必要があったのかどうかも微妙なところだし。。
まー悩んでいてもしゃーないので、とりあえず晒してみることにします。なんかおかしいところあったら指摘してほしいっす。。

# ------ forms.py ------
from itertools import chain
from django import forms
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from avatar.models import Avatar


class ImageSelectRenderer(forms.widgets.RadioFieldRenderer):
    def __init__(self, *args, **kwargs):
        self.models = kwargs.pop('models')
        self.label_info = kwargs.pop('label_info')
        return super(ImageSelectRenderer, self).__init__(*args, **kwargs)

    def make_choice(self, choice):
        choice_value = getattr(choice, self.label_info[0])
        choice_label_str = (getattr(choice, self.label_info[2]).url,
                            getattr(choice, self.label_info[1]))
        choice_label = mark_safe('<img src="%s" alt="%s" />' % choice_label_str)
        return (choice_value, choice_label)

    def __iter__(self):
        for i, choice in enumerate(self.models):
            choice = self.make_choice(choice)
            yield forms.widgets.RadioInput(self.name, self.value, self.attrs.copy(), choice, i)

    def __getitem__(self, idx):
        choice = self.make_choice(self.models[idx])
        return super(ImageSelectRenderer, self).__getitem__(self.name, self.value, self.attrs.copy(), choice, idx)


class ImageSelect(forms.widgets.RadioSelect):
    renderer = ImageSelectRenderer

    def __init__(self, *args, **kwargs):
        self.models = kwargs.pop('models')
        self.label_info = (kwargs.pop('value'), kwargs.pop('label'), kwargs.pop('image'))
        super(ImageSelect, self).__init__(*args, **kwargs)

    def get_renderer(self, name, value, attrs=None, choices=()):
        if value is None: value = ''
        str_value = force_unicode(value) # Normalize to string.
        final_attrs = self.build_attrs(attrs)
        choices = list(chain(self.choices, choices))
        return self.renderer(name, str_value, final_attrs, choices, models=self.models, label_info=self.label_info)


class AvatarForm(forms.Form):
    avatar = forms.ChoiceField(label=u'Avatar',
                               choices=Avatar.objects.all().values_list('id', 'name'),
                               # 画像を含むモデル、choices で利用する value と label、モデル内の画像フィールドを渡す
                               widget=ImageSelect(models=Avatar.objects.all(), value='id', label='name', image='image'))



# ------ models.py ------
class Avatar(models.Model):
    """
    BBSのアバター
    """
    name = models.CharField(max_length=20)
    image = models.ImageField(upload_to=settings.BBS_AVATAR_UPLOAD_DIR)

    def __unicode__(self):
        return self.name

最初は choices に直接 QuerySet を渡してそれを ImageSelectRenderer の __iter__ とかで利用するようにしてたけど、フォームを is_valid するときに not iterable とか怒られてしまったので、2つにわけて objects.all() を渡すってことになっちゃった。なんかこの辺無駄な気がするなぁ。ほんとでこれでいいんだろうか。。

そんで一応、出力されるHTMLはこんな感じになりました。

<li><label for="id_avatar_0">Avatar:</label> <ul>
<li><label for="id_avatar_0"><input checked="checked" type="radio" id="id_avatar_0" value="1" name="avatar" /> <img src="/site_media/media_file/bbs_avatar/takashiba.gif" alt="test" /></label></li>
<li><label for="id_avatar_1"><input type="radio" id="id_avatar_1" value="2" name="avatar" /> <img src="/site_media/media_file/bbs_avatar/iioka.gif" alt="test2" /></label></li>
<li><label for="id_avatar_2"><input type="radio" id="id_avatar_2" value="3" name="avatar" /> <img src="/site_media/media_file/bbs_avatar/baba.gif" alt="test3" /></label></li>
<li><label for="id_avatar_3"><input type="radio" id="id_avatar_3" value="4" name="avatar" /> <img src="/site_media/media_file/bbs_avatar/2565992s2_.gif" alt="test4" /></label></li>
<li><label for="id_avatar_4"><input type="radio" id="id_avatar_4" value="5" name="avatar" /> <img src="/site_media/media_file/bbs_avatar/2565992_50.gif" alt="test5f" /></label></li>
</ul></li>

整形した見た目はこんな感じ。

BBS - funbclab - Firefox
Uploaded with plasq's Skitch!

んーー。どうなんだぁーーー。