"Disabled" option for choiceField - Django - django

I having trouble with a simple question :
How to have some "disabled" field in a dropdown menu generated via a modelForm and choiceFied in the django Framework ?
At the moment, I cannot figure out how to obtain such an output :
-- Root 1 entry -- (disabled)
-- Elt 1 -- (not disabled)
-- Root 2 entry -- (disabled)
Do you have any advice ?
Pierre

Django's form widgets offer a way to pass a list of attributes that should be rendered on the <option> tag:
my_choices = ( ('one', 'One'), ('two', 'Two'))
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=my_choices,
widget=forms.Select(attrs={'disabled':'disabled'}))
Unfortunately, this won't work for you because the attribute will be applied to EVERY option tag that is rendered. Django has no way to automatically know which should be enabled and which should be disabled.
In your case, I recommend writing a custom widget. It's pretty easy to do, and you don't have that much custom logic to apply. The docs on this are here. In short though:
subclass forms.Select, which is the default select renderer
in your subclass, implement the render(self, name, value, attrs) method. Use your custom logic to determine if the value qualifies as needing to be disabled. Have a look at the very short implementation of render in django/forms/widgets.py if you need inspriation.
Then, define your form field to use your custom widget:
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=my_choices,
widget=MyWidget)

You can create Choices as mentioned by Bryan like this. In the below options Root 1, Root 2 are automatically disabled and they will look like Group Options
CHOICES = (
('-- Root 1--',
(
('ELT1', 'ELT1'),
('ELT2', 'ELT2'),
('ELT3', 'ELT3'),
)
),
('-- Root 2--',
(
('ELT3', 'ELT3'),
('ELT4', 'ELT4'),
)
),
)
The above option will display like this. In the below image, Root 1 and Root 2 are not selectable.
Hope this will clear your problem
-Vikram

field_choices = (
('','Make choice'),
(1,'first'),
(2,'second'),
(3,'third')
)
from django.forms import Select
class Select(Select):
def create_option(self, *args,**kwargs):
option = super().create_option(*args,**kwargs)
if not option.get('value'):
option['attrs']['disabled'] = 'disabled'
if option.get('value') == 2:
option['attrs']['disabled'] = 'disabled'
return option

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=(('on', 'On'), ('off', 'Off')))
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
if not request.user.is_superuser:
self.fields['status'].widget.disabled_choices = ['off']

It seems django 1.1 will allow "optgroup": Django documentation
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=[
('Audio', (
('vinyl', 'Vinyl'),
('cd', 'CD'),
)
),
('Video', (
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)
),
('unknown', 'Unknown'),
])
This imho is a must have.

Simple questions sometimes have complex answers in Django. I spent a lot of time getting this to work well. Combining Jarrett's overview with an important note from jnns about render_option and some help from #django on freenode I have a well-working complete example solution:
First, this example assumes a normally defined choices-type CharField in a model I call Rule. I subclass my own TimeStampedModel but you can use a models.Model:
class Rule(TimeStampedModel):
...
# Rule Type
SHORT_TERM_RULE = 'ST'
MAX_SIGHTINGS_PER_PERIOD_RULE = "MA"
WHITE_LIST_RULE = "WL"
BLACK_LIST_RULE = "BL"
RULE_CHOICES = (
(SHORT_TERM_RULE, 'Short Term Rule'),
(MAX_SIGHTINGS_PER_PERIOD_RULE, 'Max Sightings Per Period Rule'),
(WHITE_LIST_RULE, 'White List Rule'),
(BLACK_LIST_RULE, 'Black List Rule'),
)
rule_type = models.CharField(
max_length=2,
choices=RULE_CHOICES,
default=SHORT_TERM_RULE,
)
...
In forms.py, define the widget subclassing Select that accepts disabled_choices. It has a custom render_option() that adds disabled to the html output of option tags if their choice is included on the passed in disabled_choices list. Note, I left most of the render_option() code from Select as-is:
class MySelect(Select):
def __init__(self, attrs=None, choices=(), disabled_choices=()):
super(MySelect, self).__init__(attrs, choices=choices)
self.disabled_choices = disabled_choices
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
if not self.allow_multiple_selected:
selected_choices.remove(option_value)
else:
selected_html = ''
for key, value in self.disabled_choices:
if option_value in key:
return format_html('<option disabled value="{}"{}>{}</option>', option_value, selected_html,
force_text(option_label))
return format_html('<option value="{}"{}>{}</option>', option_value, selected_html, force_text(option_label))
Then, in defining the form subclassing ModelForm, check for a passed-in disabled_choices list and initialize the field accordingly. In this example, I also sneak in a default choice.
class RuleChoiceForm(ModelForm):
class Meta:
model = Rule
fields = ['rule_type']
# Add a default choice to the list defined in the Rule model
default_choice = ('DF', 'Choose a Rule Type...')
choices = list(Rule.RULE_CHOICES)
choices.insert(0, default_choice)
choices = tuple(choices)
rule_type = forms.ChoiceField(widget=MySelect(attrs={'class': 'form-control'}, disabled_choices=[]),
choices=choices)
def __init__(self, *args, disabled_choices=None, **kwargs):
super(RuleChoiceForm, self).__init__(*args, **kwargs)
if disabled_choices:
self.fields['rule_type'].widget.disabled_choices = disabled_choices
Then in your view, define disabled_choices as a list, appending choices from your _CHOICES var in your model and pass it into the form instantiation. In my logic, I use list comprehension of RULE_CHOICES to get the tuple of the choice I want to disable. Though there may be a simpler way, please feel free to post suggestions to simplify or improve this answer.
disabled_choices = []
# Logic disabling choices
if Rule.objects.filter(route=route, rule_type=Rule.SHORT_TERM_RULE).exists():
disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.SHORT_TERM_RULE in item][0])
disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.MAX_SIGHTINGS_PER_PERIOD_RULE in item][0])
rule_choice_form = RuleChoiceForm(disabled_choices=disabled_choices)

