Django使用模型中的额外信息渲染单选按钮选项

时间:2011-04-05 21:06:06

标签: django forms widget radio-button models

我有一个Location模型和一个关联的表单,它将“location”字段显示为一堆单选按钮(使用表单queryset来显示值)。有少量的位置,但它们需要是动态的。我想在每个单选框选项旁边显示位置描述,以便用户在位置上获得更多信息。

假设这是一个单选按钮列表,这就是我想要的样子:

<>东 - 这个位置是东。 <>西 - 这是西部的位置! <>北 - 这是北部的位置

我有一个类似于以下的模型:

class Location(models.Models):
    location = models.CharField(max_length=50)
    description = models.TextField(blank=True, null=True)

这样的形式:

class LocationForm(forms.Form):
    location = ExtraModelChoiceField(
               widget=RadioSelect(renderer=ExtraHorizRadioRenderer), 
               queryset = models.Locations.objects.filter(active=True))

我似乎找不到渲染表单的好方法,因此我可以显示描述以及每个选择选项。我做了很多压倒一切,但没有太多的运气。

我试图解决(但没有运气):

从我收集的内容来看,通常如果在表单字段上提供了一个查询集,Django表单逻辑会将其转换为选择tupal的tupal。每个“subtupal”包含在呈现时显示的id和标签。我正在尝试为那些“subupals”添加第三个值,这将是一个描述。

我已经定义了一个自定义渲染器来水平显示我的单选按钮并传入我的自定义选项。

class ExtraHorizRadioRenderer(forms.RadioSelect.renderer):
    def render(self):
        return mark_safe(u'\n'.join([u'%s\n' % w for w in self]))

    def __iter__(self):
        for i, choice in enumerate(self.choices):
            yield ExtraRadioInput(self.name, self.value, 
                                  self.attrs.copy(), choice, i)

    def __getitem__(self, idx):
        choice = self.choices[idx] # Let the IndexError propogate
        return ExtraRadioInput(self.name, self.value, 
                               self.attrs.copy(), choice, idx)

我已经覆盖了Django RadioInput类,所以我可以添加我需要在Radio Buttons旁边显示的描述信息。

class ExtraRadioInput(forms.widgets.RadioInput):

    def __init__(self, name, value, attrs, choice, index):
        self.name, self.value = name, value
        self.attrs = attrs
        self.choice_value = force_unicode(choice[0])
        self.choice_label = force_unicode(choice[1])
        self.choice_description = force_unicode(choice[2])   # <--- MY ADDITION; FAILS
        self.index = index

    def __unicode__(self):
        if 'id' in self.attrs:
            label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
        else:
            label_for = ''
        choice_label = conditional_escape(force_unicode(self.choice_label))
        return mark_safe(u'<label%s>%s %s</label>' % (
             label_for, self.tag(), choice_label))

    def tag(self):
        if 'id' in self.attrs:
            self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
        final_attrs = dict(self.attrs, type='radio', name=self.name, 
                      value=self.choice_value)
        if self.is_checked():
            final_attrs['checked'] = 'checked'
        return mark_safe(
           u'<input%s /><span class="description">%s</span>' % \
           (flatatt(final_attrs),self.choice_description ))  # <--- MY ADDTIONS

我还覆盖了以下两个Django类,希望传递我修改过的选择tupals。

class ExtraModelChoiceIterator(forms.models.ModelChoiceIterator  ):    

    def choice(self, obj): 
        if self.field.to_field_name:
            key = obj.serializable_value(self.field.to_field_name)
        else:
            key = obj.pk

        if obj.description:   # <-- MY ADDITIONS
            description = obj.description
        else:
            description = ""
        return (key, self.field.label_from_instance(obj),description)


class ExtraModelChoiceField(forms.models.ModelChoiceField):

    def _get_choices(self):
        if hasattr(self, '_choices'):
            return self._choices
        return ExtraModelChoiceIterator(self)  # <-- Uses MY NEW ITERATOR

使用上面的方法,我似乎无法传递我的3值tupal。我得到一个“元组索引超出范围”失败(我在上面标记失败的地方),表明我的tupal没有额外的价值。

有没有人在我的逻辑中看到一个缺陷,或者更常见的是有一种方法在使用小部件的选项列表旁边显示描述?

感谢阅读。任何评论都非常感谢。 乔

3 个答案:

答案 0 :(得分:2)

您是否看过这段代码:RadioSelectWithHelpText

答案 1 :(得分:2)

