【问题标题】:Grouped CheckboxSelectMultiple in Django templateDjango模板中的分组CheckboxSelectMultiple
【发布时间】:2012-12-03 13:53:56
【问题描述】:

如何将CheckboxSelectMultiple 生成的复选框按相关模型分组?

这是最好的例子。

models.py:

class FeatureCategory(models.Model):
    name = models.CharField(max_length=30)

class Feature(models.Model):
    name = models.CharField(max_length=30)
    category = models.ForeignKey(FeatureCategory)

class Widget(models.Model):
    name = models.CharField(max_length=30)
    features = models.ManyToManyField(Feature, blank=True)

forms.py:

class WidgetForm(forms.ModelForm):
    features = forms.ModelMultipleChoiceField(
        queryset=Feature.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=False
    )
    class Meta:
        model = Widget

views.py:

def edit_widget(request):
    form = WidgetForm()
    return render(request, 'template.html', {'form': form})

template.html:

{{ form.as_p }}

以上产生以下输出:

[] Widget 1
[] Widget 2
[] Widget 3
[] Widget 1
[] Widget 2

我想要按功能类别对功能复选框进行分组(基于ForeignKey):

Category 1:
  [] Widget 1
  [] Widget 2
  [] Widget 3

Category 2:
  [] Widget 1
  [] Widget 2

我怎样才能做到这一点?我尝试使用{% regroup %} 模板标签无济于事。

非常感谢任何建议。

谢谢。

