diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index bda703cbe..001c8d1ea 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/styles/transitional/_forms.scss b/netbox/project-static/styles/transitional/_forms.scss index b570e578b..668f60e2a 100644 --- a/netbox/project-static/styles/transitional/_forms.scss +++ b/netbox/project-static/styles/transitional/_forms.scss @@ -39,7 +39,10 @@ select[multiple] { position: sticky; top: 0; background-color: var(--bs-body-bg); + font-style: normal; font-weight: bold; - padding: 0.25rem 0.5rem; + } + option { + padding-left: 0.5rem; } } diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index b20dcbead..d5f5bbbc0 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,6 +1,8 @@ import json +from collections import defaultdict from django import forms +from django.apps import apps from django.conf import settings from django.contrib.auth import password_validation from django.contrib.postgres.forms import SimpleArrayField @@ -21,6 +23,7 @@ from utilities.forms.fields import ( DynamicModelMultipleChoiceField, JSONField, ) +from utilities.string import title from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget from utilities.permissions import qs_filter_from_constraints @@ -287,37 +290,20 @@ def get_object_types_choices(): Generate choices for object types grouped by app label using optgroups. Returns nested structure: [(app_label, [(id, model_name), ...]), ...] """ - from django.apps import apps - - choices = [] - current_app = None - current_group = [] + app_label_map = { + app_config.label: app_config.verbose_name + for app_config in apps.get_app_configs() + } + choices_by_app = defaultdict(list) for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES).order_by('app_label', 'model'): - # Get verbose app label (e.g., "NetBox Branching" instead of "netbox_branching") - try: - app_config = apps.get_app_config(ot.app_label) - app_label = app_config.verbose_name - except LookupError: - app_label = ot.app_label + app_label = app_label_map.get(ot.app_label, ot.app_label) - # Start new optgroup when app changes - if current_app != app_label: - if current_group: - choices.append((current_app, current_group)) - current_app = app_label - current_group = [] - - # Add model to current group using just the model's verbose name model_class = ot.model_class() model_name = model_class._meta.verbose_name if model_class else ot.model - current_group.append((ot.pk, model_name.title())) + choices_by_app[app_label].append((ot.pk, title(model_name))) - # Add final group - if current_group: - choices.append((current_app, current_group)) - - return choices + return list(choices_by_app.items()) class ObjectPermissionForm(forms.ModelForm): diff --git a/netbox/utilities/forms/widgets/select.py b/netbox/utilities/forms/widgets/select.py index 493d4081d..b3197a922 100644 --- a/netbox/utilities/forms/widgets/select.py +++ b/netbox/utilities/forms/widgets/select.py @@ -66,31 +66,46 @@ class SelectWithPK(forms.Select): option_template_name = 'widgets/select_option_with_pk.html' -class AvailableOptions(forms.SelectMultiple): +class SelectMultipleBase(forms.SelectMultiple): """ - Renders a including only choices that have been selected. (For unbound fields, this list + will be empty.) Employed by SplitMultiSelectWidget. + """ + def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) @@ -99,43 +114,13 @@ class AvailableOptions(forms.SelectMultiple): return context - def create_option(self, name, value, label, selected, index, subindex=None, attrs=None): - option = super().create_option(name, value, label, selected, index, subindex, attrs) - # Add title attribute to show full text on hover - option['attrs']['title'] = label - return option - -class SelectedOptions(forms.SelectMultiple): +class SelectedOptions(SelectMultipleBase): """ Renders a