Allow disabling choices in a <select>

2,310 views
Skip to first unread message

Jody McIntyre

unread,
Jun 3, 2011, 12:50:12 PM6/3/11
to django-d...@googlegroups.com
We need to be able to disable choices in a <select>, which is done by setting the disabled attribute on the <option> tag, for example:
<option value="bananas" disabled="disabled">Bananas</option>

 Currently we're doing this by subclassing the Select widget: http://djangosnippets.org/snippets/2453/

It would be nice if the built in Select widget supported this.  One way would be to replace the existing render_option method with what I've written, and I can prepare a patch if desired, but this approach changes the format of the "choices" data structure to something that won't be understood by other widgets.  Perhaps these widgets should be improved too, but I don't want to do this unless my changes have a good chance of being accepted.

I logged this as a ticket (16149) and was told to discuss it here.  To address aagustin's comments:

1. Backwards compatibility is already addressed.  If the widget is passed a regular "choices" field, the existing behavior is preserved.

2. I don't understand what you mean by "boilerplate" in the API.  A "choices" field with a disabled choice looks like:

choices = (('apples', 'Apples'),
           ('oranges', 'Oranges'),
           ('bananas', {'label': 'Bananas',
                        'disabled': True}),
           ('grobblefruit', 'Grobblefruit'))

I can't think of a more concise way of clearly representing that 'bananas' is disabled while still allowing it to have a label, unless we want to change the "choices" data structure a lot more to something like:

choices = (('apples', 'Apples'),
           ('oranges', 'Oranges'),
           ('bananas', 'Bananas',  {'disabled': True}),
           ('grobblefruit', 'Grobblefruit'))

Suggestions & other thoughts welcome :)
Jody

Calvin Spealman

unread,
Jun 3, 2011, 1:09:33 PM6/3/11
to django-d...@googlegroups.com
On Fri, Jun 3, 2011 at 12:50 PM, Jody McIntyre <jo...@trustcentric.com> wrote:
> We need to be able to disable choices in a <select>, which is done by
> setting the disabled attribute on the <option> tag, for example:
> <option value="bananas" disabled="disabled">Bananas</option>
>
>  Currently we're doing this by subclassing the Select widget:
> http://djangosnippets.org/snippets/2453/
>
> It would be nice if the built in Select widget supported this.  One way
> would be to replace the existing render_option method with what I've
> written, and I can prepare a patch if desired, but this approach changes the
> format of the "choices" data structure to something that won't be understood
> by other widgets.  Perhaps these widgets should be improved too, but I don't
> want to do this unless my changes have a good chance of being accepted.
>
> I logged this as a ticket (16149) and was told to discuss it here.  To
> address aagustin's comments:
>
> 1. Backwards compatibility is already addressed.  If the widget is passed a
> regular "choices" field, the existing behavior is preserved.

Sadly, not true. Any code inspecting the choices is going to break,
because there is a lot of code expecting it can unpack the choices
out of 2-tuples.

This must be preserved. Perhaps a Choice type could exist, a lot like
url() in our urls.py files, where we can use a tuple or a special type
to additional parameters.

choices = (('apples', 'Apples'),
('oranges', 'Oranges'),

Choice('bananas', 'Bananas', disabled=True),


('grobblefruit', 'Grobblefruit'))

> 2. I don't understand what you mean by "boilerplate" in the API.  A
> "choices" field with a disabled choice looks like:
>
> choices = (('apples', 'Apples'),
>            ('oranges', 'Oranges'),
>            ('bananas', {'label': 'Bananas',
>                         'disabled': True}),
>            ('grobblefruit', 'Grobblefruit'))
>
> I can't think of a more concise way of clearly representing that 'bananas'
> is disabled while still allowing it to have a label, unless we want to
> change the "choices" data structure a lot more to something like:
>
> choices = (('apples', 'Apples'),
>            ('oranges', 'Oranges'),
>            ('bananas', 'Bananas',  {'disabled': True}),
>            ('grobblefruit', 'Grobblefruit'))
>
> Suggestions & other thoughts welcome :)
> Jody
>

