Amyth
Amyth

Reputation: 32949

is it possible for a multi widget to use two different model fields

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

  1. Either pre-populate the value of the minmax form field from the min and max fields. [Note: Without Using Javascript or jQuery]

  2. Or to create a MultiWidget that uses the values from two different model fields.

Any help would be appreciated. Thanks.

Upvotes: 0

Views: 911

Answers (1)

warran
warran

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

Related Questions