Dinamic Form Validation - Django

I have a page with 3 forms, the forms are esentially 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 dinamic validation, and then pass 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 these 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 dinamic validation, and maybe an optional field option for the third form, any suggestions on how to do this?

I'm not sure if you will find this helpful as I didn't fully understand your requirements. But I can share a simple pattern I used recently.


We can use a 'context' in our views. From the documentation:

a dictionary mapping variable names to variable values... passed to Template.render() for rendering a template.

Src: Django: Templates > Context

This sounds like what you're looking for ("pass a keyword to the form").


Objective: I wanted a form maintained in a single HTML template. Depending on whether the user was adding or editing an entry into the model, I wanted to adjust the messaging in the form.

In my case, I pass a simple string for 'add' and 'edit'. These are then available to use in the HTML templates where you can use them as part of logic. I have 2x URL paths and 2x View functions but both of those views are pointing to a single HTML template for the form. Based on the context value, I apply different logic inside the HTML template.

In urls.py (application):

urlpatterns=[
    path('add_to_form/', views.add_to_form, name='add_to_form'),
    path('edit_to_form/', views.edit_to_form, name='edit_to_form')
]

In views.py:

def add_to_form(request):
     [...]
     context={'someModelForm': someModelForm, 'custom_flag': 'add'}
     return render(request, 'my_form.html', context)


def edit_to_form(request):
     [...]
     context={'someModelForm': someModelForm, 'custom_flag': 'edit'}
     return render(request, 'my_form.html', context)

In the html template for 'my_form':

{% if 'add' in custom_flag %}
     [Do whatever]
{% endif %}

{% if 'edit' in custom_flag %}
     [Do something else]
{% endif %}

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, I find it to be cleaner.

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 __ini__(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

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()
Back to Top