> --
> You received this message because you are subscribed to the Google Groups
> "Django developers" group.
> To post to this group, send email to django-d...@googlegroups.com.
> To unsubscribe from this group, send email to
> django-develop...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/django-developers?hl=en.
>

--
Read my blog! I depend on your acceptance of my opinion! I am interesting!
http://techblog.ironfroggy.com/
Follow me if you're into that sort of thing: http://www.twitter.com/ironfroggy

Jody McIntyre

unread,
Jun 3, 2011, 3:00:24 PM6/3/11
to django-d...@googlegroups.com
On Fri, Jun 3, 2011 at 1:09 PM, Calvin Spealman <ironf...@gmail.com> wrote:
> 1. Backwards compatibility is already addressed.  If the widget is passed a
> regular "choices" field, the existing behavior is preserved.

Sadly, not true. Any code inspecting the choices is going to break,
because there is a lot of code expecting it can unpack the choices
out of 2-tuples.

OK, you're right.  I was only considering widgets but there are lots of other things that use choices.

This must be preserved. Perhaps a Choice type could exist, a lot like
url() in our urls.py files, where we can use a tuple or a special type
to additional parameters.

choices = (('apples', 'Apples'),
          ('oranges', 'Oranges'),
          Choice('bananas', 'Bananas',  disabled=True),
          ('grobblefruit', 'Grobblefruit'))

url() in urls.py only works because it's an argument to patterns(), which understands how to deal with both tuples and RegexURLPattern objects returned by url().  I don't see how to apply this design to choices without changing everything that uses choices, which is what we're trying to avoid.

How else can we pass data in to the widget such that it is available to render_option()?  We could add a disabled_choices parameter to the widget's __init__ class, but then fields (such has ChoiceField) would need to handle the parameter in the same way they now handle choices.

Changing the choices data structure itself seems like the least bad alternative, but I'm worried about breaking things that use choices, as you mentioned.

Cheers,
Jody

Calvin Spealman

unread,
Jun 3, 2011, 3:25:33 PM6/3/11
to django-d...@googlegroups.com
On Fri, Jun 3, 2011 at 3:00 PM, Jody McIntyre <jo...@trustcentric.com> wrote:
> On Fri, Jun 3, 2011 at 1:09 PM, Calvin Spealman <ironf...@gmail.com>
> wrote:
>>
>> > 1. Backwards compatibility is already addressed.  If the widget is
>> > passed a
>> > regular "choices" field, the existing behavior is preserved.
>>
>> Sadly, not true. Any code inspecting the choices is going to break,
>> because there is a lot of code expecting it can unpack the choices
>> out of 2-tuples.
>
> OK, you're right.  I was only considering widgets but there are lots of
> other things that use choices.
>
>> This must be preserved. Perhaps a Choice type could exist, a lot like
>> url() in our urls.py files, where we can use a tuple or a special type
>> to additional parameters.
>>
>> choices = (('apples', 'Apples'),
>>           ('oranges', 'Oranges'),
>>           Choice('bananas', 'Bananas',  disabled=True),
>>           ('grobblefruit', 'Grobblefruit'))
>
> url() in urls.py only works because it's an argument to patterns(), which
> understands how to deal with both tuples and RegexURLPattern objects
> returned by url().  I don't see how to apply this design to choices without
> changing everything that uses choices, which is what we're trying to avoid.

The idea was that Choice would implement __iter__ and __getitem__ such
that it would still unpack as a 2-tuple, so current code just thinks
that is what it is.

> How else can we pass data in to the widget such that it is available to
> render_option()?  We could add a disabled_choices parameter to the widget's
> __init__ class, but then fields (such has ChoiceField) would need to handle
> the parameter in the same way they now handle choices.
>
> Changing the choices data structure itself seems like the least bad
> alternative, but I'm worried about breaking things that use choices, as you
> mentioned.
>
> Cheers,
> Jody
>

Chris Beaven

unread,
Jun 6, 2011, 7:49:06 PM6/6/11
to django-d...@googlegroups.com


