ejplatform/ej-server

View on GitHub
src/ej/components/menu.py

Summary

Maintainability
A
35 mins
Test Coverage
from collections import defaultdict

from django.utils.translation import gettext_lazy as _
from hyperpython import nav, Block, a, div, h3
from hyperpython.components import hyperlink, html_list, fa_icon

from ej.components.functional import thunk, split_with
from ..roles import link


def page_menu(*items, request=None, caller=None, **kwargs):
    """
    Creates a new menu from a list of sections.

    Sections are separated by either Ellipsis (...) or None values.

    Args:
        accessibility (bool):
            Appends an accessibility menu items (enabled by default)
        conversation (bool):
            Enable section with the conversation pages.
        about (bool):
            Enable a section with the "About" pages (faq/about-us/usage)

    Examples:

        .. code-block:: python

            page_menu(
                # Some section (document with a comment)
                'Item name </absolute-path/>',
                'Item name <app:view-name>',
                ...,
                # Another section
                a_model_instance,  # (must implement get_absolute_url)
                anything_that_hyperpython_hyperlink_understands,

                # It accepts additional keyword arguments corresponding
                # to default section names. Menu items are inserted in order.
                accessibility=False,
            )
    """
    if len(items) == 1 and isinstance(items[0], (list, tuple)):
        items = items[0]

    groups = split_with(lambda x: x in (..., None), items)
    sections = [menu_section(None, group) for group in groups]
    if caller is not None:
        data = caller().strip()
        if data:
            sections = (*items, data)
    else:
        automatic = default_implementations(request, **kwargs)
        sections = [*sections, *automatic]

    result = menu_from_sections(sections)
    return result.render() if caller else result


def menu_from_sections(sections):
    # add role="menu" in the future?
    return div(*sections, class_="page-menu", id="page-menu", is_component=True)


def menu_section(name, links, **kwargs):
    """
    Wraps a list of elements into a menu section <ul>
    """
    children = [html_list(map(menu_item, links))]
    if name:
        children.insert(0, h3(name))
        kwargs.setdefault("title", str(name))
        kwargs.setdefault("aria-label", str(name))
    return nav(children, **kwargs)


def menu_item(item):
    # add role="menuitem" in the future?
    if hasattr(item, "__html__"):
        return item
    else:
        return hyperlink(item)


def default_implementations(request=None, **kwargs):
    """
    Return a list of default implementations for common sections.

    See Also:
        :func:`menu` for more details.
    """
    kwargs.setdefault("about", True)
    kwargs.setdefault("accessibility", True)
    for name, value in kwargs.items():
        if name == "about" and value:
            yield page_menu.ABOUT(request)
        elif name == "accessibility" and value:
            yield page_menu.ACCESSIBILITY()
        else:
            raise TypeError(f"invalid parameter {name}")


#
# Sections and styles
#
def menu_links(section, request=None, object=None):
    """
    Return a list of links for some menu section.
    """
    try:
        render_functions = MENU_FUNCTIONS[section]
    except KeyError:
        raise ValueError(r"invalid section: {section!r}")
    for func in render_functions:
        yield from func(request, object)


def register_menu(section):
    """
    Register a function that generates links using the :func:`menu_links`
    function. The function must receive a request and a second object
    (which can be None) pertaining to that section. Ex.: in a route that
    configures the user profile, that object can be the profile object for the
    request user.
    """
    return lambda f: MENU_FUNCTIONS[section].append(f) or f


# Storage
MENU_FUNCTIONS = defaultdict(list)

#: Accessibility menu
page_menu.ACCESSIBILITY = thunk(
    lambda: menu_section(
        _("Accessibility"),
        [
            a([fa_icon("text-height"), _("Toggle Font Size")], href="#", is_element="toggleFontSize"),
            a([fa_icon("adjust"), _("Toggle Contrast")], href="#", is_element="toggleContrast"),
        ],
    )
)

#: Default menu
page_menu.DEFAULT_MENU_SECTIONS = lambda request: Block([page_menu.ACCESSIBILITY()])

#: Links
page_menu.links = menu_links
page_menu.register = register_menu

#: Create entire sections from links
page_menu.section = lambda title, ref, request, *args: menu_section(title, menu_links(ref))