pallets-eco / flask-admin

Simple and extensible administrative interface framework for Flask
https://flask-admin.readthedocs.io
BSD 3-Clause "New" or "Revised" License
5.79k stars 1.57k forks source link

Using standard app factory pattern problematic with Flask-Admin's way of using blueprints #910

Closed jacobsvante closed 6 years ago

jacobsvante commented 9 years ago

The example below might not be a very good use of the app factory pattern, but imagine that you're using sqla.ModelView instead. Then we would have to issue admin.add_view() within the create_app flow because we need to pass in the model and database session. Right now I use a workaround found in this SO thread. Is there some way that we could change Flask-Admin's behavior so that we can follow Flask's standard application factory workflow?

import flask
import flask_admin
import unittest

class MyAdminView(flask_admin.BaseView):
    @flask_admin.expose('/')
    def index(self):
        self.render('index.html')

admin = flask_admin.Admin()

def create_app():
    app = flask.Flask(__name__)
    admin.add_view(MyAdminView(name='myview1', endpoint='myview1'))
    admin.init_app(app)
    return app

class BlueprintCollisionTestCase(unittest.TestCase):

    def setUp(self):
        self.app = create_app()

    def test_1(self):
        assert 1 == 1

    def test_2(self):
        assert 1 == 1

if __name__ == '__main__':
    unittest.main()
