Django Forms: работа с формами в Python

Оглавление

Зачем использовать Django Forms?

Работа с HTML-формами в веб-приложении может оказаться непростой задачей: в идеале необходимо иметь стандартный способ отображения полей ввода и обработки вводимых данных. Формы Django предоставляют вам фреймворк, который позволяет сделать именно это. Django предоставляет несколько стандартных способов отображения форм с вводимыми данными различных типов, осуществляет валидацию полей и решает некоторые вопросы безопасности, которые обычно приходится решать самостоятельно. Используя формы Django, вы можете не изобретать велосипед и получить рекомендации, которые помогут вам избежать написания большого количества повторяющегося кода или создания собственного фреймворка. В этой статье мы рассмотрим, как начать работу с Django Forms, и, надеюсь, дадим вам представление о том, как использовать их для решения ваших задач.

Простой пример

Для демонстрации предположим, что мы создаем веб-приложение для отслеживания автотранспорта и хотим создать форму для ввода марки/модели/года выпуска автомобиля. В этом примере мы должны определить форму, отобразить ее и затем обработать введенные данные. Начнем с определения формы. Рекомендуется хранить формы отдельно от представлений, поэтому в файле под названием forms.py:

from django import forms

class VehicleForm(forms.Form):
    make = forms.CharField()
    model = forms.CharField()
    year = forms.IntegerField()

Здесь задается форма, содержащая три поля: Make автомобиля, Model автомобиля и Year автомобиля. Поля make и model предполагают ввод текста. Поле year ожидает ввода целого числа. Для вывода формы на поверхность пользователю в Django имеется удобный класс FormView:

from django.urls import reverse_lazy
from django.views.generic.edit import FormView

from .forms import VehicleForm


class VehicleView(FormView):
    form_class = VehicleForm
    template_name = 'vehicle.html'
    success_url = reverse_lazy('success')

В представлении указано, что форма, которую мы будем использовать для рендеринга и обработки данных, будет VehicleForm - шаблон для рендеринга формы будет vehicle.html, и после успешной отправки формы он будет перенаправлять на представление с именем success. Для рендеринга формы приведем простой шаблон, который просто рендерит форму с кнопкой submit, которая будет отправлять форму в наш VehicleView:

<!DOCTYPE html>
<html>
  <head>
    <title>Vehicle</title>
  </head>
  <body>
    <form method="post">
      {% csrf_token %}
      {{ form.as_ul }}
      <button type="submit">Submit</button>
    </form>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <title>Vehicle</title>
  </head>
  <body>
    <form method="post">
      {% csrf_token %}
      {{ form.as_ul }}
      <button type="submit">Submit</button>
    </form>
  </body>
</html>

Шаблон определяет HTML-форму с кнопкой отправки и использует наш класс VehicleForm для отображения полей формы в виде HTML (в данном случае мы указали, что хотим отобразить форму в виде неупорядоченного списка). Кроме того, шаблон использует встроенный в Django шаблонный тег csrf_token для отображения CSRF-токена как части формы. Защита CSRF встроена в формы Django, и если вы опустите эту строку, то при попытке отправить форму вы получите ошибку. Это отличная функция безопасности, которую вы получаете практически бесплатно. Наконец, если мы определим представление успеха и отправим нашу форму, то увидим, что нас перенаправят на представление успеха.

Настройка форм

Предложенное выше решение - отличное начало. При небольшом количестве кода оно дает начало очень простому решению, которое является одновременно функциональным и безопасным. Если вы откроете браузер и перейдете на страницу, соответствующую виду формы, то увидите страницу, не имеющую стилей и отображающую несколько базовых HTML-полей ввода, которые не ограничивают вводимые значения валидными. Хотя форма обрабатывает некоторую проверку в представлении, она допускает недопустимые значения для нашего случая. Оставив в стороне разговор о стилистике, который в действительности отделен от рендеринга форм в Django, давайте посмотрим, как мы можем настроить нашу форму, чтобы сделать ее более надежной.

Пользовательский рендеринг

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

Виджеты

Поле в форме Django определяет, как должны проверяться и обрабатываться данные. Соответствующий виджет определяет, какие HTML-элементы используются для отображения этого поля. Чтобы проиллюстрировать, как можно использовать виджеты, продолжим наш пример выше. Поле year в форме отображается как элемент ввода HTML number, но на самом деле мы хотим позволить пользователю указывать только допустимые года. Одним из способов обеспечить это является выпадающий список, содержащий набор возможных лет. Для этого мы можем использовать встроенный в Django виджет Select:

from django import forms

