Skip to content

Commit

Permalink
Merge 9ffba61 into 0698254
Browse files Browse the repository at this point in the history
  • Loading branch information
nickw444 committed Apr 30, 2015
2 parents 0698254 + 9ffba61 commit b14dd97
Show file tree
Hide file tree
Showing 5 changed files with 576 additions and 6 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -13,3 +13,4 @@ Contact us at `info@invenio-software.org <mailto:info@invenio-software.org>`_
* Eirini Psallida <eirini.psallida@cern.ch>
* Florian Merges <fmerges@fstarter.org>
* Marco Neumann <marco@crepererum.net>
* Nick Whyte <nick@nickwhyte.com>
46 changes: 46 additions & 0 deletions docs/index.rst
Expand Up @@ -207,6 +207,47 @@ with 3 items while processing a request for `/social/list`.
... assert current_menu.submenu('account.list').active
... current_menu.children
Flask-Classy
============

Flask-Classy is a library commonly used in Flask development and gives
additional structure to apps which already make use of blueprints.

Using Flask-Menu with Flask-Classy is rather simple:

.. code-block:: python
from flask_classy import FlaskView
import flask_menu as menu
class MyEndpoint(FlaskView):
route_base = '/'
@menu.classy_menu_item('frontend.account', 'Home', order=0)
def index(self):
# Do something.
pass
As you can see, instead of using the `@menu.register_menu` decorator, we use
classy_menu_item. All usage is otherwise the same to `register_menu`, however
you do not need to provide reference to the blueprint/app.

You do have to register the entire class with flask-menu at runtime however.

.. code-block:: python
import flask_menu as menu
from MyEndpoint import MyEndpoint
from flask import Blueprint
bp = Blueprint('bp', __name__)
MyEndpoint.register(bp)
menu.register_flaskview(bp, MyEndpoint)
.. _api:

API
Expand All @@ -226,11 +267,16 @@ Flask extension
.. autoclass:: MenuEntryMixin
:members:

.. autofunction:: register_flaskview


Decorators
^^^^^^^^^^

.. autofunction:: register_menu

.. autofunction:: classy_menu_item

Proxies
^^^^^^^

Expand Down
143 changes: 141 additions & 2 deletions flask_menu/__init__.py
Expand Up @@ -15,7 +15,7 @@
import inspect
import types

from flask import Blueprint, current_app, request, url_for
from flask import Blueprint, current_app, request, url_for, g

from werkzeug.local import LocalProxy

Expand Down Expand Up @@ -54,6 +54,12 @@ def init_app(self, app):
app.context_processor(lambda: dict(
current_menu=current_menu))

@app.url_value_preprocessor
def url_preprocessor(route, args):
"""Store the current route endpoint and arguments."""
g._menu_kwargs = args
g._menu_route = route

@staticmethod
def root():
"""Return a root entry of current application's menu."""
Expand All @@ -79,6 +85,7 @@ def __init__(self, name, parent):
self._endpoint_arguments_constructor = None
self._dynamic_list_constructor = None
self._visible_when = CONDITION_TRUE
self._expected_args = []

def _active_when(self):
"""Define condition when a menu entry is active."""
Expand All @@ -90,6 +97,7 @@ def _active_when(self):

