"""Breadcrumbs utility."""
from __future__ import annotations
from pyramid.request import Request
from ..helpers.literal import Literal
from .i18n import _
DEFAULT_ROOT_CHUNKS = 20
# =============================================================================
[docs]
class Breadcrumbs():
"""User breadcrumb trail, current title page and back URL management.
:type request: pyramid.request.Request
:param request:
Current request.
:param str sep: (optional)
Separator for brundcrumb trail.
This class uses session and stores its history in
``session['breadcrumbs']``. It is a list of crumbs. Each crumb is a tuple
such as ``(title, route_name, route_params, chunks_to_compare)``.
"""
# -------------------------------------------------------------------------
def __init__(self, request: Request, sep: str = ' ยป '):
"""Constructor method."""
self._request = request
self._sep = sep
# -------------------------------------------------------------------------
def __call__(
self,
title: str,
length: int = 10,
root_chunks: int = DEFAULT_ROOT_CHUNKS,
replace: str | None = None,
anchor: str | None = None,
forced_route: tuple | None = None,
compare_params: bool = False):
"""Add a crumb in breadcrumb trail.
:param str title:
Current page title.
:param int length: (default=10)
Maximum crumb number. If 0, it keeps the current length.
:param int root_chunks: (default=20)
Number of route path chunks to compare to highlight a menu entry.
:param str replace: (optional):
If current path is ``replace``, this method call :meth:`pop` before
any action.
:param str anchor: (optional)
Anchor to add.
:param tuple forced_route: (optional)
A tuple such as ``(route_name, route_params)`` to force the route.
:param bool compare_params: (default=False)
If ``True`` use route parameters to differentiate the current route
from those currently in breadcrumbs.
"""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# Environment
session = self._request.session
if 'breadcrumbs' not in session:
session['breadcrumbs'] = []
if not length:
length = len(session['breadcrumbs'])
# Replace
if replace and self.current_path() == replace:
self.pop()
# Scan old breadcrumb trail to find the right position
route_name = (forced_route and forced_route[0]) or \
(self._request.matched_route and self._request.matched_route.name)
if route_name is None:
session['breadcrumbs'].append((title, None, {}, root_chunks))
return
params = self._request.matchdict if forced_route is None \
else forced_route[1]
crumbs: list[tuple[str, str | None, dict, int]] = []
for crumb in session['breadcrumbs']:
if len(crumbs) >= length - 1 \
or (crumb[1] == route_name and not compare_params) \
or (crumb[1] == route_name and crumb[2] == params):
break
crumbs.append(crumb)
# Add new breadcrumb
if anchor is not None:
params['_anchor'] = anchor
crumbs.append((title, route_name, params, root_chunks))
session['breadcrumbs'] = crumbs
# -------------------------------------------------------------------------
[docs]
def is_empty(self) -> bool:
"""Check if it is necessary to display the breadcrumb trial."""
return 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2
# -------------------------------------------------------------------------
[docs]
def pop(self):
"""Pop last breadcrumb."""
session = self._request.session
if 'breadcrumbs' in session and len(session['breadcrumbs']) > 1:
session['breadcrumbs'] = session['breadcrumbs'][0:-1]
# -------------------------------------------------------------------------
[docs]
def trail(self, without_last: bool = False) -> str:
"""Output XHTML breadcrumb trail.
:param bool without_last: (default = False)
If ``True``, remove the last chunk.
:rtype: str
"""
if self.is_empty():
return Literal(' ')
translate = self._request.localizer.translate
crumbs = []
for crumb in self._request.session['breadcrumbs'][0:-1]:
if crumb[1] is not None:
crumbs.append(
'<a href="{path}">{label}</a>'.format(
path=self._request.route_path(crumb[1], **crumb[2]),
label=translate(crumb[0])))
else:
crumbs.append(translate(crumb[0]))
if not without_last:
crumbs.append(
translate(self._request.session['breadcrumbs'][-1][0]))
return Literal(self._sep.join(crumbs))
# -------------------------------------------------------------------------
[docs]
def crumb_trail(self) -> list:
"""Return a trail of crumbs to compare with menu entries. Each crumb
is tuple of a list of path chunks and a comparison length.
:rtype: list
:return:
A list of crumbs. Each crumb is a tuple such as
``([chunk1, chunk2,...], root_chunks)`` where ``root_chunks`` is a
number of route path chunks to compare to highlight a menu entry
"""
if self._request.matched_route is None:
return []
current = (
self._request.current_route_path().partition('?')[0].split('/')
[1:DEFAULT_ROOT_CHUNKS + 1], DEFAULT_ROOT_CHUNKS)
if 'breadcrumbs' not in self._request.session:
return [current]
crumbs = []
for crumb in self._request.session['breadcrumbs']:
if crumb[1] is not None:
path = self._request.route_path(crumb[1],
**crumb[2]).split('/')
crumbs.append((path[1:crumb[3] + 1], crumb[3]))
if not crumbs or crumbs[-1][0] != current[0]:
crumbs.append(current)
return crumbs
# -------------------------------------------------------------------------
[docs]
def current_title(self) -> str:
"""Title of current page.
:rtype: str
"""
if 'breadcrumbs' not in self._request.session \
or not self._request.session['breadcrumbs'] \
or not self._request.session['breadcrumbs'][-1][1]:
return self._request.localizer.translate(_('Home'))
return self._request.localizer.translate(
self._request.session['breadcrumbs'][-1][0])
# -------------------------------------------------------------------------
[docs]
def current_route_name(self) -> str:
"""Route name of current page.
:rtype: str
"""
if 'breadcrumbs' not in self._request.session \
or not self._request.session['breadcrumbs'] \
or not self._request.session['breadcrumbs'][-1][1]:
return self._request.session.get('home') or 'home'
return self._request.session['breadcrumbs'][-1][1]
# -------------------------------------------------------------------------
[docs]
def current_path(self) -> str:
"""Path of current page.
:rtype: str
"""
if 'breadcrumbs' not in self._request.session \
or not self._request.session['breadcrumbs'] \
or not self._request.session['breadcrumbs'][-1][1]:
return self._request.route_path(
self._request.session.get('home') or 'home')
return self._request.route_path(
self._request.session['breadcrumbs'][-1][1],
**self._request.session['breadcrumbs'][-1][2])
# -------------------------------------------------------------------------
[docs]
def back_title(self) -> str:
"""Output title of previous page.
:rtype: str
"""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2 \
or not self._request.session['breadcrumbs'][-2][1]:
return self._request.localizer.translate(_('Home'))
return self._request.localizer.translate(
self._request.session['breadcrumbs'][-2][0])
# -------------------------------------------------------------------------
[docs]
def back_path(self) -> str:
"""Output the path of previous page.
:rtype: str
"""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2 \
or not self._request.session['breadcrumbs'][-2][1]:
return self._request.route_path(
self._request.session.get('home') or 'home')
return self._request.route_path(
self._request.session['breadcrumbs'][-2][1],
**self._request.session['breadcrumbs'][-2][2])