__init__.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import collections
  2. import inspect
  3. from django.apps import AppConfig
  4. from django.template.loader import get_template
  5. from django.utils.module_loading import import_string
  6. from extras.registry import registry
  7. from .signals import register_detail_page_content_classes
  8. # Initialize plugin registry stores
  9. registry['plugin_nav_menu_links'] = {}
  10. #
  11. # Plugin AppConfig class
  12. #
  13. class PluginConfig(AppConfig):
  14. """
  15. Subclass of Django's built-in AppConfig class, to be used for NetBox plugins.
  16. """
  17. # Plugin metadata
  18. author = ''
  19. author_email = ''
  20. description = ''
  21. version = ''
  22. # Root URL path under /plugins. If not set, the plugin's label will be used.
  23. base_url = None
  24. # Minimum/maximum compatible versions of NetBox
  25. min_version = None
  26. max_version = None
  27. # Default configuration parameters
  28. default_settings = {}
  29. # Mandatory configuration parameters
  30. required_settings = []
  31. # Middleware classes provided by the plugin
  32. middleware = []
  33. # Caching configuration
  34. caching_config = {}
  35. def ready(self):
  36. # Register navigation menu items (if defined)
  37. register_menu_items(self.verbose_name, self.get_menu_items())
  38. def get_menu_items(self):
  39. """
  40. Default method to import navigation menu items for a plugin from the default location (menu_items in a
  41. file named navigation.py). This method may be overridden by a plugin author to import menu items from
  42. a different location if needed.
  43. """
  44. try:
  45. menu_items = import_string(f"{self.__module__}.navigation.menu_items")
  46. return menu_items
  47. except ImportError:
  48. return []
  49. #
  50. # Template content injection
  51. #
  52. class PluginTemplateContent:
  53. """
  54. This class is used to register plugin content to be injected into core NetBox templates.
  55. It contains methods that are overriden by plugin authors to return template content.
  56. The `model` attribute on the class defines the which model detail page this class renders
  57. content for. It should be set as a string in the form '<app_label>.<model_name>'.
  58. """
  59. model = None
  60. def __init__(self, obj, context):
  61. self.obj = obj
  62. self.context = context
  63. def render(self, template, extra_context=None):
  64. """
  65. Convenience menthod for rendering the provided template name. The detail page object is automatically
  66. passed into the template context as `obj` and the origional detail page's context is available as
  67. `obj_context`. An additional context dictionary may be passed as `extra_context`.
  68. """
  69. context = {
  70. 'obj': self.obj,
  71. 'obj_context': self.context
  72. }
  73. if isinstance(extra_context, dict):
  74. context.update(extra_context)
  75. return get_template(template).render(context)
  76. def left_page(self):
  77. """
  78. Content that will be rendered on the left of the detail page view. Content should be returned as an
  79. HTML string. Note that content does not need to be marked as safe because this is automatically handled.
  80. """
  81. raise NotImplementedError
  82. def right_page(self):
  83. """
  84. Content that will be rendered on the right of the detail page view. Content should be returned as an
  85. HTML string. Note that content does not need to be marked as safe because this is automatically handled.
  86. """
  87. raise NotImplementedError
  88. def full_width_page(self):
  89. """
  90. Content that will be rendered within the full width of the detail page view. Content should be returned as an
  91. HTML string. Note that content does not need to be marked as safe because this is automatically handled.
  92. """
  93. raise NotImplementedError
  94. def buttons(self):
  95. """
  96. Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
  97. should be returned as an HTML string. Note that content does not need to be marked as safe because this is
  98. automatically handled.
  99. """
  100. raise NotImplementedError
  101. def register_content_classes():
  102. """
  103. Helper method that populates the registry with all template content classes that have been registered by plugins
  104. """
  105. registry['plugin_template_content_classes'] = collections.defaultdict(list)
  106. responses = register_detail_page_content_classes.send('registration_event')
  107. for receiver, response in responses:
  108. if not isinstance(response, list):
  109. response = [response]
  110. for template_class in response:
  111. if not inspect.isclass(template_class):
  112. raise TypeError('Plugin content class {} was passes as an instance!'.format(template_class))
  113. if not issubclass(template_class, PluginTemplateContent):
  114. raise TypeError('{} is not a subclass of extras.plugins.PluginTemplateContent!'.format(template_class))
  115. if template_class.model is None:
  116. raise TypeError('Plugin content class {} does not define a valid model!'.format(template_class))
  117. registry['plugin_template_content_classes'][template_class.model].append(template_class)
  118. def get_content_classes(model):
  119. """
  120. Given a model string, return the list of all registered template content classes.
  121. Populate the registry if it is empty.
  122. """
  123. if 'plugin_template_content_classes' not in registry:
  124. register_content_classes()
  125. return registry['plugin_template_content_classes'].get(model, [])
  126. #
  127. # Navigation menu links
  128. #
  129. class PluginNavMenuLink:
  130. """
  131. This class represents a nav menu item. This constitutes primary link and its text, but also allows for
  132. specifying additional link buttons that appear to the right of the item in the van menu.
  133. Links are specified as Django reverse URL strings.
  134. Buttons are each specified as a list of PluginNavMenuButton instances.
  135. """
  136. def __init__(self, link, link_text, permission=None, buttons=None):
  137. self.link = link
  138. self.link_text = link_text
  139. self.link_permission = permission
  140. if buttons is None:
  141. self.buttons = []
  142. else:
  143. self.buttons = buttons
  144. class PluginNavMenuButton:
  145. """
  146. This class represents a button which is a part of the nav menu link item.
  147. Note that button colors should come from ButtonColorChoices
  148. """
  149. def __init__(self, link, title, icon_class, color, permission=None):
  150. self.link = link
  151. self.title = title
  152. self.icon_class = icon_class
  153. self.color = color
  154. self.permission = permission
  155. def register_menu_items(section_name, class_list):
  156. """
  157. Register a list of PluginNavMenuLink instances for a given menu section (e.g. plugin name)
  158. """
  159. # Validation
  160. for menu_link in class_list:
  161. if not isinstance(menu_link, PluginNavMenuLink):
  162. raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginNavMenuLink")
  163. for button in menu_link.buttons:
  164. if not isinstance(button, PluginNavMenuButton):
  165. raise TypeError(f"{button} must be an instance of extras.plugins.PluginNavMenuButton")
  166. registry['plugin_nav_menu_links'][section_name] = class_list