Grouped CheckboxSelectMultiple in Django template

后端 未结 2 1398
抹茶落季
抹茶落季 2020-11-30 00:43

How can I group checkboxes produced by CheckboxSelectMultiple by a related model?

This is best demonstrated by example.

models.py:

2条回答
  •  南方客
    南方客 (楼主)
    2020-11-30 01:28

    Here's a solution for current versions of 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)
    

    Then you can use {{ form.as_p }} in the template for properly grouped choices.

    If you would like to use the regroup template tag and iterate over the choices, you will also need to reference the following custom widget:

    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)
    

    Then the following should work in your template:

    {% regroup form.features by data.group as feature_list %}
    {% for group in feature_list %}
    
    {{ group.grouper|default:"Other Features" }}
      {% for choice in group.list %}
    • {{ choice }}
    • {% endfor %}
    {% endfor %}

    Credit to the following page for part of the solution:

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

提交回复
热议问题