On Saturday, June 4, 2011 4:50:12 AM UTC+12, Jody McIntyre wrote:
I can't think of a more concise way of clearly representing that 'bananas' is disabled while still allowing it to have a label [...]

I'd say it would be more backwards compatible, and still reasonably concise to just have a separate attribute:

disabled_choices = ('bananas',)

Jody McIntyre

unread,
Jun 7, 2011, 12:21:01 PM6/7/11
to django-d...@googlegroups.com
On Mon, Jun 6, 2011 at 7:49 PM, Chris Beaven <smile...@gmail.com> wrote:

I'd say it would be more backwards compatible, and still reasonably concise to just have a separate attribute:

disabled_choices = ('bananas',)

OK.  I was trying to avoid adding attributes to the widget, but it's definitely backwards compatible.

I've attached a patch to ticket 16149:
https://code.djangoproject.com/attachment/ticket/16149/select_with_disabled.diff

Do people generally agree with this approach?  I will continue work if so.  Still todo: adding similar functionality to other widgets where it makes sense and adding tests.  I could also add disabled_choices support to forms.Choicefield, similarly to the way choices is handled now.  Let me know if you think this is useful.

Thanks,
Jody

Mateusz Harasymczuk

unread,
Jun 8, 2011, 6:28:50 AM6/8/11
to django-d...@googlegroups.com
Indeed it is useful, I was using a custom made app to do so.
Thank you

--
Matt Harasymczuk

Jody McIntyre

unread,
Jun 10, 2011, 10:10:29 AM6/10/11
to django-d...@googlegroups.com
Can a core developer please advise on the preferred design here?

The main ideas are:

1. Add a 'disabled_choices' attribute to the widget that takes an
iterable of choices to disable. I've attached a WIP patch to ticket
16149 following this approach. Optionally this could be passed to the
widget by forms.ChoiceField similarly to the way choices is handled
now.

A concern was raised in the ticket (16149) that this is too specific,
and we should also be able to pass arbitrary HTML attributes like
class, style, and id. I don't understand the use case for passing
these things to an <option>, so I don't think this is worthwhile, but
it's something to consider.

2. Extend the "choices" data structure, as suggested by Calvin
Spealman in the discussion.

Thanks,
Jody

jackotonye

unread,
Apr 30, 2018, 7:33:19 PM4/30/18
to Django developers (Contributions to Django itself)
This might be a late answer but this is a simplified version that can be modified using the form instance.

You can either pass a list of values to be disabled i.e

def __init__(self, disabled_choices, *args, **kwargs):
   
self.disabled_choices = disabled_choices

OR
   
from django.forms import Select
   
   
class SelectWidget(Select):
   
"""
    Subclass of Django's select widget that allows disabling options.
    """

   
def __init__(self, *args, **kwargs):
       
self._disabled_choices = []
       
super(SelectWidget, self).__init__(*args, **kwargs)
   
   
@property
   
def disabled_choices(self):
       
return self._disabled_choices
   
   
@disabled_choices.setter
   
def disabled_choices(self, other):
       
self._disabled_choices = other
   
   
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        option_dict
= super(SelectWidget, self).create_option(
            name
, value, label, selected, index, subindex=subindex, attrs=attrs
       
)
       
if value in self.disabled_choices:
            option_dict
['attrs']['disabled'] = 'disabled'
       
return option_dict


To disabled an option based on a condition i.e user isn't a superuser.
   
class MyForm(forms.Form):
    status
= forms.ChoiceField(required=True, widget=SelectWidget, choices=Status.choices())
   
   
def __init__(self, request, *args, **kwargs):
       
if not request.user.is_superuser:
           
self.fields['status'].widget.disabled_choices = [1, 4]
       
super().__init__(*args, **kwargs)

Sebastian Van Den Noortgaete

unread,
Sep 27, 2020, 3:28:17 PM9/27/20
to Django developers (Contributions to Django itself)
@tony thanks this realy helped me!
Reply all
Reply to author
Forward
0 new messages