mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-13 15:22:16 -06:00
Compare commits
7 Commits
20239-plug
...
20911-drop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24642be351 | ||
|
|
89af9efd85 | ||
|
|
99d678502f | ||
|
|
e6300ee06d | ||
|
|
f54ed8bb7f | ||
|
|
5d0609e729 | ||
|
|
865b88e724 |
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.12
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt install -y gettext
|
run: sudo apt install -y gettext
|
||||||
|
|||||||
@@ -733,9 +733,10 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
|
|||||||
)
|
)
|
||||||
module_bay = DynamicModelChoiceField(
|
module_bay = DynamicModelChoiceField(
|
||||||
label=_('Module bay'),
|
label=_('Module bay'),
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.order_by('name'),
|
||||||
query_params={
|
query_params={
|
||||||
'device_id': '$device'
|
'device_id': '$device',
|
||||||
|
'ordering': 'name',
|
||||||
},
|
},
|
||||||
context={
|
context={
|
||||||
'disabled': 'installed_module',
|
'disabled': 'installed_module',
|
||||||
|
|||||||
@@ -259,11 +259,13 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
|||||||
module_bays = []
|
module_bays = []
|
||||||
modules = []
|
modules = []
|
||||||
while module:
|
while module:
|
||||||
if module.pk in modules or module.module_bay.pk in module_bays:
|
module_module_bay = getattr(module, "module_bay", None)
|
||||||
|
if module.pk in modules or (module_module_bay and module_module_bay.pk in module_bays):
|
||||||
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
||||||
modules.append(module.pk)
|
modules.append(module.pk)
|
||||||
module_bays.append(module.module_bay.pk)
|
if module_module_bay:
|
||||||
module = module.module_bay.module if module.module_bay else None
|
module_bays.append(module_module_bay.pk)
|
||||||
|
module = module_module_bay.module if module_module_bay else None
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
is_new = self.pk is None
|
is_new = self.pk is None
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class PluginMenuItem:
|
|||||||
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
||||||
Buttons are each specified as a list of PluginMenuButton instances.
|
Buttons are each specified as a list of PluginMenuButton instances.
|
||||||
"""
|
"""
|
||||||
|
permissions = []
|
||||||
|
buttons = []
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -52,14 +54,10 @@ class PluginMenuItem:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
else:
|
|
||||||
self.permissions = []
|
|
||||||
if buttons is not None:
|
if buttons is not None:
|
||||||
if type(buttons) not in (list, tuple):
|
if type(buttons) not in (list, tuple):
|
||||||
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
else:
|
|
||||||
self.buttons = []
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
@@ -76,6 +74,7 @@ class PluginMenuButton:
|
|||||||
ButtonColorChoices.
|
ButtonColorChoices.
|
||||||
"""
|
"""
|
||||||
color = ButtonColorChoices.DEFAULT
|
color = ButtonColorChoices.DEFAULT
|
||||||
|
permissions = []
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||||
@@ -88,8 +87,6 @@ class PluginMenuButton:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
else:
|
|
||||||
self.permissions = []
|
|
||||||
if color is not None:
|
if color is not None:
|
||||||
if color not in ButtonColorChoices.values():
|
if color not in ButtonColorChoices.values():
|
||||||
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config
|
|||||||
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
||||||
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
||||||
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
||||||
from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton
|
from netbox.plugins.navigation import PluginMenu
|
||||||
from netbox.plugins.utils import get_plugin_config
|
from netbox.plugins.utils import get_plugin_config
|
||||||
from netbox.graphql.schema import Query
|
from netbox.graphql.schema import Query
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@@ -227,46 +227,3 @@ class PluginTest(TestCase):
|
|||||||
Test the registration of webhook callbacks.
|
Test the registration of webhook callbacks.
|
||||||
"""
|
"""
|
||||||
self.assertIn(set_context, registry['webhook_callbacks'])
|
self.assertIn(set_context, registry['webhook_callbacks'])
|
||||||
|
|
||||||
|
|
||||||
class PluginNavigationTest(TestCase):
|
|
||||||
|
|
||||||
def test_plugin_menu_item_independent_permissions(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
|
||||||
item1.permissions.append('leaked_permission')
|
|
||||||
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
|
||||||
|
|
||||||
self.assertIsNot(item1.permissions, item2.permissions)
|
|
||||||
self.assertEqual(item1.permissions, ['leaked_permission'])
|
|
||||||
self.assertEqual(item2.permissions, [])
|
|
||||||
|
|
||||||
def test_plugin_menu_item_independent_buttons(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
|
||||||
button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
|
||||||
item1.buttons.append(button)
|
|
||||||
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
|
||||||
|
|
||||||
self.assertIsNot(item1.buttons, item2.buttons)
|
|
||||||
self.assertEqual(len(item1.buttons), 1)
|
|
||||||
self.assertEqual(item1.buttons[0], button)
|
|
||||||
self.assertEqual(item2.buttons, [])
|
|
||||||
|
|
||||||
def test_plugin_menu_button_independent_permissions(self):
|
|
||||||
button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
|
||||||
button1.permissions.append('leaked_permission')
|
|
||||||
|
|
||||||
button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test')
|
|
||||||
|
|
||||||
self.assertIsNot(button1.permissions, button2.permissions)
|
|
||||||
self.assertEqual(button1.permissions, ['leaked_permission'])
|
|
||||||
self.assertEqual(button2.permissions, [])
|
|
||||||
|
|
||||||
def test_explicit_permissions_remain_independent(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission'])
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission'])
|
|
||||||
|
|
||||||
self.assertIsNot(item1.permissions, item2.permissions)
|
|
||||||
self.assertEqual(item1.permissions, ['explicit_permission'])
|
|
||||||
self.assertEqual(item2.permissions, ['different_permission'])
|
|
||||||
|
|||||||
10
netbox/project-static/dist/netbox.js
vendored
10
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -75,11 +75,15 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
load(value: string) {
|
load(value: string) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
const currentValue = self.getValue();
|
||||||
|
|
||||||
// Automatically clear any cached options. (Only options included
|
// Automatically clear any cached options. (Only options included
|
||||||
// in the API response should be present.)
|
// in the API response should be present.)
|
||||||
self.clearOptions();
|
self.clearOptions();
|
||||||
|
|
||||||
// Populate the null option (if any) if not searching
|
// Clear user_options to prevent the pre-selected option from being treated specially
|
||||||
|
(self as any).user_options = {};
|
||||||
|
|
||||||
if (self.nullOption && !value) {
|
if (self.nullOption && !value) {
|
||||||
self.addOption(self.nullOption);
|
self.addOption(self.nullOption);
|
||||||
}
|
}
|
||||||
@@ -93,21 +97,33 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
addClasses(self.wrapper, self.settings.loadingClass);
|
addClasses(self.wrapper, self.settings.loadingClass);
|
||||||
self.loading++;
|
self.loading++;
|
||||||
|
|
||||||
// Make the API request
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(apiData => {
|
.then(apiData => {
|
||||||
const results: Dict[] = apiData.results;
|
const results: Dict[] = apiData.results;
|
||||||
const options: Dict[] = [];
|
|
||||||
for (const result of results) {
|
// Add options and set $order to preserve API response order
|
||||||
|
results.forEach((result, index) => {
|
||||||
const option = self.getOptionFromData(result);
|
const option = self.getOptionFromData(result);
|
||||||
options.push(option);
|
self.addOption(option);
|
||||||
|
const key = option[self.settings.valueField as string] as string;
|
||||||
|
if (self.options[key]) {
|
||||||
|
(self.options[key] as any).$order = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (self.loading > 0) {
|
||||||
|
self.loading--;
|
||||||
|
if (self.loading === 0) {
|
||||||
|
self.wrapper.classList.remove(self.settings.loadingClass as string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return options;
|
|
||||||
})
|
if (currentValue && !self.items.includes(currentValue as string)) {
|
||||||
// Pass the options to the callback function
|
self.items.push(currentValue as string);
|
||||||
.then(options => {
|
}
|
||||||
self.loadCallback(options, []);
|
|
||||||
|
self.refreshOptions(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
self.loadCallback([], []);
|
self.loadCallback([], []);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user