Валидация динамических форм - Django

У меня есть страница с 3 формами, формы по сути одинаковы в плане полей, но имеют разную валидацию, я хотел спросить, есть ли способ вместо нескольких форм иметь одну форму с динамической валидацией, а затем передать ключевое слово форме для использования одной валидации или другой.

Вот мой 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

Все три формы имеют одинаковое поле statement_file, но с разной валидацией, а третья форма имеет второе поле option.

В настоящее время это 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()

Я бы хотел сделать это чище, имея одну форму с динамической валидацией, и возможно опциональное поле option для третьей формы, есть предложения, как это сделать?

Я не уверен, что вы найдете это полезным, поскольку я не полностью понял ваши требования. Но я могу поделиться простым шаблоном, который я недавно использовал.


Мы можем использовать "контекст" в наших представлениях. Из документации:

словарь, отображающий имена переменных на значения переменных... передается в Template.render() для рендеринга шаблона.

Src: Django: Шаблоны > Контекст

Это похоже на то, что вы ищете ("передать ключевое слово в форму").


Задача: Мне нужна была форма, поддерживаемая в одном HTML-шаблоне. В зависимости от того, добавляет или редактирует пользователь запись в модели, я хотел настроить сообщение в форме.

В моем случае я передаю простую строку для 'add' и 'edit'. Затем они доступны для использования в шаблонах HTML, где вы можете использовать их как часть логики. У меня есть 2x пути URL и 2x функции View, но оба эти представления указывают на один HTML-шаблон для формы. Основываясь на значении контекста, я применяю различную логику внутри HTML-шаблона.

В urls.py (заявка):

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')
]

В 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)

В html шаблоне для 'my_form':

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

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

Вариант 1

Моим первым предложением было бы делегировать инициализацию формы фабрике. Что-то вроде следующего

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

Вместо всех этих операторов if, вы можете просто использовать

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

Тогда, для самих форм, я предлагаю вам использовать простое наследование с чем-то вроде BaseFileTypeForm. Каждый дочерний класс может иметь собственный метод clean_statement и дополнительные поля, если вы хотите.


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

Даже если это почти столько же строк кода, как и ваш текущий код, я нахожу его более чистым.

Вариант 2

Другим вариантом может быть одна форма, но с большим количеством операторов if для обработки различной логики. Этот вариант более лаконичен, но может стать очень запутанным, если в будущем потребуется добавить больше логики для каждого типа файлов

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

Вместо того, чтобы иметь 3 отдельных класса Form, создайте один класс Form и используйте метод clean() для проверки типов файлов на стороне бэкенда. Добавьте ValidationError, если типы файлов совпадают с другими полями файла.

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

Во фронтенде используйте скрипт для проверки типа файла перед отправкой формы или при изменении файла в поле. что-то вроде следующего (поместите скрипт в файл шаблона или js файл и загрузите его в файл шаблона):

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")
        }
    }
}

во views обрабатывает эти 3 файла, когда форма действительна.

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()
Вернуться на верх