1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Enable plugins to define user preferences

This commit is contained in:
jeremystretch
2021-12-22 09:10:50 -05:00
parent 1eeac7f4f4
commit 1aafcf241f
8 changed files with 119 additions and 65 deletions

View File

@ -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.

View File

@ -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

View File

@ -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'),
)
),
}

View File

@ -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.

View File

@ -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)

View File

@ -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>

View File

@ -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):

View File

@ -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'),
),
),
}