1

Short Version

In the Django template language, how do I access the attributes of a given checkbox within a CheckboxSelectMultiple widget?

Long Version

The attributes of a typical Django widget can be accessed easily:

{% for field in form %}
{{ field.widget.attrs.something }}
{% endfor %}

However, this method isn't working for a checkbox within a CheckboxSelectMultiple widget.

I have a customized CheckboxSelectMultiple widget which I'm using to display a ManyToMany ModelForm field. The customized widget adds additional attributes to each checkbox in the create_option method.

The additional attributes display appropriately within the HTML of the input element:

<input type="checkbox" name="questions" value="22" id="id_questions_12" category="Category Name" category_number="3" question="Question Name" question_number="4">

I need to access these additional attributes for purposes of display and organizing the form fields.

Hadron Tungsten
  • 157
  • 1
  • 11

2 Answers2

2

I turned back to this after letting it sit for a week or so. After playing around some more and reading into the docs for BoundField (and BoundWidget specifically), I found out how to access the attrs of an individual checkbox in a CheckboxSelectMultiple widget:

{% for field in form %}
{% for check in field.subwidgets %}
{% for a in check.data.attrs %}
Hadron Tungsten
  • 157
  • 1
  • 11
0

I was able to use the same technique given in this answer. It works perfectly for CheckboxSelectMultiple although it is not used in the answer.

I saved this in my project's forms.py:

from django.forms.widgets import CheckboxSelectMultiple, Select


class SelectWithAttrs(Select):
    """
    Select With Option Attributes:
        Subclass of Django's Select widget that allows attributes in options,
        e.g. disabled="disabled", title="help text", class="some classes",
             style="background: color;", etc.

    Pass a dict instead of a string for its label:
        choices = [ ('value_1', 'label_1'),
                    ...
                    ('value_k', {'label': 'label_k', 'foo': 'bar', ...}),
                    ... ]
    The option k will be rendered as:
        <option value="value_k" foo="bar" ...>label_k</option>
    """

    def create_option(self, name, value, label, selected, index,
                      subindex=None, attrs=None):
        if isinstance(label, dict):
            opt_attrs = label.copy()
            label = opt_attrs.pop('label')
        else:
            opt_attrs = {}
        option_dict = super().create_option(
            name, value, label, selected, index,
            subindex=subindex, attrs=attrs)
        for key, val in opt_attrs.items():
            option_dict['attrs'][key] = val
        return option_dict


class CheckboxSelectMultipleWithAttrs(
        SelectWithAttrs, CheckboxSelectMultiple):
    pass


Here is a working snippet from a project of mine that uses this example. The stuff in the beginning isn't really important, but it shows how to build your attributes dict and pass it into your choices.


from django import forms
from django.whatever import other_stuff

from project_folder import forms as project_forms


class MyForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ['employees']

    def __init__(self, *args, **kwargs)
        super().__init__(*args, **kwargs)

        self.fields['employees'].queryset =\
            self.company.employee_set.filter(is_active=True)

        existing_crews_employees = []
        for crew in existing_job_crews:
            crew_employees =\
                [employee.__str__() for employee in crew.employees.all()]
            existing_crews_employees.append({'crew_name': crew.crewtype.name,
                                             'employees': crew_employees})

        employees_choices = []
        for (index, choice) in enumerate(self.fields['employees'].choices):
            # loop over each choice and set proper paramaters for employees
            # that are unassigned/on the current crew/on a different crew
            employee_in_crew = False

            employee_name = choice[1]
            for crew_and_employees in existing_crews_employees:
                for employee in crew_and_employees['employees']:
                    if employee_name == employee.__str__():
                        crew_name = crew_and_employees['crew_name']

                        if self.obj and self.obj.crewtype.name == crew_name:
                            # check the box if employee in current crew
                            employees_choices.append((choice[0], {
                                'label': choice[1],
                                'checked': True,
                                'id': f'id_employees_{choice[0].instance.id}'
                            }))
                        else:
                            # disable the choice if employee in another crew
                            employees_choices.append((choice[0], {
                                'label':
                                    employee_name + f" (on Crew: {crew_name})",
                                'disabled': True}))
                        employee_in_crew = True

            # for valid entries, ensure that we pass the proper ID
            # so that clicking the label will also check the box
            if not employee_in_crew:
                employees_choices.append((choice[0], {
                    'label': choice[1],
                    'id': f'id_employees_{choice[0].instance.id}'}))

        self.fields['employees'].widget = project_forms.CheckboxSelectMultipleWithAttrs(
            choices=employees_choices)

There are two important things to keep in mind when using this technique:

  • Ensure that you pass the id into your attrs for your clickable options, otherwise your labels will not check the proper boxes when they are clicked.
  • This method currently requires initial values to be set using the new attributes dict. Ensure that you pass the 'checked': True key-value pair to any boxes that should be checked.
arcanemachine
  • 571
  • 4
  • 9