menu.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. from typing import List
  2. from flask import current_app, url_for
  3. from flask_babel import gettext as __
  4. from .api import BaseApi, expose
  5. from .basemanager import BaseManager
  6. from .security.decorators import permission_name, protect
  7. class MenuItem(object):
  8. def __init__(
  9. self, name, href="", icon="", label="", childs=None, baseview=None, cond=None
  10. ):
  11. self.name = name
  12. self.href = href
  13. self.icon = icon
  14. self.label = label
  15. self.childs = childs or []
  16. self.baseview = baseview
  17. self.cond = cond
  18. def should_render(self) -> bool:
  19. return bool(self.cond()) if self.cond is not None else True
  20. def get_url(self):
  21. if not self.href:
  22. if not self.baseview:
  23. return ""
  24. else:
  25. return url_for(f"{self.baseview.endpoint}.{self.baseview.default_view}")
  26. else:
  27. try:
  28. return url_for(self.href)
  29. except Exception:
  30. return self.href
  31. def __repr__(self):
  32. return self.name
  33. class Menu(object):
  34. def __init__(self, reverse=True, extra_classes=""):
  35. self.menu = []
  36. if reverse:
  37. extra_classes = extra_classes + "navbar-inverse"
  38. self.extra_classes = extra_classes
  39. @property
  40. def reverse(self):
  41. return "navbar-inverse" in self.extra_classes
  42. def get_list(self):
  43. return self.menu
  44. def get_flat_name_list(self, menu: "Menu" = None, result: List = None) -> List:
  45. menu = menu or self.menu
  46. result = result or []
  47. for item in menu:
  48. result.append(item.name)
  49. if item.childs:
  50. result.extend(self.get_flat_name_list(menu=item.childs, result=result))
  51. return result
  52. def get_data(self, menu=None):
  53. menu = menu or self.menu
  54. ret_list = []
  55. allowed_menus = current_app.appbuilder.sm.get_user_menu_access(
  56. self.get_flat_name_list()
  57. )
  58. for i, item in enumerate(menu):
  59. if not item.should_render():
  60. continue
  61. if item.name == "-" and not i == len(menu) - 1:
  62. ret_list.append("-")
  63. elif item.name not in allowed_menus:
  64. continue
  65. elif item.childs:
  66. ret_list.append(
  67. {
  68. "name": item.name,
  69. "icon": item.icon,
  70. "label": __(str(item.label)),
  71. "childs": self.get_data(menu=item.childs),
  72. }
  73. )
  74. else:
  75. ret_list.append(
  76. {
  77. "name": item.name,
  78. "icon": item.icon,
  79. "label": __(str(item.label)),
  80. "url": item.get_url(),
  81. }
  82. )
  83. return ret_list
  84. def find(self, name, menu=None):
  85. """
  86. Finds a menu item by name and returns it.
  87. :param name:
  88. The menu item name.
  89. """
  90. menu = menu or self.menu
  91. for i in menu:
  92. if i.name == name:
  93. return i
  94. else:
  95. if i.childs:
  96. ret_item = self.find(name, menu=i.childs)
  97. if ret_item:
  98. return ret_item
  99. def add_category(self, category, icon="", label="", parent_category=""):
  100. label = label or category
  101. if parent_category == "":
  102. self.menu.append(MenuItem(name=category, icon=icon, label=label))
  103. else:
  104. self.find(category).childs.append(
  105. MenuItem(name=category, icon=icon, label=label)
  106. )
  107. def add_link(
  108. self,
  109. name,
  110. href="",
  111. icon="",
  112. label="",
  113. category="",
  114. category_icon="",
  115. category_label="",
  116. baseview=None,
  117. cond=None,
  118. ):
  119. label = label or name
  120. category_label = category_label or category
  121. if category == "":
  122. self.menu.append(
  123. MenuItem(
  124. name=name,
  125. href=href,
  126. icon=icon,
  127. label=label,
  128. baseview=baseview,
  129. cond=cond,
  130. )
  131. )
  132. else:
  133. menu_item = self.find(category)
  134. if menu_item:
  135. new_menu_item = MenuItem(
  136. name=name,
  137. href=href,
  138. icon=icon,
  139. label=label,
  140. baseview=baseview,
  141. cond=cond,
  142. )
  143. menu_item.childs.append(new_menu_item)
  144. else:
  145. self.add_category(
  146. category=category, icon=category_icon, label=category_label
  147. )
  148. new_menu_item = MenuItem(
  149. name=name,
  150. href=href,
  151. icon=icon,
  152. label=label,
  153. baseview=baseview,
  154. cond=cond,
  155. )
  156. self.find(category).childs.append(new_menu_item)
  157. def add_separator(self, category="", cond=None):
  158. menu_item = self.find(category)
  159. if menu_item:
  160. menu_item.childs.append(MenuItem("-", cond=cond))
  161. else:
  162. raise Exception(
  163. "Menu separator does not have correct category {}".format(category)
  164. )
  165. class MenuApi(BaseApi):
  166. resource_name = "menu"
  167. openapi_spec_tag = "Menu"
  168. @expose("/", methods=["GET"])
  169. @protect(allow_browser_login=True)
  170. @permission_name("get")
  171. def get_menu_data(self):
  172. """An endpoint for retreiving the menu.
  173. ---
  174. get:
  175. description: >-
  176. Get the menu data structure.
  177. Returns a forest like structure with the menu the user has access to
  178. responses:
  179. 200:
  180. description: Get menu data
  181. content:
  182. application/json:
  183. schema:
  184. type: object
  185. properties:
  186. result:
  187. description: Menu items in a forest like data structure
  188. type: array
  189. items:
  190. type: object
  191. properties:
  192. name:
  193. description: >-
  194. The internal menu item name, maps to permission_name
  195. type: string
  196. label:
  197. description: Pretty name for the menu item
  198. type: string
  199. icon:
  200. description: Icon name to show for this menu item
  201. type: string
  202. url:
  203. description: The URL for the menu item
  204. type: string
  205. childs:
  206. type: array
  207. items:
  208. type: object
  209. 401:
  210. $ref: '#/components/responses/401'
  211. """
  212. return self.response(200, result=current_app.appbuilder.menu.get_data())
  213. class MenuApiManager(BaseManager):
  214. def register_views(self):
  215. if self.appbuilder.app.config.get("FAB_ADD_MENU_API", True):
  216. self.appbuilder.add_api(MenuApi)