Интеграция с WTForms

Валидаторы

Все встроенные валидаторы, кроме FileRequired можно использовать с WTForms, но импортировать их нужно из модуля flask_uploader.contrib.wtf:

# Form description module, for example - forms.py

from flask_wtf import FlaskForm
from flask_wtf.file import FileField
from flask_uploader.contrib.wtf import (
    Extension,
    FileSize,
    ImageSize,
)
from wtforms.validators import DataRequired


class SongForm(FlaskForm):
    file = FileField(validators=[
        DataRequired(),
        Extension(
            {'mp3', 'flac'},
            message='This is not an audio file.',
        ),
        FileSize('50m'),
    ])
    cover = FileField(validators=[
        Extension(Extension.IMAGES),
        FileSize('2m'),
        ImageSize(
            min_width=250, min_height=250,
            message=ImageSize.SIZE_GREATER_EQUAL,
        ),
    ])

Модуль flask_wtf.file включает часто используемые валидаторы, но я не рекомендую их использовать по объективным причинам. Рассмотрим каждый валидатор в отдельности:

  • FileRequired

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

    Вы можете использовать валидатор DataRequired, он прекрасно справляется с задачей редактирования.

  • FileSize

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

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

  • FileAllowed

    Можно использовать этот валидатор, если он вам привычнее, либо мой Extension.

Новый валидатор

Ранее я приводил пример пользовательского валидатора. Его необходимо задекорировать декоратором wrap_validator(), чтобы он работал с WTForms:

from flask_wtf import FlaskForm
from flask_wtf.file import FileField
from flask_uploader.contrib.wtf import Extension, wrap_validator


IsJSON = is_json = wrap_validator(IsJSON)


class PackageForm(FlaskForm):
    file = FileField(validators=[
        Extension({'json'}),
        IsJSON(),
    ])

Поле загрузчика

Flask-Uploader реализует элемент формы UploadField. Конструктор принимает обязательный именованный аргумент uploader - экземпляр загрузчика, который используется для сохранения файла и установки значения атрибута модели.

Рассмотрим пример с формой редактирования профиля пользователя:

from flask_wtf import FlaskForm
from flask_uploader import Uploader
from flask_uploader.contrib.wtf import (
    Extension,
    UploadField,
)
from flask_uploader.storages import FileSystemStorage
from wtforms.fields import StringField
from wtforms.validators import Length


class ProfileForm(FlaskForm):
    firstname = StringField(validators=[Length(min=1)])
    lastname = StringField(validators=[Length(min=1)])
    avatar = UploadField(
        uploader=Uploader(
            'avatars', FileSystemStorage(dest='avatars')
        ),
        overwrite=True,
        validators=[
            Extension(Extension.IMAGES),
        ]
    )

Мы создали загрузчик с именем avatars, который будет сохранять загруженную пользователем аватарку в файловой системе. После сохранения файла, атрибуту avatar будет присвоен поисковый идентификатор, который можно использовать в аргументе методов load() или remove():

form = ProfileForm()

if form.validate_on_submit():
    print(type(form.avatar.data).__name__)  # FileStorage
    form.avatar.save()
    print(type(form.avatar.data).__name__)  # str

Вызывать метод save() вручную не обязательно, в большинстве случаев вы используете метод populate_obj() для присвоения значений полей формы атрибутам модели, в этот момент для всех полей UploadField будет вызван метод save():

from flask import Blueprint
from flask_login import current_user, login_required


bp = Blueprint('auth', __name__)


@bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    user = current_user
    form = ProfileForm(obj=user)

    if form.validate_on_submit():
        form.populate_obj(user)
        # Saving a user
        return redirect(request.url)

    return render_template('profile.html', form=form)

Конструктору UploadField можно передать и другие именованные аргументы:

  • overwrite - перезаписать существующий файл, по-умолчанию False;

  • return_url - после сохранения присвоить URL-адрес файла, по-умолчанию False;

  • external - создать абсолютный URL, по-умолчанию False.