Flask-Admin создает представление с помощью контекстно-зависимых функций SQLAlchemy

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

def get_column_value_from_context(context):
    # Instructions to produce value

    return value


class MyModel(db.Model):
    id = db.Column(db.Integer,
                   primary_key=True)
    my_column = db.Column(db.String(64),
                          nullable=False,
                          default=get_column_value_from_context)
    name = db.Column(db.String(32),
                     nullable=False,
                     unique=True,
                     index=True)
    title = db.Column(db.String(128),
                      nullable=False)
    description = db.Column(db.String(256),
                            nullable=False)

Этот подход работает довольно прилично, я могу создавать строки без проблем из командной строки или с помощью скрипта.

Я также добавил ModelView в приложение с помощью Flask-Admin:

class MyModelView(ModelView):
    can_view_details = True
    can_set_page_size = True
    can_export = True


admin.add_view(MyModelView(MyModel, db.session))

Это также работает довольно прилично, пока я не нажму кнопку " Создать" в представлении списка. Я получаю эту ошибку:

AttributeError: объект 'NoneType' не имеет атрибута 'get_current_parameters'

Поскольку реализация create_model обработчика в ModelView является это:

def create_model(self, form):
    """
        Create model from form.
        :param form:
            Form instance
    """
    try:
        model = self.model()
        form.populate_obj(model)
        self.session.add(model)
        self._on_model_change(form, model, True)
        self.session.commit()
    except Exception as ex:
        if not self.handle_view_exception(ex):
            flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
            log.exception('Failed to create record.')

        self.session.rollback()

        return False
    else:
        self.after_model_change(form, model, True)


    return model

и здесь нет context при создании экземпляра модели. Итак, я создал пользовательский вид, в котором можно было бы переопределить экземпляр модели в обработчике создания:

class CustomSQLAView(ModelView):
    def __init__(self, *args, **kwargs):
        super(CustomSQLAView, self).__init__(*args, **kwargs)

    def create_model(self, form):
        """
            Create model from form.
            :param form:
                Form instance
        """
        try:
            model = self.get_populated_model(form)
            self.session.add(model)
            self._on_model_change(form, model, True)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
                log.exception('Failed to create record.')

            self.session.rollback()

            return False
        else:
            self.after_model_change(form, model, True)

        return model

    def get_populated_model(self, form):
        model = self.model()
        form.populate_obj(model)

        return model

Теперь я могу переопределить метод get_populated_model для создания экземпляра модели обычным способом:

class MyModelView(CustomSQLAView):
    can_view_details = True
    can_set_page_size = True
    can_export = True

    def get_populated_model(self, form):
        model = self.model(
            name=form.name.data,
            title=form.title.data,
            description=form.description.data,
        )

        return model

Несмотря на это, я подозреваю, что это что-то ломает. Колба-Admin имеет несколько реализации populate_obj метода форм и полей, поэтому я хотел бы, чтобы держать все в безопасности.

Каков правильный способ сделать это?

Ответ 1

Поэтому есть несколько факторов, которые возникают в этом вопросе.

Но во-первых, небольшое полнофункциональное приложение для иллюстрации:

from flask_admin.contrib.sqla import ModelView
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask import Flask

app = Flask(__name__)
db = SQLAlchemy(app)
admin = Admin(app)
app.secret_key = 'arstartartsar'

def get_column_value_from_context(context):
    print('getting value from context...')
    if context.isinsert:
        return 'its an insert!'
    else:
        return 'aww, its not an insert'

class MyModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    my_column = db.Column(db.String(64), nullable=False, default=get_column_value_from_context)
    name = db.Column(db.String(32), nullable=False, unique=True, index=True)

class MyModelView(ModelView):
    form_excluded_columns = ['my_column']

db.create_all()
admin.add_view(MyModelView(MyModel, db.session))
print('1')
x = MyModel(name='hey')
print('2')
db.session.add(x)
print('3')
db.session.commit()
print('4')

Когда мы запускаем это приложение, напечатаем:

1
2
3
getting value from context...
4

Итак, только после get_column_value_from_context функция get_column_value_from_context. Когда в представлении create вы создаете новую модель, у вас еще нет контекста, потому что вы еще ничего не делаете в базе данных. Вы получаете контекст, чтобы установить значение по умолчанию, когда вы делаете свой экземпляр в базу данных! Вот почему, в исходном коде флэшка admin, это происходит:

if getattr(default, 'is_callable', False):
    value = lambda: default.arg(None)  # noqa: E731

Они проверяют, является ли заданная вами по умолчанию функция, и если это так, они называют ее без контекста (потому что пока нет контекста!).

У вас есть несколько вариантов преодоления этого в зависимости от ваших целей:

1) Только вычислить значение my_column при добавлении его в базу данных:

Просто игнорируйте поле в форме, и оно будет определено, когда вы добавите его в db.

class MyModelView(ModelView):
    form_excluded_columns = ['my_column']

2) Рассчитывайте только при добавлении его в db, но затем редактируя его:

class MyModelView(ModelView):
    form_edit_rules = ['name', 'my_column']
    form_create_rules = ['name']