Dynamic Form Validation - Django

Viewed 147

I have a page with 3 forms, the forms are essentially the same in terms of fields but have different validation, I was wondering if instead of having multiple forms is there any way of having one form with dynamic validation, and then passing a keyword to the form to use one validation or another.

Here is my forms.py:

class FileTypeOneForm(forms.Form):
    statement = forms.FileField(label='')

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']

        # Check statement_file format and raise ValidationError in case of invalid format
        # ...

        return statement_file


class FileTypeTwoForm(forms.Form):
    statement = forms.FileField(label='')

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']

        # Check statement_file format and raise ValidationError in case of invalid format ** required format is different than FileTypeOneForm and FileTypeThreeForm required format **
        # ...

        return statement_file


class FileTypeThreeForm(forms.Form):
    Options = [
        ('', 'Select'),
        ('one', 'One'),
        ('two', 'Two')
      ]
    option = forms.ChoiceField(label='', choices=Options)
    statement = forms.FileField(label='')

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']

        # Check statement_file format and raise ValidationError in case of invalid format ** required format is different than FileTypeOneForm and FileTypeTwoForm required format **
        # ...

        return statement_file

All three forms have the same field statement_file, but with different validation, and the third form has a second field option.

Currently there is views.py:

    form_id = request.POST.get('form_id')
    
    if form_id == 'one':
      form_one = FileTypeOneForm(request.POST, request.FILES)
      # ... Process form data ...

      # Restart all the other forms
      form_two, form_three = FileTypeTwoForm(), FileTypeThreeForm()

    elif form_id == 'two':
      form_two = FileTypeTwoForm(request.POST, request.FILES)
      # ... Process form data ...

      # Restart all the other forms
      form_one, form_three = FileTypeOneForm(), FileTypeThreeForm()

    elif form_id == 'three':
      form_three = FileTypeThreeForm(request.POST, request.FILES)
      # ... Process form data ...

      # Restart all the other forms
      form_one, form_two = FileTypeOneForm(), FileTypeTwoForm()

I'd want to make this cleaner by having a single form with dynamic validation, and maybe an optional field option for the third form, any suggestions on how to do this?

2 Answers

[edit] an option 3 has been added

Option 1

My first suggestion would be to delegate the Form initialization to a factory. Something like the following

def FileTypeFormFactory(form_id: str, *args, *kwargs):
    forms = {
        'one': FileTypeOneForm,
        'two': FileTypeOneForm,
        'three': FileTypeOneForm,
    }
    return forms[form_id](*args, **kwargs)

Instead of having all those if statements, you then could just use

form_id = request.POST.get('form_id')
form = FileTypeFormFactory(form_id, request.POST, request.FILES)

Then, for the Forms themselves, I suggest you just use simple inheritance with a something like a BaseFileTypeForm. Each child class can have a custom clean_statement method, and extra fields if you want.


class BaseFileTypeForm(fors.Form):
    statement = forms.FileField(label='')

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']
        # maybe some default behavior here
        return statement_file

class FileTypeOneForm(BaseFileTypeForm):
    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']
        # something specific to type one
        return statement_file

class FileTypeOneForm(BaseFileTypeForm):
    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']
        # something specific to type two
        return statement_file

class FileTypeOneForm(BaseFileTypeForm):
    Options = [
        ('', 'Select'),
        ('one', 'One'),
        ('two', 'Two')
      ]

    option = forms.ChoiceField(label='', choices=Options)

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']
        # something specific to type three 
        return statement_file

Even if that's almost as many lines of code as your current code, it makes the logic clear for each type of form.

Option 2

Another option would be to have a single form but with many if statements to handle the different logic. This one is more concise but can get very messy if more logic is to be added to each file type in the future

class FileTypeForm(BaseFileTypeForm):
    Options = [
        ('', 'Select'),
        ('one', 'One'),
        ('two', 'Two')
      ]

    option = forms.ChoiceField(label='', choices=Options, disabled=True)
    statement = forms.FileField(label='')

    def __init__(self, form_id, *args, **kwargs):
        self.form_id = form_id
        if form_id == 'three':
            self.fields['option'].disabled = False
            

    def clean_statement(self):
        statement_file = self.cleaned_data['statement_file']
        if self.form_id == 'one':
            # something specific to type one
            ...
        elif self.form_id == 'two':
            # something specific to type two
            ...
        elif self.form_id == 'three':
            # something specific to type three
            ...
            
        return statement_file

option 3: using different validators

It's an improvement of Option 2. The idea is to use a single form but different validators depending on the file type.

def file_type_one_validator(value):
    if some_condition_is_not_met:
        raise ValidationError("error for type one")

def file_type_two_validator(value):
    # something specific to type two
    ....

def file_type_three_validator(value):
    # something specific to type three
    ....


class FileTypeForm(BaseFileTypeForm):
    Options = [
        ('', 'Select'),
        ('one', 'One'),
        ('two', 'Two')
      ]

    option = forms.ChoiceField(label='', choices=Options, disabled=True)
    statement = forms.FileField(label='')

    def __init__(self, form_id, *args, **kwargs):
        self.form_id = form_id
        if form_id == 'one':
            self.fields['statement'].validators.append(file_type_one_validator)
        if form_id == 'two':
            self.fields['statement'].validators.append(file_type_two_validator)
        if form_id == 'three':
            self.fields['option'].disabled = False
            self.fields['statement'].validators.append(file_type_three_validator)
            

Instead of having 3 separate Form classes, create one Form class with and use the clean() method to check the file types in backend side. add ValidationError if the file types match to other file fields.

class FileForm(forms.Form):
    Options = [
        ("", "Select"),
        ("one", "One"),
        ("two", "Two"),
    ]
    statement_file_1 = forms.FileField(label="File 1")
    statement_file_2 = forms.FileField(label="File 2")
    option = forms.ChoiceField(label="Options", choices=Options)
    statement_file_3 = forms.FileField(label="File 3")

    def clean(self):
        cleaned_data = super(FileForm, self).clean()
        statement_file_1 = cleaned_data.get('statement_file_1')
        statement_file_2 = cleaned_data.get('statement_file_2')
        statement_file_3 = cleaned_data.get('statement_file_3')

        if statement_file_1.content_type == statement_file_2.content_type:
            self.add_error(
                    "statement_file_1", forms.ValidationError(f"same file type({statement_file_1.content_type}). choose different file")
                )
            self.add_error(
                    "statement_file_2", forms.ValidationError(f"same file type({statement_file_2.content_type}). choose different file")
                )

        if statement_file_2.content_type == statement_file_3.content_type:
            self.add_error(
                    "statement_file_2", forms.ValidationError(f"same file type({statement_file_2.content_type}). choose different file")
                )
            self.add_error(
                    "statement_file_3", forms.ValidationError(f"same file type({statement_file_3.content_type}). choose different file")
                )

        if statement_file_1.content_type == statement_file_3.content_type:
            self.add_error(
                    "statement_file_1", forms.ValidationError(f"same file type({statement_file_1.content_type}). choose different file")
                )
            self.add_error(
                    "statement_file_3", forms.ValidationError(f"same file type({statement_file_3.content_type}). choose different file")
                )

        return cleaned_data

In frontend make use of script to check the file types before submitting the form or on changing the file in the field. something like below (place the script in template file or a js file and load it in template file):

f1 = document.getElementById('id_statement_file_1') // id is auto generated by django using the name in FileForm class
f2 = document.getElementById('id_statement_file_2')
f3 = document.getElementById('id_statement_file_3')

f1.onchange = function() { chooseFile() }
f2.onchange = function() { chooseFile() }
f3.onchange = function() { chooseFile() }

function chooseFile() {     
    if (f1.files.length > 0 && f2.files.length > 0) {
        if (f1.files[0].type == f2.files[0].type) {
            alert("statement_file_1 and statement_file_2 are of same file type")
        }
    }

    if (f2.files.length > 0 && f3.files.length > 0) {
        if (f2.files[0].type == f3.files[0].type) {
            alert("statement_file_2 and statement_file_3 are of same file type")
        }
    }

    if (f1.files.length > 0 && f3.files.length > 0) {
        if (f1.files[0].type == f3.files[0].type) {
            alert("statement_file_1 and statement_file_3 are of same file type")
        }
    }
}

in views process those 3 files when the form is valid.

def view_name(request):
    if request.method == "POST":
        form = FileForm(request.POST, request.FILES)
        if form.is_valid():
            print(form.cleaned_data)
            # file processing code
            # 3 files have to processed here
        else:
            print(form.errors)
    else:
        form = FileForm()
Related