abidibo.net

Add data attributes to option tags in django admin Select field

django-admin django

Sometimes can be usefull to add some data-attribute to the option tags of a select field in your django admin forms. For example you can then use this attributes to perform some js manipulation, something like: hide all the stuff with data-foo equal to 'bar'. For example, I have a select field named 'providers', and then an invoice field, each invoice is tied to a provider. I want to implement a cascasde select, so that when the user selects a provider, then he can choose only the related invoices. This can be achieved with data-attributes and a few lines of js.

Ok, so how can we add this data attributes? We need 3 things:

  1. create a custom widget which inherits from Select and adds the logic to set the data attributes;
  2. subclass the ModelForm class in order to instantiate the new widget;
  3. associate the new form class to the ModelAdmin class.

The custom widget

It's quite easy: we just need to add some attributes to the options tags. After looking deeply at the django source code, I understood that this can be done simply by overriding the create_option method of the ChoiceWidget class. There is no need to override templates or other things, here comes the code:

from django.forms.widgets import Select


class DataAttributesSelect(Select):

    def __init__(self, attrs=None, choices=(), data={}):
        super(DataAttributesSelect, self).__init__(attrs, choices)
        self.data = data

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None): # noqa
        option = super(DataAttributesSelect, self).create_option(name, value, label, selected, index, subindex=None, attrs=None) # noqa
        # adds the data-attributes to the attrs context var
        for data_attr, values in self.data.iteritems():
            option['attrs'][data_attr] = values[option['value']]

        return option

You can place it in a widgets.py file inside your app dir.

N.B. line 13 for python 3 becomes as follows:

for data_attr, values in self.data.items():

Subclass the ModelForm

  This code has been improved after comments suggestions, see the improved version below.

Create a forms.py file inside you app dir:

from django import forms

from .widgets import DataAttributesSelect
from .models import MyChoiceModel


class MyModelAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyModelAdminForm, self).__init__(*args, **kwargs)
        # preparation of data attributes. It is a dictionary where
        # the key is the attribute name and the value another dict.
        # In the second dict, for each key which corresponds to the option
        # value, the attribute value is given
        data = {'data-foo': {'': ''}} # empty option
        for f in MyChoiceModel.objects.all():
            data['data-foo'][f.id] = f.foo.id

        self.fields['myselectfield'].widget = DataAttributesSelect(
            choices=[('', '---------')] + [(f.id, str(f)) for f in MyChoiceModel.objects.all()], # noqa
            data=data
        )

Use the new created form inside the ModelAdmin class

Super easy:

from django.contrib import admin
from .forms import MyModelAdminForm

class MyModeAdmin(admin.ModelAdmin):
    list_display = ('bar', )
    # ...
    form = MyModelAdminForm


admin.site.register(MyModel, MyModeAdmin)

That's it!

Update 17 october 2017

As Venelin Stoykov's pointed out, the form class code can be improved, avoiding to hit the database too many times, so here comes the rewritten code:

from django import forms

from .widgets import DataAttributesSelect
from .models import MyChoiceModel


class MyModelAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyModelAdminForm, self).__init__(*args, **kwargs)

        data = {'data-foo': dict(MyModelChoice.objects.values_list('id', 'foo'))}
        data['data-foo'][''] = ''  # empty option

        self.fields['myselectfield'].widget = DataAttributesSelect(
            choices=self.fields['myselectfield'].choices,
            data=data
        )

Update 13 november 2017

The proposed method had an unpleasant drawback. The add related features in the admin site got lost, so there was no way to add or edit related models contextually.

I surfed the django admin code in order to find a way to preserve this functionality, and it turned out it was quite simple. The django way of add such functionality is a wrapper function defined in django/contrib/admin/widgets.py.
Django calls this wrapper around the fk and m2m fields - which are class attributes - before actually instantiating the form class, that's why in the init method, we can just copy some of the needed wrapper arguments:

from django import forms
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper

from .widgets import DataAttributesSelect
from .models import MyChoiceModel


class MyModelAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyModelAdminForm, self).__init__(*args, **kwargs)

        data = {'data-foo': dict(MyModelChoice.objects.values_list('id', 'foo'))}
        data['data-foo'][''] = ''  # empty option

        self.fields['myselectfield'].widget = RelatedFieldWidgetWrapper(
            DataAttributesSelect(
                choices=self.fields['myselectfield'].choices,
                data=data
            ),
            self.fields['myselectfield'].widget.rel,
            self.fields['myselectfield'].widget.admin_site,
            self.fields['myselectfield'].widget.can_add_related,
            self.fields['myselectfield'].widget.can_change_related,
            self.fields['myselectfield'].widget.can_delete_related,
        )

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured