211 lines
6.8 KiB
Python
211 lines
6.8 KiB
Python
|
import collections
|
||
|
|
||
|
from flask import url_for, request, Markup
|
||
|
|
||
|
from .utils import freeze_dict, join_html_attrs
|
||
|
|
||
|
|
||
|
class Item(object):
|
||
|
"""The navigation item object.
|
||
|
|
||
|
:param label: the display label of this navigation item.
|
||
|
:param endpoint: the unique name of this navigation item.
|
||
|
If this item point to a internal url, this parameter
|
||
|
should be acceptable for ``url_for`` which will generate
|
||
|
the target url.
|
||
|
:param args: optional. If this parameter be provided, it will be passed to
|
||
|
the ``url_for`` with ``endpoint`` together.
|
||
|
Maybe this arguments need to be decided in the Flask app
|
||
|
context, then this parameter could be a function to delay the
|
||
|
execution.
|
||
|
:param url: optional. If this parameter be provided, the target url of
|
||
|
this navigation will be it. The ``endpoint`` and ``args`` will
|
||
|
not been used to generate url.
|
||
|
:param html_attrs: optional. This :class:`dict` will be used for
|
||
|
representing html.
|
||
|
|
||
|
The ``endpoint`` is the identity name of this navigation item. It will be
|
||
|
unique in whole application. In mostly situation, it should be a endpoint
|
||
|
name of a Flask view function.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, label, endpoint, args=None, url=None, html_attrs=None,
|
||
|
items=None):
|
||
|
self.label = label
|
||
|
self.endpoint = endpoint
|
||
|
self._args = args
|
||
|
self._url = url
|
||
|
self.html_attrs = {} if html_attrs is None else html_attrs
|
||
|
self.items = ItemCollection(items or None)
|
||
|
|
||
|
def __html__(self):
|
||
|
attrs = dict(self.html_attrs)
|
||
|
|
||
|
# adds ``active`` to class list
|
||
|
html_class = attrs.get('class', [])
|
||
|
if self.is_active:
|
||
|
html_class.append('active')
|
||
|
|
||
|
# joins class list
|
||
|
attrs['class'] = ' '.join(html_class)
|
||
|
if not attrs['class']:
|
||
|
del attrs['class']
|
||
|
attrs['href'] = self.url
|
||
|
attrs_template, attrs_values = join_html_attrs(attrs)
|
||
|
|
||
|
return Markup('<a %s>{label}</a>' % attrs_template).format(
|
||
|
*attrs_values, label=self.label)
|
||
|
|
||
|
def __html_format__(self, format_spec):
|
||
|
if format_spec == 'li':
|
||
|
li_attrs = Markup(' class="active"') if self.is_active else ''
|
||
|
return Markup('<li{0}>{1}</li>').format(li_attrs, self.__html__())
|
||
|
elif format_spec:
|
||
|
raise ValueError('Invalid format spec')
|
||
|
return self.__html__()
|
||
|
|
||
|
@property
|
||
|
def args(self):
|
||
|
"""The arguments which will be passed to ``url_for``.
|
||
|
|
||
|
:type: :class:`dict`
|
||
|
"""
|
||
|
if self._args is None:
|
||
|
return {}
|
||
|
if callable(self._args):
|
||
|
return dict(self._args())
|
||
|
return dict(self._args)
|
||
|
|
||
|
@property
|
||
|
def url(self):
|
||
|
"""The final url of this navigation item.
|
||
|
|
||
|
By default, the value is generated by the :attr:`self.endpoint` and
|
||
|
:attr:`self.args`.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
The :attr:`url` property require the app context without a provided
|
||
|
config value :const:`SERVER_NAME`, because of :func:`flask.url_for`.
|
||
|
|
||
|
:type: :class:`str`
|
||
|
"""
|
||
|
if self.is_internal:
|
||
|
return url_for(self.endpoint, **self.args)
|
||
|
return self._url
|
||
|
|
||
|
@property
|
||
|
def is_active(self):
|
||
|
"""``True`` if the item should be presented as active, and ``False``
|
||
|
always if the request context is not bound.
|
||
|
"""
|
||
|
return bool(request and self.is_current)
|
||
|
|
||
|
@property
|
||
|
def is_internal(self):
|
||
|
"""``True`` if the target url is internal of current app."""
|
||
|
return self._url is None
|
||
|
|
||
|
@property
|
||
|
def is_current(self):
|
||
|
"""``True`` if current request has same endpoint with the item.
|
||
|
|
||
|
The property should be used in a bound request context, or the
|
||
|
:class:`RuntimeError` may be raised.
|
||
|
"""
|
||
|
if not self.is_internal:
|
||
|
return False # always false for external url
|
||
|
has_same_endpoint = (request.endpoint == self.endpoint)
|
||
|
has_same_args = (request.view_args == self.args)
|
||
|
return has_same_endpoint and has_same_args # matches the endpoint
|
||
|
|
||
|
@property
|
||
|
def ident(self):
|
||
|
"""The identity of this item.
|
||
|
|
||
|
:type: :class:`~flask.ext.navigation.Navigation.ItemReference`
|
||
|
"""
|
||
|
return ItemReference(self.endpoint, self.args)
|
||
|
|
||
|
|
||
|
class ItemCollection(collections.MutableSequence,
|
||
|
collections.Iterable):
|
||
|
"""The collection of navigation items.
|
||
|
|
||
|
This collection is a mutable sequence. All items have order index, and
|
||
|
could be found by its endpoint name. e.g.::
|
||
|
|
||
|
c = ItemCollection()
|
||
|
c.append(Item(endpoint='doge'))
|
||
|
|
||
|
print(c['doge']) # output: Item(endpoint='doge')
|
||
|
print(c[0]) # output: Item(endpoint='doge')
|
||
|
print(c) # output: ItemCollection([Item(endpoint='doge')])
|
||
|
print(len(c)) # output: 1
|
||
|
|
||
|
c.append(Item(endpoint='lumpy', args={'num': 4}))
|
||
|
|
||
|
print(c[1]) # output: Item(endpoint='lumpy', args={'num': 4})
|
||
|
assert c['lumpy', {'num': 4}] is c[1]
|
||
|
"""
|
||
|
|
||
|
def __init__(self, iterable=None):
|
||
|
#: the item collection
|
||
|
self._items = []
|
||
|
#: the mapping collection of endpoint -> item
|
||
|
self._items_mapping = {}
|
||
|
#: initial extending
|
||
|
self.extend(iterable or [])
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'ItemCollection(%r)' % self._items
|
||
|
|
||
|
def __getitem__(self, index):
|
||
|
if isinstance(index, int):
|
||
|
return self._items[index]
|
||
|
|
||
|
if isinstance(index, tuple):
|
||
|
endpoint, args = index
|
||
|
else:
|
||
|
endpoint, args = index, {}
|
||
|
ident = ItemReference(endpoint, args)
|
||
|
return self._items_mapping[ident]
|
||
|
|
||
|
def __setitem__(self, index, item):
|
||
|
# remove the old reference
|
||
|
old_item = self._items[index]
|
||
|
del self._items_mapping[old_item.ident]
|
||
|
|
||
|
self._items[index] = item
|
||
|
self._items_mapping[item.ident] = item
|
||
|
|
||
|
def __delitem__(self, index):
|
||
|
item = self[index]
|
||
|
del self._items[index]
|
||
|
del self._items_mapping[item.ident]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._items)
|
||
|
|
||
|
def __iter__(self):
|
||
|
return iter(self._items)
|
||
|
|
||
|
def insert(self, index, item):
|
||
|
self._items.insert(index, item)
|
||
|
self._items_mapping[item.ident] = item
|
||
|
|
||
|
|
||
|
class ItemReference(collections.namedtuple('ItemReference', 'endpoint args')):
|
||
|
"""The identity tuple of navigation item.
|
||
|
|
||
|
:param endpoint: the endpoint of view function.
|
||
|
:type endpoint: ``str``
|
||
|
:param args: the arguments of view function.
|
||
|
:type args: ``dict``
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, endpoint, args=()):
|
||
|
if isinstance(args, dict):
|
||
|
args = freeze_dict(args)
|
||
|
return super(cls, ItemReference).__new__(cls, endpoint, args)
|