DevGang
Авторизоваться

Flask Admin: загрузка файлов и обработка формы в модели

Мне довольно часто требуется кастомизировать базовую модель Flask Admin для того чтобы например загружать изображения. В интернете есть довольно много примеров как сделать загрузку файлов через `flask_admin.form.upload`, но он не дает полного контроля над происходящим и если потребуется сделать что-то более сложное, придется импровизировать.

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

Начнем

Есть модель:

class StorageModel(db.Model):
    __tablename__ = 'storage'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode(64))
    path = db.Column(db.Unicode(128))
    type = db.Column(db.Unicode(3))
    create_date = db.Column(db.DateTime, default=datetime.datetime.now)

Создаем view и переопределяем обработчики форм

from flask_admin.contrib import sqla
from flask_admin import form

import random
import os


class StorageAdminModel(sqla.ModelView):
    form_extra_fields = {
        'file': form.FileUploadField('file')
    }

    def _change_path_data(self, _form):
        try:
            storage_file = _form.file.data

            if storage_file is not None:
                hash = random.getrandbits(128)
                ext = storage_file.filename.split('.')[-1]
                path = '%s.%s' % (hash, ext)

                storage_file.save(
                    os.path.join(app.config['STORAGE'], path)
                )

                _form.name.data = _form.name.data or storage_file.filename
                _form.path.data = path
                _form.type.data = ext

                del _form.file

        except Exception as ex:
            pass

        return _form

    def edit_form(self, obj=None):
        return self._change_path_data(
            super(StorageAdminModel, self).edit_form(obj)
        )

    def create_form(self, obj=None):
        return self._change_path_data(
            super(StorageAdminModel, self).create_form(obj)
        )


admin.add_view(StorageAdminModel(StorageModel, db.session))
  • edit_form - вызывается при редактировании записи
  • create_form - соответственно при создании

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

Через form_extra_fields мы добавляем новое поле file и задаем ему тип FileUploadField. Это добавит в форму новый field с возможностью загрузки файла. После обработки данных удаляем его из формы del _form.file

Вот вообщем и все, мы получили дополнительные данные, записали их в нужные нам поля.

В результате у вас должно получится как ниже на скриншоте:

Бонус

На списке объектов, было бы неплохо сразу видеть то что мы загрузи, если это картинка, показать картинку, если это запись, дать возможность ее воспроизвести. С этим нам поможет column_formatters. Добавим в наш view еще немного магии

def _list_thumbnail(view, context, model, name):
    if not model.path:
        return ''

    url = url_for('static', filename=os.path.join('storage/', model.path))

    if model.type in ['jpg', 'jpeg', 'png', 'svg', 'gif']:
        return Markup('<img src="%s" width="100">' % url)

    if model.type in ['mp3']:
        return Markup('<audio controls="controls"><source src="%s" type="audio/mpeg" /></audio>' % url)

column_formatters = {
    'path': _list_thumbnail
}

И смотрим что получилось:

Весь файл целиком:

from app import app
from app import admin
from app import db

from app.models.StorageModel import StorageModel

from jinja2 import Markup
from flask import url_for
from flask_admin import form

from flask_admin.contrib import sqla

import random
import os


class StorageAdminModel(sqla.ModelView):
    def _list_thumbnail(view, context, model, name):
        if not model.path:
            return ''

        url = url_for('static', filename=os.path.join('storage/', model.path))

        if model.type in ['jpg', 'jpeg', 'png', 'svg', 'gif']:
            return Markup('' % url)

        if model.type in ['mp3']:
            return Markup('' % url)

    column_formatters = {
        'path': _list_thumbnail
    }

    form_extra_fields = {
        'file': form.FileUploadField('file')
    }

    def _change_path_data(self, _form):
        try:
            storage_file = _form.file.data

            if storage_file is not None:
                hash = random.getrandbits(128)
                ext = storage_file.filename.split('.')[-1]
                path = '%s.%s' % (hash, ext)

                storage_file.save(
                    os.path.join(app.config['STORAGE'], path)
                )

                _form.name.data = _form.name.data or storage_file.filename
                _form.path.data = path
                _form.type.data = ext

                del _form.file

        except Exception as ex:
            pass

        return _form

    def edit_form(self, obj=None):
        return self._change_path_data(
            super(StorageAdminModel, self).edit_form(obj)
        )

    def create_form(self, obj=None):
        return self._change_path_data(
            super(StorageAdminModel, self).create_form(obj)
        )


admin.add_view(StorageAdminModel(StorageModel, db.session))
#Flask
Комментарии 6
Anton Volkov 20.02.2020 в 23:10

Спасибо за полезную статью!

Скажите, пожалуйста, можно ли как-то через форму создания сделать импорт данных? Идея такая: на форме есть поле для ввода и поле для выбора файла + дополнительная кнопка "импортировать". Если заполнить поле ввода и сохранить форму, то создастся одна запись с данными этого поля. Если выбрать файл и нажать "импортировать", то файл должен передаться в обработчик, который его распарсит и загрузить данные из него в таблицу.

Форму я уже смог создать, но пока так и не разобрался с тем, как получить файл после нажатия на кастомную кнопку.

form_extra_fields = { 'market_order_id': StringField('Market order id'), 'file': FileUploadField('File to import'), } form_create_rules = ( rules.Header('New order'), rules.Field('market_order_id'), rules.Header('Import'), rules.Field('file'), rules.HTML( '''<a href="/importordersdelivered" class="btn btn-danger" role="button"> Import delivered orders </a>''' ), )

LegGnom 21.02.2020 в 22:07

Это можно сделать с помощью jquery (подписаться на событие клика например) и отдельный роутер в модели админ. Если интересно могу отдельную статью написать, нечто подобное делал для этого блога

Anton Volkov 29.03.2020 в 20:59

Спасибо за совет. Да, было бы интересно прочитать статью на эту тему.

Spirit412 03.09.2020 в 14:25

Может вы мне поможете? Всё никак не найду способа редактировать через админку (flask-admin) конфиг файла фласка (config.py). Хочется это реализовать в админке.

Valera Chornenkiiy 27.04.2021 в 22:24

Добрый вечер, выбивает такую ошибку "ValueError: FileUploadField field requires base_path to be set" при нажатии кнопки загрузить файл, кто знает в чем проблема?

LegGnom 30.04.2021 в 08:09

А можно пример кода?

Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу