melchoir55
melchoir55

Reputation: 7276

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?

Upvotes: 3

Views: 1662

Answers (1)

pjcunningham
pjcunningham

Reputation: 8046

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)

Upvotes: 10

Related Questions