mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge pull request #8143 from netbox-community/7759-user-preferences
Closes #7759: User preferences framework
This commit is contained in:
commit
d64c88786e
@ -66,6 +66,22 @@ CUSTOM_VALIDATORS = {
|
||||
|
||||
---
|
||||
|
||||
## DEFAULT_USER_PREFERENCES
|
||||
|
||||
This is a dictionary defining the default preferences to be set for newly-created user accounts. For example, to set the default page size for all users to 100, define the following:
|
||||
|
||||
```python
|
||||
DEFAULT_USER_PREFERENCES = {
|
||||
"pagination": {
|
||||
"per_page": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a complete list of available preferences, log into NetBox and navigate to `/user/preferences/`. A period in a preference name indicates a level of nesting in the JSON data. The example above maps to `pagination.per_page`.
|
||||
|
||||
---
|
||||
|
||||
## ENFORCE_GLOBAL_UNIQUE
|
||||
|
||||
Default: False
|
||||
|
@ -4,8 +4,9 @@ The `users.UserConfig` model holds individual preferences for each user in the f
|
||||
|
||||
## Available Preferences
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) |
|
||||
| pagination.per_page | The number of items to display per page of a paginated table |
|
||||
| tables.TABLE_NAME.columns | The ordered list of columns to display when viewing the table |
|
||||
| Name | Description |
|
||||
|-------------------------|-------------|
|
||||
| data_format | Preferred format when rendering raw data (JSON or YAML) |
|
||||
| pagination.per_page | The number of items to display per page of a paginated table |
|
||||
| tables.${table}.columns | The ordered list of columns to display when viewing the table |
|
||||
| ui.colormode | Light or dark mode in the user interface |
|
||||
|
@ -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.
|
||||
|
||||
|
@ -33,6 +33,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
||||
('NAPALM', {
|
||||
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
|
||||
}),
|
||||
('User Preferences', {
|
||||
'fields': ('DEFAULT_USER_PREFERENCES',),
|
||||
}),
|
||||
('Miscellaneous', {
|
||||
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
|
||||
}),
|
||||
|
@ -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.
|
||||
|
@ -296,9 +296,9 @@ class ConfigContextView(generic.ObjectView):
|
||||
if request.GET.get('format') in ['json', 'yaml']:
|
||||
format = request.GET.get('format')
|
||||
if request.user.is_authenticated:
|
||||
request.user.config.set('extras.configcontext.format', format, commit=True)
|
||||
request.user.config.set('data_format', format, commit=True)
|
||||
elif request.user.is_authenticated:
|
||||
format = request.user.config.get('extras.configcontext.format', 'json')
|
||||
format = request.user.config.get('data_format', 'json')
|
||||
else:
|
||||
format = 'json'
|
||||
|
||||
@ -341,9 +341,9 @@ class ObjectConfigContextView(generic.ObjectView):
|
||||
if request.GET.get('format') in ['json', 'yaml']:
|
||||
format = request.GET.get('format')
|
||||
if request.user.is_authenticated:
|
||||
request.user.config.set('extras.configcontext.format', format, commit=True)
|
||||
request.user.config.set('data_format', format, commit=True)
|
||||
elif request.user.is_authenticated:
|
||||
format = request.user.config.get('extras.configcontext.format', 'json')
|
||||
format = request.user.config.get('data_format', 'json')
|
||||
else:
|
||||
format = 'json'
|
||||
|
||||
|
@ -131,6 +131,15 @@ PARAMS = (
|
||||
field=forms.JSONField
|
||||
),
|
||||
|
||||
# User preferences
|
||||
ConfigParam(
|
||||
name='DEFAULT_USER_PREFERENCES',
|
||||
label='Default preferences',
|
||||
default={},
|
||||
description="Default preferences for new users",
|
||||
field=forms.JSONField
|
||||
),
|
||||
|
||||
# Miscellaneous
|
||||
ConfigParam(
|
||||
name='MAINTENANCE_MODE',
|
||||
|
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)
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,7 +1,6 @@
|
||||
import { initConnectionToggle } from './connectionToggle';
|
||||
import { initDepthToggle } from './depthToggle';
|
||||
import { initMoveButtons } from './moveOptions';
|
||||
import { initPreferenceUpdate } from './preferences';
|
||||
import { initReslug } from './reslug';
|
||||
import { initSelectAll } from './selectAll';
|
||||
|
||||
@ -11,7 +10,6 @@ export function initButtons(): void {
|
||||
initConnectionToggle,
|
||||
initReslug,
|
||||
initSelectAll,
|
||||
initPreferenceUpdate,
|
||||
initMoveButtons,
|
||||
]) {
|
||||
func();
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { setColorMode } from '../colorMode';
|
||||
import { getElement } from '../util';
|
||||
|
||||
/**
|
||||
* Perform actions in the UI based on the value of user profile updates.
|
||||
*
|
||||
* @param event Form Submit
|
||||
*/
|
||||
function handlePreferenceSave(event: Event): void {
|
||||
// Create a FormData instance to access the form values.
|
||||
const form = event.currentTarget as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Update the UI color mode immediately when the user preference changes.
|
||||
if (formData.get('ui.colormode') === 'dark') {
|
||||
setColorMode('dark');
|
||||
} else if (formData.get('ui.colormode') === 'light') {
|
||||
setColorMode('light');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize handlers for user profile updates.
|
||||
*/
|
||||
export function initPreferenceUpdate(): void {
|
||||
const form = getElement<HTMLFormElement>('preferences-update');
|
||||
if (form !== null) {
|
||||
form.addEventListener('submit', handlePreferenceSave);
|
||||
}
|
||||
}
|
@ -1,57 +1,40 @@
|
||||
{% extends 'users/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}User Preferences{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="" id="preferences-update">
|
||||
{% csrf_token %}
|
||||
<div class="field-group mb-3">
|
||||
<h5>Color Mode</h5>
|
||||
<p class="text-muted">Set preferred UI color mode</p>
|
||||
{% with color_mode=preferences|get_key:'ui.colormode'%}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="ui.colormode" id="color-mode-preference-dark" value="dark"{% if color_mode == 'dark'%} checked{% endif %}>
|
||||
<label class="form-check-label" for="color-mode-preference-dark">Dark</label>
|
||||
|
||||
{% for group, fields in form.Meta.fieldsets %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">{{ group }}</h5>
|
||||
</div>
|
||||
{% for name in fields %}
|
||||
{% render_field form|getfield:name %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="ui.colormode" id="color-mode-preference-light" value="light"{% if color_mode == 'light'%} checked{% endif %}>
|
||||
<label class="form-check-label" for="color-mode-preference-light">Light</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
|
||||
{% with plugin_fields=form.plugin_fields %}
|
||||
{% if plugin_fields %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Plugins</h5>
|
||||
</div>
|
||||
{% for name in plugin_fields %}
|
||||
{% render_field form|getfield:name %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="text-end my-3">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'user:preferences' %}">Cancel</a>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save </button>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-primary" name="_update">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if preferences %}
|
||||
<div class="field-group mb-3">
|
||||
<h5>Other Preferences</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" class="toggle form-check-input" title="Toggle All"></th>
|
||||
<th>Preference</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, value in preferences.items %}
|
||||
<tr>
|
||||
<td class="min-width"><input class="form-check-input" type="checkbox" name="pk" value="{{ key }}"></td>
|
||||
<td><samp>{{ key }}</samp></td>
|
||||
<td><samp>{{ value }}</samp></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-danger" name="_delete">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Clear Selected
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<h3 class="text-muted text-center">No preferences found</h3>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -1,8 +1,11 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
from utilities.forms import BootstrapMixin, DateTimePicker
|
||||
from .models import Token
|
||||
from netbox.preferences import PREFERENCES
|
||||
from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect
|
||||
from utilities.utils import flatten_dict
|
||||
from .models import Token, UserConfig
|
||||
|
||||
|
||||
class LoginForm(BootstrapMixin, AuthenticationForm):
|
||||
@ -13,6 +16,67 @@ class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
|
||||
pass
|
||||
|
||||
|
||||
class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
|
||||
# Emulate a declared field for each supported user preference
|
||||
preference_fields = {}
|
||||
for field_name, preference in PREFERENCES.items():
|
||||
description = f'{preference.description}<br />' if preference.description else ''
|
||||
help_text = f'{description}<code>{field_name}</code>'
|
||||
field_kwargs = {
|
||||
'label': preference.label,
|
||||
'choices': preference.choices,
|
||||
'help_text': mark_safe(help_text),
|
||||
'coerce': preference.coerce,
|
||||
'required': False,
|
||||
'widget': StaticSelect,
|
||||
}
|
||||
preference_fields[field_name] = forms.TypedChoiceField(**field_kwargs)
|
||||
attrs.update(preference_fields)
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMetaclass):
|
||||
|
||||
class Meta:
|
||||
model = UserConfig
|
||||
fields = ()
|
||||
fieldsets = (
|
||||
('User Interface', (
|
||||
'pagination.per_page',
|
||||
'ui.colormode',
|
||||
)),
|
||||
('Miscellaneous', (
|
||||
'data_format',
|
||||
)),
|
||||
)
|
||||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
|
||||
# Get initial data from UserConfig instance
|
||||
initial_data = flatten_dict(instance.data)
|
||||
kwargs['initial'] = initial_data
|
||||
|
||||
super().__init__(*args, instance=instance, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set UserConfig data
|
||||
for pref_name, value in self.cleaned_data.items():
|
||||
self.instance.set(pref_name, value, commit=False)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def plugin_fields(self):
|
||||
return [
|
||||
name for name in self.fields.keys() if name.startswith('plugins.')
|
||||
]
|
||||
|
||||
|
||||
class TokenForm(BootstrapMixin, forms.ModelForm):
|
||||
key = forms.CharField(
|
||||
required=False,
|
||||
|
@ -10,6 +10,7 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from netbox.config import get_config
|
||||
from netbox.models import BigIDModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import flatten_dict
|
||||
@ -79,13 +80,25 @@ class UserConfig(models.Model):
|
||||
keys = path.split('.')
|
||||
|
||||
# Iterate down the hierarchy, returning the default value if any invalid key is encountered
|
||||
for key in keys:
|
||||
if type(d) is dict and key in d:
|
||||
d = d.get(key)
|
||||
else:
|
||||
return default
|
||||
try:
|
||||
for key in keys:
|
||||
d = d[key]
|
||||
return d
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
return d
|
||||
# If the key is not found in the user's config, check for an application-wide default
|
||||
config = get_config()
|
||||
d = config.DEFAULT_USER_PREFERENCES
|
||||
try:
|
||||
for key in keys:
|
||||
d = d[key]
|
||||
return d
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
# Finally, return the specified default value (if any)
|
||||
return default
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
@ -166,7 +179,8 @@ def create_userconfig(instance, created, **kwargs):
|
||||
Automatically create a new UserConfig when a new User is created.
|
||||
"""
|
||||
if created:
|
||||
UserConfig(user=instance).save()
|
||||
config = get_config()
|
||||
UserConfig(user=instance, data=config.DEFAULT_USER_PREFERENCES).save()
|
||||
|
||||
|
||||
#
|
||||
|
10
netbox/users/preferences.py
Normal file
10
netbox/users/preferences.py
Normal file
@ -0,0 +1,10 @@
|
||||
class UserPreference:
|
||||
"""
|
||||
Represents a configurable user preference.
|
||||
"""
|
||||
def __init__(self, label, choices, default=None, description='', coerce=lambda x: x):
|
||||
self.label = label
|
||||
self.choices = choices
|
||||
self.default = default if default is not None else choices[0]
|
||||
self.description = description
|
||||
self.coerce = coerce
|
@ -1,8 +1,6 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from users.models import UserConfig
|
||||
|
||||
|
||||
class UserConfigTest(TestCase):
|
||||
|
||||
|
39
netbox/users/tests/test_preferences.py
Normal file
39
netbox/users/tests/test_preferences.py
Normal file
@ -0,0 +1,39 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings, TestCase
|
||||
|
||||
from users.preferences import UserPreference
|
||||
|
||||
|
||||
DEFAULT_USER_PREFERENCES = {
|
||||
'pagination': {
|
||||
'per_page': 250,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserPreferencesTest(TestCase):
|
||||
|
||||
def test_userpreference(self):
|
||||
CHOICES = (
|
||||
('foo', 'Foo'),
|
||||
('bar', 'Bar'),
|
||||
)
|
||||
kwargs = {
|
||||
'label': 'Test Preference',
|
||||
'choices': CHOICES,
|
||||
'default': CHOICES[0][0],
|
||||
'description': 'Description',
|
||||
}
|
||||
userpref = UserPreference(**kwargs)
|
||||
|
||||
self.assertEqual(userpref.label, kwargs['label'])
|
||||
self.assertEqual(userpref.choices, kwargs['choices'])
|
||||
self.assertEqual(userpref.default, kwargs['default'])
|
||||
self.assertEqual(userpref.description, kwargs['description'])
|
||||
|
||||
@override_settings(DEFAULT_USER_PREFERENCES=DEFAULT_USER_PREFERENCES)
|
||||
def test_default_preferences(self):
|
||||
user = User.objects.create(username='User 1')
|
||||
userconfig = user.config
|
||||
|
||||
self.assertEqual(userconfig.data, DEFAULT_USER_PREFERENCES)
|
@ -19,7 +19,7 @@ from extras.models import ObjectChange
|
||||
from extras.tables import ObjectChangeTable
|
||||
from netbox.config import get_config
|
||||
from utilities.forms import ConfirmationForm
|
||||
from .forms import LoginForm, PasswordChangeForm, TokenForm
|
||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||
from .models import Token
|
||||
|
||||
|
||||
@ -137,32 +137,28 @@ class UserConfigView(LoginRequiredMixin, View):
|
||||
template_name = 'users/preferences.html'
|
||||
|
||||
def get(self, request):
|
||||
userconfig = request.user.config
|
||||
form = UserConfigForm(instance=userconfig)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'preferences': request.user.config.all(),
|
||||
'form': form,
|
||||
'active_tab': 'preferences',
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
userconfig = request.user.config
|
||||
data = userconfig.all()
|
||||
form = UserConfigForm(request.POST, instance=userconfig)
|
||||
|
||||
# Delete selected preferences
|
||||
if "_delete" in request.POST:
|
||||
for key in request.POST.getlist('pk'):
|
||||
if key in data:
|
||||
userconfig.clear(key)
|
||||
# Update specific values
|
||||
elif "_update" in request.POST:
|
||||
for key in request.POST:
|
||||
if not key.startswith('_') and not key.startswith('csrf'):
|
||||
for value in request.POST.getlist(key):
|
||||
userconfig.set(key, value)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
userconfig.save()
|
||||
messages.success(request, "Your preferences have been updated.")
|
||||
messages.success(request, "Your preferences have been updated.")
|
||||
return redirect('user:preferences')
|
||||
|
||||
return redirect('user:preferences')
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'active_tab': 'preferences',
|
||||
})
|
||||
|
||||
|
||||
class ChangePasswordView(LoginRequiredMixin, View):
|
||||
|
@ -282,7 +282,7 @@ def flatten_dict(d, prefix='', separator='.'):
|
||||
for k, v in d.items():
|
||||
key = separator.join([prefix, k]) if prefix else k
|
||||
if type(v) is dict:
|
||||
ret.update(flatten_dict(v, prefix=key))
|
||||
ret.update(flatten_dict(v, prefix=key, separator=separator))
|
||||
else:
|
||||
ret[key] = v
|
||||
return ret
|
||||
|
Loading…
Reference in New Issue
Block a user