Search code examples
flaskflask-sqlalchemyflask-admin

How to compute options of dropdown for column filter in Flask Admin?


In Flask Admin, I got a column of String values (I can't change the DB definition). When filtering this column in the index view, the user should be able to select the value from a dropdown.

The possible options for the filter should be computed when the user selects the filter. So I need a way to set the options based on a custom model query. Querying is rather fast as the column is indexed.

The SQLA custom filter example only shows how to apply a custom filter, but not how to make the options of a filter dynamic.


Solution

  • The options parameter can be a callable so do something like the following (using the example you've linked to):

    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()
    

    See single file demo below. Add new users and they will appear in the 'Last Name' filter. As pointed out by @sortas this version doesn't work when using the app factory method.

    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)
    

    Single file example when using app factory.

    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)