Source code for feretui.menus

# This file is a part of the FeretUI project
#
#    Copyright (C) 2024 Jean-Sebastien SUZANNE <js.suzanne@gmail.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file,You can
# obtain one at http://mozilla.org/MPL/2.0/.
"""Menu mecanism.

Define the menu class to display its and define the actions call when the
menus are clicked.

The menus are splited in Three groups.

* Toolbar menu

  * :class:`.ToolBarMenu`
  * :class:`.ToolBarDropDownMenu`
  * :class:`.ToolBarUrlMenu`
  * :class:`.ToolBarDividerMenu`
  * :class:`.ToolBarButtonMenu`
  * :class:`.ToolBarButtonsMenu`
  * :class:`.ToolBarButtonUrlMenu`

* Aside: The menu is display in the aside-menu page

  * :class:`.AsideMenu`
  * :class:`.AsideHeaderMenu`
  * :class:`.AsideUrlMenu`

* Sitemap: Only for the sitemap page

  * :class:`.SitemapMenu`

::

    myferet.register_aside_menus('aside1', [
        AsideHeaderMenu('Menu A1', children=[
            AsideMenu('Sub Menu A11', page='submenu11'),
            AsideMenu('Sub Menu A12', page='submenu12'),
        ]),
    ])
    myferet.register_aside_menus('aside2', [
        AsideHeaderMenu('Menu A2', children=[
            AsideMenu('Sub Menu A21', page='submenu21'),
            AsideMenu('Sub Menu A22', page='submenu22'),
        ]),
    ])
    myferet.register_toolbar_left_menus([
        ToolBarDropDownMenu('Menu Tb1', children=[
            ToolBarMenu(
                'Menu Tb11', page="aside-menu", aside="aside1",
                aside_page='submenu11',
            ),
            ToolBarDividerMenu(),
            ToolBarMenu(
                'Menu Tb12', page="aside-menu", aside="aside2",
                aside_page='submenu22',
            ),
        ]),
        ToolBarMenu('Menu Tb2', page="my-page"),
    ])


Helper exist to compute the visibility:

* :func:`feretui.helper.menu_for_authenticated_user`
* :func:`feretui.helper.menu_for_unauthenticated_user`

::

    myferet.register_toolbar_left_menus([
        ToolBarDropDownMenu('Menu Tb1', children=[
            ToolBarMenu(
                'Menu Tb11', page="aside-menu", aside="aside1",
                aside_page='submenu11',
            ),
            ToolBarDividerMenu(),
            ToolBarMenu(
                'Menu Tb12', page="aside-menu", aside="aside2",
                aside_page='submenu22',
            ),
        ], visible_callback=menu_for_authenticated_user
        ),
        ToolBarMenu(
            'Menu Tb2',
            page="my-page",
            visible_callback=menu_for_unauthenticated_user
        ),
    ])


"""

from collections.abc import Callable
from typing import TYPE_CHECKING

from markupsafe import Markup

from feretui.context import ContextProperties
from feretui.exceptions import MenuError
from feretui.helper import menu_for_authenticated_user
from feretui.session import Session

if TYPE_CHECKING:
    from feretui.feretui import FeretUI






