Grouping CheckboxSelectMultiple Options in Django
Question:
In my Django App I have the following model:
class SuperCategory(models.Model):
name = models.CharField(max_length=100,)
slug = models.SlugField(unique=True,)
class Category(models.Model):
name = models.CharField(max_length=100,)
slug = models.SlugField(unique=True,)
super_category = models.ForeignKey(SuperCategory)
What I’m trying to accomplish in Django’s Admin Interface is the rendering of Category using widget CheckboxSelectMultiple but with Category somehow grouped by SuperCategory, like this:
Category:
Sports: <- Item of SuperCategory
[ ] Soccer <- Item of Category
[ ] Baseball <- Item of Category
[ ] …
Politics: <- Another item of SuperCategory
[ ] Latin America
[ ] North america
[ ] …
Does anybody have a nice suggestion on how to do this?
Many thanks.
Answers:
After some struggle, here is what I got.
First, make ModelAdmin call a ModelForm:
class OptionAdmin(admin.ModelAdmin):
form = forms.OptionForm
Then, in the form, use use a custom widget to render:
category = forms.ModelMultipleChoiceField(queryset=models.Category.objects.all(),widget=AdminCategoryBySupercategory)
Finally, the widget:
class AdminCategoryBySupercategory(forms.CheckboxSelectMultiple):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = []
has_id = attrs and 'id' in attrs
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<ul>']
# Normalize to strings
str_values = set([force_unicode(v) for v in value])
supercategories = models.SuperCategory.objects.all()
for supercategory in supercategories:
output.append(u'<li>%s</li>'%(supercategory.name))
output.append(u'<ul>')
del self.choices
self.choices = []
categories = models.Category.objects.filter(super_category=supercategory)
for category in categories:
self.choices.append((category.id,category.name))
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
if has_id:
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
label_for = u' for="%s"' % final_attrs['id']
else:
label_for = ''
cb = forms.CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
option_value = force_unicode(option_value)
rendered_cb = cb.render(name, option_value)
option_label = conditional_escape(force_unicode(option_label))
output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
output.append(u'</ul>')
output.append(u'</li>')
output.append(u'</ul>')
return mark_safe(u'n'.join(output))
Not the most elegant solution, but hey, it worked.
I had a slightly different case, but I hope I adapted the code properly to the OPs case. Then the following should do the trick in Django 4.2:
# Custom UI Component
class GroupedCheckboxSelectMultiple(CheckboxSelectMultiple):
def render(self, name, value, attrs=None, renderer=None):
widget_id = f"id_{name}"
html = ""
html += f'<div id="{widget_id}" class="grouped-checkbox-select-multiple">'
# sort since groupby needs sorted data to work "properly"
sorted_choices = sorted(self.choices, key=lambda choice: choice.super_category or "")
id_count = 0
for group, choices in itertools.groupby(
sorted_choices, lambda choice: choice.super_category
):
html += '<div class="choice-group">'
html += f'<span class="group-title">{group}</span>'
for choice in choices:
identifier = f"{widget_id}_{id_count}"
html += f"""<div class="choice-wrapper">
<input id="{identifier}" name="{name}" type="checkbox" value={choice.name}>"""
html += f' <label for="{identifier}">{choice.name}</label>'
html += "</div>"
id_count += 1
html += "</div>"
html += "</div>"
return html
which I then use in a form like this:
class ProjectDataForm(Form):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
raw_choices = fetch_choices()
choices = list(map(lambda item: (item.name, item.name), raw_choices))
self.fields["category"].choices = choices
self.fields["category"].widget.choices = raw_choices
category = MultipleChoiceField(widget=GroupedCheckboxSelectMultiple())
In my Django App I have the following model:
class SuperCategory(models.Model):
name = models.CharField(max_length=100,)
slug = models.SlugField(unique=True,)
class Category(models.Model):
name = models.CharField(max_length=100,)
slug = models.SlugField(unique=True,)
super_category = models.ForeignKey(SuperCategory)
What I’m trying to accomplish in Django’s Admin Interface is the rendering of Category using widget CheckboxSelectMultiple but with Category somehow grouped by SuperCategory, like this:
Category:
Sports: <- Item of SuperCategory
[ ] Soccer <- Item of Category
[ ] Baseball <- Item of Category
[ ] …Politics: <- Another item of SuperCategory
[ ] Latin America
[ ] North america
[ ] …
Does anybody have a nice suggestion on how to do this?
Many thanks.
After some struggle, here is what I got.
First, make ModelAdmin call a ModelForm:
class OptionAdmin(admin.ModelAdmin):
form = forms.OptionForm
Then, in the form, use use a custom widget to render:
category = forms.ModelMultipleChoiceField(queryset=models.Category.objects.all(),widget=AdminCategoryBySupercategory)
Finally, the widget:
class AdminCategoryBySupercategory(forms.CheckboxSelectMultiple):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = []
has_id = attrs and 'id' in attrs
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<ul>']
# Normalize to strings
str_values = set([force_unicode(v) for v in value])
supercategories = models.SuperCategory.objects.all()
for supercategory in supercategories:
output.append(u'<li>%s</li>'%(supercategory.name))
output.append(u'<ul>')
del self.choices
self.choices = []
categories = models.Category.objects.filter(super_category=supercategory)
for category in categories:
self.choices.append((category.id,category.name))
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
if has_id:
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
label_for = u' for="%s"' % final_attrs['id']
else:
label_for = ''
cb = forms.CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
option_value = force_unicode(option_value)
rendered_cb = cb.render(name, option_value)
option_label = conditional_escape(force_unicode(option_label))
output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
output.append(u'</ul>')
output.append(u'</li>')
output.append(u'</ul>')
return mark_safe(u'n'.join(output))
Not the most elegant solution, but hey, it worked.
I had a slightly different case, but I hope I adapted the code properly to the OPs case. Then the following should do the trick in Django 4.2:
# Custom UI Component
class GroupedCheckboxSelectMultiple(CheckboxSelectMultiple):
def render(self, name, value, attrs=None, renderer=None):
widget_id = f"id_{name}"
html = ""
html += f'<div id="{widget_id}" class="grouped-checkbox-select-multiple">'
# sort since groupby needs sorted data to work "properly"
sorted_choices = sorted(self.choices, key=lambda choice: choice.super_category or "")
id_count = 0
for group, choices in itertools.groupby(
sorted_choices, lambda choice: choice.super_category
):
html += '<div class="choice-group">'
html += f'<span class="group-title">{group}</span>'
for choice in choices:
identifier = f"{widget_id}_{id_count}"
html += f"""<div class="choice-wrapper">
<input id="{identifier}" name="{name}" type="checkbox" value={choice.name}>"""
html += f' <label for="{identifier}">{choice.name}</label>'
html += "</div>"
id_count += 1
html += "</div>"
html += "</div>"
return html
which I then use in a form like this:
class ProjectDataForm(Form):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
raw_choices = fetch_choices()
choices = list(map(lambda item: (item.name, item.name), raw_choices))
self.fields["category"].choices = choices
self.fields["category"].widget.choices = raw_choices
category = MultipleChoiceField(widget=GroupedCheckboxSelectMultiple())