Override ChoiceField widget in Django ModelForm without re-specifying choices - django

I am trying to override the default widget for a Django ChoiceField while preserving the choices generated in the model. Here is the relevant part of my model:
class UserGroup(models.Model):
icon = models.CharField(max_length=100, choices=<function call>)
And of my form:
class UserGroupForm(forms.ModelForm):
icon = models.ChoiceField(widget=IconPicker)
class Meta:
model = UserGroup
fields = [ 'icon', ]
Overriding the ChoiceField's widget like this clobbers the form.fields['icon'].choices attribute normally inherited from the model and sets it to [] because Django. If I remove the icon field definition from the form, the choices are preserved - but of course the widget defaults to a Select.
(The function which generates the choices for the model field is not accessible from the form code unfortunately.)
The best I have come up with so far for is to change the icon form field definition to
icon = ChoiceField(choices=UserGroup._meta.get_field_by_name('icon')[0].choices,
widget=IconPicker)
but this is clunky and I would rather have the choices automatically passed on as in the introspected ChoiceField behavior. (I tried subclassing ChoiceField to IconChoiceField which was identical but for a default widget of IconPicker but Django converts it back to a TypedChoiceField with the default Select widget because of this issue.)
Is there a way to override the ChoiceField's widget attribute while preserving the behavior of inheriting choices from the model?

I think the reason you are losing the choices you specified in your model i.e. "sets it to []" is not "because Django", but because you override the icon field with the line icon = models.ChoiceField(widget=IconPicker)
Please note that if you are using a modelform then it is not necessary to override the widget at init, instead the widget should be specified in the Meta class in the widgets dictionary.
class UserGroupForm(forms.ModelForm):
class Meta:
model = UserGroup
fields = [ 'icon', ]
widgets = {
'icon': IconPicker
}
As for overriding the choices you can simply do self.fields['icon'].choices = UserGroupForm.ICON_CHOICES, but I don't think you need to override choices in this instance.

Figured it out. Just needed a bit of self-reference in UserGroupForm's __init__:
def __init__(self, *args, **kwargs):
super(UserGroupForm, self).__init__(*args, **kwargs)
self.fields['icon'].widget = IconPicker(choices=self.fields['icon'].choices)

Related

Django widget that depends on model

I am pretty new to django and have a question. I got a ModelForm using Widgets. Since I have a field called discount which I only want to be editable if the displayed model fullfills some requirements, I make it read-only using a widget entry:
class Meta:
widgets = {'discount': forms.TextInput(attrs={'readonly': True})}
Now I want to make it possible to write to this field again, iff the Model (here called Order) has its field type set to integer value 0.
I tried to do so in the html template but failed.
So my next idea is to make the widget somehow dependent to the model it displays, so in kinda pseudocode:
class Meta:
widgets = {'discount': forms.TextInput(attrs={'readonly': currentModel.type == 0})}
Is there a proper way to do something like this?
Thanks in advance
You can overwrite __init__ of your model form class to modify the widget:
class MyModelForm(...):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs):
instance = kwargs['instance']
self.fields['discount'].widget.attrs['readonly'] = instance.type == 0
Might be worth noting that setting the widget to readonly does not prevent a malicious user to modify the field anyways. Make sure to properly validate on the server side.
Unless you define a model instance on the form creation, you have a generic model definition, not a model instance.
Looks like you may want to change the form behavior after user interaction, to do that you have to use a javascript.

Displaying only one field on form in combobox when editing ForeignKey

I am a django newbie so please forgive me the basic question.
I have a class in my model that has a ForeignKey. I am using django.views.generic.UpdateView for editing the fields. For the ForeignKey a combobox is displayed which is exactly what I want but all the fields of the referenced table appear in the combobox. I want to display only 2 in the combobox, for example: "field1 [field2]".
How can I control this behaviour?
Thanks,
V.
This is because:
By default ForeignKey (which is a model field) will default to ModelChoiceField (form field).
By default ModelChoiceField's queryset is all model objects.
If you want to change this behavior and give only few selected model objects for user to choose in your ModelChoiceField, you will have to override this field with queryset on your own. For example:
class YourForm(forms.ModelForm):
class Meta:
model = YourModel
your_foreignkey_field = forms.ModelChoiceField(queryset=YourOtherModel.objects.filter(some_criteria))
Then you can specify form_class = YourForm in UpdateView.
All I had to do is to define the __str__ function for my model class.
class Systm(ParentModel):
...
def __str__(self):
return "%s [%s]" % (self.name, self.type)

Django admin: don't send all options for a field?

