I'm utilizing Django Forms for my web application's front-end filter functionality, and I'm making a few Field customizations so that I may present multi-select checkboxes with custom labels as follows:
[x] Doug Funny (1)
[ ] Skeeter Valentine(5)
[x] Patti Mayonnaise(3)
[ ] Roger Klotz (9)
Upon selecting an option, I'm able to dynamically update the checkbox field labels (the counts, specifically) by overriding my Forms init method as follows:
class FiltersForm(forms.Form):
...
studentCheckbox = MyModelMultipleChoiceField(widget=MyMultiSelectWidget, queryset=Student.objects.all(), required=False)
...
def __init__(self, *args, **kwargs):
super(FiltersForm, self).__init__(*args, **kwargs)
students = Student.objects.values(...).annotate(count=Count(...))
self.fields['studentCheckbox'].queryset = Student.objects.all()
# dynamically updating the field's label here to include a count
self.fields['studentCheckbox'].label_from_instance = lambda obj: "%s (%s)" % (students.get(pk=obj.pk)['name'], students.get(pk=obj.pk)['count'])
But rather than "hardcode" the counts in the field label, I'd like to dynamically set the count as a 'data-count' attribute on each of the widget's option fields. In my attempt to do so, I've subclassed forms.ModelMultipleChoiceField
as MyModelMultipleChoiceField
.
It's my hope to override the label_from_instance
function in MyModelMultipleChoiceField
to dynamically access the obj (by pk) and set a data-count attribute in the process. For some reason, however, the label_from_instance
function isn't being called from the lambda call in my Form's init (self.fields['studentCheckbox'].label_from_instance
). I've also tried overriding the label_from_instance
function on both the Form and the custom widget (MyMultiSelectWidget) to no avail.
class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
print(obj) # nothing prints
if hasattr(obj, 'count'):
self.widget.attrs.update({obj.pk: {'data-count': obj.count}})
return obj
# I probably don't need to subclass the Widget, but just in case...
# I originally thought I could do something with create_option(count=None), but I need access to the
# obj, as I can't use a lambda with self.fields['studentCheckbox'].widget.count = lambda...
class MyMultiSelectWidget(widgets.SelectMultiple):
def __init__(self, count=None, *args, **kwargs):
super().__init__(*args, **kwargs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
options = super(MyMultiSelectWidget, self).create_option(name, value, label, selected, index, subindex=None, attrs=None)
return options
I'm pretty new to Django, and I feel like I've hit so many of its edge cases, so I'd appreciate any help!
I've realized that, in my form's init, I'm not calling the field's label_from_instance
function, but rather defining it with self.fields['studentCheckbox'].label_from_instance = lambda obj: "%s (%s)" % (students.get(pk=obj.pk)['name'], students.get(pk=obj.pk)['count'])
.
As such, I've commented out that line and now the overridden function is called. While I'm now able to access the obj's count, it's still not appearing in the rendered HTML. Updated code is below.
class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
print(obj.count) # this works now
if hasattr(obj, 'count'):
# no error, but not appearing in rendered html
self.widget.attrs.update({obj.pk: {'data-count': obj.count}})
return obj
class FiltersForm(forms.Form):
...
studentCheckbox = MyModelMultipleChoiceField(queryset=Student.objects.all(), required=False)
...
def __init__(self, *args, **kwargs):
super(FiltersForm, self).__init__(*args, **kwargs)
students = Student.objects.annotate(count=Count(...))
# These objects feed into the overridden label_from_instance function
self.fields['studentCheckbox'].queryset = students
#self.fields['studentCheckbox'].label_from_instance = lambda obj: "%s (%s)" % (students.get(pk=obj.pk)['name'], students.get(pk=obj.pk)['count'])
Inspired by an answer on another post (Django form field choices, adding an attribute), I've finally got it working. It turns out I do need to subclass the SelectMultiple widget. Then, I can simply set a count property on it that can be accessed in the template via <input class="form-check-input" type="checkbox" data-count="{{widget.data.count}}"
.
class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
print(obj.count) # this works now
if hasattr(obj, 'count'):
self.widget.count = obj.count
# or, alternatively, add to widgets attrs...
# self.widget.custom_attrs.update({obj.pk: {'count': obj.count}})
return "%s (%s)" % (obj, obj.count)
class MyMultiSelectWidget(widgets.SelectMultiple):
def __init__(self, *args, **kwargs):
self.count = None
# self.custom_attrs = {}
super().__init__(*args, **kwargs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
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)
# alternatively, setting the attributes here for the option
#if len(self.custom_attrs) > 0:
# if value in self.custom_attrs:
# custom_attr = self.custom_attrs[value]
# for k, v in custom_attr.items():
# option_attrs.update({k: v})
return {
'name': name,
'count': str(self.count),
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
}
class FiltersForm(forms.Form):
...
studentCheckbox = MyModelMultipleChoiceField(queryset=Student.objects.all(), required=False)
...
def __init__(self, *args, **kwargs):
super(FiltersForm, self).__init__(*args, **kwargs)
students = Student.objects.annotate(count=Count(...))
# These objects feed into the overridden label_from_instance function
self.fields['studentCheckbox'].queryset = students
#self.fields['studentCheckbox'].label_from_instance = lambda obj: "%s (%s)" % (students.get(pk=obj.pk)['name'], students.get(pk=obj.pk)['count'])
If there's other more optimal implementations, please let me know!