很抱歉回答我自己的问题,但我想我有办法解决这个问题。和往常一样,它似乎比我以前做的更简单。覆盖扩展的ModelChoiceField上的label_from_instance方法似乎允许我访问模型对象实例以便能够打印出额外的信息。

from django.utils.encoding import smart_unicode, force_unicode

class ExtraModelChoiceField(forms.models.ModelChoiceField):

    def label_from_instance(self, obj):
        return mark_safe(
            "<span>%s</span><span class=\"desc\" id=\"desc_%s\">%s</span>" % (
            mart_unicode(obj), obj.id, smart_unicode(obj.description),))


class HorizRadioRenderer(forms.RadioSelect.renderer):
    # for displaying select options horizontally. 
    # https://wikis.utexas.edu/display/~bm6432/Django-Modifying+RadioSelect+Widget+to+have+horizontal+buttons
    def render(self):
        return mark_safe(u'\n'.join([u'%s\n' % w for w in self]))


class LocationForm(forms.Form):
    location = ExtraModelChoiceField(widget=forms.RadioSelect(renderer=HorizRadioRenderer),
        queryset=models.Location.objects.filter(active=True))

如果您知道更好的方法,我会很高兴看到它。否则,这将是必须的。谢谢阅读。希望这可以拯救某人我的挫败感。

答案 2 :(得分:0)

问题在于 Django 没有提供一种简单的方法来向表单字段添加额外的属性。特别是,选择字段值被限制为简单的字符串。

此解决方案提供了一种为选择值和额外属性提供类似字符串的对象的方法。

考虑一个具有多个选项作为值的简单模型。

# models.py
from django import models

_value_choices = ['value#1', 'value#2', 'value#3'] # the literal values for users
# list creates a list rather than generator
# enumerate provides integers for storage
VALUE_CHOICES = list(zip(enumerate(_value_choices)))

class MyModel(models.Model):
    value = models.PositiveSmallIntegerField(choices=VALUE_CHOICES)

我们像往常一样创建一个 ModelForm

# forms.py
from django import forms

from . import models

class MyModel(forms.ModelForm):
    class Meta:
        model = models.MyModel
        fields = ['value']
        widgets = {
            'value': forms.RadioSelect(),
        }

现在假设我们有以下模板:

{# template #}
{% for radio in field %}
<li>
    <div>
        {{ radio.tag }}
        <label for="{{ radio.id_for_label }}">
    </div>
</li>
{% endfor %}

我们现在面临的问题是扩展模板,以便每个标签可以有与选择相关联的额外文本。

解决方案由两部分组成: I - 对可以强制为字符串的选择值使用特殊类; II - 创建一个关于如何从存储值转换为完整对象的解构方法。

I:为可见的选择值创建一个特殊的类

这很简单。

class RadioChoice:
    def __init__(self, label, arg1, arg2): # as many as you want
        self.label = label
        self.arg1 = arg1
        self.arg2 = arg2

    def __str__(self): # only the label attribute is official
        return self.label

现在重写上面的_value_choices以使用这个类

_value_choices = [
    RadioChoice('value#1', 'value_arg1_1', 'value_arg1_2'),
    RadioChoice('value#2', 'value_arg2_1', 'value_arg2_2'),
    RadioChoice('value#3', 'value_arg3_1', 'value_arg3_2'),
]

在模板中包含新属性。

{% for radio in field %}
    <li>
        <div>
            {{ radio.tag }}
            <label for="{{ radio.id_for_label }}"><span>{{ radio.choice_label }}</span> <span>{{ radio.choice_label.arg1 }}</span></label>
            <small>{{ radio.choice_label.arg2 }}</small>
        </div>
    </li>
{% endfor %}

现在测试以确保它按预期工作。

II:添加 deconstruct() 方法并运行迁移

一旦您确定它正常工作,您将需要为模型中的更改创建一个新的迁移。

class RadioChoice:
    def __init__(self, label, arg1, arg2): # as many as you want
        self.label = label
        self.arg1 = arg1
        self.arg2 = arg2

    def __str__(self): # only the label attribute is official
        return self.label

    def deconstruct(self):
        # https://docs.djangoproject.com/en/3.1/topics/migrations/#adding-a-deconstruct-method
        # you must return three arguments: path (to the module from the project root), args and kwargs
        path = "app_name.models.RadioChoice"
        args = (self.label, self.arg1, self.arg2)
        kwargs = dict()
        return path, args, kwargs

最后,运行 python manage.py makemigrations && python manage.py migrate