diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 126b2a47b..56cd46d4a 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -107,9 +107,9 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = Provider field_groups = [ - ['q'], - ['region_id', 'site_id'], - ['asn', 'tag'], + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['asn'], ] q = forms.CharField( required=False, @@ -122,11 +122,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm): label=_('Region'), fetch_trigger='open' ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' @@ -202,7 +209,10 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = ProviderNetwork - field_order = ['q', 'provider_id', 'tag'] + field_groups = ( + ('q', 'tag'), + ('provider_id',), + ) q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), @@ -368,17 +378,12 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = Circuit - field_order = [ - 'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', - 'tenant_id', 'commit_rate', - ] field_groups = [ - ['q'], - ['type_id', 'status', 'commit_rate'], + ['q', 'tag'], ['provider_id', 'provider_network_id'], - ['region_id', 'site_id'], + ['type_id', 'status', 'commit_rate'], + ['region_id', 'site_group_id', 'site_id'], ['tenant_group_id', 'tenant_id'], - ['tag'] ] q = forms.CharField( required=False, @@ -417,11 +422,18 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte label=_('Region'), fetch_trigger='open' ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 079e43007..02749ba1c 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -831,6 +831,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet): to_field_name='slug', label='Site name (slug)', ) + location_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__location', + queryset=Location.objects.all(), + label='Location (ID)', + ) + location = django_filters.ModelMultipleChoiceFilter( + field_name='device__location__slug', + queryset=Location.objects.all(), + to_field_name='slug', + label='Location (slug)', + ) device_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', @@ -1053,39 +1064,6 @@ class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): method='search', label='Search', ) - region_id = TreeNodeMultipleChoiceFilter( - queryset=Region.objects.all(), - field_name='device__site__region', - lookup_expr='in', - label='Region (ID)', - ) - region = TreeNodeMultipleChoiceFilter( - queryset=Region.objects.all(), - field_name='device__site__region', - lookup_expr='in', - to_field_name='slug', - label='Region (slug)', - ) - site_id = django_filters.ModelMultipleChoiceFilter( - field_name='device__site', - queryset=Site.objects.all(), - label='Site (ID)', - ) - site = django_filters.ModelMultipleChoiceFilter( - field_name='device__site__slug', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site name (slug)', - ) - device_id = django_filters.ModelChoiceFilter( - queryset=Device.objects.all(), - label='Device (ID)', - ) - device = django_filters.ModelChoiceFilter( - queryset=Device.objects.all(), - to_field_name='name', - label='Device (name)', - ) parent_id = django_filters.ModelMultipleChoiceFilter( queryset=InventoryItem.objects.all(), label='Parent inventory item (ID)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5c85ac661..3456eee35 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -58,11 +58,6 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm): field_order = [ 'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id', ] - field_groups = [ - ['q'], - ['name', 'label'], - ['region_id', 'site_group_id', 'site_id'], - ] q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), @@ -90,16 +85,27 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm): queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' ) + location_id = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site_id', + }, + label=_('Location'), + fetch_trigger='open' + ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, query_params={ - 'site_id': '$site_id' + 'site_id': '$site_id', + 'location_id': '$location_id', }, label=_('Device'), fetch_trigger='open' @@ -247,15 +253,22 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RegionFilterForm(BootstrapMixin, CustomFieldModelFilterForm): - model = Site + model = Region field_groups = [ ['q'], + ['parent_id'], ] q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), label=_('Search') ) + parent_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Parent region'), + fetch_trigger='open' + ) # @@ -311,12 +324,19 @@ class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = SiteGroup field_groups = [ ['q'], + ['parent_id'], ] q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), label=_('Search') ) + parent_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Parent group'), + fetch_trigger='open' + ) # @@ -476,10 +496,9 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo model = Site field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], - ['status', 'region_id'], + ['q', 'tag'], + ['status', 'region_id', 'group_id'], ['tenant_group_id', 'tenant_id'], - ['tag'] ] q = forms.CharField( required=False, @@ -500,7 +519,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Group'), + label=_('Site group'), fetch_trigger='open' ) tag = TagFilterField(model) @@ -607,11 +626,18 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm): label=_('Region'), fetch_trigger='open' ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' @@ -897,9 +923,10 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo model = Rack field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], - ['status', 'role_id'], + ['q', 'tag'], ['region_id', 'site_id', 'location_id'], + ['status', 'role_id'], + ['type', 'width', 'asset_tag'], ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( @@ -1134,9 +1161,10 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo model = RackReservation field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], + ['q', 'tag'], + ['user_id'], ['region_id', 'site_id', 'location_id'], - ['user_id', 'tenant_group_id', 'tenant_id'], + ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( required=False, @@ -1155,7 +1183,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo query_params={ 'region_id': '$region_id' }, - label=_('Region'), + label=_('Site'), fetch_trigger='open' ) location_id = DynamicModelMultipleChoiceField( @@ -1292,12 +1320,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = DeviceType field_groups = [ - ['q'], + ['q', 'tag'], ['manufacturer_id', 'subdevice_role'], - ['console_ports', 'console_server_ports'], - ['power_ports', 'power_outlets'], - ['interfaces', 'pass_through_ports'], - ['tag'] + ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], ] q = forms.CharField( required=False, @@ -2526,12 +2551,15 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt 'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip', ] field_groups = [ - ['q'], - ['region_id', 'site_id', 'location_id', 'rack_id'], - ['status', 'role_id', 'asset_tag'], + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'], + ['status', 'role_id', 'asset_tag', 'mac_address'], + ['manufacturer_id', 'device_type_id', 'platform_id'], ['tenant_group_id', 'tenant_id'], - ['manufacturer_id', 'device_type_id'], - ['mac_address', 'has_primary_ip'], + [ + 'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', + 'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', + ], ] q = forms.CharField( required=False, @@ -2547,7 +2575,8 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group') + label=_('Site group'), + fetch_trigger='open' ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -2723,11 +2752,9 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentFor class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort field_groups = [ - ['q'], - ['name', 'label'], - ['type', 'speed'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['q', 'tag'], + ['name', 'label', 'type', 'speed'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, @@ -2831,11 +2858,9 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort field_groups = [ - ['q'], - ['name', 'label'], - ['type', 'speed'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['q', 'tag'], + ['name', 'label', 'type', 'speed'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, @@ -2939,10 +2964,9 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort field_groups = [ - ['q'], + ['q', 'tag'], ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PowerPortTypeChoices, @@ -3045,10 +3069,9 @@ class PowerPortCSVForm(CustomFieldModelCSVForm): class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet field_groups = [ - ['q'], + ['q', 'tag'], ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PowerOutletTypeChoices, @@ -3218,11 +3241,9 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm): class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface field_groups = [ - ['q'], - ['name', 'label', 'type', 'enabled'], - ['mgmt_only', 'mac_address'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'], + ['q', 'tag'], + ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=InterfaceTypeChoices, @@ -3578,10 +3599,9 @@ class InterfaceCSVForm(CustomFieldModelCSVForm): class FrontPortFilterForm(DeviceComponentFilterForm): field_groups = [ - ['q'], + ['q', 'tag'], ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] model = FrontPort type = forms.MultipleChoiceField( @@ -3768,10 +3788,9 @@ class FrontPortCSVForm(CustomFieldModelCSVForm): class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort field_groups = [ - ['q'], + ['q', 'tag'], ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] type = forms.MultipleChoiceField( choices=PortTypeChoices, @@ -3870,10 +3889,9 @@ class RearPortCSVForm(CustomFieldModelCSVForm): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay field_groups = [ - ['q'], + ['q', 'tag'], ['name', 'label'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] tag = TagFilterField(model) @@ -4122,11 +4140,9 @@ class InventoryItemBulkEditForm( class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem field_groups = [ - ['q'], - ['name', 'label', 'manufacturer_id'], - ['serial', 'asset_tag', 'discovered'], - ['region_id', 'site_group_id', 'site_id'], - ['tag'] + ['q', 'tag'], + ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], + ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), @@ -4598,11 +4614,10 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = Cable field_groups = [ - ['q'], + ['q', 'tag'], + ['site_id', 'rack_id', 'device_id'], ['type', 'status', 'color'], - ['device_id', 'rack_id'], - ['region_id', 'site_id', 'tenant_id'], - ['tag'] + ['tenant_id'], ] q = forms.CharField( required=False, @@ -4672,11 +4687,6 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -4704,11 +4714,6 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): class PowerConnectionFilterForm(BootstrapMixin, forms.Form): - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -4736,11 +4741,6 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -5010,10 +5010,9 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod model = VirtualChassis field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], + ['q', 'tag'], ['region_id', 'site_group_id', 'site_id'], ['tenant_group_id', 'tenant_id'], - ['tag'] ] q = forms.CharField( required=False, @@ -5036,7 +5035,8 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' @@ -5159,6 +5159,10 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = PowerPanel + field_groups = ( + ('q', 'tag'), + ('region_id', 'site_group_id', 'site_id', 'location_id') + ) q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), @@ -5180,7 +5184,8 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm): queryset=Site.objects.all(), required=False, query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' @@ -5402,12 +5407,10 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = PowerFeed field_groups = [ - ['q'], + ['q', 'tag'], ['region_id', 'site_group_id', 'site_id'], ['power_panel_id', 'rack_id'], - ['type', 'supply', 'max_utilization'], - ['phase', 'voltage', 'amperage'], - ['status', 'tag'] + ['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'], ] q = forms.CharField( required=False, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index d17bdd3f7..2afe77638 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1512,10 +1512,18 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -1584,6 +1592,13 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'device': [devices[0].name, devices[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cabled(self): params = {'cabled': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1624,10 +1639,18 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -1689,6 +1712,13 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -1736,10 +1766,18 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -1809,6 +1847,13 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -1856,10 +1901,18 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -1925,6 +1978,13 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -1972,10 +2032,18 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -2082,6 +2150,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -2143,10 +2218,18 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -2217,6 +2300,13 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -2264,10 +2354,18 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections ) Device.objects.bulk_create(devices) @@ -2332,6 +2430,13 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -2379,10 +2484,18 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests): device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), ) Device.objects.bulk_create(devices) @@ -2426,6 +2539,13 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_device(self): devices = Device.objects.all()[:2] params = {'device_id': [devices[0].pk, devices[1].pk]} @@ -2474,10 +2594,18 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): ) Site.objects.bulk_create(sites) + locations = ( + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), + ) + for location in locations: + location.save() + devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]), ) Device.objects.bulk_create(devices) @@ -2541,13 +2669,19 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'site': [sites[0].slug, sites[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_location(self): + locations = Location.objects.all()[:2] + params = {'location_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'location': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_device(self): - # TODO: Allow multiple values - device = Device.objects.first() - params = {'device_id': device.pk} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'device': device.name} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + devices = Device.objects.all()[:2] + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'device': [devices[0].name, devices[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_parent_id(self): parent_items = InventoryItem.objects.filter(parent__isnull=True)[:2] diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 37dddf202..841ea213d 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -88,12 +88,14 @@ class CustomFieldFilterForm(BootstrapMixin, forms.Form): ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') + limit_choices_to=FeatureQuery('custom_fields'), + required=False ) type = forms.MultipleChoiceField( choices=CustomFieldTypeChoices, required=False, - widget=StaticSelectMultiple() + widget=StaticSelectMultiple(), + label=_('Field type') ) weight = forms.IntegerField( required=False @@ -174,8 +176,7 @@ class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm): class CustomLinkFilterForm(BootstrapMixin, forms.Form): field_groups = [ ['q'], - ['content_type'], - ['weight', 'new_window'], + ['content_type', 'weight', 'new_window'], ] q = forms.CharField( required=False, @@ -184,7 +185,8 @@ class CustomLinkFilterForm(BootstrapMixin, forms.Form): ) content_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') + limit_choices_to=FeatureQuery('custom_fields'), + required=False ) weight = forms.IntegerField( required=False @@ -265,8 +267,7 @@ class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class ExportTemplateFilterForm(BootstrapMixin, forms.Form): field_groups = [ ['q'], - ['content_type', 'mime_type'], - ['file_extension', 'as_attachment'], + ['content_type', 'mime_type', 'file_extension', 'as_attachment'], ] q = forms.CharField( required=False, @@ -275,10 +276,12 @@ class ExportTemplateFilterForm(BootstrapMixin, forms.Form): ) content_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') + limit_choices_to=FeatureQuery('custom_fields'), + required=False ) mime_type = forms.CharField( - required=False + required=False, + label=_('MIME type') ) file_extension = forms.CharField( required=False @@ -377,8 +380,8 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm): class WebhookFilterForm(BootstrapMixin, forms.Form): field_groups = [ ['q'], - ['content_types', 'http_method'], - ['enabled', 'type_create', 'type_update', 'type_delete'], + ['content_types', 'http_method', 'enabled'], + ['type_create', 'type_update', 'type_delete'], ] q = forms.CharField( required=False, @@ -387,12 +390,14 @@ class WebhookFilterForm(BootstrapMixin, forms.Form): ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') + limit_choices_to=FeatureQuery('custom_fields'), + required=False ) http_method = forms.MultipleChoiceField( choices=WebhookHttpMethodChoices, required=False, - widget=StaticSelectMultiple() + widget=StaticSelectMultiple(), + label=_('HTTP method') ) enabled = forms.NullBooleanField( required=False, @@ -693,16 +698,12 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): class ConfigContextFilterForm(BootstrapMixin, forms.Form): - field_order = [ - 'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', - 'cluster_id', 'tenant_group_id', 'tenant_id', - ] field_groups = [ - ['q'], + ['q', 'tag'], ['region_id', 'site_group_id', 'site_id'], - ['device_type_id', 'role_id', 'platform_id'], + ['device_type_id', 'platform_id', 'role_id'], ['cluster_group_id', 'cluster_id'], - ['tenant_group_id', 'tenant_id', 'tag'] + ['tenant_group_id', 'tenant_id'] ] q = forms.CharField( required=False, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 78805822c..376e4b919 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -107,12 +107,10 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEdi class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = VRF - field_order = ['q', 'import_target_id', 'export_target_id', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], + ['q', 'tag'], ['import_target_id', 'export_target_id'], ['tenant_group_id', 'tenant_id'], - ['tag'] ] q = forms.CharField( required=False, @@ -186,9 +184,8 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = RouteTarget - field_order = ['q', 'name', 'tenant_group_id', 'tenant_id', 'importing_vrfs', 'exporting_vrfs'] field_groups = [ - ['q'], + ['q', 'tag'], ['importing_vrf_id', 'exporting_vrf_id'], ['tenant_group_id', 'tenant_id'], ] @@ -348,9 +345,8 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = Aggregate - field_order = ['q', 'family', 'rir', 'tenant_group_id', 'tenant_id'] field_groups = [ - ['q'], + ['q', 'tag'], ['family', 'rir_id'], ['tenant_group_id', 'tenant_id'] ] @@ -628,17 +624,13 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = Prefix - field_order = [ - 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', - 'region_id', 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', - 'is_pool', 'mark_utilized', - ] field_groups = [ - ['q'], - ['role_id', 'within_include', 'family', 'mask_length'], - ['vrf_id', 'present_in_vrf_id', 'is_pool', 'mark_utilized'], + ['q', 'tag'], + ['within_include', 'family', 'status', 'role_id'], + ['vrf_id', 'present_in_vrf_id'], + ['mask_length', 'is_pool', 'mark_utilized'], ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id', 'status', 'tag'] + ['tenant_group_id', 'tenant_id'] ] q = forms.CharField( required=False, @@ -838,13 +830,10 @@ class IPRangeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = IPRange - field_order = [ - 'q', 'family', 'vrf_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id', - ] field_groups = [ - ['q'], + ['q', 'tag'], ['family', 'vrf_id', 'status', 'role_id'], - ['tenant_group_id', 'tenant_id', 'tag'], + ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( required=False, @@ -1280,10 +1269,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil 'assigned_to_interface', 'tenant_group_id', 'tenant_id', ] field_groups = [ - ['q'], - ['parent', 'family', 'mask_length'], - ['status', 'vrf_id', 'present_in_vrf_id'], - ['role', 'assigned_to_interface'], + ['q', 'tag'], + ['parent', 'family', 'status', 'role'], + ['vrf_id', 'present_in_vrf_id'], + ['mask_length', 'assigned_to_interface'], ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( @@ -1489,8 +1478,7 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class VLANGroupFilterForm(BootstrapMixin, forms.Form): field_groups = [ ['q'], - ['region', 'sitegroup', 'site'], - ['location', 'rack'] + ['region', 'sitegroup', 'site', 'location', 'rack'] ] q = forms.CharField( required=False, @@ -1707,14 +1695,10 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = VLAN - field_order = [ - 'q', 'region_id', 'site_group_id', 'site_id', 'group_id', 'status', 'role_id', - 'tenant_group_id', 'tenant_id', - ] field_groups = [ - ['q'], + ['q', 'tag'], ['region_id', 'site_group_id', 'site_id'], - ['group_id', 'role_id', 'status'], + ['group_id', 'status', 'role_id'], ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( @@ -1818,6 +1802,10 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm): class ServiceFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = Service + field_groups = ( + ('q', 'tag'), + ('protocol', 'port'), + ) q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index 4da03b5cc..cf1ec342e 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -4,42 +4,52 @@
- {% for field in filter_form.hidden_fields %} - {{ field }} - {% endfor %} - {% if filter_form.field_groups %} - {% for group in filter_form.field_groups %} -
- {% for name in group %} - {% with field=filter_form|get_item:name %} - {% render_field field %} - {% endwith %} - {% endfor %} -
-
- {% endfor %} - {% for name in filter_form.custom_field_filters %} -
- {% with field=filter_form|get_item:name %} - {% render_field field %} - {% endwith %} -
- {% endfor %} - {% else %} - {% for field in filter_form.visible_fields %} -
- {% render_field field %} -
- {% endfor %} + {% for field in filter_form.hidden_fields %} + {{ field }} + {% endfor %} + {% if filter_form.field_groups %} + {# List filters by group #} + {% for group in filter_form.field_groups %} +
+ {% for name in group %} + {% with field=filter_form|get_item:name %} + {% render_field field %} + {% endwith %} + {% endfor %} +
+ {% if not forloop.last %} +
{% endif %} + {% endfor %} + {% else %} + {# List all non-customfield filters as declared in the form class #} + {% for field in filter_form.visible_fields %} + {% if not filter_form.custom_field_filters or field.name not in filter_form.custom_field_filters %} +
+ {% render_field field %} +
+ {% endif %} + {% endfor %} + {% endif %} + {% if filter_form.custom_field_filters %} + {# List all custom field filters #} +
+ {% for name in filter_form.custom_field_filters %} +
+ {% with field=filter_form|get_item:name %} + {% render_field field %} + {% endwith %} +
+ {% endfor %} + {% endif %}
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 58c1ae60a..63dcdd468 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -135,6 +135,10 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm): model = Tenant + field_groups = ( + ('q', 'tag'), + ('group_id',), + ) q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 6744cd6de..efb787eca 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -228,11 +228,10 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte 'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id', ] field_groups = [ - ['q'], - ['type_id'], - ['region_id', 'site_id'], + ['q', 'tag'], + ['group_id', 'type_id'], + ['region_id', 'site_group_id', 'site_id'], ['tenant_group_id', 'tenant_id'], - ['tag'], ] q = forms.CharField( required=False, @@ -251,12 +250,19 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte label=_('Region'), fetch_trigger='open' ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group'), + fetch_trigger='open' + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, null_option='None', query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'site_group_id': '$site_group_id', }, label=_('Site'), fetch_trigger='open' @@ -541,18 +547,12 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = VirtualMachine - field_order = [ - 'q', 'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_group_id', - 'site_id', 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address', - ] field_groups = [ - ['q'], - ['status', 'role_id'], - ['platform_id', 'mac_address'], + ['q', 'tag'], ['cluster_group_id', 'cluster_type_id', 'cluster_id'], - ['region_id', 'site_id'], + ['region_id', 'site_group_id', 'site_id'], + ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip'], ['tenant_group_id', 'tenant_id'], - ] q = forms.CharField( required=False, @@ -878,10 +878,9 @@ class VMInterfaceBulkRenameForm(BulkRenameForm): class VMInterfaceFilterForm(BootstrapMixin, forms.Form): model = VMInterface field_groups = [ - ['q'], + ['q', 'tag'], ['cluster_id', 'virtual_machine_id'], ['enabled', 'mac_address'], - ['tag'] ] q = forms.CharField( required=False,