Are you trying to create a menu in which the list items are separated into categories, and you don't want the categories themselves to be selectable?
If so, you can achieve this by having your template render the field using tags, e.g.
<select name="my_field" id="id_my_field">
<optgroup label="-- Root 1 entry --">
<option value="1">Elt 1</option>
<option value="2">Elt 2</option>
<option value="3">Elt 3</option>
</optgroup>
<optgroup label="--- Root 2 entry ---">
<option value="4">Elt 4</option>
<option value="5">Elt 5</option>
</optgroup>
</select>

Related

Setting Django admin default action

I would like to change the default selected action named "---------" (BLANK_CHOICE_DASH) to another specific action. Is there a better way to implement this than adding some javascript code that would override the action in load time?
1.Override the get_action_choices() method in your ModelAdmin, clear the default blank choice and reorder the list。
class YourModelAdmin(ModelAdmin):
def get_action_choices(self, request):
choices = super(YourModelAdmin, self).get_action_choices(request)
# choices is a list, just change it.
# the first is the BLANK_CHOICE_DASH
choices.pop(0)
# do something to change the list order
# the first one in list will be default option
choices.reverse()
return choices
2.Specific action.Override ModelAdmin.changelist_view, use extra_context to update action_form
ChoiceField.initial used to set the default selected choice.
so if your action name is "print_it", you can do this.
class YourModelAdmin(ModelAdmin):
def changelist_view(self,request, **kwargs):
choices = self.get_action_choices(request)
choices.pop(0) # clear default_choices
action_form = self.action_form(auto_id=None)
action_form.fields['action'].choices = choices
action_form.fields['action'].initial = 'print_it'
extra_context = {'action_form': action_form}
return super(DocumentAdmin, self).changelist_view(request, extra_context)
I think you can override the get_action_choices() method in the ModelAdmin.
class MyModelAdmin(admin.ModelAdmin):
def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
"""
Return a list of choices for use in a form object. Each choice is a
tuple (name, description).
"""
choices = [] + default_choices
for func, name, description in six.itervalues(self.get_actions(request)):
choice = (name, description % model_format_dict(self.opts))
choices.append(choice)
return choices
in your admin.py file
class MyModelAdmin(admin.ModelAdmin):
def get_action_choices(self, request, **kwargs):
choices = super(MyModelAdmin, self).get_action_choices(request)
# choices is a list, just change it.
# the first is the BLANK_CHOICE_DASH
choices.pop(0)
# do something to change the list order
# the first one in list will be default option
choices.reverse()
return choices
and in your class
class TestCaseAdmin(MyModelAdmin):

How can we add dynamic html5 data attribute in django admin form

