11
votes

I'm having trouble with the creation of blueprints by Flask-Admin when I'm testing my app.

This is my View class (using SQLAlchemy)

##
# All views that only admins are allowed to see should inherit from this class.
#
class AuthView(ModelView):
    def is_accessible(self):
        return current_user.is_admin()

class UserView(AuthView):
    column_list = ('name', 'email', 'role_code')

This is how I initialize the views:

# flask-admin
admin.add_view(UserView(User, db.session))
admin.init_app(app)

However, when I try to run more then one test (the fault always occurs on the second test and all the other tests that follow), I always get following error message:

======================================================================
ERROR: test_send_email (tests.test_views.TestUser)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/lib/python2.7/site-packages/nose/case.py", line 133, in run
    self.runTest(result)
  File "/lib/python2.7/site-packages/nose/case.py", line 151, in runTest
    test(result)
  File "/lib/python2.7/site-packages/flask_testing.py", line 72, in __call__
    self._pre_setup()
  File "/lib/python2.7/site-packages/flask_testing.py", line 80, in _pre_setup
    self.app = self.create_app()
  File "/tests/test_init.py", line 27, in create_app
    app = create_app(TestConfig)
  File "/fbone/app.py", line 41, in create_app
    configure_extensions(app)
  File "/fbone/app.py", line 98, in configure_extensions
    admin.add_view(UserView(User, db.session))
  File "/lib/python2.7/site-packages/flask_admin/base.py", line 484, in add_view
    self.app.register_blueprint(view.create_blueprint(self))
  File "/lib/python2.7/site-packages/flask/app.py", line 62, in wrapper_func
    return f(self, *args, **kwargs)
  File "/lib/python2.7/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 0x110576910> and <flask.blueprints.Blueprint object at 0x1103bd3d0>.  Both share the same name "userview".  Blueprints that are created on the fly need unique names.

The strange thing is that this only happens on the second test and never when I just run the app.

When I debugged the tests, the first time it did exactly what I expected and added the blueprint to the app after the init_app(app). The second time however the process immediately stopped when reaching the add_view step (which I think is strange because the blueprints get registered in the init_app(app) call?)

4
When you say "the first time" do you mean in your first test or are you saying that your unit test completes and you are running the entire test suit again.AlexLordThorsen
I mean the first testarnoutaertgeerts
Make sure you create new instance of Admin class in your application factory for each test. Looks like you keep adding views to the existing Admin class instance for each test run.Joes

4 Answers

11
votes

The same thing happened to me while using Flask-Admin and testing with pytest. I was able to fix it without creating teardown functions for my tests by moving the creation of the admin instance into the app factory.

Before:

# extensions.py
from flask.ext.admin import Admin
admin = Admin()

# __init__.py
from .extensions import admin

def create_app():
    app = Flask('flask_app')

    admin.add_view(sqla.ModelView(models.User, db.session))
    admin.init_app(app)

    return app

After:

# __init__.py
from flask.ext.admin import Admin

def create_app():
    app = Flask('flask_app')

    admin = Admin()

    admin.add_view(sqla.ModelView(models.User, db.session))    
    admin.init_app(app)

    return app

Because pytest runs the app factory each time it no longer tries to register multiple views on a global admin instance. This isn't consistent with typical Flask extension usage, but it works and it'll keep your app factory from stumbling over Flask-Admin views.

1
votes

I had to add the following to my test case tearDown. It cleans up the views that were added to the admin extension in the test setup

from flask.ext.testing import TestCase
from flask.ext.admin import BaseView

# My application wide instance of the Admin manager
from myapp.extensions import admin 


class TestView(BaseView):
    ...


class MyTestCase(TestCase):
    def setUp(self):
        admin.add_view(TestView())

    def tearDown(self):
       admin._views.pop(-1)
       admin._menu.pop(-1)

This is certainly a bit of a hack, but it got the job done while I had this problem.

1
votes

Just in case this helps anyone, another way to handle this is to do:

class MyTestCase(TestCase):
    def setUp(self):
        admin._views = []

this way you don't have to set the Admin() initialization inside the factory. It seems more appropiate to me.

0
votes

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'))