John Peters
John Peters

Reputation: 1179

How to render validation errors in a MultiValueField / MultiWidget per field

In a form I am using a MultiValueField (MVF) with a MultiWidget that has several fields. If there is a validation error in one of the fields of the MVF, this gets handled (displayed) at the MVF level, rather than at the individual sub-fields, which can lead to:

* Ensure this value is greater than or equal to 1.
* Ensure this value is greater than or equal to -100.0.

Number of days: -1
...
...
Threshold: -200

Where the first error refers to the first field of the MVF and the second error the the last field of the MVF.

Is it possible to put those error messages "inside" the MVF at the field where they belong? (maybe in the format_output method of the MultiWidget?)

Upvotes: 3

Views: 791

Answers (1)

Maxime R.
Maxime R.

Reputation: 9991

The following solution doesn't use a MultiValueField but instead:

  • dynamically replaces the original field with several ones on form's __init__
  • reconstruct valid data for the original field during the form validation on _post_clean

Here is some test code that needs to be adapted to each case:

class MyMultiField(CharField):

    def split(self, form):
        name = 'test'
        form.fields_backup[name] = form.fields[name]
        del form.fields[name]
        # here is where you define your individual fields:
        for i in range(3):
            form.fields[name + '_' + str(i)] = CharField()
            # you need to extract the initial data for these fields
            form.initial[name + '_' + str(i)] = somefunction(form.initial[name])
        form.fields['test_1'] = DecimalField() # because I only want numbers in the 2nd field

    def restore(self, form):
        # here is where you describe how to joins the individual fields:
        value = ''.join([unicode(v) for k, v in form.cleaned_data.items() if 'test_' in k])
        # extra step to validate the combined value against the original field:
        try:
            restored_data = form.cleaned_data.copy()
            restored_data["test"] = form.fields_backup["test"].clean(value)
            for k in form.cleaned_data:
                if k.startswith("test_"):
                    del restored_data[k]
            form.cleaned_data = restored_data
        except Exception, e:
            form._errors[NON_FIELD_ERRORS] = form.error_class(e)


class MyForm(Form):

    test = MyMultiField()

    def __init__(self, *args, **kwargs):
        super(ItemForm, self).__init__(*args, **kwargs)
        self.fields_backup = {}
        self.fields['data'].split(self)

    def _post_clean(self):
        self.fields_backup['data'].restore(self)
        return super(MyForm, self)._post_clean()

Before:

original form field

After (validating some input):

multifield

I'm not sure if it's possible to decouple this Field/Form code further using this approach. I'm also not quite satisfied with this code as the new field class needs to inherit from the original one.

Nonetheless, the basic idea is there and I successfully used it to individually validate form fields built from a dictionary stored in a single model field with PostgreSQL hstore.

Upvotes: 1

Related Questions