Search code examples
pythonrelationshipflask-admin

flask admin edit child objects on click


Flask-Admin shows child objects defined by relationships in its standard edit view. For example, if User objects have Address children, looking at the edit view for User will show the Address child in the appropriate field. The user can then remove the object, or add another one.

I want users to be able to click through, or otherwise have the ability to enter the edit view of child objects. In the example I'm describing, the user should be able to access the edit view of the Address object directly from the edit view of the User object.

The only thing I've found at all related is inline_models, but this isn't a solution. The implementation is extremely fragile (it can't handle long distance relationships, for example). Flask-Admin is aware of child objects! I can see them in the view! I just want them to become a link to their own edit view...

Anyone have any idea how to accomplish this or can link to an example?


Solution

  • Here is a single file simple example of placing a link to another model's edit view in an edit view. It may help you or not.

    enter image description here

    I've used a User - Address relationship, a user has an address and address can have many users.

    I've used Faker to generate sample data so you'll need to pip install faker into your environment.

    The idea is to use Flask-Admin form rules and in this case I'm configuring form_edit_rules.

    I've created two custom rules:

    Link, inheriting BaseRule. The constructor takes three values; an endpoint, a name of an attribute to pass along with the endpoint in the Flask url_for method and finally the text to appear as the link. In this example the endpoint is 'address.edit_view' because this is the view we want to link to.

    MultiLink, similar to Link accepts it works with a relation.

    Here's the code (there's little error checking):

    from random import randint
    from flask import Flask, url_for
    from flask_admin.contrib import sqla
    from flask_admin import Admin
    from flask_admin.form.rules import BaseRule
    from faker import Faker
    from flask_sqlalchemy import SQLAlchemy
    from markupsafe import Markup
    from sqlalchemy import func, select
    from sqlalchemy.ext.hybrid import hybrid_property
    
    fake = Faker()
    
    # Create application
    app = Flask(__name__)
    
    # Create dummy secrey key so we can use sessions
    app.config['SECRET_KEY'] = '123456790'
    
    # Create in-memory database
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    # 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>'
    
    
    class Address(db.Model):
    
        __tablename__ = 'addresses'
    
        id = db.Column(db.Integer, primary_key=True)
        number = db.Column(db.String(255))
        street = db.Column(db.String(255))
        city = db.Column(db.String(255))
        country = db.Column(db.String(255))
    
        @hybrid_property
        def user_count(self):
            return len(self.users)
    
        @user_count.expression
        def user_count(cls):
            return select([func.count(User.id)]).where(User.address_id == cls.id).label("user_count")
    
        def __unicode__(self):
            return ', '.join(filter(None, [self.number, self.street, self.city, self.country]))
    
    
    class User(db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        first_name = db.Column(db.String(255))
        last_name = db.Column(db.String(255))
        email = db.Column(db.String(254))
    
        address_id = db.Column(db.Integer, db.ForeignKey('addresses.id'), index=True)
        address = db.relationship(Address, backref=db.backref('users'))
    
        def __str__(self):
            return unicode(self).encode('utf-8')
    
        def __unicode__(self):
            return '{} {}'.format(self.first_name, self.last_name)
    
    
    class Link(BaseRule):
        def __init__(self, endpoint, attribute, text):
            super(Link, self).__init__()
            self.endpoint = endpoint
            self.text = text
            self.attribute = attribute
    
        def __call__(self, form, form_opts=None, field_args=None):
            if not field_args
                field_args = {}
    
            _id = getattr(form._obj, self.attribute, None)
    
            if _id:
                return Markup('<a href="{url}">{text}</a>'.format(url=url_for(self.endpoint, id=_id), text=self.text))
    
    
    class MultiLink(BaseRule):
        def __init__(self, endpoint, relation, attribute):
            super(MultiLink, self).__init__()
            self.endpoint = endpoint
            self.relation = relation
            self.attribute = attribute
    
        def __call__(self, form, form_opts=None, field_args=None):
            if not field_args
                field_args = {}
            _hrefs = []
            _objects = getattr(form._obj, self.relation)
            for _obj in _objects:
                _id = getattr(_obj, self.attribute, None)
                _link = '<a href="{url}">Edit {text}</a>'.format(url=url_for(self.endpoint, id=_id), text=str(_obj))
                _hrefs.append(_link)
    
            return Markup('<br>'.join(_hrefs))
    
    
    class UserAdmin(sqla.ModelView):
        can_view_details = True
    
        form_edit_rules = (
            'first_name',
            'last_name',
            'email',
            'address',
            Link(endpoint='address.edit_view', attribute='address_id', text='Edit Address')
        )
    
    
    class AddressAdmin(sqla.ModelView):
        can_view_details = True
    
        column_list = ['number', 'street', 'city', 'country', 'user_count', 'users']
    
        form_edit_rules = (
            'number',
            'street',
            'city',
            'country',
            'users',
            MultiLink(endpoint='user.edit_view', relation='users', attribute='id')
        )
    
    
    admin = Admin(app, template_mode="bootstrap3")
    admin.add_view(UserAdmin(User, db.session))
    admin.add_view(AddressAdmin(Address, db.session))
    
    
    def build_db():
        db.drop_all()
        db.create_all()
    
        for _ in range(0, 20):
            _users = []
            for _ in range(0, randint(1, 10)):
                _user = User(
                    first_name=fake.first_name(),
                    last_name=fake.last_name(),
                    email=fake.safe_email(),
                )
                _users.append(_user)
    
            _address = Address(
                number=fake.random_digit_not_null(),
                street=fake.secondary_address(),
                city=fake.city(),
                country=fake.country(),
                users = _users
            )
    
            db.session.add(_address)
    
        db.session.commit()
    
    
    @app.before_first_request
    def first_request():
        build_db()
    
    
    if __name__ == '__main__':
        app.run(port=5000, debug=True)