Commit 7a8433ae authored by bryhoyt's avatar bryhoyt

Merge pull request #5 from mrjoes/master

Merge from central repo into bryhoyt's fork
parents 48ebb0fc 0b81e59d
...@@ -84,6 +84,10 @@ if __name__ == '__main__': ...@@ -84,6 +84,10 @@ if __name__ == '__main__':
admin.add_link(NotAuthenticatedMenuLink(name='Login', admin.add_link(NotAuthenticatedMenuLink(name='Login',
endpoint='login_view')) endpoint='login_view'))
# Add links with categories
admin.add_link(MenuLink(name='Google', category='Links', url='http://www.google.com/'))
admin.add_link(MenuLink(name='Mozilla', category='Links', url='http://mozilla.org/'))
# Add logout link by endpoint # Add logout link by endpoint
admin.add_link(AuthenticatedMenuLink(name='Logout', admin.add_link(AuthenticatedMenuLink(name='Logout',
endpoint='logout_view')) endpoint='logout_view'))
......
from functools import wraps from functools import wraps
from re import sub
from flask import Blueprint, render_template, url_for, abort, g from flask import Blueprint, render_template, abort, g
from flask.ext.admin import babel from flask.ext.admin import babel
from flask.ext.admin._compat import with_metaclass from flask.ext.admin._compat import with_metaclass
from flask.ext.admin import helpers as h from flask.ext.admin import helpers as h
# For compatibility reasons import MenuLink
from flask.ext.admin.menu import MenuCategory, MenuView, MenuLink
def expose(url='/', methods=('GET',)): def expose(url='/', methods=('GET',)):
""" """
...@@ -211,7 +213,7 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -211,7 +213,7 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
# If name is not povided, use capitalized endpoint name # If name is not povided, use capitalized endpoint name
if self.name is None: if self.name is None:
self.name = self._prettify_name(self.__class__.__name__) self.name = self._prettify_class_name(self.__class__.__name__)
# Create blueprint and register rules # Create blueprint and register rules
self.blueprint = Blueprint(self.endpoint, __name__, self.blueprint = Blueprint(self.endpoint, __name__,
...@@ -253,14 +255,14 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -253,14 +255,14 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
return render_template(template, **kwargs) return render_template(template, **kwargs)
def _prettify_name(self, name): def _prettify_class_name(self, name):
""" """
Prettify a class name by splitting the name on capitalized characters. So, 'MySuperClass' becomes 'My Super Class' Split words in PascalCase string into separate words.
:param name: :param name:
String to prettify String to prettify
""" """
return sub(r'(?<=.)([A-Z])', r' \1', name) return h.prettify_class_name(name)
def is_visible(self): def is_visible(self):
""" """
...@@ -344,79 +346,6 @@ class AdminIndexView(BaseView): ...@@ -344,79 +346,6 @@ class AdminIndexView(BaseView):
return self.render(self._template) return self.render(self._template)
class MenuItem(object):
"""
Simple menu tree hierarchy.
"""
def __init__(self, name, view=None):
self.name = name
self._view = view
self._children = []
self._children_urls = set()
self._cached_url = None
self.url = None
if view is not None:
self.url = view.url
def add_child(self, view):
self._children.append(view)
self._children_urls.add(view.url)
def get_url(self):
if self._view is None:
return None
if self._cached_url:
return self._cached_url
self._cached_url = url_for('%s.%s' % (self._view.endpoint, self._view._default_view))
return self._cached_url
def is_active(self, view):
if view == self._view:
return True
return view.url in self._children_urls
def is_visible(self):
if self._view is None:
return False
return self._view.is_visible()
def is_accessible(self):
if self._view is None:
return False
return self._view.is_accessible()
def is_category(self):
return self._view is None
def get_children(self):
return [c for c in self._children if c.is_accessible() and c.is_visible()]
class MenuLink(object):
"""
Additional menu links.
"""
def __init__(self, name, url=None, endpoint=None):
self.name = name
self.url = url
self.endpoint = endpoint
def get_url(self):
return self.url or url_for(self.endpoint)
def is_visible(self):
return True
def is_accessible(self):
return True
class Admin(object): class Admin(object):
""" """
Collection of the admin views. Also manages menu structure. Collection of the admin views. Also manages menu structure.
...@@ -493,7 +422,8 @@ class Admin(object): ...@@ -493,7 +422,8 @@ class Admin(object):
# If app was provided in constructor, register view with Flask app # If app was provided in constructor, register view with Flask app
if self.app is not None: if self.app is not None:
self.app.register_blueprint(view.create_blueprint(self)) self.app.register_blueprint(view.create_blueprint(self))
self._add_view_to_menu(view)
self._add_view_to_menu(view)
def add_link(self, link): def add_link(self, link):
""" """
...@@ -502,26 +432,33 @@ class Admin(object): ...@@ -502,26 +432,33 @@ class Admin(object):
:param link: :param link:
Link to add. Link to add.
""" """
self._menu_links.append(link) if link.category:
self._add_menu_item(link, link.category)
else:
self._menu_links.append(link)
def _add_view_to_menu(self, view): def _add_menu_item(self, menu_item, target_category):
""" """
Add a view to the menu tree Add a view to the menu tree
:param view: :param view:
View to add View to add
""" """
if view.category: if target_category:
category = self._menu_categories.get(view.category) category = self._menu_categories.get(target_category)
if category is None: if category is None:
category = MenuItem(view.category) category = MenuCategory(target_category)
self._menu_categories[view.category] = category self._menu_categories[target_category] = category
self._menu.append(category) self._menu.append(category)
category.add_child(MenuItem(view.name, view)) category.add_child(menu_item)
else: else:
self._menu.append(MenuItem(view.name, view)) self._menu.append(menu_item)
def _add_view_to_menu(self, view):
self._add_menu_item(MenuView(view.name, view), view.category)
def init_app(self, app): def init_app(self, app):
""" """
...@@ -537,7 +474,6 @@ class Admin(object): ...@@ -537,7 +474,6 @@ class Admin(object):
# Register views # Register views
for view in self._views: for view in self._views:
app.register_blueprint(view.create_blueprint(self)) app.register_blueprint(view.create_blueprint(self))
self._add_view_to_menu(view)
def _init_extension(self): def _init_extension(self):
if not hasattr(self.app, 'extensions'): if not hasattr(self.app, 'extensions'):
......
...@@ -36,14 +36,15 @@ class InlineModelFormList(InlineFieldList): ...@@ -36,14 +36,15 @@ class InlineModelFormList(InlineFieldList):
def display_row_controls(self, field): def display_row_controls(self, field):
return field.get_pk() is not None return field.get_pk() is not None
def process(self, formdata, data=None): # *** bryhoyt removed def process() entirely, because I believe it was buggy
if not formdata: # (but worked because another part of the code had a complimentary bug)
attr = getattr(self.model, self.prop) # and I'm not sure why it was necessary anyway.
data = self.model.select().where(attr == data).execute() # If we want it back in, we need to fix the following bogus query:
else: # self.model.select().where(attr == data).execute() # `data` is not an ID, and only happened to be so because we patched it in in .contribute() below
data = None #
# For reference:
return super(InlineModelFormList, self).process(formdata, data) # .process() introduced in https://github.com/mrjoes/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709
# Fixed, brokenly I think, in https://github.com/mrjoes/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
pass pass
...@@ -234,7 +235,8 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -234,7 +235,8 @@ class InlineModelConverter(InlineModelConverterBase):
allow_pk=True, allow_pk=True,
converter=converter) converter=converter)
prop_name = 'fa_%s' % model.__name__
prop_name = reverse_field.related_name
label = self.get_label(info, prop_name) label = self.get_label(info, prop_name)
...@@ -246,10 +248,6 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -246,10 +248,6 @@ class InlineModelConverter(InlineModelConverterBase):
info, info,
label=label or info.model.__name__)) label=label or info.model.__name__))
setattr(field.rel_model,
prop_name,
property(lambda self: self.id))
return form_class return form_class
......
...@@ -116,7 +116,7 @@ class ModelView(BaseModelView): ...@@ -116,7 +116,7 @@ class ModelView(BaseModelView):
class MyModelView(ModelView): class MyModelView(ModelView):
inline_models = ((Post, dict(form_label='Hello'))) inline_models = ((Post, dict(form_label='Hello')))
2. Using target model name with `fa_` prefis: 2. Using field's related_name:
class Model1(Base): class Model1(Base):
# ... # ...
...@@ -124,11 +124,11 @@ class ModelView(BaseModelView): ...@@ -124,11 +124,11 @@ class ModelView(BaseModelView):
class Model2(Base): class Model2(Base):
# ... # ...
pass model1 = ForeignKeyField(related_name="model_twos")
class MyModel1View(Base): class MyModel1View(Base):
inline_models = (Model2,) inline_models = (Model2,)
column_labels = {'fa_Model2': 'Hello'} column_labels = {'model_ones': 'Hello'}
""" """
def __init__(self, model, name=None, def __init__(self, model, name=None,
......
...@@ -32,6 +32,7 @@ def get_primary_key(model): ...@@ -32,6 +32,7 @@ def get_primary_key(model):
pks.append(get_column_for_current_model(p).key) pks.append(get_column_for_current_model(p).key)
else: else:
pks.append(p.key) pks.append(p.key)
if len(pks) == 1: if len(pks) == 1:
return pks[0] return pks[0]
elif len(pks) > 1: elif len(pks) > 1:
......
...@@ -106,7 +106,7 @@ class ImageUploadInput(object): ...@@ -106,7 +106,7 @@ class ImageUploadInput(object):
if field.url_relative_path: if field.url_relative_path:
filename = urljoin(field.url_relative_path, filename) filename = urljoin(field.url_relative_path, filename)
return url_for(field.endpoint, filename=field.data) return url_for(field.endpoint, filename=filename)
# Fields # Fields
......
from re import sub
from jinja2 import contextfunction from jinja2 import contextfunction
from flask import g, request from flask import g, request
from wtforms.validators import DataRequired, InputRequired from wtforms.validators import DataRequired, InputRequired
...@@ -85,3 +86,13 @@ def get_render_ctx(): ...@@ -85,3 +86,13 @@ def get_render_ctx():
Get view template context. Get view template context.
""" """
return getattr(g, '_admin_render_ctx', None) return getattr(g, '_admin_render_ctx', None)
def prettify_class_name(name):
"""
Split words in PascalCase string into separate words.
:param name:
String to split
"""
return sub(r'(?<=.)([A-Z])', r' \1', name)
from flask import url_for
class BaseMenu(object):
"""
Base menu item
"""
def __init__(self, name):
self.name = name
self._children = []
def add_child(self, menu):
self._children.append(menu)
def get_url(self):
raise NotImplemented()
def is_category(self):
return False
def is_active(self, view):
for c in self._children:
if c.is_active(view):
return True
return False
def is_visible(self):
return True
def is_accessible(self):
return True
def get_children(self):
return [c for c in self._children if c.is_accessible() and c.is_visible()]
class MenuCategory(BaseMenu):
"""
Menu category item.
"""
def get_url(self):
return None
def is_category(self):
return True
def is_visible(self):
for c in self._children:
if c.is_visible():
return True
return False
def is_accessible(self):
for c in self._children:
if c.is_accessible():
return True
return False
class MenuView(BaseMenu):
"""
Admin view menu item
"""
def __init__(self, name, view=None):
super(MenuView, self).__init__(name)
self._view = view
self._cached_url = None
def get_url(self):
if self._view is None:
return None
if self._cached_url:
return self._cached_url
self._cached_url = url_for('%s.%s' % (self._view.endpoint, self._view._default_view))
return self._cached_url
def is_active(self, view):
if view == self._view:
return True
return super(MenuView, self).is_active(view)
def is_visible(self):
if self._view is None:
return False
return self._view.is_visible()
def is_accessible(self):
if self._view is None:
return False
return self._view.is_accessible()
class MenuLink(BaseMenu):
"""
Link item
"""
def __init__(self, name, url=None, endpoint=None, category=None):
super(MenuLink, self).__init__(name)
self.category = category
self.url = url
self.endpoint = endpoint
def get_url(self):
return self.url or url_for(self.endpoint)
...@@ -515,7 +515,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -515,7 +515,7 @@ class BaseModelView(BaseView, ActionsMixin):
# If name not provided, it is model name # If name not provided, it is model name
if name is None: if name is None:
name = '%s' % self.prettify_name(model.__name__) name = '%s' % self._prettify_class_name(model.__name__)
# If endpoint not provided, it is model name + 'view' # If endpoint not provided, it is model name + 'view'
if endpoint is None: if endpoint is None:
...@@ -640,7 +640,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -640,7 +640,7 @@ class BaseModelView(BaseView, ActionsMixin):
if self.column_labels and field in self.column_labels: if self.column_labels and field in self.column_labels:
return self.column_labels[field] return self.column_labels[field]
else: else:
return self.prettify_name(field) return self._prettify_name(field)
def get_list_columns(self): def get_list_columns(self):
""" """
...@@ -960,7 +960,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -960,7 +960,7 @@ class BaseModelView(BaseView, ActionsMixin):
raise NotImplemented() raise NotImplemented()
# Various helpers # Various helpers
def prettify_name(self, name): def _prettify_name(self, name):
""" """
Prettify pythonic variable name. Prettify pythonic variable name.
......
...@@ -262,11 +262,16 @@ def test_submenu(): ...@@ -262,11 +262,16 @@ def test_submenu():
eq_(admin._menu[1].name, 'Test') eq_(admin._menu[1].name, 'Test')
eq_(len(admin._menu[1]._children), 2) eq_(len(admin._menu[1]._children), 2)
# Categories don't have URLs and they're not accessible # Categories don't have URLs
eq_(admin._menu[1].get_url(), None) eq_(admin._menu[1].get_url(), None)
eq_(admin._menu[1].is_accessible(), False)
eq_(len(admin._menu[1].get_children()), 1) # Categories are only accessible if there is at least one accessible child
eq_(admin._menu[1].is_accessible(), True)
children = admin._menu[1].get_children()
eq_(len(children), 1)
ok_(children[0].is_accessible())
def test_delayed_init(): def test_delayed_init():
...@@ -354,3 +359,8 @@ def test_menu_links(): ...@@ -354,3 +359,8 @@ def test_menu_links():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('TestMenuLink1' in data) ok_('TestMenuLink1' in data)
ok_('TestMenuLink2' in data) ok_('TestMenuLink2' in data)
def check_class_name():
view = MockView()
eq_(view.name, 'Mock View')
...@@ -329,3 +329,11 @@ def test_custom_form(): ...@@ -329,3 +329,11 @@ def test_custom_form():
eq_(view._edit_form_class, TestForm) eq_(view._edit_form_class, TestForm)
ok_(not hasattr(view._create_form_class, 'col1')) ok_(not hasattr(view._create_form_class, 'col1'))
def check_class_name():
class DummyView(MockModelView):
pass
view = DummyView(Model)
eq_(view.name, 'Dummy View')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment