mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-17 22:49:59 +02:00
* Fixes #20759: Group object types by app in permission form Modified the ObjectPermissionForm to use optgroups for organizing object types by application. This shortens the display names (e.g., "permission" instead of "Authentication and Authorization | permission") while maintaining clear organization through visual grouping. Changes: - Updated get_object_types_choices() to return nested optgroup structure - Enhanced AvailableOptions and SelectedOptions widgets to handle optgroups - Modified TypeScript moveOptions to preserve optgroup structure - Added hover text showing full model names - Styled optgroups with bold, padded labels * Address PR feedback
This commit is contained in:
@@ -66,17 +66,45 @@ class SelectWithPK(forms.Select):
|
||||
option_template_name = 'widgets/select_option_with_pk.html'
|
||||
|
||||
|
||||
class AvailableOptions(forms.SelectMultiple):
|
||||
class SelectMultipleBase(forms.SelectMultiple):
|
||||
"""
|
||||
Base class for select widgets that filter choices based on selected values.
|
||||
Subclasses should set `include_selected` to control filtering behavior.
|
||||
"""
|
||||
include_selected = False
|
||||
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
filtered_choices = []
|
||||
include_selected = self.include_selected
|
||||
|
||||
for choice in self.choices:
|
||||
if isinstance(choice[1], (list, tuple)): # optgroup
|
||||
group_label, group_choices = choice
|
||||
filtered_group = [
|
||||
c for c in group_choices if (str(c[0]) in value) == include_selected
|
||||
]
|
||||
|
||||
if filtered_group: # Only include optgroup if it has choices left
|
||||
filtered_choices.append((group_label, filtered_group))
|
||||
else: # option, e.g. flat choice
|
||||
if (str(choice[0]) in value) == include_selected:
|
||||
filtered_choices.append(choice)
|
||||
|
||||
self.choices = filtered_choices
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
option['attrs']['title'] = label # Add title attribute to show full text on hover
|
||||
return option
|
||||
|
||||
|
||||
class AvailableOptions(SelectMultipleBase):
|
||||
"""
|
||||
Renders a <select multiple=true> including only choices that have been selected. (For unbound fields, this list
|
||||
will be empty.) Employed by SplitMultiSelectWidget.
|
||||
"""
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
self.choices = [
|
||||
choice for choice in self.choices if str(choice[0]) not in value
|
||||
]
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
@@ -87,17 +115,12 @@ class AvailableOptions(forms.SelectMultiple):
|
||||
return context
|
||||
|
||||
|
||||
class SelectedOptions(forms.SelectMultiple):
|
||||
class SelectedOptions(SelectMultipleBase):
|
||||
"""
|
||||
Renders a <select multiple=true> including only choices that have _not_ been selected. (For unbound fields, this
|
||||
will include _all_ choices.) Employed by SplitMultiSelectWidget.
|
||||
"""
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
self.choices = [
|
||||
choice for choice in self.choices if str(choice[0]) in value
|
||||
]
|
||||
value = [] # Clear selected choices
|
||||
return super().optgroups(name, value, attrs)
|
||||
include_selected = True
|
||||
|
||||
|
||||
class SplitMultiSelectWidget(forms.MultiWidget):
|
||||
|
||||
@@ -7,6 +7,7 @@ from utilities.forms.bulk_import import BulkImportForm
|
||||
from utilities.forms.fields.csv import CSVSelectWidget
|
||||
from utilities.forms.forms import BulkRenameForm
|
||||
from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
|
||||
from utilities.forms.widgets.select import AvailableOptions, SelectedOptions
|
||||
|
||||
|
||||
class ExpandIPAddress(TestCase):
|
||||
@@ -481,3 +482,71 @@ class CSVSelectWidgetTest(TestCase):
|
||||
widget = CSVSelectWidget()
|
||||
data = {'test_field': 'valid_value'}
|
||||
self.assertFalse(widget.value_omitted_from_data(data, {}, 'test_field'))
|
||||
|
||||
|
||||
class SelectMultipleWidgetTest(TestCase):
|
||||
"""
|
||||
Validate filtering behavior of AvailableOptions and SelectedOptions widgets.
|
||||
"""
|
||||
|
||||
def test_available_options_flat_choices(self):
|
||||
"""AvailableOptions should exclude selected values from flat choices"""
|
||||
widget = AvailableOptions(choices=[
|
||||
(1, 'Option 1'),
|
||||
(2, 'Option 2'),
|
||||
(3, 'Option 3'),
|
||||
])
|
||||
widget.optgroups('test', ['2'], None)
|
||||
|
||||
self.assertEqual(len(widget.choices), 2)
|
||||
self.assertEqual(widget.choices[0], (1, 'Option 1'))
|
||||
self.assertEqual(widget.choices[1], (3, 'Option 3'))
|
||||
|
||||
def test_available_options_optgroups(self):
|
||||
"""AvailableOptions should exclude selected values from optgroups"""
|
||||
widget = AvailableOptions(choices=[
|
||||
('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
|
||||
('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
|
||||
])
|
||||
|
||||
# Select options 2 and 3
|
||||
widget.optgroups('test', ['2', '3'], None)
|
||||
|
||||
# Should have 2 groups with filtered choices
|
||||
self.assertEqual(len(widget.choices), 2)
|
||||
self.assertEqual(widget.choices[0][0], 'Group A')
|
||||
self.assertEqual(widget.choices[0][1], [(1, 'Option 1')])
|
||||
self.assertEqual(widget.choices[1][0], 'Group B')
|
||||
self.assertEqual(widget.choices[1][1], [(4, 'Option 4')])
|
||||
|
||||
def test_selected_options_flat_choices(self):
|
||||
"""SelectedOptions should include only selected values from flat choices"""
|
||||
widget = SelectedOptions(choices=[
|
||||
(1, 'Option 1'),
|
||||
(2, 'Option 2'),
|
||||
(3, 'Option 3'),
|
||||
])
|
||||
|
||||
# Select option 2
|
||||
widget.optgroups('test', ['2'], None)
|
||||
|
||||
# Should only have option 2
|
||||
self.assertEqual(len(widget.choices), 1)
|
||||
self.assertEqual(widget.choices[0], (2, 'Option 2'))
|
||||
|
||||
def test_selected_options_optgroups(self):
|
||||
"""SelectedOptions should include only selected values from optgroups"""
|
||||
widget = SelectedOptions(choices=[
|
||||
('Group A', [(1, 'Option 1'), (2, 'Option 2')]),
|
||||
('Group B', [(3, 'Option 3'), (4, 'Option 4')]),
|
||||
])
|
||||
|
||||
# Select options 2 and 3
|
||||
widget.optgroups('test', ['2', '3'], None)
|
||||
|
||||
# Should have 2 groups with only selected choices
|
||||
self.assertEqual(len(widget.choices), 2)
|
||||
self.assertEqual(widget.choices[0][0], 'Group A')
|
||||
self.assertEqual(widget.choices[0][1], [(2, 'Option 2')])
|
||||
self.assertEqual(widget.choices[1][0], 'Group B')
|
||||
self.assertEqual(widget.choices[1][1], [(3, 'Option 3')])
|
||||
|
||||
Reference in New Issue
Block a user