I just want to ask how we can add additional dynamic html5 data attribute in each fiels in django admin forms.
For example
<option value ="1" data-desc="desc1">hello1</option>
<option value ="2" data-desc="desc2">hello2</option>
Thanks,
Kel
You need to subclass Django's ModelChoiceField and modify it's render_options() and render_option() methods to display the object's attributes you need. I guess you also need your own subclass of ModelChoiceIterator so that it not only spits out id/label tuples but all the data you need.
Example
I just found an implementation of a custom SelectWidget in OpenStack's dashboard:
class SelectWidget(widgets.Select):
"""
Customizable select widget, that allows to render
data-xxx attributes from choices.
.. attribute:: data_attrs
Specifies object properties to serialize as
data-xxx attribute. If passed ('id', ),
this will be rendered as:
<option data-id="123">option_value</option>
where 123 is the value of choice_value.id
.. attribute:: transform
A callable used to render the display value
from the option object.
"""
def __init__(self, attrs=None, choices=(), data_attrs=(), transform=None):
self.data_attrs = data_attrs
self.transform = transform
super(SelectWidget, self).__init__(attrs, choices)
def render_option(self, selected_choices, option_value, option_label):
option_value = force_unicode(option_value)
other_html = (option_value in selected_choices) and \
u' selected="selected"' or ''
if not isinstance(option_label, (basestring, Promise)):
for data_attr in self.data_attrs:
data_value = html.conditional_escape(
force_unicode(getattr(option_label,
data_attr, "")))
other_html += ' data-%s="%s"' % (data_attr, data_value)
if self.transform:
option_label = self.transform(option_label)
return u'<option value="%s"%s>%s</option>' % (
html.escape(option_value), other_html,
html.conditional_escape(force_unicode(option_label)))
EDIT
As long as you provide choices like this, it should work:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'dummy':
widget = SelectWidget(data_attrs=('bar',))
choices = [(foo.id, foo) for foo in Foo.objects.all()]
form_field = forms.ChoiceField(
choices=choices, widget=widget)
return form_field
return super().formfield_for_foreignkey(db_field, request, **kwargs)
You can use the get_form() method to tweak the form that is used by a ModelAdmin.
That way you can change the widgets of the fields in question:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(MyModelAdmin, self).get_form(request, obj, **kwargs)
form.fields['your_field'].widget.attrs.update({'data-hello': 'world'})
return form

Django: Set field choices from view?

