From e074570b8fd8eac213b49750360982199043153c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 22 Sep 2022 10:01:19 -0700 Subject: [PATCH 1/7] 9071 add header to plugin menu --- netbox/extras/plugins/__init__.py | 14 ++++++++++--- netbox/netbox/navigation_menu.py | 34 ++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 0b57e6f05..95e88ca8c 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -58,6 +58,7 @@ class PluginConfig(AppConfig): # integrated components. graphql_schema = 'graphql.schema' menu_items = 'navigation.menu_items' + menu_header = 'navigation.menu_heading' template_extensions = 'template_content.template_extensions' user_preferences = 'preferences.preferences' @@ -70,9 +71,14 @@ class PluginConfig(AppConfig): register_template_extensions(template_extensions) # Register navigation menu items (if defined) + try: + menu_header = import_object(f"{self.__module__}.{self.menu_header}") + except AttributeError: + menu_header = None + menu_items = import_object(f"{self.__module__}.{self.menu_items}") if menu_items is not None: - register_menu_items(self.verbose_name, menu_items) + register_menu_items(self.verbose_name, menu_header, menu_items) # Register GraphQL schema (if defined) graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}") @@ -246,7 +252,7 @@ class PluginMenuButton: self.color = color -def register_menu_items(section_name, class_list): +def register_menu_items(section_name, menu_header, class_list): """ Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name) """ @@ -258,7 +264,9 @@ def register_menu_items(section_name, class_list): if not isinstance(button, PluginMenuButton): raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton") - registry['plugins']['menu_items'][section_name] = class_list + registry['plugins']['menu_items'][section_name] = {} + registry['plugins']['menu_items'][section_name]['header'] = menu_header + registry['plugins']['menu_items'][section_name]['items'] = class_list # diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index a495f17c9..d4970aa35 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -408,18 +408,28 @@ MENUS = [ if registry['plugins']['menu_items']: plugin_menu_groups = [] - for plugin_name, items in registry['plugins']['menu_items'].items(): - plugin_menu_groups.append( - MenuGroup( - label=plugin_name, - items=items + for plugin_name, data in registry['plugins']['menu_items'].items(): + if data['header']: + menu_groups = [MenuGroup(label=plugin_name, items=data["items"])] + icon = data["header"]["icon"] + MENUS.append(Menu( + label=data["header"]["title"], + icon_class=f"mdi {icon}", + groups=menu_groups + )) + else: + plugin_menu_groups.append( + MenuGroup( + label=plugin_name, + items=data["items"] + ) ) + + if plugin_menu_groups: + PLUGIN_MENU = Menu( + label="Plugins", + icon_class="mdi mdi-puzzle", + groups=plugin_menu_groups ) - PLUGIN_MENU = Menu( - label="Plugins", - icon_class="mdi mdi-puzzle", - groups=plugin_menu_groups - ) - - MENUS.append(PLUGIN_MENU) + MENUS.append(PLUGIN_MENU) From b134d2a7b0daa61aa20769390cfc9f797a440108 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 14:23:53 -0700 Subject: [PATCH 2/7] 9071 fix test --- netbox/extras/tests/test_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 299cab9ef..733ae3a39 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -63,7 +63,7 @@ class PluginTest(TestCase): Check that plugin MenuItems and MenuButtons are registered. """ self.assertIn('Dummy plugin', registry['plugins']['menu_items']) - menu_items = registry['plugins']['menu_items']['Dummy plugin'] + menu_items = registry['plugins']['menu_items']['Dummy plugin']['items'] self.assertEqual(len(menu_items), 2) self.assertEqual(len(menu_items[0].buttons), 2) From 7deb9fde9e3822c83076b08896de62b8ed578f7a Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 14:41:46 -0700 Subject: [PATCH 3/7] 9071 add documentation --- docs/plugins/development/navigation.md | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index 52ae953a7..b4a872ae2 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -32,6 +32,41 @@ A `PluginMenuItem` has the following attributes: | `permissions` | - | A list of permissions required to display this link | | `buttons` | - | An iterable of PluginMenuButton instances to include | +## Optional Header + +Plugin menus normally appear under the "Plugins" header. An optional menu_heading can be defined to make the plugin menu to appear as a top level menu header. An example is shown below: + +```python +from extras.plugins import PluginMenuButton, PluginMenuItem +from utilities.choices import ButtonColorChoices + +menu_heading = { + "title": "Animal Sound", + "icon": "mdi-puzzle" +} + +menu_items = ( + PluginMenuItem( + link='plugins:netbox_animal_sounds:random_animal', + link_text='Random sound', + buttons=( + PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE), + PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN), + ) + ), +) +``` + +The `menu_heading` has the following attributes: + +| Attribute | Required | Description | +|---------------|----------|------------------------------------------------------| +| `title` | Yes | The text that will show in the menu header | +| `icon` | Yes | The icon to use next to the headermdi | + +!!! tip + The icon names can be found at [Material Design Icons](https://materialdesignicons.com/) + ## Menu Buttons A `PluginMenuButton` has the following attributes: From 00d2dcda68be0935962c8e5d11a66d784638fb1e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 28 Sep 2022 15:56:09 -0400 Subject: [PATCH 4/7] Refactor navigation resources and menu --- docs/development/adding-models.md | 2 +- netbox/netbox/navigation/__init__.py | 92 +++++++++++++++++++ .../menu.py} | 83 +---------------- netbox/utilities/templatetags/navigation.py | 2 +- 4 files changed, 95 insertions(+), 84 deletions(-) create mode 100644 netbox/netbox/navigation/__init__.py rename netbox/netbox/{navigation_menu.py => navigation/menu.py} (86%) diff --git a/docs/development/adding-models.md b/docs/development/adding-models.md index f4d171f48..aef11d666 100644 --- a/docs/development/adding-models.md +++ b/docs/development/adding-models.md @@ -60,7 +60,7 @@ Create the HTML template for the object view. (The other views each typically em ## 10. Add the model to the navigation menu -Add the relevant navigation menu items in `netbox/netbox/navigation_menu.py`. +Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`. ## 11. REST API components diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py new file mode 100644 index 000000000..7b5729843 --- /dev/null +++ b/netbox/netbox/navigation/__init__.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass +from typing import Sequence, Optional + +from utilities.choices import ButtonColorChoices + + +__all__ = ( + 'get_model_item', + 'get_model_buttons', + 'Menu', + 'MenuGroup', + 'MenuItem', + 'MenuItemButton', +) + + +# +# Navigation menu data classes +# + +@dataclass +class MenuItemButton: + + link: str + title: str + icon_class: str + permissions: Optional[Sequence[str]] = () + color: Optional[str] = None + + +@dataclass +class MenuItem: + + link: str + link_text: str + permissions: Optional[Sequence[str]] = () + buttons: Optional[Sequence[MenuItemButton]] = () + + +@dataclass +class MenuGroup: + + label: str + items: Sequence[MenuItem] + + +@dataclass +class Menu: + + label: str + icon_class: str + groups: Sequence[MenuGroup] + + +# +# Utility functions +# + +def get_model_item(app_label, model_name, label, actions=('add', 'import')): + return MenuItem( + link=f'{app_label}:{model_name}_list', + link_text=label, + permissions=[f'{app_label}.view_{model_name}'], + buttons=get_model_buttons(app_label, model_name, actions) + ) + + +def get_model_buttons(app_label, model_name, actions=('add', 'import')): + buttons = [] + + if 'add' in actions: + buttons.append( + MenuItemButton( + link=f'{app_label}:{model_name}_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=[f'{app_label}.add_{model_name}'], + color=ButtonColorChoices.GREEN + ) + ) + if 'import' in actions: + buttons.append( + MenuItemButton( + link=f'{app_label}:{model_name}_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=[f'{app_label}.add_{model_name}'], + color=ButtonColorChoices.CYAN + ) + ) + + return buttons diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation/menu.py similarity index 86% rename from netbox/netbox/navigation_menu.py rename to netbox/netbox/navigation/menu.py index d4970aa35..9eb762c23 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation/menu.py @@ -1,86 +1,5 @@ -from dataclasses import dataclass -from typing import Sequence, Optional - from extras.registry import registry -from utilities.choices import ButtonColorChoices - - -# -# Nav menu data classes -# - -@dataclass -class MenuItemButton: - - link: str - title: str - icon_class: str - permissions: Optional[Sequence[str]] = () - color: Optional[str] = None - - -@dataclass -class MenuItem: - - link: str - link_text: str - permissions: Optional[Sequence[str]] = () - buttons: Optional[Sequence[MenuItemButton]] = () - - -@dataclass -class MenuGroup: - - label: str - items: Sequence[MenuItem] - - -@dataclass -class Menu: - - label: str - icon_class: str - groups: Sequence[MenuGroup] - - -# -# Utility functions -# - -def get_model_item(app_label, model_name, label, actions=('add', 'import')): - return MenuItem( - link=f'{app_label}:{model_name}_list', - link_text=label, - permissions=[f'{app_label}.view_{model_name}'], - buttons=get_model_buttons(app_label, model_name, actions) - ) - - -def get_model_buttons(app_label, model_name, actions=('add', 'import')): - buttons = [] - - if 'add' in actions: - buttons.append( - MenuItemButton( - link=f'{app_label}:{model_name}_add', - title='Add', - icon_class='mdi mdi-plus-thick', - permissions=[f'{app_label}.add_{model_name}'], - color=ButtonColorChoices.GREEN - ) - ) - if 'import' in actions: - buttons.append( - MenuItemButton( - link=f'{app_label}:{model_name}_import', - title='Import', - icon_class='mdi mdi-upload', - permissions=[f'{app_label}.add_{model_name}'], - color=ButtonColorChoices.CYAN - ) - ) - - return buttons +from .navigation import * # diff --git a/netbox/utilities/templatetags/navigation.py b/netbox/utilities/templatetags/navigation.py index ef0657446..a34ef9816 100644 --- a/netbox/utilities/templatetags/navigation.py +++ b/netbox/utilities/templatetags/navigation.py @@ -2,7 +2,7 @@ from typing import Dict from django import template from django.template import Context -from netbox.navigation_menu import MENUS +from netbox.navigation.menu import MENUS register = template.Library() From db90b084cf779c109100a6688d0e16ec07cb1978 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 28 Sep 2022 16:08:03 -0400 Subject: [PATCH 5/7] Enable plugins to create root-level navigation menus --- netbox/extras/plugins/__init__.py | 51 +++++++++++++++++++---------- netbox/extras/tests/test_plugins.py | 2 +- netbox/netbox/navigation/menu.py | 42 +++++++++--------------- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 95e88ca8c..a5fdbea10 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -6,15 +6,16 @@ from django.apps import AppConfig from django.core.exceptions import ImproperlyConfigured from django.template.loader import get_template -from extras.registry import registry -from utilities.choices import ButtonColorChoices - from extras.plugins.utils import import_object +from extras.registry import registry +from netbox.navigation import MenuGroup +from utilities.choices import ButtonColorChoices # Initialize plugin registry registry['plugins'] = { 'graphql_schemas': [], + 'menus': [], 'menu_items': {}, 'preferences': {}, 'template_extensions': collections.defaultdict(list), @@ -57,8 +58,8 @@ class PluginConfig(AppConfig): # Default integration paths. Plugin authors can override these to customize the paths to # integrated components. graphql_schema = 'graphql.schema' + menu = 'navigation.menu' menu_items = 'navigation.menu_items' - menu_header = 'navigation.menu_heading' template_extensions = 'template_content.template_extensions' user_preferences = 'preferences.preferences' @@ -70,15 +71,11 @@ class PluginConfig(AppConfig): if template_extensions is not None: register_template_extensions(template_extensions) - # Register navigation menu items (if defined) - try: - menu_header = import_object(f"{self.__module__}.{self.menu_header}") - except AttributeError: - menu_header = None - - menu_items = import_object(f"{self.__module__}.{self.menu_items}") - if menu_items is not None: - register_menu_items(self.verbose_name, menu_header, menu_items) + # Register navigation menu or menu items (if defined) + if menu := import_object(f"{self.__module__}.{self.menu}"): + register_menu(menu) + if menu_items := import_object(f"{self.__module__}.{self.menu_items}"): + register_menu_items(self.verbose_name, menu_items) # Register GraphQL schema (if defined) graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}") @@ -206,6 +203,22 @@ def register_template_extensions(class_list): # Navigation menu links # +class PluginMenu: + icon = 'mdi-puzzle' + + def __init__(self, label, groups, icon=None): + self.label = label + self.groups = [ + MenuGroup(label, items) for label, items in groups + ] + if icon is not None: + self.icon = icon + + @property + def icon_class(self): + return f'mdi {self.icon}' + + class PluginMenuItem: """ This class represents a navigation menu item. This constitutes primary link and its text, but also allows for @@ -252,7 +265,13 @@ class PluginMenuButton: self.color = color -def register_menu_items(section_name, menu_header, class_list): +def register_menu(menu): + if not isinstance(menu, PluginMenu): + raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu") + registry['plugins']['menus'].append(menu) + + +def register_menu_items(section_name, class_list): """ Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name) """ @@ -264,9 +283,7 @@ def register_menu_items(section_name, menu_header, class_list): if not isinstance(button, PluginMenuButton): raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton") - registry['plugins']['menu_items'][section_name] = {} - registry['plugins']['menu_items'][section_name]['header'] = menu_header - registry['plugins']['menu_items'][section_name]['items'] = class_list + registry['plugins']['menu_items'][section_name] = class_list # diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 733ae3a39..299cab9ef 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -63,7 +63,7 @@ class PluginTest(TestCase): Check that plugin MenuItems and MenuButtons are registered. """ self.assertIn('Dummy plugin', registry['plugins']['menu_items']) - menu_items = registry['plugins']['menu_items']['Dummy plugin']['items'] + menu_items = registry['plugins']['menu_items']['Dummy plugin'] self.assertEqual(len(menu_items), 2) self.assertEqual(len(menu_items[0].buttons), 2) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 9eb762c23..400a7bf5a 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -1,5 +1,5 @@ from extras.registry import registry -from .navigation import * +from . import * # @@ -324,31 +324,19 @@ MENUS = [ # Add plugin menus # +for menu in registry['plugins']['menus']: + MENUS.append(menu) + if registry['plugins']['menu_items']: - plugin_menu_groups = [] - for plugin_name, data in registry['plugins']['menu_items'].items(): - if data['header']: - menu_groups = [MenuGroup(label=plugin_name, items=data["items"])] - icon = data["header"]["icon"] - MENUS.append(Menu( - label=data["header"]["title"], - icon_class=f"mdi {icon}", - groups=menu_groups - )) - else: - plugin_menu_groups.append( - MenuGroup( - label=plugin_name, - items=data["items"] - ) - ) - - if plugin_menu_groups: - PLUGIN_MENU = Menu( - label="Plugins", - icon_class="mdi mdi-puzzle", - groups=plugin_menu_groups - ) - - MENUS.append(PLUGIN_MENU) + # Build the default plugins menu + groups = [ + MenuGroup(label=label, items=items) + for label, items in registry['plugins']['menu_items'].items() + ] + plugins_menu = Menu( + label="Plugins", + icon_class="mdi mdi-puzzle", + groups=groups + ) + MENUS.append(plugins_menu) From d0465242a336061b5894299037a10125f0f8c438 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 28 Sep 2022 16:44:16 -0400 Subject: [PATCH 6/7] Add documentation for PluginMenu --- docs/plugins/development/navigation.md | 115 +++++++++++++------------ netbox/extras/plugins/__init__.py | 12 +-- 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index b4a872ae2..a52a9803a 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -1,25 +1,67 @@ # Navigation +## Menus + +!!! note + This feature was introduced in NetBox v3.4. + +A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below. + +```python title="navigation.py" +from extras.plugins import PluginMenu + +menu = PluginMenu( + label='My Plugin', + groups=( + ('Foo', (item1, item2, item3)), + ('Bar', (item4, item5)), + ), + icon='mdi mdi-router' +) +``` + +Note that each group is a two-tuple containing a label and an iterable of menu items. The group's label serves as the section header within the submenu. A group label is required even if you have only one group of items. + +!!! tip + The path to the menu class can be modified by setting `menu` in the PluginConfig instance. + +A `PluginMenu` has the following attributes: + +| Attribute | Required | Description | +|--------------|----------|---------------------------------------------------| +| `label` | Yes | The text displayed as the menu heading | +| `groups` | Yes | An iterable of named groups containing menu items | +| `icon_class` | - | The CSS name of the icon to use for the heading | + +!!! tip + Supported icons can be found at [Material Design Icons](https://materialdesignicons.com/) + +### The Default Menu + +If your plugin has only a small number of menu items, it may be desirable to use NetBox's shared "Plugins" menu rather than creating your own. To do this, simply declare `menu_items` as a list of `PluginMenuItems` in `navigation.py`. The listed items will appear under a heading bearing the name of your plugin in the "Plugins" submenu. + +```python title="navigation.py" +menu_items = (item1, item2, item3) +``` + +!!! tip + The path to the menu items list can be modified by setting `menu_items` in the PluginConfig instance. + ## Menu Items -To make its views easily accessible to users, a plugin can inject items in NetBox's navigation menu under the "Plugins" header. Menu items are added by defining a list of PluginMenuItem instances. By default, this should be a variable named `menu_items` in the file `navigation.py`. An example is shown below. +Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below. -!!! tip - The path to declared menu items can be modified by setting `menu_items` in the PluginConfig instance. - -```python +```python filename="navigation.py" from extras.plugins import PluginMenuButton, PluginMenuItem from utilities.choices import ButtonColorChoices -menu_items = ( - PluginMenuItem( - link='plugins:netbox_animal_sounds:random_animal', - link_text='Random sound', - buttons=( - PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE), - PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN), - ) - ), +item1 = PluginMenuItem( + link='plugins:myplugin:myview', + link_text='Some text', + buttons=( + PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE), + PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN), + ) ) ``` @@ -32,54 +74,21 @@ A `PluginMenuItem` has the following attributes: | `permissions` | - | A list of permissions required to display this link | | `buttons` | - | An iterable of PluginMenuButton instances to include | -## Optional Header - -Plugin menus normally appear under the "Plugins" header. An optional menu_heading can be defined to make the plugin menu to appear as a top level menu header. An example is shown below: - -```python -from extras.plugins import PluginMenuButton, PluginMenuItem -from utilities.choices import ButtonColorChoices - -menu_heading = { - "title": "Animal Sound", - "icon": "mdi-puzzle" -} - -menu_items = ( - PluginMenuItem( - link='plugins:netbox_animal_sounds:random_animal', - link_text='Random sound', - buttons=( - PluginMenuButton('home', 'Button A', 'fa fa-info', ButtonColorChoices.BLUE), - PluginMenuButton('home', 'Button B', 'fa fa-warning', ButtonColorChoices.GREEN), - ) - ), -) -``` - -The `menu_heading` has the following attributes: - -| Attribute | Required | Description | -|---------------|----------|------------------------------------------------------| -| `title` | Yes | The text that will show in the menu header | -| `icon` | Yes | The icon to use next to the headermdi | - -!!! tip - The icon names can be found at [Material Design Icons](https://materialdesignicons.com/) - ## Menu Buttons +Each menu item can include a set of buttons. These can be handy for providing shortcuts related to the menu item. For instance, most items in NetBox's navigation menu include buttons to create and import new objects. + A `PluginMenuButton` has the following attributes: | Attribute | Required | Description | |---------------|----------|--------------------------------------------------------------------| | `link` | Yes | Name of the URL path to which this button links | | `title` | Yes | The tooltip text (displayed when the mouse hovers over the button) | -| `icon_class` | Yes | Button icon CSS class* | +| `icon_class` | Yes | Button icon CSS class | | `color` | - | One of the choices provided by `ButtonColorChoices` | | `permissions` | - | A list of permissions required to display this button | -*NetBox supports [Material Design Icons](https://materialdesignicons.com/). +Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons. -!!! note - Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons. +!!! tip + Supported icons can be found at [Material Design Icons](https://materialdesignicons.com/) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index a5fdbea10..9fdf172e3 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -204,19 +204,15 @@ def register_template_extensions(class_list): # class PluginMenu: - icon = 'mdi-puzzle' + icon_class = 'mdi-puzzle' - def __init__(self, label, groups, icon=None): + def __init__(self, label, groups, icon_class=None): self.label = label self.groups = [ MenuGroup(label, items) for label, items in groups ] - if icon is not None: - self.icon = icon - - @property - def icon_class(self): - return f'mdi {self.icon}' + if icon_class is not None: + self.icon_class = icon_class class PluginMenuItem: From 3fbd514417755f886d09a352b3fc1a4f7240430c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 28 Sep 2022 16:57:40 -0400 Subject: [PATCH 7/7] Add test for plugin menu registration --- netbox/extras/plugins/__init__.py | 2 +- netbox/extras/tests/dummy_plugin/navigation.py | 10 ++++++++-- netbox/extras/tests/test_plugins.py | 11 ++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 9fdf172e3..ef1106aea 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -204,7 +204,7 @@ def register_template_extensions(class_list): # class PluginMenu: - icon_class = 'mdi-puzzle' + icon_class = 'mdi mdi-puzzle' def __init__(self, label, groups, icon_class=None): self.label = label diff --git a/netbox/extras/tests/dummy_plugin/navigation.py b/netbox/extras/tests/dummy_plugin/navigation.py index 88ac3f7c9..a475b1cde 100644 --- a/netbox/extras/tests/dummy_plugin/navigation.py +++ b/netbox/extras/tests/dummy_plugin/navigation.py @@ -1,7 +1,7 @@ -from extras.plugins import PluginMenuButton, PluginMenuItem +from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem -menu_items = ( +items = ( PluginMenuItem( link='plugins:dummy_plugin:dummy_models', link_text='Item 1', @@ -23,3 +23,9 @@ menu_items = ( link_text='Item 2', ), ) + +menu = PluginMenu( + label='Dummy', + groups=(('Group 1', items),), +) +menu_items = items diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 299cab9ef..e0ff67a2b 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured from django.test import Client, TestCase, override_settings from django.urls import reverse +from extras.plugins import PluginMenu from extras.registry import registry from extras.tests.dummy_plugin import config as dummy_config from netbox.graphql.schema import Query @@ -58,9 +59,17 @@ class PluginTest(TestCase): response = client.get(url) self.assertEqual(response.status_code, 200) + def test_menu(self): + """ + Check menu registration. + """ + menu = registry['plugins']['menus'][0] + self.assertIsInstance(menu, PluginMenu) + self.assertEqual(menu.label, 'Dummy') + def test_menu_items(self): """ - Check that plugin MenuItems and MenuButtons are registered. + Check menu_items registration. """ self.assertIn('Dummy plugin', registry['plugins']['menu_items']) menu_items = registry['plugins']['menu_items']['Dummy plugin']