How to add attributes to option tags?

Viewed 18578

I have to add title attribute to options of the ModelChoiceField. Here is my admin code for that:

class LocModelForm(forms.ModelForm):
        def __init__(self,*args,**kwargs):
            super(LocModelForm,self).__init__(*args,**kwargs)
            self.fields['icons'] = forms.ModelChoiceField(queryset = Photo.objects.filter(galleries__title_slug = "markers"))
            self.fields['icons'].widget.attrs['class'] = 'mydds'
                

        class Meta:
            model = Loc
            widgets = {
                'icons' : forms.Select(attrs={'id':'mydds'}), 
                }
        
        class Media:
            css = {
                "all":("/media/css/dd.css",)
                }
            js=(
                '/media/js/dd.js',
                )

class LocAdmin(admin.ModelAdmin):
    form = LocModelForm

I can add any attribute to select widget, but I don't know how to add attributes to option tags. Any idea?

6 Answers

Here is a solution if you want to use the instance to set the attribute value.

class IconSelectWidget(forms.Select):
    def create_option(self, name, value, *args, **kwargs):
        option = super().create_option(name, value, *args, **kwargs)
        if value:
            icon = self.choices.queryset.get(pk=value)  # get icon instance
            option['attrs']['title'] = icon.title  # set option attribute
        return option

class LocModelForm(forms.ModelForm):
    icons = forms.ModelChoiceField(
        queryset=Photo.objects.filter(galleries__title_slug='markers'),
        widget=IconSelectWidget
    )

From django 1.11 and above the render_option method was removed. see this link: https://docs.djangoproject.com/en/1.11/releases/1.11/#changes-due-to-the-introduction-of-template-based-widget-rendering

Here is a solution that worked for me different than Kayoz's. I did not adapt the names as in the example but i hope it is still clear. In the model form I overwrite the field:

class MyForm(forms.ModelForm):
    project = ProjectModelChoiceField(label=_('Project'), widget=ProjectSelect())

Then I declare the classes from above and one extra, the iterator:

class ProjectModelChoiceIterator(django.forms.models.ModelChoiceIterator):
    def choice(self, obj):
        # return (self.field.prepare_value(obj), self.field.label_from_instance(obj)) #it used to be like this, but we need the extra context from the object not just the label. 
        return (self.field.prepare_value(obj), obj)

class ProjectModelChoiceField(django.forms.models.ModelChoiceField):
   def _get_choices(self):
       if hasattr(self, '_choices'):
           return self._choices
       return ProjectModelChoiceIterator(self)


class ProjectSelect(django.forms.Select):

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        context = super(ProjectSelect, self).create_option(name, value, label, selected, index, subindex=None, attrs=None)

        context['attrs']['extra-attribute'] = label.extra_attribute #label is now an object, not just a string.
        return context

Working with Django 1.11 I discovered another way to this this using the documented APIs. If you override get_context and dig down enough into the structure you'll see the individual option attributes in context['widget']['optgroups'][1][option_idx]['attrs']. For example, in my subclass I have this code:

class SelectWithData(widgets.Select):
    option_data = {}

    def __init__(self, attrs=None, choices=(), option_data={}):
        super(SelectWithData, self).__init__(attrs, choices)
        self.option_data = option_data

    def get_context(self, name, value, attrs):
        context = super(SelectWithData, self).get_context(name, value, attrs)
        for optgroup in context['widget'].get('optgroups', []):
            for option in optgroup[1]:
                for k, v in six.iteritems(self.option_data.get(option['value'], {})):
                    option['attrs']['data-' + escape(k)] = escape(v)
        return context
Related