I've got some <selects> that I need to populate with some choices that depend on the currently logged in user. I don't think this is possible (or easy) to do from inside the form class, so can I just leave the choices blank and set them in the view instead? Or what approach should I take?
Not sure if this is the best answer, but in the past I have set the choices of a choice field in the init of the form - you could potentially pass your choices to the constructor of your form...
You could build your form dynamically in you view (well, actually i would rather keep the code outside the view in it's own function and just call it in the view but that's just details)
I did it like this in one project:
user_choices = [(1, 'something'), (2, 'something_else')]
fields['choice'] = forms.ChoiceField(
choices=user_choices,
widget=forms.RadioSelect,
)
MyForm = type('SelectableForm', (forms.BaseForm,), { 'base_fields': fields })
form = MyForm()
Obviously, you will want to create the user_choices depending on current user and add whatever field you need along with the choices, but this is a basic principle, I'll leave the rest as the reader exercise.
Considering that you have included the user as a parameter, I would solve this using a custom tag.
In your app/templatetags/custom_tags.py something like this:
#register.simple_tag
def combo(user, another_param):
objects = get_objects(user, another_param)
str = '<select name="example" id="id_example">'
for object in objects:
str += '<option value="%s">%s</option>' % (object.id, object.name)
str += '</select>'
return mark_safe(str)
Then in your template:
{% load custom_tags %}
{% special_select user another_param %}
More about custom tags http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
Django create dynamic forms - It works !!
Forms.py
class MyForm(forms.Form):
""" Initialize form values from views"""
select=forms.BooleanField(label='',required=False)
field_1=forms.CharField(label='',widget=forms.TextInput(attrs= \
{'size':'20','readonly':'readonly',}))
field_2=forms.ChoiceField(widget=forms.Select(), \
choices=((test.id,test.value) for test in test.objects.all()))
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
input=kwargs.get('initial',{})
get_field_1_initial_input_from_views=re.sub("\[|\]|u'|'","",str (input.values()))
# override field_2 choices based on field_1 input
try:
# filter choices
self.fields[‘field_2'].choices=((test.id,test.value) for test in test.objects.filter ( --------)))
except:
pass
Views.py
def views_function(request,val):
"""Dynamically generate input data for formset."""
Initial_data=[]
initial_data.append({'field_1':data.info})
#Initializing formset
MyFormSet=formset_factory(MyForm,extra=0)
formset=MyFormSet(initial=initial_data)
context={'formset':formset}
if request.method == 'POST':
formset=MyFormSet(request.POST,request.FILES)
if formset.is_valid():
# You can work with the formset dictionary elements in the views function (or) pass it to
#Forms.py script through an instance of MyForm
return HttpResponse(formset.cleaned_data)
return render_to_response(‘test.html', context,context_instance=RequestContext(request))

In Django form, custom SelectField and SelectMultipleField

I am using Django everyday now for three month and it is really great. Fast web application development.
I have still one thing that I cannot do exactly how I want to.
It is the SelectField and SelectMultiple Field.
I want to be able to put some args to an option of a Select.
I finally success with the optgroup :
class EquipmentField(forms.ModelChoiceField):
def __init__(self, queryset, **kwargs):
super(forms.ModelChoiceField, self).__init__(**kwargs)
self.queryset = queryset
self.to_field_name=None
group = None
list = []
self.choices = []
for equipment in queryset:
if not group:
group = equipment.type
if group != equipment.type:
self.choices.append((group.name, list))
group = equipment.type
list = []
else:
list.append((equipment.id, equipment.name))
But for another ModelForm, I have to change the background color of every option, using the color property of the model.
Do you know how I can do that ?
Thank you.
What you need to do, is to change the output which is controlled by the widget. Default is the select widget, so you can subclass it. It looks like this:
class Select(Widget):
def __init__(self, attrs=None, choices=()):
super(Select, self).__init__(attrs)
# choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
self.choices = list(choices)
def render(self, name, value, attrs=None, choices=()):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
return mark_safe(u'\n'.join(output))
def render_options(self, choices, selected_choices):
def render_option(option_value, option_label):
option_value = force_unicode(option_value)
selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
return u'<option value="%s"%s>%s</option>' % (
escape(option_value), selected_html,
conditional_escape(force_unicode(option_label)))
# Normalize to strings.
selected_choices = set([force_unicode(v) for v in selected_choices])
output = []
for option_value, option_label in chain(self.choices, choices):
if isinstance(option_label, (list, tuple)):
output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
for option in option_label:
output.append(render_option(*option))
output.append(u'</optgroup>')
else:
output.append(render_option(option_value, option_label))
return u'\n'.join(output)
It's a lot of code. But what you need to do, is to make your own widget with an altered render method. It's the render method that determines the html that is created. In this case, it's the render_options method you need to change. Here you could include some check to determine when to add a class, which you could style.
Another thing, in your code above it doesn't look like you append the last group choices. Also you might want to add an order_by() to the queryset, as you need it to be ordered by the type. You could do that in the init method, so you don't have to do it all over when you use the form field.
render_option has been removed from Django 1.11 onwards. This is what I did to achieve this. A little bit of digging and this seems straightforward and neat. Works with Django 2.0+
class CustomSelect(forms.Select):
def __init__(self, attrs=None, choices=()):
self.custom_attrs = {}
super().__init__(attrs, choices)
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)
# 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,
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
}
class MyModelChoiceField(ModelChoiceField):
# custom method to label the option field
def label_from_instance(self, obj):
# since the object is accessible here you can set the extra attributes
if hasattr(obj, 'type'):
self.widget.custom_attrs.update({obj.pk: {'type': obj.type}})
return obj.get_display_name()
The form:
class BookingForm(forms.ModelForm):
customer = MyModelChoiceField(required=True,
queryset=Customer.objects.filter(is_active=True).order_by('name'),
widget=CustomSelect(attrs={'class': 'chosen-select'}))
The output which I needed is as:
<select name="customer" class="chosen-select" required="" id="id_customer">
<option value="" selected="">---------</option>
<option value="242" type="CNT">AEC Transcolutions Private Limited</option>
<option value="243" type="CNT">BBC FREIGHT CARRIER</option>
<option value="244" type="CNT">Blue Dart Express Limited</option>
I run into this question many times when searching by
'how to customize/populate Django SelectField options'
The answer provided by Dimitris Kougioumtzis is quite easy
Hope it could help somebody like me.
# forms.py
from django.forms import ModelForm, ChoiceField
from .models import MyChoices
class ProjectForm(ModelForm):
choice = ChoiceField(choices=[
(choice.pk, choice) for choice in MyChoices.objects.all()])
# admin.py
class ProjectAdmin(BaseAdmin):
form = ProjectForm
....
You should not mess with form fields for adding some custom attributes to the rendered html tag. But you should subclass and add a these to the Widget.
From the docs: customizing-widget-instances
You can submit attrs dictionary to the form Widgets, that render as attributes on the output form widgets.
class CommentForm(forms.Form):
name = forms.CharField(
widget=forms.TextInput(attrs={'class':'special'}))
url = forms.URLField()
comment = forms.CharField(
widget=forms.TextInput(attrs={'size':'40'}))
Django will then include the extra attributes in the rendered output:
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr>
<tr><th>Url:</th><td><input type="text" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
http://code.djangoproject.com/browser/django/trunk/django/newforms/widgets.py?rev=7083
As seen under the class Select(Widget):, there is no way to add the style attribute to an option tag. To this, you will have to subclass this widget and add such functionality.
The class Select(Widget): definition only adds style attribute to the main select tag.

