Просмотр исходного кода

Enable plugins to define user preferences

jeremystretch 4 лет назад
Родитель
Сommit
1aafcf241f

+ 17 - 16
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.
 

+ 19 - 0
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

+ 20 - 0
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'),
+        )
+    ),
+}

+ 9 - 0
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.

+ 49 - 0
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)

+ 4 - 0
netbox/templates/users/preferences.html

@@ -8,6 +8,7 @@
   <form method="post" action="" id="preferences-update">
     {% csrf_token %}
 
+    {% comment %}
     {% for group, fields in form.Meta.fieldsets %}
       <div class="field-group my-5">
         <div class="row mb-2">
@@ -18,6 +19,9 @@
         {% endfor %}
       </div>
     {% endfor %}
+    {% endcomment %}
+
+    {% render_form form %}
 
     <div class="text-end my-3">
       <a class="btn btn-outline-secondary" href="{% url 'user:preferences' %}">Cancel</a>

+ 1 - 10
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):
 

+ 0 - 39
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'),
-        ),
-    ),
-
-}