Search code examples
flaskflask-sqlalchemyflask-admin

How to modify available actions for each item in a list in Flask-Admin


I want to have some rows editable, and some view-only, based on some condition, how do I achieve that?


Solution

  • You can accomplish this by overriding the list_row_actions block of the Flask-Admin list template. This block will call a method defined in the view passing the action and row (i.e. the model) as parameters for each row action defined in the view. The method returns True or False depending if the action should be allowed. A simple example follows.

    In templates/admin directory create list.html:

    {% extends 'admin/model/list.html' %}
    
    {% block list_row_actions scoped %}
    
        {% for action in list_row_actions %}
            {% if admin_view.allow_row_action(action, row) %}
                {{ action.render_ctx(get_pk_value(row), row) }}
            {% endif %}
        {% endfor %}
    
    {% endblock %}
    

    Note admin_view is the current view and we have created a method allow_row_action taking parameters action and row (the model) in the view.

    In the view code define a mixin:

    class RowActionListMixin(object):
    
        list_template = 'admin/list.html'
    
        def allow_row_action(self, action, model):
            return True
    

    Any view implementing this mixin will use the overridden list template defined above. It also defines the method allow_row_action that returns True.

    Now define any view that you want to control the row actions as follows:

    class Student1View(RowActionListMixin, sqla.ModelView):
        column_default_sort = ('last_name', False)
        column_searchable_list = ('first_name', 'last_name')
        column_filters = ('allow_edit', 'allow_delete')
    
        def _can_edit(self, model):
            # Put your logic here to allow edit per model
            # return True to allow edit
            return model.allow_edit
    
        def _can_delete(self, model):
            # Put your logic here to allow delete per model
            # return True to allow delete
            return model.allow_delete
    
        def allow_row_action(self, action, model):
    
            # # Deal with Edit Action
            if isinstance(action, EditRowAction):
                return self._can_edit(model)
    
            # # Deal with Delete Action
            if isinstance(action, DeleteRowAction):
                return self._can_delete(model)
    
            # # Deal with other actions etc
    
            # otherwise whatever the inherited method returns
            return super().allow_row_action()
    

    Note the overridden allow_row_method. It checks what the action is and passes the decision making on to an appropriate local method.

    Here is a self contained single file example, you'll need the list template defined above too. The Student model has two fields allow_edit and allow_delete along with first_name and last_name fields. The allow_edit and allow_delete allow you to dynamically switch if a Student can be edited and/or deleted'. Student1View allows editing/deleting based on a student's allow_edit and allow_delete values. Student2View doesn't override allow_row_action so row actions are allowed to happen because the base method defined in RowActionListMixin class returns True.

    enter image description here

    from faker import Faker
    import click
    from flask import Flask
    from flask_admin.model.template import EditRowAction, DeleteRowAction
    from flask_sqlalchemy import SQLAlchemy
    from flask_admin import Admin
    from flask_admin.contrib import sqla
    
    db = SQLAlchemy()
    
    
    class Student(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        first_name = db.Column(db.Text(length=255), nullable=False)
        last_name = db.Column(db.Text(length=255), nullable=False)
        allow_edit = db.Column(db.Boolean(), default=False, nullable=False)
        allow_delete = db.Column(db.Boolean(), default=False, nullable=False)
    
        def __str__(self):
            return f"ID: {self.id}; First Name: {self.first_name}; Last Name: {self.last_name}"
    
    
    app = Flask(__name__)
    
    app.config['SECRET_KEY'] = '123456790'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample.sqlite'
    
    db.init_app(app)
    
    
    @app.cli.command('create-database', short_help='Create student database')
    @click.option('--count', default=100, help='Number of students (default 100)')
    def create_database(count):
    
        """
            Create database with "count" students
        """
    
        db.drop_all()
        db.create_all()
        _faker = Faker()
        for _ in range(0, count):
            _student = Student(
                first_name=_faker.first_name(),
                last_name=_faker.last_name(),
                allow_edit=_faker.boolean(),
                allow_delete=_faker.boolean()
            )
            db.session.add(_student)
    
        db.session.commit()
    
    
    class RowActionListMixin(object):
    
        list_template = 'admin/list.html'
    
        def allow_row_action(self, action, model):
            return True
    
    
    class Student1View(RowActionListMixin, sqla.ModelView):
        column_default_sort = ('last_name', False)
        column_searchable_list = ('first_name', 'last_name')
        column_filters = ('allow_edit', 'allow_delete')
    
        def _can_edit(self, model):
            # Put your logic here to allow edit per model
            # return True to allow edit
            return model.allow_edit
    
        def _can_delete(self, model):
            # Put your logic here to allow delete per model
            # return True to allow delete
            return model.allow_delete
    
        def allow_row_action(self, action, model):
    
            # # Deal with Edit Action
            if isinstance(action, EditRowAction):
                return self._can_edit(model)
    
            # # Deal with Delete Action
            if isinstance(action, DeleteRowAction):
                return self._can_delete(model)
    
            # # Deal with other actions etc
    
            # otherwise whatever the inherited method returns
            return super().allow_row_action()
    
    
    class Student2View(RowActionListMixin, sqla.ModelView):
        column_default_sort = ('last_name', False)
        column_searchable_list = ('first_name', 'last_name')
        column_filters = ('allow_edit', 'allow_delete')
    
    
    # Flask views
    @app.route('/')
    def index():
        return '<a href="/admin/">Click me to get to Admin!</a>'
    
    
    admin = Admin(app, template_mode="bootstrap3")
    admin.add_view(Student1View(Student, db.session, name='Student 1', category='Students', endpoint='student-1'))
    admin.add_view(Student2View(Student, db.session, name='Student 2', category='Students', endpoint='student-2'))
    
    
    if __name__ == '__main__':
        app.run()