From 1aafcf241fe537025f8a5860d468320c1d2202d5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 22 Dec 2021 09:10:50 -0500 Subject: [PATCH] Enable plugins to define user preferences --- docs/plugins/development.md | 33 +++++++------ netbox/extras/plugins/__init__.py | 19 +++++++ .../extras/tests/dummy_plugin/preferences.py | 20 ++++++++ netbox/extras/tests/test_plugins.py | 9 ++++ netbox/netbox/preferences.py | 49 +++++++++++++++++++ netbox/templates/users/preferences.html | 4 ++ netbox/users/forms.py | 11 +---- netbox/users/preferences.py | 39 --------------- 8 files changed, 119 insertions(+), 65 deletions(-) create mode 100644 netbox/extras/tests/dummy_plugin/preferences.py create mode 100644 netbox/netbox/preferences.py diff --git a/docs/plugins/development.md b/docs/plugins/development.md index 89436a321..d20f73cb6 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -99,22 +99,23 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i #### PluginConfig Attributes -| Name | Description | -| ---- | ----------- | -| `name` | Raw plugin name; same as the plugin's source directory | -| `verbose_name` | Human-friendly name for the plugin | -| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) | -| `description` | Brief description of the plugin's purpose | -| `author` | Name of plugin's author | -| `author_email` | Author's public email address | -| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. | -| `required_settings` | A list of any configuration parameters that **must** be defined by the user | -| `default_settings` | A dictionary of configuration parameters and their default values | -| `min_version` | Minimum version of NetBox with which the plugin is compatible | -| `max_version` | Maximum version of NetBox with which the plugin is compatible | -| `middleware` | A list of middleware classes to append after NetBox's build-in middleware | -| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | -| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | +| Name | Description | +| ---- |---------------------------------------------------------------------------------------------------------------| +| `name` | Raw plugin name; same as the plugin's source directory | +| `verbose_name` | Human-friendly name for the plugin | +| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) | +| `description` | Brief description of the plugin's purpose | +| `author` | Name of plugin's author | +| `author_email` | Author's public email address | +| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. | +| `required_settings` | A list of any configuration parameters that **must** be defined by the user | +| `default_settings` | A dictionary of configuration parameters and their default values | +| `min_version` | Minimum version of NetBox with which the plugin is compatible | +| `max_version` | Maximum version of NetBox with which the plugin is compatible | +| `middleware` | A list of middleware classes to append after NetBox's build-in middleware | +| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | +| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | +| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) | All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index f9a7856ea..5b02b5ab7 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -15,6 +15,7 @@ from extras.plugins.utils import import_object # Initialize plugin registry stores registry['plugin_template_extensions'] = collections.defaultdict(list) registry['plugin_menu_items'] = {} +registry['plugin_preferences'] = {} # @@ -54,6 +55,7 @@ class PluginConfig(AppConfig): # integrated components. template_extensions = 'template_content.template_extensions' menu_items = 'navigation.menu_items' + user_preferences = 'preferences.preferences' def ready(self): @@ -67,6 +69,12 @@ class PluginConfig(AppConfig): if menu_items is not None: register_menu_items(self.verbose_name, menu_items) + # Register user preferences + user_preferences = import_object(f"{self.__module__}.{self.user_preferences}") + if user_preferences is not None: + plugin_name = self.name.rsplit('.', 1)[1] + register_user_preferences(plugin_name, user_preferences) + @classmethod def validate(cls, user_config, netbox_version): @@ -242,3 +250,14 @@ def register_menu_items(section_name, class_list): raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton") registry['plugin_menu_items'][section_name] = class_list + + +# +# User preferences +# + +def register_user_preferences(plugin_name, preferences): + """ + Register a list of user preferences defined by a plugin. + """ + registry['plugin_preferences'][plugin_name] = preferences diff --git a/netbox/extras/tests/dummy_plugin/preferences.py b/netbox/extras/tests/dummy_plugin/preferences.py new file mode 100644 index 000000000..f925ee6e0 --- /dev/null +++ b/netbox/extras/tests/dummy_plugin/preferences.py @@ -0,0 +1,20 @@ +from users.preferences import UserPreference + + +preferences = { + 'pref1': UserPreference( + label='First preference', + choices=( + ('foo', 'Foo'), + ('bar', 'Bar'), + ) + ), + 'pref2': UserPreference( + label='Second preference', + choices=( + ('a', 'A'), + ('b', 'B'), + ('c', 'C'), + ) + ), +} diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index 2508ffb83..4bea9933e 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -74,6 +74,15 @@ class PluginTest(TestCase): self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site']) + def test_user_preferences(self): + """ + Check that plugin UserPreferences are registered. + """ + self.assertIn('dummy_plugin', registry['plugin_preferences']) + user_preferences = registry['plugin_preferences']['dummy_plugin'] + self.assertEqual(type(user_preferences), dict) + self.assertEqual(list(user_preferences.keys()), ['pref1', 'pref2']) + def test_middleware(self): """ Check that plugin middleware is registered. diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py new file mode 100644 index 000000000..4cad8cf24 --- /dev/null +++ b/netbox/netbox/preferences.py @@ -0,0 +1,49 @@ +from extras.registry import registry +from users.preferences import UserPreference +from utilities.paginator import EnhancedPaginator + + +def get_page_lengths(): + return [ + (v, str(v)) for v in EnhancedPaginator.default_page_lengths + ] + + +PREFERENCES = { + + # User interface + 'ui.colormode': UserPreference( + label='Color mode', + choices=( + ('light', 'Light'), + ('dark', 'Dark'), + ), + default='light', + ), + 'pagination.per_page': UserPreference( + label='Page length', + choices=get_page_lengths(), + description='The number of objects to display per page', + coerce=lambda x: int(x) + ), + + # Miscellaneous + 'data_format': UserPreference( + label='Data format', + choices=( + ('json', 'JSON'), + ('yaml', 'YAML'), + ), + ), + +} + +# Register plugin preferences +if registry['plugin_preferences']: + plugin_preferences = {} + + for plugin_name, preferences in registry['plugin_preferences'].items(): + for name, userpreference in preferences.items(): + PREFERENCES[f'plugins.{plugin_name}.{name}'] = userpreference + + PREFERENCES.update(plugin_preferences) diff --git a/netbox/templates/users/preferences.html b/netbox/templates/users/preferences.html index 254b5b8ff..06a48b431 100644 --- a/netbox/templates/users/preferences.html +++ b/netbox/templates/users/preferences.html @@ -8,6 +8,7 @@
{% csrf_token %} + {% comment %} {% for group, fields in form.Meta.fieldsets %}
@@ -18,6 +19,9 @@ {% endfor %}
{% endfor %} + {% endcomment %} + + {% render_form form %}
Cancel diff --git a/netbox/users/forms.py b/netbox/users/forms.py index 721c68e43..a6c606c4b 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -2,10 +2,10 @@ from django import forms from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm from django.utils.html import mark_safe +from netbox.preferences import PREFERENCES from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect from utilities.utils import flatten_dict from .models import Token, UserConfig -from .preferences import PREFERENCES class LoginForm(BootstrapMixin, AuthenticationForm): @@ -44,15 +44,6 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe class Meta: model = UserConfig fields = () - fieldsets = ( - ('User Interface', ( - 'pagination.per_page', - 'ui.colormode', - )), - ('Miscellaneous', ( - 'data_format', - )), - ) def __init__(self, *args, instance=None, **kwargs): diff --git a/netbox/users/preferences.py b/netbox/users/preferences.py index 635393913..c66bc96c0 100644 --- a/netbox/users/preferences.py +++ b/netbox/users/preferences.py @@ -1,12 +1,3 @@ -from utilities.paginator import EnhancedPaginator - - -def get_page_lengths(): - return [ - (v, str(v)) for v in EnhancedPaginator.default_page_lengths - ] - - class UserPreference: def __init__(self, label, choices, default=None, description='', coerce=lambda x: x): @@ -15,33 +6,3 @@ class UserPreference: self.default = default if default is not None else choices[0] self.description = description self.coerce = coerce - - -PREFERENCES = { - - # User interface - 'ui.colormode': UserPreference( - label='Color mode', - choices=( - ('light', 'Light'), - ('dark', 'Dark'), - ), - default='light', - ), - 'pagination.per_page': UserPreference( - label='Page length', - choices=get_page_lengths(), - description='The number of objects to display per page', - coerce=lambda x: int(x) - ), - - # Miscellaneous - 'data_format': UserPreference( - label='Data format', - choices=( - ('json', 'JSON'), - ('yaml', 'YAML'), - ), - ), - -}