Closed jacobsvante closed 6 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
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).
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.
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
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:
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).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.
@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.
On a side note.. if you have to call add_view
in the app factory, what's the point of making admin
instance global?
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
Problem, as I see it, is related to mixing of singleton state with dynamic state.
Let me illustrate it:
app
objectapp
object to initialize themselves before there's "current" application contextapp
instance for them to do something with itadmin
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.
- Only way to do it is to go through all CMS modules and pass new app instance for them to do something with it
- 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
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.
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.
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.
Thanks @drcongo !
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'))
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)
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 issueadmin.add_view()
within thecreate_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?