Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionnal endpoint namespace support #120

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion doc/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
-----------

Expand Down
12 changes: 11 additions & 1 deletion flask_superadmin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions flask_superadmin/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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)
Expand Down
74 changes: 66 additions & 8 deletions flask_superadmin/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down