diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index ddb4ef358..d6dad2e46 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -53,6 +53,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='circuits.ProviderAccount', filters={'provider_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['provider'], actions=[ actions.AddObject( 'circuits.ProviderAccount', url_params={'provider': lambda ctx: ctx['object'].pk} @@ -62,6 +63,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='circuits.Circuit', filters={'provider_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['provider'], actions=[ actions.AddObject('circuits.Circuit', url_params={'provider': lambda ctx: ctx['object'].pk}), ], @@ -161,6 +163,7 @@ class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='circuits.Circuit', filters={'provider_account_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['provider_account'], actions=[ actions.AddObject( 'circuits.Circuit', @@ -257,6 +260,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='circuits.VirtualCircuit', filters={'provider_network_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['provider_network'], actions=[ actions.AddObject( 'circuits.VirtualCircuit', url_params={'provider_network': lambda ctx: ctx['object'].pk} @@ -801,6 +805,7 @@ class VirtualCircuitView(generic.ObjectView): model='circuits.VirtualCircuitTermination', title=_('Terminations'), filters={'virtual_circuit_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['virtual_circuit'], actions=[ actions.AddObject( 'circuits.VirtualCircuitTermination', diff --git a/netbox/core/views.py b/netbox/core/views.py index 6531fb7b2..328679fa9 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -94,6 +94,7 @@ class DataSourceView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='core.DataFile', filters={'source_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['source'], ), ], ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index acf335b69..5ebb22c6b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -258,6 +258,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.Region', title=_('Child Regions'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}), ], @@ -390,6 +391,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.SiteGroup', title=_('Child Groups'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject('dcim.SiteGroup', url_params={'parent': lambda ctx: ctx['object'].pk}), ], @@ -540,6 +542,7 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView): ObjectsTablePanel( model='dcim.Location', filters={'site_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['site'], actions=[ actions.AddObject('dcim.Location', url_params={'site': lambda ctx: ctx['object'].pk}), ], @@ -552,6 +555,7 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView): 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, }, + exclude_columns=['site'], actions=[ actions.AddObject('dcim.Device', url_params={'site': lambda ctx: ctx['object'].pk}), ], @@ -674,6 +678,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.Location', title=_('Child Locations'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject( 'dcim.Location', @@ -692,6 +697,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, }, + exclude_columns=['location'], actions=[ actions.AddObject( 'dcim.Device', @@ -1686,6 +1692,7 @@ class ModuleTypeProfileView(generic.ObjectView): filters={ 'profile_id': lambda ctx: ctx['object'].pk, }, + exclude_columns=['profile'], actions=[ actions.AddObject( 'dcim.ModuleType', @@ -2427,6 +2434,7 @@ class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.DeviceRole', title=_('Child Device Roles'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject('dcim.DeviceRole', url_params={'parent': lambda ctx: ctx['object'].pk}), ], @@ -2527,6 +2535,7 @@ class PlatformView(GetRelatedModelsMixin, generic.ObjectView): model='dcim.Platform', title=_('Child Platforms'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject('dcim.Platform', url_params={'parent': lambda ctx: ctx['object'].pk}), ], @@ -2605,6 +2614,7 @@ class DeviceView(generic.ObjectView): ObjectsTablePanel( model='dcim.VirtualDeviceContext', filters={'device_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['device'], actions=[ actions.AddObject('dcim.VirtualDeviceContext', url_params={'device': lambda ctx: ctx['object'].pk}), ], @@ -2617,6 +2627,7 @@ class DeviceView(generic.ObjectView): model='ipam.Service', title=_('Application Services'), filters={'device_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject( 'ipam.Service', @@ -3376,11 +3387,13 @@ class InterfaceView(generic.ObjectView): model='ipam.IPAddress', filters={'interface_id': lambda ctx: ctx['object'].pk}, title=_('IP Addresses'), + exclude_columns=['assigned', 'assigned_object', 'assigned_object_parent'], ), ObjectsTablePanel( model='dcim.MACAddress', filters={'interface_id': lambda ctx: ctx['object'].pk}, title=_('MAC Addresses'), + exclude_columns=['assigned_object', 'assigned_object_parent'], ), ObjectsTablePanel( model='ipam.VLAN', diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3f8f9eba2..4fc30342b 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1331,6 +1331,7 @@ class VLANTranslationPolicyView(generic.ObjectView): 'ipam.vlantranslationrule', filters={'policy_id': lambda ctx: ctx['object'].pk}, title=_('VLAN translation rules'), + exclude_columns=['policy'], actions=[ actions.AddObject( 'ipam.vlantranslationrule', @@ -1628,6 +1629,7 @@ class VLANView(generic.ObjectView): 'ipam.prefix', filters={'vlan_id': lambda ctx: ctx['object'].pk}, title=_('Prefixes'), + exclude_columns=['vlan'], actions=[ actions.AddObject( 'ipam.prefix', diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index d016cbf28..931da4f77 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -185,6 +185,18 @@ class BaseTable(tables.Table): columns = getattr(self.Meta, 'default_columns', self.Meta.fields) self._set_columns(columns) + + # Apply column inclusion/exclusion (overrides user preferences) + if columns_param := request.GET.get('include_columns'): + for column_name in columns_param.split(','): + if column_name in self.columns.names(): + self.columns.show(column_name) + if exclude_columns := request.GET.get('exclude_columns'): + exclude_columns = exclude_columns.split(',') + for column_name in exclude_columns: + if column_name in self.columns.names() and column_name not in self.exempt_columns: + self.columns.hide(column_name) + self._apply_prefetching() if ordering is not None: self.order_by = ordering diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index f58e98449..fa26cf754 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -282,11 +282,13 @@ class ObjectsTablePanel(Panel): model (str): The dotted label of the model to be added (e.g. "dcim.site") filters (dict): A dictionary of arbitrary URL parameters to append to the table's URL. If the value of a key is a callable, it will be passed the current template context. + include_columns (list): A list of column names to always display (overrides user preferences) + exclude_columns (list): A list of column names to hide from the table (overrides user preferences) """ template_name = 'ui/panels/objects_table.html' title = None - def __init__(self, model, filters=None, **kwargs): + def __init__(self, model, filters=None, include_columns=None, exclude_columns=None, **kwargs): super().__init__(**kwargs) # Resolve the model class from its app.name label @@ -297,6 +299,8 @@ class ObjectsTablePanel(Panel): raise ValueError(f"Invalid model label: {model}") self.filters = filters or {} + self.include_columns = include_columns or [] + self.exclude_columns = exclude_columns or [] # If no title is specified, derive one from the model name if self.title is None: @@ -308,6 +312,10 @@ class ObjectsTablePanel(Panel): } if 'return_url' not in url_params and 'object' in context: url_params['return_url'] = context['object'].get_absolute_url() + if self.include_columns: + url_params['include_columns'] = ','.join(self.include_columns) + if self.exclude_columns: + url_params['exclude_columns'] = ','.join(self.exclude_columns) return { **super().get_context(context), 'viewname': get_viewname(self.model, 'list'), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 26b0ac5ab..7ebc1ab82 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -57,6 +57,7 @@ class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView): 'tenancy.tenantgroup', filters={'parent_id': lambda ctx: ctx['object'].pk}, title=_('Child Groups'), + exclude_columns=['parent'], actions=[ actions.AddObject( 'tenancy.tenantgroup', @@ -235,6 +236,7 @@ class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView): 'tenancy.contactgroup', filters={'parent_id': lambda ctx: ctx['object'].pk}, title=_('Child Groups'), + exclude_columns=['parent'], actions=[ actions.AddObject( 'tenancy.contactgroup', @@ -414,6 +416,7 @@ class ContactView(generic.ObjectView): 'tenancy.contactassignment', filters={'contact_id': lambda ctx: ctx['object'].pk}, title=_('Assignments'), + exclude_columns=['contact'], ), ], ) diff --git a/netbox/users/views.py b/netbox/users/views.py index 59ebe518f..928775101 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -200,7 +200,10 @@ class GroupView(generic.ObjectView): OrganizationalObjectPanel(), ], right_panels=[ - ObjectsTablePanel('users.User', filters={'group_id': lambda ctx: ctx['object'].pk}), + ObjectsTablePanel( + 'users.User', + filters={'group_id': lambda ctx: ctx['object'].pk}, + ), ObjectsTablePanel( 'users.ObjectPermission', title=_('Assigned Permissions'), @@ -345,6 +348,7 @@ class OwnerGroupView(generic.ObjectView): 'users.Owner', filters={'group_id': lambda ctx: ctx['object'].pk}, title=_('Members'), + exclude_columns=['group'], actions=[ actions.AddObject( 'users.Owner', @@ -412,8 +416,14 @@ class OwnerView(GetRelatedModelsMixin, generic.ObjectView): layout = layout.SimpleLayout( left_panels=[ panels.OwnerPanel(), - ObjectsTablePanel('users.Group', filters={'owner_id': lambda ctx: ctx['object'].pk}), - ObjectsTablePanel('users.User', filters={'owner_id': lambda ctx: ctx['object'].pk}), + ObjectsTablePanel( + 'users.Group', + filters={'owner_id': lambda ctx: ctx['object'].pk}, + ), + ObjectsTablePanel( + 'users.User', + filters={'owner_id': lambda ctx: ctx['object'].pk}, + ), ], right_panels=[ RelatedObjectsPanel(), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 27cd0cb98..d6a758dad 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -492,6 +492,7 @@ class VirtualMachineView(generic.ObjectView): model='ipam.Service', title=_('Application Services'), filters={'virtual_machine_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject( 'ipam.Service', @@ -508,6 +509,7 @@ class VirtualMachineView(generic.ObjectView): ObjectsTablePanel( model='virtualization.VirtualDisk', filters={'virtual_machine_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['virtual_machine'], actions=[ actions.AddObject( 'virtualization.VirtualDisk', url_params={'virtual_machine': lambda ctx: ctx['object'].pk} @@ -649,6 +651,7 @@ class VMInterfaceView(generic.ObjectView): ObjectsTablePanel( model='ipam.IPaddress', filters={'vminterface_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['assigned', 'assigned_object', 'assigned_object_parent'], actions=[ actions.AddObject( 'ipam.IPaddress', @@ -662,6 +665,7 @@ class VMInterfaceView(generic.ObjectView): ObjectsTablePanel( model='dcim.MACAddress', filters={'vminterface_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['assigned_object', 'assigned_object_parent'], actions=[ actions.AddObject( 'dcim.MACAddress', url_params={'vminterface': lambda ctx: ctx['object'].pk} diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 7da05e007..e74c53b64 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -129,6 +129,7 @@ class TunnelView(generic.ObjectView): ObjectsTablePanel( 'vpn.tunneltermination', filters={'tunnel_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['tunnel'], actions=[ actions.AddObject( 'vpn.tunneltermination', @@ -223,6 +224,7 @@ class TunnelTerminationView(generic.ObjectView): 'tunnel_id': lambda ctx: ctx['object'].tunnel.pk, 'id__n': lambda ctx: ctx['object'].pk, }, + exclude_columns=['tunnel'], title=_('Peer Terminations'), ), ], @@ -675,6 +677,7 @@ class L2VPNView(generic.ObjectView): ObjectsTablePanel( 'vpn.l2vpntermination', filters={'l2vpn_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['l2vpn'], actions=[ actions.AddObject( 'vpn.l2vpntermination', diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index fecec1ef9..422b46cc5 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -53,6 +53,7 @@ class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView): model='wireless.WirelessLANGroup', title=_('Child Groups'), filters={'parent_id': lambda ctx: ctx['object'].pk}, + exclude_columns=['parent'], actions=[ actions.AddObject( 'wireless.WirelessLANGroup',