【问题讨论】:

    标签: django django-forms django-templates


    【解决方案1】:

    您必须编写自定义 CheckboxSelectMultiple 小部件。使用snippet,我尝试通过在字段attrs 中添加category_name 作为属性来使CheckboxSelectMultiple 字段可迭代。这样我以后可以在模板中使用regroup标签。

    下面的代码是根据你的需要从sn-p修改而来的,显然这段代码可以变得更简洁更通用,但此时它不是通用的。

    forms.py

    from django import forms
    from django.forms import Widget
    from django.forms.widgets import SubWidget
    from django.forms.util import flatatt
    from django.utils.html import conditional_escape
    from django.utils.encoding import StrAndUnicode, force_unicode
    from django.utils.safestring import mark_safe
    
    from itertools import chain
    import ast
    
    from mysite.models import Widget as wid # your model name is conflicted with django.forms.Widget
    from mysite.models import Feature
    
    class CheckboxInput(SubWidget):
        """
        An object used by CheckboxRenderer that represents a single
        <input type='checkbox'>.
        """
        def __init__(self, name, value, attrs, choice, index):
            self.name, self.value = name, value
            self.attrs = attrs
            self.choice_value = force_unicode(choice[1])
            self.choice_label = force_unicode(choice[2])
    
            self.attrs.update({'cat_name': choice[0]})
    
            self.index = index
    
        def __unicode__(self):
            return self.render()
    
        def render(self, name=None, value=None, attrs=None, choices=()):
            name = name or self.name
            value = value or self.value
            attrs = attrs or self.attrs
    
            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 is_checked(self):
            return self.choice_value in self.value
    
        def tag(self):
            if 'id' in self.attrs:
                self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
            final_attrs = dict(self.attrs, type='checkbox', name=self.name, value=self.choice_value)
            if self.is_checked():
                final_attrs['checked'] = 'checked'
            return mark_safe(u'<input%s />' % flatatt(final_attrs))
    
    class CheckboxRenderer(StrAndUnicode):
        def __init__(self, name, value, attrs, choices):
            self.name, self.value, self.attrs = name, value, attrs
            self.choices = choices
    
        def __iter__(self):
            for i, choice in enumerate(self.choices):
                yield CheckboxInput(self.name, self.value, self.attrs.copy(), choice, i)
    
        def __getitem__(self, idx):
            choice = self.choices[idx] # Let the IndexError propogate
            return CheckboxInput(self.name, self.value, self.attrs.copy(), choice, idx)
    
        def __unicode__(self):
            return self.render()
    
        def render(self):
            """Outputs a <ul> for this set of checkbox fields."""
            return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
                    % force_unicode(w) for w in self]))
    
    class CheckboxSelectMultipleIter(forms.CheckboxSelectMultiple):
        """
        Checkbox multi select field that enables iteration of each checkbox
        Similar to django.forms.widgets.RadioSelect
        """
        renderer = CheckboxRenderer
    
        def __init__(self, *args, **kwargs):
            # Override the default renderer if we were passed one.
            renderer = kwargs.pop('renderer', None)
            if renderer:
                self.renderer = renderer
            super(CheckboxSelectMultipleIter, self).__init__(*args, **kwargs)
    
        def subwidgets(self, name, value, attrs=None, choices=()):
            for widget in self.get_renderer(name, value, attrs, choices):
                yield widget
    
        def get_renderer(self, name, value, attrs=None, choices=()):
            """Returns an instance of the renderer."""
    
            choices_ = [ast.literal_eval(i[1]).iteritems() for i in self.choices]
            choices_ = [(a[1], b[1], c[1]) for a, b, c in choices_]
    
            if value is None: value = ''
            str_values = set([force_unicode(v) for v in value]) # Normalize to string.
            if attrs is None:
                attrs = {}
            if 'id' not in attrs:
                attrs['id'] = name
            final_attrs = self.build_attrs(attrs)
            choices = list(chain(choices_, choices))
            return self.renderer(name, str_values, final_attrs, choices)
    
        def render(self, name, value, attrs=None, choices=()):
            return self.get_renderer(name, value, attrs, choices).render()
    
        def id_for_label(self, id_):
            if id_:
                id_ += '_0'
            return id_
    
    class WidgetForm(forms.ModelForm):
        features = forms.ModelMultipleChoiceField(
            queryset=Feature.objects.all().values('id', 'name', 'category__name'),
            widget=CheckboxSelectMultipleIter,
            required=False
        )
        class Meta:
            model = wid
    

    然后在模板中:

    {% for field in form %}
    {% if field.name == 'features' %} 
        {% regroup field by attrs.cat_name as list %}
    
        <ul>
        {% for el in list %}
            <li>{{el.grouper}}
            <ul>
                {% for e in el.list %}
                    {{e}} <br />
                {% endfor %}
            </ul>
            </li>
        {% endfor %}
        </ul>
    {% else %}
        {{field.label}}: {{field}}
    {% endif %}
    
    {% endfor %}
    

    结果: 我在类别表中添加了国家名称,在特征表中添加了城市名称,因此在模板中我能够根据国家(类别)重新组合城市(特征)

    【讨论】:

    • 我希望有一个内置的方式,但这正是我想要的。谢谢。
    • 这段代码还能用吗?我只是尝试使用它,却迷失在与 ast 相关的错误消息中。
    • @stickwithjosh 如果您能提供有关您遇到的问题的更多详细信息,也许我可以提供帮助。
    • 这个答案可以看到一个简约的、最新的覆盖 CheckboxSelectMultiple 的例子:stackoverflow.com/questions/39257050/…
    【解决方案2】:

    这是当前版本的 Django (~2.1) 的解决方案。

    ## forms.py
    
    from itertools import groupby
    from django import forms
    from django.forms.models import ModelChoiceIterator, ModelMultipleChoiceField
    
    from .models import Feature, Widget
    
    
    class GroupedModelMultipleChoiceField(ModelMultipleChoiceField):
    
        def __init__(self, group_by_field, group_label=None, *args, **kwargs):
            """
            ``group_by_field`` is the name of a field on the model
            ``group_label`` is a function to return a label for each choice group
    
            """
            super(GroupedModelMultipleChoiceField, self).__init__(*args, **kwargs)
            self.group_by_field = group_by_field
            if group_label is None:
                self.group_label = lambda group: group
            else:
                self.group_label = group_label
    
        def _get_choices(self):
            if hasattr(self, '_choices'):
                return self._choices
            return GroupedModelChoiceIterator(self)
        choices = property(_get_choices, ModelMultipleChoiceField._set_choices)
    
    
    class GroupedModelChoiceIterator(ModelChoiceIterator):
    
        def __iter__(self):
            """Now yields grouped choices."""            
            if self.field.empty_label is not None:
                yield ("", self.field.empty_label)
            for group, choices in groupby(
                    self.queryset.all(),
                    lambda row: getattr(row, self.field.group_by_field)):
                if group is None:
                    for ch in choices:
                        yield self.choice(ch)
                else:
                    yield (
                        self.field.group_label(group),
                        [self.choice(ch) for ch in choices])
    
    
    class WidgetForm(forms.ModelForm):
        class Meta:
            model = Widget
            fields = ['features',]
    
        def __init__(self, *args, **kwargs):
            super(WidgetForm, self).__init__(*args, **kwargs)
            self.fields['features'] = GroupedModelMultipleChoiceField(
                group_by_field='category',
                queryset=Feature.objects.all(),
                widget=forms.CheckboxSelectMultiple(),
                required=False)
    

    然后您可以在模板中使用{{ form.as_p }} 进行正确分组的选择。

    如果您想使用 regroup 模板标签并遍历选项,您还需要引用以下自定义小部件:

    class GroupedCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
    
        def optgroups(self, name, value, attrs=None):
            """
            The group name is passed as an argument to the ``create_option`` method (below).
    
            """
            groups = []
            has_selected = False
    
            for index, (option_value, option_label) in enumerate(self.choices):
                if option_value is None:
                    option_value = ''
    
                subgroup = []
                if isinstance(option_label, (list, tuple)):
                    group_name = option_value
                    subindex = 0
                    choices = option_label
                else:
                    group_name = None
                    subindex = None
                    choices = [(option_value, option_label)]
                groups.append((group_name, subgroup, index))
    
                for subvalue, sublabel in choices:
                    selected = (
                        str(subvalue) in value and
                        (not has_selected or self.allow_multiple_selected)
                    )
                    has_selected |= selected
                    subgroup.append(self.create_option(
                        name, subvalue, sublabel, selected, index,
                        subindex=subindex, attrs=attrs, group=group_name,
                    ))
                    if subindex is not None:
                        subindex += 1
            return groups
    
        def create_option(self, name, value, label, selected, index, subindex=None, attrs=None, group=None):
            """
            Added a ``group`` argument which is included in the returned dictionary.
    
            """
            index = str(index) if subindex is None else "%s_%s" % (index, subindex)
            if attrs is None:
                attrs = {}
            option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
            if selected:
                option_attrs.update(self.checked_attribute)
            if 'id' in option_attrs:
                option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
            return {
                'name': name,
                'value': value,
                'label': label,
                'selected': selected,
                'index': index,
                'attrs': option_attrs,
                'type': self.input_type,
                'template_name': self.option_template_name,
                'wrap_label': True,
                'group': group,
            }
    
    
    class WidgetForm(forms.ModelForm):
        class Meta:
            model = Widget
            fields = ['features',]
    
        def __init__(self, *args, **kwargs):
            super(WidgetForm, self).__init__(*args, **kwargs)
            self.fields['features'] = GroupedModelMultipleChoiceField(
                group_by_field='category',
                queryset=Feature.objects.all(),
                widget=GroupedCheckboxSelectMultiple(),
                required=False)
    

    那么以下内容应该可以在您的模板中使用:

    {% regroup form.features by data.group as feature_list %}
    {% for group in feature_list %}
    <h6>{{ group.grouper|default:"Other Features" }}</h6>
    <ul>
      {% for choice in group.list %}
      <li>{{ choice }}</li>
      {% endfor %}
    </ul>
    </div>
    {% endfor %}
    

    部分解决方案归功于以下页面:

    https://mounirmesselmeni.github.io/2013/11/25/django-grouped-select-field-for-modelchoicefield-or-modelmultiplechoicefield/

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-07-09
      • 2018-10-18
      • 1970-01-01
      • 2013-02-25
      • 2014-10-11
      • 2019-07-06
      • 2022-09-27
      相关资源
      最近更新 更多