diff --git a/doc/quickstart.rst b/doc/quickstart.rst index 9ac9c7a9..25f23bbd 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -27,7 +27,7 @@ Here is absolutely valid administrative piece:: So, how does it help structuring administrative interface? With such building blocks, you're implementing reusable functional pieces that are highly customizable. -For example, Flask-SuperAdmin provides ready-to-use SQLAlchemy, Mongoengine and Django model interface. +For example, Flask-SuperAdmin provides ready-to-use SQLAlchemy, Mongoengine and Django model interface. For SQLAlchemy it is implemented as a class which accepts two parameters: model and a database session, otherwise just the model parameter. @@ -199,6 +199,16 @@ code to get URL will look like:: is not provided. Model-based views will be explained in the next section. +You can specify a namespace for the endpoint and all endpoints will be prefixed:: + + admin = Admin(app, namespace='admin') + admin.add_view(MyView(endpoint='testadmin')) + + url_for('admin.index') # The IndexView is in the namespace root + url_for('admin.testadmin.index') + url_for('admin.myview.index') + + Model Views ----------- diff --git a/flask_superadmin/base.py b/flask_superadmin/base.py index 76ccce75..8d6337a8 100644 --- a/flask_superadmin/base.py +++ b/flask_superadmin/base.py @@ -135,6 +135,10 @@ def create_blueprint(self, admin): if self.name is None: self.name = self._prettify_name(self.__class__.__name__) + # Prefix endpoint if a namespace is specified + if admin.namespace and not self.endpoint.startswith(admin.namespace): + self.endpoint = '.'.join([admin.namespace, self.endpoint]) + # Create blueprint and register rules self.blueprint = Blueprint(self.endpoint, __name__, url_prefix=self.url, @@ -277,7 +281,7 @@ class Admin(object): app = None def __init__(self, app=None, name=None, url=None, index_view=None, - translations_path=None): + namespace=None, translations_path=None): """ Constructor. @@ -287,6 +291,8 @@ def __init__(self, app=None, name=None, url=None, index_view=None, Application name. Will be displayed in main menu and as a page title. If not provided, defaulted to "Admin" `index_view` Home page view to use. If not provided, will use `AdminIndexView`. + `namespace` + The endpoint namespace. All registered views endpoint will be prefixed with this value. `translations_path` Location of the translation message catalogs. By default will use translations shipped with the Flask-SuperAdmin. @@ -328,8 +334,12 @@ def __init__(self, app=None, name=None, url=None, index_view=None, # Localizations self.locale_selector_func = None + self.namespace = namespace + # Add predefined index view self.index_view = index_view or AdminIndexView(url=self.url) + if self.namespace: + self.index_view.endpoint = namespace self.add_view(self.index_view) if app is not None: diff --git a/flask_superadmin/tests/test_base.py b/flask_superadmin/tests/test_base.py index aa21a354..340a39b7 100644 --- a/flask_superadmin/tests/test_base.py +++ b/flask_superadmin/tests/test_base.py @@ -52,6 +52,9 @@ def test_base_defaults(): eq_(len(admin._views), 1) eq_(admin._views[0], admin.index_view) + # Ensure index view has a default endpoint + eq_(admin.index_view.endpoint, 'admin') + def test_base_registration(): app = Flask(__name__) @@ -110,6 +113,47 @@ def test_baseview_registration(): eq_(view.url, '/test/test') +def test_baseview_registration_with_namespace(): + admin = base.Admin(namespace='custom') + + view = MockView() + bp = view.create_blueprint(admin) + + # Base properties + eq_(view.admin, admin) + ok_(view.blueprint is not None) + + # Calculated properties + eq_(view.endpoint, 'custom.mockview') + eq_(view.url, '/admin/mockview') + eq_(view.name, 'Mock View') + + # Verify generated blueprint properties + eq_(bp.name, view.endpoint) + eq_(bp.url_prefix, view.url) + eq_(bp.template_folder, 'templates') + eq_(bp.static_folder, view.static_folder) + + # Verify customizations + view = MockView(name='Test', endpoint='foobar') + view.create_blueprint(admin) + + eq_(view.name, 'Test') + eq_(view.endpoint, 'custom.foobar') + eq_(view.url, '/admin/foobar') + + view = MockView(url='test') + view.create_blueprint(base.Admin()) + eq_(view.url, '/admin/test') + + view = MockView(url='/test/test') + view.create_blueprint(base.Admin()) + eq_(view.url, '/test/test') + + # Ensure index view is on namespace root + eq_(admin.index_view.endpoint, admin.namespace) + + def test_baseview_urls(): app = Flask(__name__) admin = base.Admin(app) diff --git a/flask_superadmin/tests/test_model.py b/flask_superadmin/tests/test_model.py index 43a1e423..b087bf1f 100644 --- a/flask_superadmin/tests/test_model.py +++ b/flask_superadmin/tests/test_model.py @@ -114,17 +114,17 @@ def delete_models(self, *pks): return True -def setup(): +def setup_admin(namespace=None): app = Flask(__name__) app.config['WTF_CSRF_ENABLED'] = False app.secret_key = '1' - admin = Admin(app) + admin = Admin(app, namespace=namespace) return app, admin def test_mockview(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model) admin.add_view(view) @@ -181,8 +181,66 @@ def test_mockview(): eq_(rv.headers['location'], 'http://localhost/admin/model/') +def test_mockview_with_namespace(): + app, admin = setup_admin('admin') + + view = MockModelView(Model) + admin.add_view(view) + + eq_(view.model, Model) + + eq_(view.name, 'Model') + eq_(view.url, '/admin/model') + eq_(view.endpoint, 'admin.model') + ok_(view.blueprint is not None) + + client = app.test_client() + + # Make model view requests + rv = client.get('/admin/model/') + eq_(rv.status_code, 200) + + # Test model creation view + rv = client.get('/admin/model/add/') + eq_(rv.status_code, 200) + + rv = client.post('/admin/model/add/', + data=dict(col1='test1', col2='test2', col3='test3')) + eq_(rv.status_code, 302) + eq_(len(view.created_models), 1) + + model = view.created_models.pop() + eq_(model.id, 3) + eq_(model.col1, 'test1') + eq_(model.col2, 'test2') + eq_(model.col3, 'test3') + + # Try model edit view + rv = client.get('/admin/model/3/') + eq_(rv.status_code, 200) + ok_('test1' in rv.data) + + rv = client.post('/admin/model/3/', + data=dict(col1='test!', col2='test@', col3='test#')) + eq_(rv.status_code, 302) + eq_(len(view.updated_models), 1) + + model = view.updated_models.pop() + eq_(model.col1, 'test!') + eq_(model.col2, 'test@') + eq_(model.col3, 'test#') + + rv = client.get('/admin/modelview/4/') + eq_(rv.status_code, 404) + + # Attempt to delete model + rv = client.post('/admin/model/3/delete/', data=dict(confirm_delete=True)) + eq_(rv.status_code, 302) + eq_(rv.headers['location'], 'http://localhost/admin/model/') + + def test_permissions(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model) admin.add_view(view) @@ -205,7 +263,7 @@ def test_permissions(): def test_permissions_and_add_delete_buttons(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model) admin.add_view(view) @@ -247,7 +305,7 @@ def test_permissions_and_add_delete_buttons(): def test_templates(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model) admin.add_view(view) @@ -269,7 +327,7 @@ def test_templates(): def test_list_display_header(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model, list_display=['test_header']) admin.add_view(view) @@ -283,7 +341,7 @@ def test_list_display_header(): def test_search_fields(): - app, admin = setup() + app, admin = setup_admin() view = MockModelView(Model, search_fields=['col1', 'col2']) admin.add_view(view)