Flask-Admin создает представление с контекстно-зависимыми функциями SQLAlchemy - PullRequest
0 голосов
/ 28 июня 2018

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

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

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

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

1 Ответ

0 голосов
/ 22 сентября 2018

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

Но сначала небольшое полнофункциональное приложение для иллюстрации:

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. Когда в представлении создания вы «создаете» новую модель, у вас еще нет контекста, потому что вы еще ничего не добавляете в базу данных. Вы получаете контекст, чтобы установить значение по умолчанию, когда вы фиксируете свой экземпляр в базе данных! Поэтому в исходном коде фляги admin это происходит:

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

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

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

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

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

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

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

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