Reputation: 32949
i have been really pulling my hair out to get this done. I have 2 integer fields in one of my models as follows:
#models.py
class TestModel(models.Model):
"""
This is a test model.
"""
max = models.IntegerField()
min = models.IntegerField()
As the above two values are inter-related i wanted them to use a custom widget. Now here is the point, I want these fields to use a single custom widget (MultiWidget) but i also want to keep the values in separate columns in the database [Don't really want to parse stuff and moreover the search functionality will use the above fields separately so just want to store them separately].
A custom widget would only accept value from a single model field so i decided to create a custom form field to use in the ModelForm for the above model.
Now this is how my form looks.
class MinMaxField(forms.MultiValueField):
def __init__(self, *args, **kwargs):
all_fields = (
forms.CharField(),
forms.CharField(),
)
super(MinMaxField, self).__init__(all_fields, *args, **kwargs)
def compress(self, values):
if values:
return '|'.join(values)
return ''
class TestModelForm(forms.ModelForm):
minmax = MinMaxField(widget=widgets.MinMaxWidget(),
required=False)
class Meta:
model = models.TestModel
fields = ('min', 'max', 'minmax')
widgets = {
'min' : forms.HiddenInput(),
'max' : forms.HiddenInput(),
}
def full_clean(self, *args, **kwargs):
if 'minmax_0' in self.data:
newdata = self.data.copy()
newdata['min'] = self.data['minmax_0']
newdata['max'] = self.data['minmax_1']
self.data = newdata
super(TestModelForm, self).full_clean(*args, **kwargs)
The above form basically hides the model fields and shows only the minmax
field that uses a custom widget as follows:
class MinMaxWidget(widgets.MultiWidget):
def __init__(self, attrs=None):
mwidgets = (widgets.Select(attrs=attrs, choices=MIN),
widgets.Select(attrs=attrs, choices=MAX))
super(MinMaxWidget, self).__init__(mwidgets, attrs)
def value_from_datadict(self, data, files, name):
try:
values = [widget.value_from_datadict(data, files, name + '_%s' % i)\
for i, widget in enumerate(self.widgets)]
value = [int(values[0]), int(values[1])]
except ValueError:
raise ValueError('Value %s for %s did not validate. \
a list or a tuple expected' % (value, self.widget))
return value
def decompress(self, value):
if value:
if isinstance(value, list):
return value
return [value[0], value[1]]
return [None, None]
def format_output(self, rendered_widgets):
rendered_widgets.insert(0, '<span class="multi_selects">')
rendered_widgets.insert(-1, '<label id="min">Min</label>')
rendered_widgets.append('<label id="max">Max</label></span>')
return u''.join(rendered_widgets)
Now at this point everything works fine, the values get saved separately into their respective fields. But the problem arises when i am trying to edit the form with an instance of a TestModel
object. In the case of edit, when the form is rendered the actual values are stored in the hidden min
and max
input fields and the minmax field does not have a value. [ofcourse as it is a form field.]
What are the options i have to
Either pre-populate the value of the minmax
form field from the min
and max
fields. [Note: Without Using Javascript or jQuery]
Or to create a MultiWidget that uses the values from two different model fields.
Any help would be appreciated. Thanks.
Upvotes: 0
Views: 911
Reputation: 184
I've currently faced similiar problem and there is a way to use widget for two fields. I'm using version 1.4 and haven't tried in any other.
In your view, you probably have something like this:
form = TestModelForm(instance=instance)
where instance
is object from your database you want to edit.
Let's see what Django is doing when it creates your form
# django/forms/models.py
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
empty_permitted=False, instance=None):
...
if instance is None:
...
else:
self.instance = instance
object_data = model_to_dict(instance, opts.fields, opts.exclude)
# if initial was provided, it should override the values from instance
if initial is not None:
object_data.update(initial)
...
super(BaseModelForm, self).__init__(data, files, auto_id, prefix,
object_data, error_class,
label_suffix, empty_permitted)
then, when form is rendered, BoundField
class uses this object_data
to assign initial value to the widget.
Therefore all you need to do is provide initial
in your form constructor.
class TestModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
initial = dictionary with initial values
initial.update(kwargs['initial'] or {})
...
super(TestModelForm, self).__init__(initial=initial, *args, **kwargs)
Also remember to add your field to your forms fields
list.
That way you don't have two unused hidden widgets, no need to use javascript and imo, code is more explanatory.
Upvotes: 1