[docs] class ChildrenMenu: """Mixin children class. This mixin add children behaviour for: * :class:`.ToolBarDropDownMenu` * :class:`.AsideHeaderMenu` """ def __init__(self: "ChildrenMenu", children: list[Menu]) -> None: """Initialize the children. :param children: The list of the children :type children: list[:class:`.Menu` """ if not children: raise MenuError(f"{self.__class__.__name__} must have children") self.children = children
[docs] def render(self: "Menu", feretui: "FeretUI", session: Session) -> str: """Return the html of the menu. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param session: The session of the user :type session: :class:`feretui.session.Session` :return: The html :rtype: str """ return Markup.unescape( feretui.render_template( session, self.template_id, label=self.get_label(feretui, session), description=self.get_description(feretui, session), icon=self.icon, children=self.children, ), )
[docs] def is_visible(self: "Menu", session: Session) -> bool: # noqa: ARG002 """Return True if the menu can be rendering. :param session: The session of the user :type session: :class:`feretui.session.Session` :return: True :rtype: bool """ if self.visible_callback and not self.visible_callback(session): return False return all(child.is_visible(session) for child in self.children)
[docs] class UrlMenu: """Mixin class for give an external url."""
[docs] def get_url( self: "Menu", feretui: "FeretUI", # noqa: ARG002 querystring: dict[str, str], ) -> str: """Return the external url from the querystring. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param querystring: The querysting to pass at the api :type querysting: dict[str, str] :return: The url :rtype: str """ return querystring["url"]
[docs] class ToolBarMenu(Menu): """Menu class for the toolbar. :: menu = ToolBarMenu('My label') if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-menu" def __init__( self: "ToolBarMenu", label: str, **kwargs: dict[str, str], ) -> None: """Call the Menu constructor and update the context. see :class:`.Menu` """ super().__init__(label, **kwargs) self.context = "menu:toolbar:" + ":".join( f"{key}:{value}" for key, value in self.querystring.items() )
[docs] class ToolBarDropDownMenu(ChildrenMenu, ToolBarMenu): """DropDown for toolbar. :: menu = ToolBarDropDownMenu( 'Label', children=[ToolBarMenu('My label')]) if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-dropdown-menu" def __init__( self: "ToolBarDropDownMenu", label: str, children: list[ToolBarMenu] = None, **kwargs: dict[str, str], ) -> None: """Construct the dropdown menu. Inherits of ToolbarMenu and ChildrenMenu """ kwargs.setdefault("visible_callback", None) ToolBarMenu.__init__(self, label, type="dropdown", **kwargs) ChildrenMenu.__init__(self, children) for child in children: if isinstance(child, ToolBarDropDownMenu): raise MenuError("ToolBarDropDownMenu menu can not be cascaded")
[docs] class ToolBarDividerMenu(ToolBarMenu): """Simple Divider.""" template_id: str = "toolbar-divider-menu" def __init__( self: "ToolBarDividerMenu", visible_callback: Callable = menu_for_authenticated_user, ) -> None: """Separate two menu in DropDown menu.""" self.context = "" self.visible_callback = visible_callback
[docs] def render( self: "ToolBarDividerMenu", feretui: "FeretUI", session: Session, ) -> str: """Return the html of the menu. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param session: The session of the user :type session: :class:`feretui.session.Session` :return: The html :rtype: str """ return Markup.unescape( feretui.render_template(session, self.template_id), )
[docs] class ToolBarUrlMenu(UrlMenu, ToolBarMenu): """Menu class to add a link to another web api. :: menu = ToolBarUrlMenu('My label', url="https://bulma.io") if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-url-menu" def __init__( self: "ToolBarUrlMenu", label: str, url: str, **kw: dict[str, str], ) -> None: """Call the menu constructor and update the context. see :class:`.menu` :param label: the label of the menu :type label: str :param url: the http url :type url: str :param icon: the icon html class used in the render :type icon: str :param description: the description, it is a helper to understand the role of the menu :type description: str """ super().__init__(label, url=url, **kw)
[docs] class ToolBarButtonMenu(Menu): """Menu class for the toolbar. :: menu = ToolBarButtonMenu('My label') if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-button-menu" def __init__( self: "ToolBarButtonMenu", label: str, css_class: str = None, **kwargs: dict[str, str], ) -> None: """Call the Menu constructor and update the context. see :class:`.Menu` :param css_class: CCS class name to add at the button :type css_class: str """ super().__init__(label, **kwargs) self.css_class = css_class self.context = "menu:toolbar:button:" + ":".join( f"{key}:{value}" for key, value in self.querystring.items() )
[docs] def render( self: "ToolBarButtonMenu", feretui: "FeretUI", session: Session, ) -> str: """Return the html of the menu. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param session: The session of the user :type session: :class:`feretui.session.Session` :return: The html :rtype: str """ return Markup.unescape( feretui.render_template( session, self.template_id, label=self.get_label(feretui, session), description=self.get_description(feretui, session), icon=self.icon, url=self.get_url(feretui, self.querystring), css_class=self.css_class, ), )
[docs] class ToolBarButtonsMenu(ChildrenMenu, ToolBarButtonMenu): """Menu class for the toolbar. :: menu = ToolBarButtonMenu([ ToolBarButtonMenu('My label'), ]) if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-buttons-menu" def __init__( self: "ToolBarButtonsMenu", children: ToolBarMenu, visible_callback: Callable = None, ) -> None: """Construct the dropdown menu. Inherits of ToolbarMenu and ChildrenMenu """ ToolBarButtonMenu.__init__( self, None, type="buttons", visible_callback=visible_callback, ) ChildrenMenu.__init__(self, children) for child in children: if isinstance(child, ChildrenMenu): raise MenuError("ToolBarButtonsMenu menu can not be cascaded")
[docs] class ToolBarButtonUrlMenu(UrlMenu, ToolBarButtonMenu): """Menu class for the toolbar. :: menu = ToolBarButtonUrlMenu('My label') if menu.is_visible(session): menu.render(myferet, session) """ template_id = "toolbar-button-url-menu" def __init__( self: "ToolBarButtonUrlMenu", label: str, url: str, visible_callback: Callable = menu_for_authenticated_user, **kw: dict[str, str], ) -> None: """Call the menu constructor and update the context. see :class:`.menu` :param label: the label of the menu :type label: str :param url: the http url :type url: str :param icon: the icon html class used in the render :type icon: str :param description: the description, it is a helper to understand the role of the menu :type description: str """ super().__init__(label, url=url, **kw) self.visible_callback = visible_callback
[docs] class AsideMenu(Menu): """Menu class for the aside menu page. :: menu = AsideMenu('My label') if menu.is_visible(session): menu.render(myferet, session) """ template_id = "aside-menu" def __init__( self: "AsideMenu", label: str, **kwargs: dict[str, str], ) -> None: """Call the Menu constructor and update the context. see :class:`.Menu` """ super().__init__(label, **kwargs) self.context = "menu:aside:" + ":".join( f"{key}:{value}" for key, value in self.querystring.items() ) self.aside = ""
[docs] def get_href( self: "AsideMenu", feretui: "FeretUI", querystring: dict[str, str], ) -> str: """Return the url to put in href attribute of the a tag. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param querystring: The querysting to pass at the api :type querysting: dict[str, str] :return: The url :rtype: str """ querystring = querystring.copy() querystring["in-aside"] = [self.aside] return super().get_href(feretui, querystring)
[docs] def get_url( self: "AsideMenu", feretui: "FeretUI", querystring: dict[str, str], ) -> str: """Return the url to put in hx-get attribute of the a tag. :param feretui: The feretui client instance. :type feretui: :class:`feretui.feretui.FeretUI` :param querystring: The querysting to pass at the api :type querysting: dict[str, str] :return: The url :rtype: str """ querystring = querystring.copy() querystring["in-aside"] = [self.aside] return super().get_url(feretui, querystring)
[docs] class AsideHeaderMenu(ChildrenMenu, AsideMenu): """Hieracal menu for aside. :: menu = AsideHeaderMenu( 'Label', children=[AsideMenu('My label')]) if menu.is_visible(session): menu.render(myferet, session) """ template_id = "aside-header-menu" def __init__( self: "AsideHeaderMenu", label: str, children: list[ToolBarMenu] = None, **kwargs: dict[str, str], ) -> None: """Construct the dropdown menu. Inherits of ToolbarMenu and ChildrenMenu """ kwargs.setdefault("visible_callback", None) AsideMenu.__init__(self, label, type="header", **kwargs) ChildrenMenu.__init__(self, children)
[docs] class AsideUrlMenu(UrlMenu, AsideMenu): """Menu class to add a link to another web api. :: menu = AsideUrlMenu('My label', url="https://bulma.io") if menu.is_visible(session): menu.render(myferet, session) """ template_id = "aside-url-menu" def __init__( self: "AsideUrlMenu", label: str, url: str, **kw: dict[str, str], ) -> None: """Call the menu constructor and update the context. see :class:`.menu` :param label: the label of the menu :type label: str :param url: the http url :type url: str :param icon: the icon html class used in the render :type icon: str :param description: the description, it is a helper to understand the role of the menu :type description: str """ super().__init__(label, url=url, **kw)
[docs] class SitemapMenu: """Menu class for sitemap page.""" def __init__(self: "SitemapMenu", feretui: "FeretUI", menu: Menu) -> None: """Instanciate the Sitemap Menu. :param feretui: The instance of client :type feretui: `feretui.feretui.FeretUI` :param menu: An instance of menu to wrap :type menu: `feretui.menus.Menu` """ self.menu = menu self.children = [ SitemapMenu(feretui, child) for child in getattr(menu, "children", []) ] aside = menu.querystring.get("aside") if aside: self.children.extend( [ SitemapMenu(feretui, child) for child in feretui.get_aside_menus(aside) ], )
[docs] def is_visible(self: "SitemapMenu", session: Session) -> bool: """Check if the wrapped menu is visible.""" return self.menu.is_visible(session)
[docs] def render( self: "SitemapMenu", feretui: "FeretUI", session: Session, ) -> Markup: """Return the menu.""" key = "sitemap-header-menu" if len(self.children) else "sitemap-menu" menu = self.menu return Markup.unescape( feretui.render_template( session, key, label=menu.get_label(feretui, session), description=menu.get_description(feretui, session), icon=menu.icon, href=menu.get_href(feretui, menu.querystring), url=menu.get_url(feretui, menu.querystring), children=self.children, ), )