One of my Django admin "edit object" pages started loading very slowly because of a ForeignKey on another object there that has a lot of instances. Is there a way I could tell Django to render the field, but not send any options, because I'm going to pull them via AJAX based on a choice in another SelectBox?
You can set the queryset of that ModelChoiceField to empty in your ModelForm.
class MyAdminForm(forms.ModelForm):
def __init__(self):
self.fields['MY_MODEL_CHOIE_FIELD'].queryset = RelatedModel.objects.empty()
class Meta:
model = MyModel
fields = [...]
I think you can try raw_id_fields
By default, Django’s admin uses a select-box interface () for fields that are ForeignKey. Sometimes you don’t want to incur the overhead of having to select all the related instances to display in the drop-down.
raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField
Or you need to create a custom admin form
MY_CHOICES = (
('', '---------'),
)
class MyAdminForm(forms.ModelForm):
my_field = forms.ChoiceField(choices=MY_CHOICES)
class Meta:
model = MyModel
fields = [...]
class MyAdmin(admin.ModelAdmin):
form = MyAdminForm
Neither of the other answers worked for me, so I read Django's internals and tried on my own:
class EmptySelectWidget(Select):
"""
A class that behaves like Select from django.forms.widgets, but doesn't
display any options other than the empty and selected ones. The remaining
ones can be pulled via AJAX in order to perform chaining and save
bandwidth and time on page generation.
To use it, specify the widget as described here in "Overriding the
default fields":
https://docs.djangoproject.com/en/1.9/topics/forms/modelforms/
This class is related to the following StackOverflow problem:
> One of my Django admin "edit object" pages started loading very slowly
> because of a ForeignKey on another object there that has a lot of
> instances. Is there a way I could tell Django to render the field, but
> not send any options, because I'm going to pull them via AJAX based on
> a choice in another SelectBox?
Source: http://stackoverflow.com/q/37327422/1091116
"""
def render_options(self, *args, **kwargs):
# copy the choices so that we don't risk affecting validation by
# references (I hadn't checked if this works without this trick)
choices_copy = self.choices
self.choices = [('', '---------'), ]
ret = super(EmptySelectWidget, self).render_options(*args, **kwargs)
self.choices = choices_copy
return ret

Django Contrib Comments: How to override the comment's textarea widget?

Using django.contrib.comments, I've defined a custom comment app. I wanted to override the text area widget so that the textbox appears smaller.
So what I created was this:
#forms.py
class CustomCommentForm(CommentForm):
#...otherstuff...
comment = forms.CharField(label=_('Comment'),
widget=forms.Textarea(attrs={'rows':4}),
max_length=COMMENT_MAX_LENGTH)
But really I don't want to have to redefine the comment field. I want to just redefine the widget that's used by the field. Ie something that it seems only ModelForms can do:
class Meta:
widgets = {
'comment': Textarea(attrs={'rows': 4}),
}
Is there a way to redefine the widget without redefining the field? Or should I just set the height using CSS?
You are correct that you can only use the widgets option for a model form's Meta class.
However you don't need to redefine the entire comment field. Instead, override the form's __init__ method and change the field's widget there.
class CustomCommentForm(CommentForm):
#...otherstuff...
def __init__(self, *args, **kwargs):
super(CustomCommentForm, self).__init__(*args, **kwargs)
self.fields['comment'].widget = forms.Textarea(attrs={'rows':4})

Python/Django BooleanField model with RadioSelect form default to empty

I'm using a Django ModelForm where my model contains a BooleanField and the form widget associated with that BooleanField is a RadioSelect widget. I'd like the the RadioSelect widget that renders to have no options selected so the user has to explicitly make a choice, but the form validation to fail if they make no selection. Is there a way to do this?
models.py
myboolean = models.BooleanField(choices=YES_NO)
forms.py
class myModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(myModelForm, self).__init__(*args, **kwargs)
self.fields['myboolean'].widget = forms.RadioSelect(choices=YES_NO)
Your code actually does what you need. It renders the radio buttons with no options selected and generate the error message if nothing is selected.
A small note about your form code. I would do it like this:
class myModelForm(forms.ModelForm):
myboolean = forms.BooleanField(widget=forms.RadioSelect(choices=YES_NO))
class Meta:
model = MyModel
Unfortunately, this is less of a Django issue than an HTML question. The HTML specification (RFC1866) says:
At all times, exactly one of the radio buttons in a set is checked. If none of the <INPUT> elements of a set of radio buttons specifies `CHECKED', then the user agent must check the first radio button of the set initially.
However, browsers have historically ignored this and implemented radio buttons in different ways.
HTML also makes this difficult because the "checked" attribute of the <INPUT> tag doesn't take a parameter, so you can't use a customized Django widget that sets this attribute to False or No.
A possible workaround is to put in a little Javascript that runs as part of the document's onLoad event that finds all the radio buttons on the page and sets the 'checked' attribute to false (using JQuery, for example).
see this:
http://docs.djangoproject.com/en/dev/topics/forms/modelforms/#a-full-example
I creates custom field with default widget.
Cut of my models.py:
class Order(models.Model):
...
# transport = models.CharField(choices=transport.choices,max_length=25,null=True)
transport = SelectField(choices=transport.choices,max_length=25,null=True)
...
Field definition:
from django.db import models
from django.forms import widgets
class SelectField(models.CharField):
def formfield(self, **kwargs):
if self._choices:
defaults = {'widget': widgets.RadioSelect}
defaults.update(kwargs)
return super(SelectField, self).formfield(**defaults)