带有来自模型的额外信息的 Django 渲染单选按钮选项

Posted

技术标签:

【中文标题】带有来自模型的额外信息的 Django 渲染单选按钮选项【英文标题】:Django Rendering Radio Button Options with Extra Information from Model 【发布时间】:2011-07-30 08:00:04 【问题描述】:

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

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

东 - 这个位置在东。 West - 这是西部的位置! 北 - 这是北位置

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

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 和标签。我正在尝试为那些“subtupals”添加第三个值,这将是一个描述。

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

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 类,因此我可以添加我需要在单选按钮旁边显示的描述信息。

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 类,希望传递我修改后的选择 tupal。

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。我得到一个“元组索引超出范围”失败(我在上面标记 FAILURE 的地方)表明我的 tupal 不知何故没有额外的价值。

有没有人看到我的逻辑有缺陷,或者更一般地说,有一种方法可以使用小部件在选项列表旁边显示描述?

感谢阅读。任何 cmets 都非常感谢。 乔

【问题讨论】:

您是否考虑过只使用 CSS 来做到这一点?对我来说似乎容易多了。我觉得 Python/Django 开发人员不需要担心使用服务器端代码进行这样的格式化。 【参考方案1】:

你看过这个sn-p:RadioSelectWithHelpText吗?

【讨论】:

【参考方案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))

如果您知道更好的方法,我会很高兴看到它。否则,这将不得不做。谢谢阅读。希望这可以减轻我的挫败感。

【讨论】:

【参考方案3】:

问题在于 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 &amp;&amp; python manage.py migrate

【讨论】:

以上是关于带有来自模型的额外信息的 Django 渲染单选按钮选项的主要内容,如果未能解决你的问题,请参考以下文章

Django如何从带有额外字段的ManyToManyField中删除?

Axure:切换单选按扭,控制界面显示不同内容

带有 <image>( X ) 标签的 simple_form 渲染单选按钮组与 twitter 引导程序?

带有额外选择的 Django 查询集计数

如何在 Django 中呈现单个单选按钮选项?

带有来自模型的自定义验证消息的 Django 1.5 基于类的视图