19π
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)
0π
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:
- Django β settings.py seems to load multiple times?
- Django raw_id_fields widget not showing search icon
- Can Django flush its database(s) between every unit test?
0π
I have recently been looking for this too, working with Django 4.2. Here is what I came up with, I hope it helps someone.
First, a generic widget. Note that there is some styling done here, with classes like form-check
being added to the rendered output. Mine comes from Bootstrap, but you can of course define the css the way you want.
## widgets.py
from django.forms import CheckboxInput
from django.forms.widgets import CheckboxSelectMultiple
from django.utils.encoding import force_str
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
class CheckboxSelectMultipleByCategory(CheckboxSelectMultiple):
"""
Displays a ModelMultipleChoiceField, organizing the choices by categories
child_model contains the choices
category_model contains the categories
assumption : child_model has a category_id field,
ForeignKey to category_model
"""
def __init__(self, child_model, category_model):
super().__init__()
self.category_model = category_model
self.child_model = child_model
def render(self, name, value, renderer, attrs=None):
if value is None:
value = []
has_id = attrs and "id" in attrs
final_attrs = self.build_attrs(attrs)
output = ["<div>"]
# Normalize to strings
str_values = set([force_str(v) for v in value])
supercategories = self.category_model.objects.all()
for supercategory in supercategories:
output.append(
'<span class="form-category">%s</span>' % (str(supercategory))
)
output.append('<div class="form-check">')
options = self.child_model.objects.filter(category_id=supercategory)
for option in options:
option_value = force_str(option.pk)
option_label = str(option)
if has_id:
final_attrs = dict(
final_attrs, id="%s_%s" % (attrs["id"], option_value)
)
final_attrs["class"] = "form-check-input"
label_for = ' for="%s"' % final_attrs["id"]
else:
label_for = ""
cb = CheckboxInput(
final_attrs, check_test=lambda value: value in str_values
)
rendered_cb = cb.render(name, option_value)
option_label = conditional_escape(force_str(option_label))
output.append(
'<div>%s<label class="form-check-label" %s>%s</label></div>'
% (rendered_cb, label_for, option_label)
)
output.append("</div>")
output.append("</div>")
return mark_safe("\n".join(output))
Then, the models. In the above widget, we call str(supercategory)
and str(option)
, so make sure to define __str__
for your models, to have the desired human-readable output :
## models.py
class Category(models.Model) :
id = models.AutoField(primary_key=True)
category_name = models.CharField()
def __str__(self):
return self.category_name
class Categorized(models.Model) :
id = models.AutoField(primary_key=True)
name = models.CharField()
category = models.ForeignKey("Category")
def __str__(self):
return self.name
In any form, you can now specify that you want to use this widget. Instantiate it with the models you want to use, like this :
## forms.py
from django import forms
from .models import Category, Categorized
from .widgets import CheckboxSelectMultipleByCategory
class MyForm(forms.ModelForm) :
## ...
categories = forms.ModelMultipleChoiceField(
queryset=Categorized.objects.all(),
widget = CheckboxSelectMultipleByCategory(
Categorized, Category
),
required=False
)
## ...
Finally, there isnβt much left to do in your template :
...
{{ form.categories }}
...
- Django β inline β Search for existing record instead of adding a new one
- Unit testing elastic search inside Django app
- How to store django objects as session variables ( object is not JSON serializable)?
- How to purge all tasks of a specific queue with celery in python?
- Django error: [<class 'decimal.InvalidOperation'>]