Grouped CheckboxSelectMultiple in Django template

后端 未结 2 1359
抹茶落季
抹茶落季 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:21

    You have to write the custom CheckboxSelectMultiple widget. Using the snippet I have tried make the CheckboxSelectMultiple field iterable by adding the category_name as an attribute in field attrs. So that I can use regroup tag in template later on.

    The below code is modified from snippet according to your need, obviously this code can be made more cleaner and more generic, but at this moment its not generic.

    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
    

    Then in template:

    {% 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 %}
    

    Results: I added countries name in category table, and cities name in features table so in template I was able to regroup the cities (features) according to country (category)

    enter image description here

    0 讨论(0)
  • 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 %}
    <h6>{{ group.grouper|default:"Other Features" }}</h6>
    <ul>
      {% for choice in group.list %}
      <li>{{ choice }}</li>
      {% endfor %}
    </ul>
    </div>
    {% 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/

    0 讨论(0)
提交回复
热议问题