"""Navigation help for Flask applications."""
from copy import deepcopy
from functools import partial, total_ordering
import os
import pkg_resources
import sys
from flask import url_for
from sortedcontainers import SortedList
from werkzeug.routing import Rule
__all__ = ('Copilot',)
try:
_dist = pkg_resources.get_distribution(__package__)
if not __file__.startswith(os.path.join(_dist.location, __package__)):
# Manually raise the exception if there is a distribution but
# it's installed from elsewhere.
raise pkg_resources.DistributionNotFound
except pkg_resources.DistributionNotFound:
__version__ = 'development'
else:
__version__ = _dist.version
MODERN_PYTHON = sys.version_info.major >= 3
if MODERN_PYTHON:
basestring = str
[docs]class Copilot(object):
"""The Flask-Copilot extension.
Args:
app (Optional[flask.Flask]): The application instance that this
copilot should be registered to.
"""
def __init__(self, app=None):
"""Initialize the extension."""
self.navbar_entries = SortedList()
if app is not None:
self.init_app(app)
[docs] def init_app(self, app):
"""Register the extension with the application.
Args:
app (flask.Flask): The application to register with.
"""
app.url_rule_class = partial(NavigationRule, copilot=self)
app.context_processor(self.inject_context)
[docs] def inject_context(self):
"""Return a dict used for a template context."""
navbar = filter(lambda entry: entry.visible, self.navbar_entries)
return {'navbar': navbar}
[docs] def register_entry(self, navbar_kwargs):
"""Register a navbar entry with the copilot.
Args:
navbar_kwargs (dict): Arguments passed to the
:class:`NavbarEntry` instance.
"""
# Add a new rule for each level in the path.
path = navbar_kwargs.pop('path')
# If a single object is used rather than an iterable (including
# a single string), wrap it before using.
if not hasattr(path, '__iter__') or isinstance(path, basestring):
path = [path]
entry_group = self.navbar_entries
# HACK: I'd like to intelligently replace the URL rule in the
# case where the intended rule is provided, but the function has
# already created a blank "placeholder" rule for it. There are
# probably nicer ways to approach this, but it works.
for name, is_last in iter_islast(path):
kwargs = deepcopy(navbar_kwargs)
kwargs['name'] = name
for existing_entry in entry_group:
# If there's an existing entry for this "link", use it
# instead of creating a new one. If this existing entry
# has no rule and this is the last item in ``path``, the
# rule was intended to be assigned to this entry, so
# overwrite the blank rule with the one provided via
# ``navbar_kwargs``.
if existing_entry.name == name:
entry = existing_entry
if is_last:
entry.endpoint = kwargs['endpoint']
break
else:
# If we can't find an existing entry, create one with a
# blank endpoint. If this rule is not the final one in
# the list, the endpoint was not intended for this, so
# don't assign it.
if not is_last:
kwargs['endpoint'] = None
entry = NavbarEntry(**kwargs)
entry_group.add(entry)
entry_group = entry.children
@total_ordering
[docs]class NavbarEntry(object):
"""Stores information related to a routing rule's navbar position.
Args:
name (str): The text of the link in the navbar. This element is
converted to a ``str`` when added to the navbar and used for
display and sorting purposes. This value should be unique
within a group (i.e. a single name should not have more than
one URL rule associated with it).
endpoint (str): The endpoint assigned to the URL rule this
``NavbarEntry`` belongs to. This should be used to generate
the URL for the link.
url_for_kwargs (dict): Keyword arguments to be passed to
``url_for`` when generating URLs from ``endpoint``.
order: An object used to sort the navigation entries. If not
provided, the entry's name (the last element of ``path``)
will be used. All entries in the same group must be
orderable together.
when (Optional[callable]): If provided, the link will appear
only when the value returned by ``when()`` is truthy.
"""
def __init__(self, name, endpoint,
url_for_kwargs=None, order=None, when=None):
"""Initialize the instance."""
# Set the real defaults.
if url_for_kwargs is None:
url_for_kwargs = {}
if order is None:
order = name
# Store the information needed to generate the navbar links.
self.name = str(name)
self.endpoint = endpoint
self.url_for_kwargs = url_for_kwargs
self.children = SortedList()
self.order = order
self.when = when
def __lt__(self, other):
"""Return sort order based on ``self.order`` and ``other.order``."""
return self.order < other.order
@property
def visible(self):
"""Return ``True`` when this entry should be visible.
Rules dictating visibility, in order or precedence:
1. If an entry has a ``when`` method, its result is returned.
2. If an entry has children and no endpoint (i.e. it exists
solely as a container for other links), it is visible when
any of its children are.
3. Finally, when the first two rules don't apply, visibility
defaults to ``True``.
"""
if self.when:
return self.when()
elif self.children and not self.endpoint:
return any(self.visible_children)
return True
@property
def visible_children(self):
"""Return visible children."""
return filter(lambda child: child.visible, self.children)
[docs] def url(self, default_href='#'):
"""Return a rendered URL for this entry.
Args:
default_href (str): The value to return if no endpoint is
assigned to this entry. Defaults to ``'#'``.
"""
if not self.endpoint:
return default_href
return url_for(self.endpoint, **self.url_for_kwargs)
class NavigationRule(Rule):
"""Store navigation information along with routing rules.
Args:
*args (list): Positional arguments passed to the underlying
`werkzeug.routing.Rule` object.
**kwargs (dict): Keyword arguments passed to the underlying
:class:`werkzeug.routing.Rule` object.
copilot (Copilot): The Flask-Copilot instance that this rule's
navbar entry will be registered with. This argument must be
passed as a keyword argument.
navbar_kwargs (Optional[dict]): Keyword args passed to
:class:`NavbarEntry`. This argument must be passed as a
keyword argument.
"""
def __init__(self, *args, **kwargs):
"""Initialize the rule."""
copilot = kwargs.pop('copilot')
navbar_kwargs = kwargs.pop('navbar_kwargs', None)
super(NavigationRule, self).__init__(*args, **kwargs)
if navbar_kwargs:
navbar_kwargs['endpoint'] = self.endpoint
copilot.register_entry(navbar_kwargs)
# NOTE: Taken and slightly modified from the ActiveState Code Recipe at
# https://code.activestate.com/recipes/392015-finding-the-last-item-in-a-loop/
def iter_islast(iterable):
"""Generate (item, islast) pairs for an iterable.
Generates pairs where the first element is an item from the iterable
source and the second element is a boolean flag indicating if it is
the last item in the sequence.
"""
it = iter(iterable)
prev = next(it)
for item in it:
yield prev, False
prev = item
yield prev, True