$ python testflaskadmin.py
.F
======================================================================
FAIL: test_2 (__main__.BlueprintCollisionTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testflaskadmin.py", line 25, in setUp
    self.app = create_app()
  File "testflaskadmin.py", line 18, in create_app
    admin.add_view(MyAdminView(name='myview1', endpoint='myview1'))
  File "/Users/jacob/venvs/myvenv/lib/python3.4/site-packages/Flask_Admin-1.2.0-py3.4.egg/flask_admin/base.py", line 526, in add_view
    self.app.register_blueprint(view.create_blueprint(self))
  File "/Users/jacob/venvs/myvenv/lib/python3.4/site-packages/flask/app.py", line 62, in wrapper_func
    return f(self, *args, **kwargs)
  File "/Users/jacob/venvs/myvenv/lib/python3.4/site-packages/flask/app.py", line 885, in register_blueprint
    (blueprint, self.blueprints[blueprint.name], blueprint.name)
AssertionError: A blueprint's name collision occurred between <flask.blueprints.Blueprint object at 0x1018ebc18> and <flask.blueprints.Blueprint object at 0x1018eb2b0>.  Both share the same name "myview1".  Blueprints that are created on the fly need unique names.

----------------------------------------------------------------------
Ran 2 tests in 0.003s

FAILED (failures=1)
mrjoes commented 9 years ago

Why do you create Admin instance outside of create_app, but Flask instance inside of it?

Admin instance contains state for one Flask application, so it is logical to have different states if you have more than one Flask application.

def create_app():
    app = flask.Flask(__name__)
    admin = flask_admin.Admin(app)
    admin.add_view(MyAdminView(name='myview1', endpoint='myview1'))
    return app, admin
jacobsvante commented 9 years ago

I'm using it because that's the way it's supposed to be done if you're using app factories in Flask. I think the idea is to not store state of the Flask app in your extension's instance, because it might be used by multiple apps. If you read the extension development doc page you'll see that they recommend storing data on the context (see point number 4 under the extension code example).

mrjoes commented 9 years ago

It is slightly different. I understand the difference (storing configuration data in the application context vs storing it locally), but right now there's one to one relation between Admin and Flask objects.

I'll see how easy it would be to refactor Admin class to store all data in the context of the application.

rochacbruno commented 9 years ago

I am using the lazy initialization for admin.

# core.admin.__init__.py
from flask_admin import Admin

def create_admin(app=None, *args, **kwargs):
    # I do some things here
    return Admin(app, *args, **kwargs) 

Now it allows me to initiate admin instance without the need of an app.

# myapp.py
from flask import Flask
from core.admin import create_admin

admin = create_admin()

def create_app():
    app = Flask(....)

    # admin registering and config changes
    admin.locale_selector(....)
    admin.add_view(....)

    # avoid registering twice
    # THAT IS THE TRICK
    if admin.app is None:
        admin.init_app(app)

    return app

This also allows me to use from myapp import admin from another modules

mrjoes commented 9 years ago

OK, I know what's the problem. It is not about application, contexts or anything like that - it is about multiple calls to add_view. Each call adds duplicate Blueprint and Flask does not like it.

Closest analogy would be this:

admin = Blueprint('admin', __name__, url_prefix='/admin')

def create_app():
  app = Flask()

  # We'll keep adding same route again and again to singleton Blueprint whenever `create_app` is called
  @admin.route('/')
  def index():
      return render_template(current_app.config['INDEX_TEMPLATE'])

  app.register_blueprint(admin)

  return app

Only reason why it works - add_url_rule can be called multiple times with same URL and register_blueprint can not.

So, I see two options:

  1. Move admin configuration outside of create_app - add views to the admin before create_app is called. In lots of the cases this is not possible (you need database session for model views, etc).
  2. Create new Admin instance in create_app and initialize it there.

Also, there's additional reason why Admin updates self.app in the Admin.init_app method:

admin = Admin()
admin.add_view(MyView())
admin.init_app(app)
# We want next `add_view` to work with the same app we used `init_app` with:
admin.add_view(MyView2())

And I'm not sure how to solve this one, as when init_app is called there's no app context yet, so there's no "current app" that we can use to store various data.

mrjoes commented 9 years ago

@rochacbruno Yeah, but you keep adding views over and over again, every time create_app called. It is kind of hack, because you already registered your views for the first run of the create_app and they'll use that database connection.

mrjoes commented 9 years ago

On a side note.. if you have to call add_view in the app factory, what's the point of making admin instance global?

rochacbruno commented 9 years ago

On a side note.. if you have to call add_view in the app factory, what's the point of making admin instance global?

@mrjoes for me it is because of the ability to import "admin" and use it to register new views from de-coupled blueprints. In my case I talk about a CMS and the modules are developed separated and it allows developers to do:

from quokka import admin
from .models import SomeDataModel
from flask import Blueprint

new_module = Blueprint(....)

@new_module.route(...)
def bla():
    pass

admin.add_view(SomeDataModel)
admin.A_LOT_OF_THINGS

There is another way of allowing de-coupled blueprints to register new views in to admin if it is created inside the context of create_app?

I've tried current_app.admin (or current_app.blueprints['admin'].add_view and I dont remember the reason of unsuccess)

BTW: I created a lot of customization in the CMS side which allows the use of admin.register(ModelView, DataModel) inspired by the old/dead flask-superadmin

mrjoes commented 9 years ago

Problem, as I see it, is related to mixing of singleton state with dynamic state.

Let me illustrate it:

  1. There's application factory. There's no such thing as a global app object
  2. Lets assume you have CMS modules and they need app object to initialize themselves before there's "current" application context
  3. Only way to do it is to go through all CMS modules and pass new app instance for them to do something with it
  4. Lets assume you also pass admin instance to these init functions and that's the place where CMS modules would add their dynamic administrative views.

I can't add current_app.admin because there can be multiple admin interfaces registered for the app. Flask-Admin already adds current_app.extensions['admin'] which is a list with all currently registered Admin instances for the app.

I just don't know how to make it fit Flask application factory approach with singleton object.

If anyone knows "clean" solution - shoot it.

rochacbruno commented 9 years ago
  1. Only way to do it is to go through all CMS modules and pass new app instance for them to do something with it
  2. Lets assume you also pass admin instance to these init functions and that's the place where CMS modules would add their dynamic administrative views.

That is exactly the way I do that!

https://github.com/quokkaproject/quokka/blob/master/quokka/modules/posts/admin.py

from quokka import admin
from quokka.core.admin.models import BaseContentAdmin
from quokka.core.widgets import TextEditor, PrepopulatedText
from .models import Post

class PostAdmin(BaseContentAdmin):
    column_searchable_list = ('title', 'body', 'summary')
    form_args = {
        'body': {'widget': TextEditor()},
        'slug': {'widget': PrepopulatedText(master='title')}
    }

admin.register(Post, PostAdmin, category="Content", name="Post")

That is a Quokka-Module admin

Yeah it looks like Django!

Now in Quokka create_app I load all the blueprints just before the admin.init_app got called.

def create_app(config=None, test=False, admin_instance=None, **settings):
    app = QuokkaApp('quokka')
    ....
    # this is a bug, needed to import things inside
    from .ext import configure_extensions
    # all modules are loaded here, admins and commands registered and after all admin.init_app is called
    configure_extensions(app, admin_instance or admin)

    return app
mrjoes commented 9 years ago

Yeah, you just delayed PostAdmin creation to a later stage, so it does not have to resolve app-related stuff immediately. That was my #1 suggestion from the previous answer.

Sure, you can work around the problem with custom code (delayed view creation, etc). Question is how to make admin singleton coexist with add_view inside of create_app factory without introducing too much hacks.

Or just keep everything as is - if you use singleton Admin instance, then you have to initialize it in "global" context as well.

ondoheer commented 9 years ago

I know this has gone in a different direction, but would it solve jmagnusson first question to do:

` class BlueprintCollisionTestCase(unittest.TestCase):

def setUp(self):
    admin._views = [] # this is the initialized extension
    self.app = create_app()

def test_1(self):
    assert 1 == 1

def test_2(self):
    assert 1 == 1

if __name__ == '__main__': unittest.main() this would be similar to the way some people handle Flask-restful collision as well.

drcongo commented 8 years ago

Thought I'd chip in with my solution to this issue, as I forget about it every time and I'll only end up finding this through google again.

I have this function in admin/core.py

def init_admin(f_admin):
    f_admin.add_view(UserView(db.session, name="Users", endpoint="users"))
    [...]

my create_app function has this in it, after app has been created

    f_admin = Admin(
        app,
        name='Admin',
        index_view=CustomAdminIndexView(template='admin/dashboard.html')
    )

and then further on, in the create_app after all my other blueprints have been registered I call the init_admin function and pass it the f_admin object.

    init_admin(f_admin)

Weirdly, if I do the traditional init_app method everything seemed to work when running the site on the dev server, but I would get the blueprint name collisions when running my tests under pytest.

petrus-jvrensburg commented 6 years ago

Thanks @drcongo !

miaohf commented 5 years ago

It work out in this way. just for your reference.

#YourApp/init.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin

db = SQLAlchemy()
admin = Admin(name='TuozhanOA', template_mode='bootstrap3')
def create_app(config_class=Config):
    app = Flask(name)
    app.config.from_object(Config)
    db.init_app(app)
    admin.init_app(app)
    from YourApp.main.routes import main
    app.register_blueprint(main)
    from YourApp.adminbp.routes import adminbp, user_datastore
    app.register_blueprint(adminbp)
    security = Security(app, user_datastore)
    return app

#YourApp/adminbp/routes.py
from flask import render_template, Blueprint
from YourApp.models import User, Role
from YourApp import db, admin
from flask_admin.contrib.sqla import ModelView
from wtforms.fields import PasswordField
from flask_admin.contrib.fileadmin import FileAdmin
import os.path as op

from flask_security import current_user, login_required, RoleMixin, Security, 
SQLAlchemyUserDatastore, UserMixin, utils

adminbp = Blueprint('adminbp', name)
admin.add_view(ModelView(User, db.session, category="Team"))
admin.add_view(ModelView(Role, db.session, category="Team"))

path = op.join(op.dirname(file), 'tuozhan')
admin.add_view(FileAdmin(path, '/static/tuozhan/', name='File Explore'))
reubano commented 3 years ago

Here's my solution

admin.py

from flask_admin import Admin, AdminIndexView
from flask_admin.contrib.sqla import ModelView
from app.models import model

def init_app(app, db, name="Admin", url_prefix="/admin", **kwargs):
    vkwargs = {"name": name, "endpoint": "admin", "url": url_prefix}

    akwargs = {
        "template_mode": "bootstrap3",
        "static_url_path": f"/templates/{url_prefix}",
        "index_view": AdminIndexView(**vkwargs),
    }

    admin = Admin(app, **akwargs)
    admin.add_view(ModelView(model, db.session))

__init__.py


from flask_sqlalchemy import SQLAlchemy
from app import admin

db = SQLAlchemy()

def create_app(script_info=None, **kwargs):
    app = Flask(__name__, template_folder="templates", static_folder="static")
    # ...
    db.init_app(app)
    admin.init_app(app, db)