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('{label}' % 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('{1}').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)