__init__.py 6.8 KB

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