【问题标题】:How to compute options of dropdown for column filter in Flask Admin?如何计算 Flask Admin 中列过滤器的下拉选项?
【发布时间】:2016-11-16 07:58:49
【问题描述】:

在 Flask Admin 中,我得到了一列字符串值(我无法更改数据库定义)。在索引视图中过滤此列时,用户应该能够从下拉列表中选择值。

应该在用户选择过滤器时计算过滤器的可能选项。所以我需要一种基于自定义模型查询设置选项的方法。由于列已编入索引,因此查询速度相当快。

SQLA custom filter example 仅显示如何应用自定义过滤器,而不是如何使过滤器的选项动态化。

【问题讨论】:

    标签: flask flask-sqlalchemy flask-admin


    【解决方案1】:

    options 参数可以是可调用的,因此请执行以下操作(使用您链接到的示例):

    def get_all_last_names():
        unique_last_names = User.query.with_entities(User.last_name.distinct().label("last_name")).order_by(User.last_name.asc()).all()
        return [(user.last_name, user.last_name) for user in unique_last_names]
    
    class UserAdmin(sqla.ModelView):
    
        column_filters = [
            FilterEqual(column=User.last_name, name='Last Name', options=get_all_last_names),
        ]
    
        # This is probably NOT the correct way to refresh the filters cache
        @expose('/')
        def index_view(self):
            self._refresh_filters_cache()
            return super(UserAdmin, self).index_view()
    

    请参见下面的单个文件演示。添加新用户,他们将出现在“姓氏”过滤器中。正如@sortas 所指出的,这个版本在使用应用工厂方法时不起作用。

    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    
    from flask_admin.contrib import sqla
    from flask_admin import Admin, expose
    
    # required for creating custom filters
    from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
    
    app = Flask(__name__)
    
    # Create dummy secrey key so we can use sessions
    app.config['SECRET_KEY'] = '123456790'
    
    # Create in-memory database
    app.config['DATABASE_FILE'] = 'sample_db.sqlite'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
    app.config['SQLALCHEMY_ECHO'] = True
    db = SQLAlchemy(app)
    
    
    # Flask views
    @app.route('/')
    def index():
        return '<a href="/admin/">Click me to get to Admin!</a>'
    
    
    # Create model
    class User(db.Model):
    
        id = db.Column(db.Integer, primary_key=True)
        first_name = db.Column(db.String(100))
        last_name = db.Column(db.String(100))
        username = db.Column(db.String(80), unique=True)
        email = db.Column(db.String(120), unique=True)
    
        # Required for admin interface. For python 3 please use __str__ instead.
        def __unicode__(self):
            return self.username
    
    
    # Create custom filter class
    class FilterLastNameBrown(BaseSQLAFilter):
        def apply(self, query, value, alias=None):
            if value == '1':
                return query.filter(self.column == "Brown")
            else:
                return query.filter(self.column != "Brown")
    
        def operation(self):
            return 'is Brown'
    
    
    def get_options():
        return [('1', 'Yes'), ('0', 'No')]
    
    
    def get_all_last_names():
        unique_last_names = User.query.with_entities(User.last_name.distinct().label("last_name")).order_by(
            User.last_name.asc()).all()
        return [(user.last_name, user.last_name) for user in unique_last_names]
    
    
    # Add custom filter and standard FilterEqual to ModelView
    class UserAdmin(sqla.ModelView):
    
        column_filters = [
            FilterEqual(column=User.last_name, name='Last Name', options=get_all_last_names),
            FilterLastNameBrown(column=User.last_name, name='Last Name', options=(('1', 'Yes'), ('0', 'No')))
        ]
    
        # This is probably NOT the correct way to refresh the filters cache
        @expose('/')
        def index_view(self):
            self._refresh_filters_cache()
            return super(UserAdmin, self).index_view()
    
    
    admin = Admin(app, template_mode="bootstrap3")
    admin.add_view(UserAdmin(User, db.session))
    
    
    def build_sample_db():
        db.drop_all()
        db.create_all()
        user_obj1 = User(first_name="Paul", last_name="Brown", username="pbrown", email="paul@gmail.com")
        user_obj2 = User(first_name="Luke", last_name="Brown", username="lbrown", email="luke@gmail.com")
        user_obj3 = User(first_name="Serge", last_name="Koval", username="skoval", email="serge@gmail.com")
    
        db.session.add_all([user_obj1, user_obj2, user_obj3])
        db.session.commit()
    
    
    if __name__ == '__main__':
        build_sample_db()
        app.run(port=5000, debug=True)
    

    使用应用工厂时的单个文件示例。

    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    
    from flask_admin.contrib import sqla
    from flask_admin import Admin, expose
    
    # required for creating custom filters
    from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
    
    db = SQLAlchemy()
    
    
    class User(db.Model):
    
        id = db.Column(db.Integer, primary_key=True)
        first_name = db.Column(db.String(100))
        last_name = db.Column(db.String(100))
        username = db.Column(db.String(80), unique=True)
        email = db.Column(db.String(120), unique=True)
    
        # Required for admin interface. For python 3 please use __str__ instead.
        def __unicode__(self):
            return self.username
    
    
    # Create custom filter class
    class FilterLastNameBrown(BaseSQLAFilter):
        def apply(self, query, value, alias=None):
            if value == '1':
                return query.filter(self.column == "Brown")
            else:
                return query.filter(self.column != "Brown")
    
        def operation(self):
            return 'is Brown'
    
    
    def get_options():
        return [('1', 'Yes'), ('0', 'No')]
    
    
    def get_all_last_names():
        unique_last_names = User.query.with_entities(User.last_name.distinct().label("last_name")).order_by(
            User.last_name.asc()).all()
        return [(user.last_name, user.last_name) for user in unique_last_names]
    
    
    # Add custom filter and dynamic FilterEqual to ModelView
    class UserAdmin(sqla.ModelView):
    
        column_filters = [
            FilterLastNameBrown(column=User.last_name, name='Last Name', options=(('1', 'Yes'), ('0', 'No')))
        ]
    
        def get_filters(self):
            _dynamic_filters = getattr(self, 'dynamic_filters', None)
            if _dynamic_filters:
                return (super(UserAdmin, self).get_filters() or []) + _dynamic_filters
            else:
                return super(UserAdmin, self).get_filters()
    
        @expose('/')
        def index_view(self):
            self.dynamic_filters = []
            self.dynamic_filters.extend([
                FilterEqual(column=User.last_name, name='Last Name', options=get_all_last_names),
                # Add further dynamic filters here
            ])
            self._refresh_filters_cache()
            return super(UserAdmin, self).index_view()
    
    
    def create_app():
    
        app = Flask(__name__)
    
        # Create dummy secrey key so we can use sessions
        app.config['SECRET_KEY'] = '123456790'
    
        # Create in-memory database
        app.config['DATABASE_FILE'] = 'sample_db.sqlite'
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
        app.config['SQLALCHEMY_ECHO'] = True
        db.init_app(app)
        admin = Admin(app, template_mode="bootstrap3")
        admin.add_view(UserAdmin(User, db.session))
    
        # Flask views
        @app.route('/')
        def index():
            return '<a href="/admin/">Click me to get to Admin!</a>'
    
        @app.before_first_request
        def build_sample_db():
            db.drop_all()
            db.create_all()
            user_obj1 = User(first_name="Paul", last_name="Brown", username="pbrown", email="paul@gmail.com")
            user_obj2 = User(first_name="Luke", last_name="Brown", username="lbrown", email="luke@gmail.com")
            user_obj3 = User(first_name="Serge", last_name="Koval", username="skoval", email="serge@gmail.com")
    
            db.session.add_all([user_obj1, user_obj2, user_obj3])
            db.session.commit()
    
        return app
    
    
    if __name__ == '__main__':
        app = create_app()
        app.run(port=5000, debug=True)
    

    【讨论】:

    • 嗯,这个函数在应用启动时被调用。即使页面重新加载也不会触发它再次加载:-(
    • @Sebi - 你需要更新过滤器缓存 - 我已经更新了我的答案
    • @pjcunningham 过滤器即使与 app.app_context() 一起使用也不会刷新,只能手动重新加载应用程序。有任何想法吗? :)
    • @sortas - 添加了单个文件演示。
    • @pjcunningham 谢谢,尝试了同样的事情,但看起来它不适用于应用程序工厂(create_app() 等)。所以现在我正在尝试覆盖 _refresh_filters_cache 方法以在每次重新加载页面时从 db 查询选项。
    【解决方案2】:

    你可以使用flaskhas_request_context函数 检查返回过滤器选项元组的函数中是否存在请求上下文。 它可以帮助在选项从DB获取时的情况下,但请求上下文不存在。

    def _get_options_for_filter()
        if not has_request_context():
            return ()
        return tuple([(o.id, o.title) for o in Options.query.all()])
    

    【讨论】:

    • 非常感谢!由于启动时过滤器缓存的刷新,管理员中的动态过滤器以某种方式锁定了数据库表,从而阻止了我的 Alembic 迁移。测试请求上下文解决了我的问题
    【解决方案3】:

    Flask 应用工厂的版本(create_appcurrent_appapp_context 等):)

    # Dynamic filter update method for Flask app factories
    # Override version of base _refresh_filters_cache method
    def update_dynamic_filters(self, dynamic_filters):
    
        # Get base filters
        self._filters = self.get_filters()
    
        # Add dynamic filters (to the beginning)
        for dft in dynamic_filters:
            self._filters.insert(0, dft)
    
        if self._filters:
            self._filter_groups = OrderedDict()
            self._filter_args = {}
    
            for i, flt in enumerate(self._filters):
                key = as_unicode(flt.name)
                if key not in self._filter_groups:
                    self._filter_groups[key] = FilterGroup(flt.name)
                self._filter_groups[key].append(
                    {
                        'index': i,
                        'arg': self.get_filter_arg(i, flt),
                        'operation': flt.operation(),
                        'options': flt.get_options(self) or None,
                        'type': flt.data_type,
                    }
                )
                self._filter_args[self.get_filter_arg(i, flt)] = (i, flt)
        else:
            self._filter_groups = None
            self._filter_args = None
    
    # Update view every page reload (thanks to the @pjcunningham)
    @expose('/')
    def index_view(self):
        dynamic_filters = []
        # Working with db in app_context
        with app.app_context():
            # Adding dynamic filters
            dynamic_filters.append(
                FilterInList(
                    column=Record.record_condition,
                    name='Condition',
                    options=[
                        (x[0], x[0])
                        for x in db.session.query(Record.record_condition)
                        .distinct()
                        .all()
                    ],
                )
            )
        # Add dynamic filters to view
        self.update_dynamic_filters(dynamic_filters)
        return super(RecordView, self).index_view()
    

    【讨论】:

    • 我认为最好直接覆盖get_filters 而不是重复基本代码。另外,调用index_view 时应用上下文不是已经建立了吗?我已根据您的代码在我的答案中添加了一个应用工厂示例。
    猜你喜欢
    • 1970-01-01
    • 2018-02-06
    • 1970-01-01
    • 2023-01-14
    • 2014-09-02
    • 2022-01-01
    • 1970-01-01
    • 2019-08-30
    相关资源
    最近更新 更多