How to handle error messages when validating a form across different FieldLists of FormFields?

Viewed 72

I have a form that contains (among other fields) 2 FieldLists. Each FieldList is a list of FormFields. In each FieldList, the forms all represent projects. One fieldlist is for projects having budget taken away, and the other list is for projects that will be the recipients of that budget. This is a dynamic form; there can be an arbitrary number of deallocations or allocations. There will be a lot of validations in the overall form, but a simple case that I cannot get working is making sure no project appears twice in the form (that is to say that across both lists, project IDs must be unique). I can test for this and recognize when this situation occurs, but I am having trouble associating error messages with the offending project IDs.

A simplified version of my form classes is:

class AllocationForm(Form):
    project_id(StringField, label='Project ID')
    ... details about amount being allocated here

class DeallocationForm(Form):
    project_id(StringField, label='Project ID')
    ...details about amount being deallocated here

class RequestForm(FlaskForm):

    def validate(self):

        project_id_seen = {}
        project_list = [a for a in self.allocations] + [d for d in self.deallocations]

        for formfield in project_list:
            if project_id_seen.get(formfield.data.get('project_id')) is None:
                project_id_seen[formfield.data.get('project_id')] = 1
            else:
                # *** Here's where I'm having an issue ***
                somethingsomethingsomething['project_id'] = 'A project ID cannot appear more than once in a request'
                return False

        # other form validations happen here
        if not FlaskForm.validate(self):
            return False

    allocations = FieldList(FormField(AllocationForm))
    deallocations = FieldList(FormField(DeallocationForm))

As you can see, I'm overriding RequestForm's validate method. Where I return False in the else clause, I'd like to associate an error message with the project ID in question so that I can display it when the form is re-rendered, but I'm having difficulty accessing the specific project_id field in the subform. Any ideas?

EDIT: It occurs to me that another approach might be to put validators on the project ID fields in the AllocationForm and DeallocationForm classes instead of RequestForm. Is this a more feasible approach? Associating the error message with the field would be easier, but how would the validator in AllocationForm access the project IDs in DeallocationForm and vice versa?

1 Answers

Your approach of using validate makes sense to me and I think you are close to reaching your goal.
In order to be able to assign the error message to a field, you can temporarily store the necessary fields and then add an entry to the tuple of error messages of the respective field for as long as it is necessary.

from collections import defaultdict

class AllocationForm(Form):
    project_id = StringField('Project ID')
    # ...

class DeallocationForm(Form):
    project_id = StringField('Project ID')
    # ...

class RequestForm(FlaskForm):
    allocations = FieldList(FormField(AllocationForm))
    deallocations = FieldList(FormField(DeallocationForm))

    def validate(self):
        data_fields = defaultdict(list)
        for field in (self.allocations, self.deallocations):
            for form in field:
                if form.project_id.data:
                    data_fields[form.project_id.data].append(form.project_id)

        success = True
        for data,fields in data_fields.items():
            if len(fields) > 1:
                for field in fields:
                    msg = 'A project ID cannot appear more than once in a request'
                    field.errors += (msg,)
                success = False

        return success and super().validate()
Related