from typing import List from flask import current_app, url_for from flask_babel import gettext as __ from .api import BaseApi, expose from .basemanager import BaseManager from .security.decorators import permission_name, protect class MenuItem(object): def __init__( self, name, href="", icon="", label="", childs=None, baseview=None, cond=None ): self.name = name self.href = href self.icon = icon self.label = label self.childs = childs or [] self.baseview = baseview self.cond = cond def should_render(self) -> bool: return bool(self.cond()) if self.cond is not None else True def get_url(self): if not self.href: if not self.baseview: return "" else: return url_for(f"{self.baseview.endpoint}.{self.baseview.default_view}") else: try: return url_for(self.href) except Exception: return self.href def __repr__(self): return self.name class Menu(object): def __init__(self, reverse=True, extra_classes=""): self.menu = [] if reverse: extra_classes = extra_classes + "navbar-inverse" self.extra_classes = extra_classes @property def reverse(self): return "navbar-inverse" in self.extra_classes def get_list(self): return self.menu def get_flat_name_list(self, menu: "Menu" = None, result: List = None) -> List: menu = menu or self.menu result = result or [] for item in menu: result.append(item.name) if item.childs: result.extend(self.get_flat_name_list(menu=item.childs, result=result)) return result def get_data(self, menu=None): menu = menu or self.menu ret_list = [] allowed_menus = current_app.appbuilder.sm.get_user_menu_access( self.get_flat_name_list() ) for i, item in enumerate(menu): if not item.should_render(): continue if item.name == "-" and not i == len(menu) - 1: ret_list.append("-") elif item.name not in allowed_menus: continue elif item.childs: ret_list.append( { "name": item.name, "icon": item.icon, "label": __(str(item.label)), "childs": self.get_data(menu=item.childs), } ) else: ret_list.append( { "name": item.name, "icon": item.icon, "label": __(str(item.label)), "url": item.get_url(), } ) return ret_list def find(self, name, menu=None): """ Finds a menu item by name and returns it. :param name: The menu item name. """ menu = menu or self.menu for i in menu: if i.name == name: return i else: if i.childs: ret_item = self.find(name, menu=i.childs) if ret_item: return ret_item def add_category(self, category, icon="", label="", parent_category=""): label = label or category if parent_category == "": self.menu.append(MenuItem(name=category, icon=icon, label=label)) else: self.find(category).childs.append( MenuItem(name=category, icon=icon, label=label) ) def add_link( self, name, href="", icon="", label="", category="", category_icon="", category_label="", baseview=None, cond=None, ): label = label or name category_label = category_label or category if category == "": self.menu.append( MenuItem( name=name, href=href, icon=icon, label=label, baseview=baseview, cond=cond, ) ) else: menu_item = self.find(category) if menu_item: new_menu_item = MenuItem( name=name, href=href, icon=icon, label=label, baseview=baseview, cond=cond, ) menu_item.childs.append(new_menu_item) else: self.add_category( category=category, icon=category_icon, label=category_label ) new_menu_item = MenuItem( name=name, href=href, icon=icon, label=label, baseview=baseview, cond=cond, ) self.find(category).childs.append(new_menu_item) def add_separator(self, category="", cond=None): menu_item = self.find(category) if menu_item: menu_item.childs.append(MenuItem("-", cond=cond)) else: raise Exception( "Menu separator does not have correct category {}".format(category) ) class MenuApi(BaseApi): resource_name = "menu" openapi_spec_tag = "Menu" @expose("/", methods=["GET"]) @protect(allow_browser_login=True) @permission_name("get") def get_menu_data(self): """An endpoint for retreiving the menu. --- get: description: >- Get the menu data structure. Returns a forest like structure with the menu the user has access to responses: 200: description: Get menu data content: application/json: schema: type: object properties: result: description: Menu items in a forest like data structure type: array items: type: object properties: name: description: >- The internal menu item name, maps to permission_name type: string label: description: Pretty name for the menu item type: string icon: description: Icon name to show for this menu item type: string url: description: The URL for the menu item type: string childs: type: array items: type: object 401: $ref: '#/components/responses/401' """ return self.response(200, result=current_app.appbuilder.menu.get_data()) class MenuApiManager(BaseManager): def register_views(self): if self.appbuilder.app.config.get("FAB_ADD_MENU_API", True): self.appbuilder.add_api(MenuApi)