def register(self, endpoint, text, order=0,
endpoint_arguments_constructor=None,
expected_args=[],
dynamic_list_constructor=None,
active_when=None,
visible_when=None,
Expand All @@ -98,6 +106,7 @@ def register(self, endpoint, text, order=0,
self._endpoint = endpoint
self._text = text
self._order = order
self._expected_args = expected_args
self._endpoint_arguments_constructor = endpoint_arguments_constructor
self._dynamic_list_constructor = dynamic_list_constructor
if active_when is not None:
Expand Down Expand Up @@ -217,7 +226,17 @@ def url(self):
if self._endpoint_arguments_constructor:
return url_for(self._endpoint,
**self._endpoint_arguments_constructor())
return url_for(self._endpoint)

# Inject current args. Allows usage when inside a blueprint with a url
# param.
# Filter out any arguments which don't need to be passed.
args = {}
if hasattr(g, '_menu_kwargs') and g._menu_kwargs:
for key in g._menu_kwargs:
if key in self._expected_args:
args[key] = g._menu_kwargs[key]

return url_for(self._endpoint, **args)

@property
def active(self):
Expand All @@ -240,6 +259,19 @@ def has_active_child(self, recursive=True):
return True
return False

def has_visible_child(self, recursive=True):
result = False
for child in self._child_entries.values():
if child.visible:
return True

if recursive:
for child in self._child_entries.values():
if child.visible or child.has_visible_child(recursive=True):
return True

return False


def register_menu(app, path, text, order=0,
endpoint_arguments_constructor=None,
Expand Down Expand Up @@ -289,6 +321,8 @@ def menu_decorator(f):
endpoint = f.__name__
before_first_request = app.before_first_request

expected = _get_true_argspec(f).args

@before_first_request
def _register_menu_item():
# str(path) allows path to be a string-convertible object
Expand All @@ -298,6 +332,7 @@ def _register_menu_item():
endpoint,
text,
order,
expected_args=expected,
endpoint_arguments_constructor=endpoint_arguments_constructor,
dynamic_list_constructor=dynamic_list_constructor,
active_when=active_when,
Expand All @@ -308,6 +343,110 @@ def _register_menu_item():
return menu_decorator


def register_flaskview(app, classy_view):
"""Register a Flask-Classy FlaskView's menu items with the menu register.
Example::
bp = Blueprint('bp', __name__)
menu.register_flaskview(bp, MyEndpoint)
:param app: Application or Blueprint which owns the
function view.
:param classy_view: The Flask-Classy FlaskView class to register
menu items for.
"""
if isinstance(app, Blueprint):
endpoint_prefix = app.name + '.'
before_first_request = app.before_app_first_request
else:
endpoint_prefix = ''
before_first_request = app.before_first_request

@before_first_request
def _register_menu_items():
for meth_str in dir(classy_view):
meth = getattr(classy_view, meth_str)

if hasattr(meth, '_menu_items'):
for menu_item in meth._menu_items:
endpoint = "{0}{1}:{2}".format(
endpoint_prefix,
classy_view.__name__,
meth.__name__
)
path = menu_item.pop('path')
item = current_menu.submenu(path)
item.register(
endpoint,
**menu_item
)


def classy_menu_item(path, text, **kwargs):
"""Register an endpoint within a Flask-Classy class.
All usage is otherwise the same to `register_menu`, however you do not need
to provide reference to the blueprint/app.
Example::
class MyEndpoint(FlaskView):
route_base = '/'
@menu.classy_menu_item('frontend.account', 'Home', order=0)
def index(self):
# Do something.
pass
:param path: Path to this item in menu hierarchy,
for example 'main.category.item'. Path can be an object
with custom __str__ method: it will be converted on first request,
therefore you can use current_app inside this __str__ method.
:param text: Text displayed as link.
:param order: Index of item among other items in the same menu.
:param endpoint_arguments_constructor: Function returning dict of
arguments passed to url_for when creating the link.
:param active_when: Function returning True when the item
should be displayed as active.
:param visible_when: Function returning True when this item
should be displayed.
:param dynamic_list_constructor: Function returning a list of
entries to be displayed by this item. Every object should
have 'text' and 'url' properties/dict elements. This property
will not be directly affect the menu system, but allows
other systems to use it while rendering.
:param kwargs: Additional arguments will be available as attributes
on registered :class:`MenuEntryMixin` instance.
.. versionchanged:: 0.2.0
The *kwargs* arguments.
"""
def func_wrap(f):
expected = _get_true_argspec(f).args
if 'self' in expected:
expected.remove('self')
item = dict(path=path, text=text, expected_args=expected, **kwargs)

if hasattr(f, '_menu_items'):
f._menu_items.append(item)
else:
f._menu_items = [item]

return f

return func_wrap


def _get_true_argspec(method):
"""Locate the argspec of a method.
Looks recursively through deeper wrapped methods to find the true
arguments the method accepts.
"""
argspec = inspect.getargspec(method)
return argspec

#: Global object that is proxy to the current application menu.
current_menu = LocalProxy(Menu.root)

Expand Down
4 changes: 3 additions & 1 deletion setup.py
Expand Up @@ -52,7 +52,8 @@ def run_tests(self):
'pytest-cov>=1.8.0',
'pytest-pep8>=1.0.6',
'pytest>=2.6.1',
'coverage<4.0a1'
'coverage<4.0a1',
'flask-classy>=0.6.10'
]

setup(
Expand All @@ -75,6 +76,7 @@ def run_tests(self):
],
extras_require={
'docs': ['sphinx'],
'classy': ['flask-classy>=0.6.10'],
},
tests_require=tests_require,
cmdclass={'test': PyTest},
Expand Down

0 comments on commit b14dd97

Please sign in to comment.