forms.SelectMultiple from models.CommaSeparatedIntegerField

I have a model with field:
class Movie(models.Model):
genre = models.CommaSeparatedIntegerField(max_length=100, choices=GENRE_CHOICES, blank=True, default=0)
lang = models.CommaSeparatedIntegerField(max_length=100, choices=LANG_CHOICES, blank=True, default=0)
And I need to get multiple select fields (not checkboxes) from that.
One way, that i found, is to redefine form from ModelAdmin
class MyMovieAdminForm(forms.ModelForm):
genre = forms.MultipleChoiceField(choices=GENRE_CHOICES)
lang = forms.MultipleChoiceField(choices=LANG_CHOICES)
class MovieAdmin(admin.ModelAdmin):
form = MyMovieAdminForm
admin.site.register(Movie, MovieAdmin)
But it need to redeclare 'label' and 'initial' for each field, that isn't good for DRY principle. And I doesn't understand, how can I set current value of object for initial value of each field?
And other way, that I found in manual is formfield-overrides. I use dev version from trunk and I try to use this code, but it didn't change my select fields to multiselect in admin interface:
class MovieAdmin(admin.ModelAdmin):
formfield_overrides = {
models.CommaSeparatedIntegerField: {'widget': forms.SelectMultiple},
}
May be anyone know, what is the best way to define multiple select fields? Thanks!
I don't find any working answer for making models.CommaSeparatedIntegerField as forms.SelectMultiple. So I changed models.CommaSeparatedIntegerField to models.ManyToManyField and form field becomes works very well! It is more suitable in cases, where you need to make queries on this field.
1) Remove choices from models.CommaSeparatedIntegerField in models
2) Create CommaSeparatedCharField as replacement
class CommaSeparatedCharField(forms.CharField):
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return u''
csv = smart_unicode(','.join(value) )
return csv
class CommaSeparatedSelectMultiple(forms.SelectMultiple):
def render(self, *args, **kwargs):
print args, kwargs
args_=list(args)
if type(args_[1])!=list:
args_[1] = args_[1].split(',')
return super(CommaSeparatedSelectMultiple, self).render(*args_, **kwargs)
3) Apply
class MyAdmin(admin.ModelAdmin):
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
for field_name, choices in [
('minutes', [(i,str(i)) for i in range(60)]),
]:
self.fields[field_name] = CommaSeparatedCharField(
label=self.fields[field_name].label,
initial=self.fields[field_name].initial,
widget=forms.CommaSeparatedSelectMultiple(choices=choices)
)
Work nice on 1.2 trunk
I use the following for somewhat-DRYer form manipulation, and it works with 1.0. It's verbose, but it works.
class MyMovieAdminForm(forms.ModelForm):
## Meta, etc
def __init__(self, *args, **kwargs):
super(MyMovieAdminForm, self).__init__(*args, **kwargs)
self.fields["genre"].widget = forms.SelectMultiple(choices=foo)
self.fields["genre"].initial = self.instance.genre
# Doesn't require redefining label, etc.