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:
@ -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.
|
||||
|
||||
|
@ -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
netbox/extras/tests/dummy_plugin/preferences.py
Normal file
20
netbox/extras/tests/dummy_plugin/preferences.py
Normal 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'),
|
||||
)
|
||||
),
|
||||
}
|
@ -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
netbox/netbox/preferences.py
Normal file
49
netbox/netbox/preferences.py
Normal 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)
|
@ -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>
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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'),
|
||||
),
|
||||
),
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user