class VehicleForm(forms.Form):
    make = forms.CharField()
    model = forms.CharField()
    year = forms.IntegerField(
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

В приведенном фрагменте виджет, используемый IntegerField, заменен на виджет Select, который использует элемент HTML select для вывода списка опций. Виджет принимает список кортежей для указания допустимых вариантов, поэтому мы передаем список чисел от самого раннего года, который может представлять VIN, до текущего года. После этих изменений можно увидеть, что поле year теперь отображает список вариантов.

Templates

Когда вы хотите изменить внешний вид существующих виджетов или настроить отображение конкретной формы, вы всегда можете полностью отменить поведение по умолчанию. Для этого можно либо переопределить стандартные шаблоны полей форм Django, либо вручную создать HTML-форму в шаблоне страницы, обойдя механизмы рендеринга формы по умолчанию. Однако в этой заметке я не буду рассматривать шаблоны Django, поскольку они представляют собой несколько иную тему.

Валидация и манипулирование данными

До сих пор в нашем примере мы показывали, как можно настроить поле, чтобы ограничить доступные варианты допустимыми значениями. Это не помешает кому-то отправить на вашу конечную точку недействительные данные, поэтому все же важно выполнять проверку данных в форме. Django рекомендует выполнять валидацию в нескольких местах, но на уровне формы можно использовать валидаторы или включить валидацию в методы clean: clean или clean_<field_name>.

Валидаторы

Валидаторы - это функции, принимающие значение, вызывающие исключение ValueError при неудачной проверке и None при успешной. Продолжая наш пример, мы можем добавить валидацию в поле year, чтобы предотвратить ввод неправильных годов.

from django import forms

def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
    make = forms.CharField()
    model = forms.CharField()
    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

Теперь, когда форма VehicleForm проверяет данные, она выполнит нашу функцию validate_year, чтобы определить, что у нас есть правильный год.

Clean Методы

Для дальнейшей кастомизации форм Django будет вызывать методы clean, если они определены на вашей форме. Для дополнительной проверки полей формы можно реализовать метод clean_<field_name>, который будет использоваться для "очистки" данных. В качестве примера рассмотрим поля "марка" и "модель". Допустим, мы хотим поддерживать определенное количество марок и моделей автомобилей и хотим убедиться в том, что данные, выводимые на форму, имеют формат заглавных букв. Мы можем обеспечить это с помощью методов clean:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
    make = forms.CharField()
    model = forms.CharField()
    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

    def clean_make(self):
        make = self.cleaned_data['make']
        if make.lower() not in {'chevrolet', 'ford'}:
            raise forms.ValidationError('Unrecognized Make')
        return title(make)

    def clean_model(self):
        model = self.cleaned_data['model']
        if model.lower() not in {'el camino', 'mustang'}:
            raise forms.ValidationError('Unrecognized Model')
        return title(model)

Методы clean_make и clean_model следят за тем, чтобы обрабатываемые значения в форме были в заглавном регистре (первая буква каждого слова прописная), а также выполняют некоторую валидацию, убеждаясь, что они имеют правильные значения. Теперь давайте рассмотрим конкретный пример автомобиля - El Camino. Это автомобиль, модели которого существовали только до 1987 года. В данном случае у нас есть логика проверки, которой требуется информация из нескольких полей. Мы можем использовать метод clean для выполнения такого рода проверки, например, так:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')

class VehicleForm(forms.Form):
    make = forms.CharField()
    model = forms.CharField()
    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

    def clean_make(self):
        make = self.cleaned_data['make']
        if make.lower() not in {'chevrolet', 'ford'}:
            raise forms.ValidationError('Unrecognized Make')
        return title(make)

    def clean_model(self):
        model = self.cleaned_data['model']
        if model.lower() not in {'el camino', 'mustang'}:
            raise forms.ValidationError('Unrecognized Model')
        return title(model)

    def clean(self):
        cleaned_data = super().clean()
        make = cleaned_data.get('make')
        model = cleaned_data.get('model')
        year = cleaned_data.get('year')

        if model and model.lower() == 'el camino':
            if make and make.lower() != 'chevrolet':
                raise forms.ValidationError('Make & Model do not match!')
            if year and year > 1987:
                raise forms.ValidationError(
                    'This Make & Model was not produced in provided year'
                )

В нашем методе clean мы можем подтвердить, что если мы указываем El Camino, то остальные значения должны совпадать, чтобы не принимать недействительный ввод. Если вы указываете El Camino, то лучше, чтобы это был Chevrolet, выпущенный в период с 1981 по 1987 год.

Поля пользовательской формы

Если у вас есть поле с пользовательской логикой на форме, которое вы хотите использовать снова и снова, вы можете создать пользовательское поле формы. Поля формы являются достаточно гибкой и более сложной темой, поэтому в рамках данной статьи рассмотрим простой пример замены некоторой пользовательской логики на пользовательское поле формы, чтобы уменьшить количество дублирующегося кода. В нашем предыдущем примере методы clean_make и clean_model очень похожи, поэтому давайте посмотрим, можно ли уменьшить дублирование кода, создав пользовательское поле формы:

from django import forms
from django.template.defaultfilters import title

def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')

class TitleChoiceField(forms.CharField):
    def __init__(self, *args, **kwargs):
        self.choices = kwargs.pop('choices', None)
        super(TitleChoiceField, self).__init__(*args, **kwargs)

    def clean(self, value):
        if value.lower() not in self.choices:
            raise forms.ValidationError('Invalid value. Must be one of {}'.format(self.choices))
        return title(value)

class VehicleForm(forms.Form):
    make = TitleChoiceField(choices={'chevrolet', 'ford'})
    model = TitleChoiceField(choices={'el camino', 'mustang'})
    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

    def clean(self):
        cleaned_data = super().clean()
        make = cleaned_data.get('make')
        model =cleaned_data.get('model')
        year = cleaned_data.get('year')

        if model and model.lower() == 'el camino':
            if make and make.lower() != 'chevrolet':
                raise forms.ValidationError('Make & Model do not match!')
            if year and year > 1987:
                raise forms.ValidationError('This Make & Model was not produced in provided year')

Поля "марка" и "модель" имеют схожие функциональные возможности. Определив пользовательское поле формы, выполняющее аналогичную логику, мы можем сократить количество повторяющегося кода и стандартизировать работу с полями определенного типа в нашем приложении. В конечном итоге, при необходимости, можно также указать собственный виджет для дальнейшей настройки отображения поля.

Использование ModelForm

Обычно в Django, если вы создаете форму, вы хотите каким-либо образом сохранить данные, отправленные в эту форму. Во многих случаях форма будет содержать поля, которые непосредственно соответствуют одной из ваших моделей Django ORM. Это достаточно распространенный паттерн, поэтому для него существует специальный тип формы, называемый ModelForm. Использование ModelForm упрощает определение форм, основанных на ваших моделях и обладающих дополнительной возможностью сохранения данных с использованием указанной модели. Чтобы проиллюстрировать, как это может выглядеть, давайте рассмотрим наш предыдущий пример с новым подходом и начнем с определения некоторых моделей Django ORM:

from django.db import models

class Make(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

class Model(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Vehicle(models.Model):
    make = models.ForeignKey(
        'Make', on_delete=models.CASCADE, related_name='vehicles'
    )
    model = models.ForeignKey(
        'Model', on_delete=models.CASCADE, related_name='vehicles'
    )
    year = models.IntegerField(db_index=True)

Такая модель сохранения автомобиля позволяет нам заранее определить марки и модели, которые мы хотим поддерживать. Пример намеренно упрощен, но можно создать отношения, которые помогут более точно определить ограничения. Например, можно добавить связь между моделями Make и Model, чтобы ограничить определенные модели определенной маркой. Предполагая такое простое отношение, мы получаем возможную результирующую форму для соответствия нашему предыдущему примеру:

from django import forms
from django.template.defaultfilters import title

from .models import Vehicle

def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')


class VehicleForm(forms.ModelForm):
    class Meta:
        model = Vehicle
        fields = ('make', 'model', 'year')

    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

Как видите, определение формы стало несколько лаконичнее. Вы можете просто указать, какие поля следует включить в форму из модели, и, если вас устраивают значения по умолчанию, она просто работает. Для полей, где требуется дополнительная настройка, например, для поля "Год" в данном примере, можно указать поле, чтобы отменить объявление по умолчанию. Если взять наш пример и изменить VehicleView так, чтобы оно наследовалось от CreateView Django, то при отправке формы будет вызываться метод сохранения VehicleForm, который теперь будет автоматически сохраняться.

Настройка ModelForm

А ModelForm - это просто форма, поэтому все настройки, которые применяются к обычной форме, применяются и к ModelForm. Кроме того, можно настроить процесс сохранения данных из формы в базу данных. Чтобы настроить этот процесс, можно переопределить метод save. В качестве примера предположим, что мы хотим автоматически заполнять марку автомобиля на основе выбранной модели автомобиля, чтобы избежать ситуации, когда кто-то может указать несовпадающие марки и модели:

from django import forms
from django.template.defaultfilters import title

from vehicles.models import Vehicle


def validate_year(year):
    if year < 1981 or year > 2019:
        raise forms.ValidationError('Not a valid year for a VIN')


class VehicleForm(forms.ModelForm):
    class Meta:
        model = Vehicle
        fields = ('model', 'year')

    year = forms.IntegerField(
        validators=[validate_year],
        widget=forms.Select(choices=[(v, v) for v in range(1981, 2020)])
    )

    _model_make_map = {
        'El Camino': 'Chevrolet',
        'Mustang': 'Ford',
    }

    def save(self, commit=True):
        instance = super(VehicleForm, self).save(commit=False)
        make_name = self._model_make_map[instance.model.name]
        make = Make.objects.get(name=make_name)
        instance.make = make
        if commit:
            instance.save()
        return instance

При выборе конкретной модели в данном примере соответствующая марка автомобиля выбирается на основе имеющегося у нас отображения. Убрана возможность указывать марку автомобиля отдельно от модели. При выборе модели марка определяется и заполняется автоматически.

Заключение

Формы Django обладают значительной функциональностью, которая может помочь вам очень быстро начать работу - особенно если вы генерируете их из своих моделей с помощью ModelForm. Если вам нужна пользовательская логика в форме, то существует множество мест, где вы можете настроить поведение, включая добавление собственных полей формы и виджетов для стандартизации поведения в вашем приложении. Формы, несомненно, являются очень мощным инструментом, который Django предоставляет для работы с HTML-формами стандартизированным и безопасным способом!

Вернуться на верх