diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 97be1cf57..cbf1fb82d 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -95,7 +95,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet): class Meta: model = ProviderAccount - fields = ['id', 'name', 'account', 'description'] + fields = ('id', 'name', 'account', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -122,7 +122,7 @@ class ProviderNetworkFilterSet(NetBoxModelFilterSet): class Meta: model = ProviderNetwork - fields = ['id', 'name', 'service_id', 'description'] + fields = ('id', 'name', 'service_id', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -139,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet): class Meta: model = CircuitType - fields = ['id', 'name', 'slug', 'color', 'description'] + fields = ('id', 'name', 'slug', 'color', 'description') class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): @@ -158,6 +158,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte queryset=ProviderAccount.objects.all(), label=_('Provider account (ID)'), ) + provider_account = django_filters.ModelMultipleChoiceFilter( + field_name='provider_account__account', + queryset=Provider.objects.all(), + to_field_name='account', + label=_('Provider account (account)'), + ) provider_network_id = django_filters.ModelMultipleChoiceFilter( field_name='terminations__provider_network', queryset=ProviderNetwork.objects.all(), @@ -214,10 +220,18 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte to_field_name='slug', label=_('Site (slug)'), ) + termination_a_id = django_filters.ModelMultipleChoiceFilter( + queryset=CircuitTermination.objects.all(), + label=_('Termination A (ID)'), + ) + termination_z_id = django_filters.ModelMultipleChoiceFilter( + queryset=CircuitTermination.objects.all(), + label=_('Termination A (ID)'), + ) class Meta: model = Circuit - fields = ['id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate'] + fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate') def search(self, queryset, name, value): if not value.strip(): @@ -258,7 +272,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet): class Meta: model = CircuitTermination - fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end'] + fields = ( + 'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected', + 'pp_info', 'cable_end', + ) def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index 6553179ec..bbd2438d7 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -330,6 +330,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CircuitTermination.objects.all() filterset = CircuitTerminationFilterSet + ignore_fields = ('cable',) @classmethod def setUpTestData(cls): diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index 902e240ee..c5d332b68 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet): class Meta: model = DataSource - fields = ('id', 'name', 'enabled', 'description') + fields = ('id', 'name', 'enabled', 'description', 'source_url', 'last_synced') def search(self, queryset, name, value): if not value.strip(): @@ -115,7 +115,7 @@ class JobFilterSet(BaseFilterSet): class Meta: model = Job - fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user') + fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id') def search(self, queryset, name, value): if not value.strip(): @@ -134,9 +134,7 @@ class ConfigRevisionFilterSet(BaseFilterSet): class Meta: model = ConfigRevision - fields = [ - 'id', - ] + fields = ('id', 'created', 'comment') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py index 8ff104142..aefb9eed0 100644 --- a/netbox/core/tests/test_filtersets.py +++ b/netbox/core/tests/test_filtersets.py @@ -10,6 +10,7 @@ from ..models import * class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DataSource.objects.all() filterset = DataSourceFilterSet + ignore_fields = ('ignore_rules', 'parameters') @classmethod def setUpTestData(cls): @@ -70,6 +71,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests): class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DataFile.objects.all() filterset = DataFileFilterSet + ignore_fields = ('data',) @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 082659b8f..aa8a68296 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -18,11 +18,12 @@ from tenancy.models import * from utilities.choices import ColorChoices from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, - TreeNodeMultipleChoiceFilter, + NumericArrayFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster from vpn.models import L2VPN from wireless.choices import WirelessRoleChoices, WirelessChannelChoices +from wireless.models import WirelessLAN, WirelessLink from .choices import * from .constants import * from .models import * @@ -105,7 +106,7 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class Meta: model = Region - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): @@ -135,7 +136,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class Meta: model = SiteGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): @@ -178,12 +179,11 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe queryset=ASN.objects.all(), label=_('AS (ID)'), ) + time_zone = MultiValueCharFilter() class Meta: model = Site - fields = ( - 'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description' - ) + fields = ('id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -270,7 +270,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM class Meta: model = Location - fields = ['id', 'name', 'slug', 'status', 'description'] + fields = ('id', 'name', 'slug', 'status', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -285,7 +285,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = RackRole - fields = ['id', 'name', 'slug', 'color', 'description'] + fields = ('id', 'name', 'slug', 'color', 'description') class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): @@ -364,10 +364,10 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe class Meta: model = Rack - fields = [ + fields = ( 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', - ] + ) def search(self, queryset, name, value): if not value.strip(): @@ -447,10 +447,14 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet): to_field_name='username', label=_('User (name)'), ) + unit = NumericArrayFilter( + field_name='units', + lookup_expr='contains' + ) class Meta: model = RackReservation - fields = ['id', 'created', 'description'] + fields = ('id', 'created', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -467,7 +471,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet) class Meta: model = Manufacturer - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class DeviceTypeFilterSet(NetBoxModelFilterSet): @@ -538,10 +542,22 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet): class Meta: model = DeviceType - fields = [ + fields = ( 'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description', - ] + + # Counters + 'console_port_template_count', + 'console_server_port_template_count', + 'power_port_template_count', + 'power_outlet_template_count', + 'interface_template_count', + 'front_port_template_count', + 'rear_port_template_count', + 'device_bay_template_count', + 'module_bay_template_count', + 'inventory_item_template_count', + ) def search(self, queryset, name, value): if not value.strip(): @@ -635,7 +651,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet): class Meta: model = ModuleType - fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description'] + fields = ('id', 'model', 'part_number', 'weight', 'weight_unit', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -675,12 +691,15 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet): method='search', label=_('Search'), ) - devicetype_id = django_filters.ModelMultipleChoiceFilter( + device_type_id = django_filters.ModelMultipleChoiceFilter( queryset=DeviceType.objects.all(), field_name='device_type_id', label=_('Device type (ID)'), ) + # TODO: Remove in v4.1 + devicetype_id = device_type_id + def search(self, queryset, name, value): if not value.strip(): return queryset @@ -691,32 +710,35 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet): class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet): - moduletype_id = django_filters.ModelMultipleChoiceFilter( + module_type_id = django_filters.ModelMultipleChoiceFilter( queryset=ModuleType.objects.all(), field_name='module_type_id', label=_('Module type (ID)'), ) + # TODO: Remove in v4.1 + moduletype_id = module_type_id + class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate - fields = ['id', 'name', 'type', 'description'] + fields = ('id', 'name', 'label', 'type', 'description') class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'name', 'type', 'description'] + fields = ('id', 'name', 'label', 'type', 'description') class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class Meta: model = PowerPortTemplate - fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] + fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): @@ -724,10 +746,14 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType choices=PowerOutletFeedLegChoices, null_value=None ) + power_port_id = django_filters.ModelMultipleChoiceFilter( + queryset=PowerPortTemplate.objects.all(), + label=_('Power port (ID)'), + ) class Meta: model = PowerOutletTemplate - fields = ['id', 'name', 'type', 'feed_leg', 'description'] + fields = ('id', 'name', 'label', 'type', 'feed_leg', 'description') class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): @@ -751,7 +777,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo class Meta: model = InterfaceTemplate - fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description'] + fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description') class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): @@ -759,10 +785,13 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo choices=PortTypeChoices, null_value=None ) + rear_port_id = django_filters.ModelMultipleChoiceFilter( + queryset=RearPort.objects.all() + ) class Meta: model = FrontPortTemplate - fields = ['id', 'name', 'type', 'color', 'description'] + fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description') class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): @@ -773,21 +802,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom class Meta: model = RearPortTemplate - fields = ['id', 'name', 'type', 'color', 'positions', 'description'] + fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description') class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = ModuleBayTemplate - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'label', 'position', 'description') class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'label', 'description') class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): @@ -820,7 +849,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo class Meta: model = InventoryItemTemplate - fields = ['id', 'name', 'label', 'part_id', 'description'] + fields = ('id', 'name', 'label', 'part_id', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -841,7 +870,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = DeviceRole - fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description'] + fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description') class PlatformFilterSet(OrganizationalModelFilterSet): @@ -867,7 +896,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') @extend_schema_field(OpenApiTypes.STR) def get_for_device_type(self, queryset, name, value): @@ -979,6 +1008,11 @@ class DeviceFilterSet( queryset=Rack.objects.all(), label=_('Rack (ID)'), ) + parent_bay_id = django_filters.ModelMultipleChoiceFilter( + field_name='parent_bay', + queryset=DeviceBay.objects.all(), + label=_('Parent bay (ID)'), + ) cluster_id = django_filters.ModelMultipleChoiceFilter( queryset=Cluster.objects.all(), label=_('VM cluster (ID)'), @@ -1068,10 +1102,22 @@ class DeviceFilterSet( class Meta: model = Device - fields = [ + fields = ( 'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority', 'description', - ] + + # Counters + 'console_port_count', + 'console_server_port_count', + 'power_port_count', + 'power_outlet_count', + 'interface_count', + 'front_port_count', + 'rear_port_count', + 'device_bay_count', + 'module_bay_count', + 'inventory_item_count', + ) def search(self, queryset, name, value): if not value.strip(): @@ -1134,24 +1180,29 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim device_id = django_filters.ModelMultipleChoiceFilter( field_name='device', queryset=Device.objects.all(), - label='VDC (ID)', + label=_('VDC (ID)') ) device = django_filters.ModelMultipleChoiceFilter( field_name='device', queryset=Device.objects.all(), - label='Device model', + label=_('Device model') + ) + interface_id = django_filters.ModelMultipleChoiceFilter( + field_name='interfaces', + queryset=Interface.objects.all(), + label=_('Interface (ID)') ) status = django_filters.MultipleChoiceFilter( choices=VirtualDeviceContextStatusChoices ) has_primary_ip = django_filters.BooleanFilter( method='_has_primary_ip', - label='Has a primary IP', + label=_('Has a primary IP') ) class Meta: model = VirtualDeviceContext - fields = ['id', 'device', 'name', 'description'] + fields = ('id', 'device', 'name', 'identifier', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -1217,7 +1268,7 @@ class ModuleFilterSet(NetBoxModelFilterSet): class Meta: model = Module - fields = ['id', 'status', 'asset_tag', 'description'] + fields = ('id', 'status', 'asset_tag', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -1361,6 +1412,10 @@ class ModularDeviceComponentFilterSet(DeviceComponentFilterSet): class CabledObjectFilterSet(django_filters.FilterSet): + cable_id = django_filters.ModelMultipleChoiceFilter( + queryset=Cable.objects.all(), + label=_('Cable (ID)'), + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -1402,7 +1457,7 @@ class ConsolePortFilterSet( class Meta: model = ConsolePort - fields = ['id', 'name', 'label', 'description', 'cable_end'] + fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') class ConsoleServerPortFilterSet( @@ -1418,7 +1473,7 @@ class ConsoleServerPortFilterSet( class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'label', 'description', 'cable_end'] + fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') class PowerPortFilterSet( @@ -1434,7 +1489,9 @@ class PowerPortFilterSet( class Meta: model = PowerPort - fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end'] + fields = ( + 'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end', + ) class PowerOutletFilterSet( @@ -1451,10 +1508,16 @@ class PowerOutletFilterSet( choices=PowerOutletFeedLegChoices, null_value=None ) + power_port_id = django_filters.ModelMultipleChoiceFilter( + queryset=PowerPort.objects.all(), + label=_('Power port (ID)'), + ) class Meta: model = PowerOutlet - fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end'] + fields = ( + 'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end', + ) class CommonInterfaceFilterSet(django_filters.FilterSet): @@ -1569,27 +1632,37 @@ class InterfaceFilterSet( vdc_id = django_filters.ModelMultipleChoiceFilter( field_name='vdcs', queryset=VirtualDeviceContext.objects.all(), - label='Virtual Device Context', + label=_('Virtual Device Context') ) vdc_identifier = django_filters.ModelMultipleChoiceFilter( field_name='vdcs__identifier', queryset=VirtualDeviceContext.objects.all(), to_field_name='identifier', - label='Virtual Device Context (Identifier)', + label=_('Virtual Device Context (Identifier)') ) vdc = django_filters.ModelMultipleChoiceFilter( field_name='vdcs__name', queryset=VirtualDeviceContext.objects.all(), to_field_name='name', - label='Virtual Device Context', + label=_('Virtual Device Context') + ) + wireless_lan_id = django_filters.ModelMultipleChoiceFilter( + field_name='wireless_lans', + queryset=WirelessLAN.objects.all(), + label=_('Wireless LAN') + ) + wireless_link_id = django_filters.ModelMultipleChoiceFilter( + queryset=WirelessLink.objects.all(), + label=_('Wireless link') ) class Meta: model = Interface - fields = [ + fields = ( 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role', - 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end', - ] + 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', + 'cable_id', 'cable_end', + ) def filter_virtual_chassis_member(self, queryset, name, value): try: @@ -1618,10 +1691,15 @@ class FrontPortFilterSet( choices=PortTypeChoices, null_value=None ) + rear_port_id = django_filters.ModelMultipleChoiceFilter( + queryset=RearPort.objects.all() + ) class Meta: model = FrontPort - fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end'] + fields = ( + 'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end', + ) class RearPortFilterSet( @@ -1636,21 +1714,38 @@ class RearPortFilterSet( class Meta: model = RearPort - fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end'] + fields = ( + 'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end', + ) class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): + installed_module_id = django_filters.ModelMultipleChoiceFilter( + field_name='installed_module', + queryset=ModuleBay.objects.all(), + label=_('Installed module (ID)'), + ) class Meta: model = ModuleBay - fields = ['id', 'name', 'label', 'description'] + fields = ('id', 'name', 'label', 'position', 'description') class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): + installed_device_id = django_filters.ModelMultipleChoiceFilter( + queryset=Device.objects.all(), + label=_('Installed device (ID)'), + ) + installed_device = django_filters.ModelMultipleChoiceFilter( + field_name='installed_device__name', + queryset=Device.objects.all(), + to_field_name='name', + label=_('Installed device (name)'), + ) class Meta: model = DeviceBay - fields = ['id', 'name', 'label', 'description'] + fields = ('id', 'name', 'label', 'description') class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): @@ -1686,7 +1781,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class Meta: model = InventoryItem - fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered'] + fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered') def search(self, queryset, name, value): if not value.strip(): @@ -1705,7 +1800,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = InventoryItemRole - fields = ['id', 'name', 'slug', 'color', 'description'] + fields = ('id', 'name', 'slug', 'color', 'description') class VirtualChassisFilterSet(NetBoxModelFilterSet): @@ -1770,7 +1865,7 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet): class Meta: model = VirtualChassis - fields = ['id', 'domain', 'name', 'description'] + fields = ('id', 'domain', 'name', 'description', 'member_count') def search(self, queryset, name, value): if not value.strip(): @@ -1875,7 +1970,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class Meta: model = Cable - fields = ['id', 'label', 'length', 'length_unit', 'description'] + fields = ('id', 'label', 'length', 'length_unit', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -1953,12 +2048,12 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): return self.filter_by_termination_object(queryset, CircuitTermination, value) -class CableTerminationFilterSet(BaseFilterSet): +class CableTerminationFilterSet(ChangeLoggedModelFilterSet): termination_type = ContentTypeFilter() class Meta: model = CableTermination - fields = ['id', 'cable', 'cable_end', 'termination_type', 'termination_id'] + fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id') class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): @@ -2007,7 +2102,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class Meta: model = PowerPanel - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -2073,10 +2168,10 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi class Meta: model = PowerFeed - fields = [ - 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end', - 'description', - ] + fields = ( + 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', + 'available_power', 'mark_connected', 'cable_end', 'description', + ) def search(self, queryset, name, value): if not value.strip(): @@ -2135,18 +2230,18 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet): class Meta: model = ConsolePort - fields = ['name'] + fields = ('name',) class PowerConnectionFilterSet(ConnectionFilterSet): class Meta: model = PowerPort - fields = ['name'] + fields = ('name',) class InterfaceConnectionFilterSet(ConnectionFilterSet): class Meta: model = Interface - fields = [] + fields = tuple() diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 89793528d..e35055851 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -754,7 +754,7 @@ class DeviceFilterForm( ) has_oob_ip = forms.NullBooleanField( required=False, - label='Has an OOB IP', + label=_('Has an OOB IP'), widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index f1eeddbb5..1e46d66ac 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -196,6 +196,7 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Site.objects.all() filterset = SiteFilterSet + ignore_fields = ('physical_address', 'shipping_address') @classmethod def setUpTestData(cls): @@ -467,6 +468,7 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests): class RackTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Rack.objects.all() filterset = RackFilterSet + ignore_fields = ('units',) @classmethod def setUpTestData(cls): @@ -726,6 +728,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RackReservation.objects.all() filterset = RackReservationFilterSet + ignore_fields = ('units',) @classmethod def setUpTestData(cls): @@ -889,6 +892,7 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DeviceType.objects.all() filterset = DeviceTypeFilterSet + ignore_fields = ('front_image', 'rear_image') @classmethod def setUpTestData(cls): @@ -1880,6 +1884,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Device.objects.all() filterset = DeviceFilterSet + ignore_fields = ('local_context_data', 'oob_ip', 'primary_ip4', 'primary_ip6', 'vc_master_for') @classmethod def setUpTestData(cls): @@ -2332,6 +2337,7 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Module.objects.all() filterset = ModuleFilterSet + ignore_fields = ('local_context_data',) @classmethod def setUpTestData(cls): @@ -3229,6 +3235,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = Interface.objects.all() filterset = InterfaceFilterSet + ignore_fields = ('tagged_vlans', 'untagged_vlan', 'vdcs') @classmethod def setUpTestData(cls): @@ -5332,6 +5339,7 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests): class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VirtualDeviceContext.objects.all() filterset = VirtualDeviceContextFilterSet + ignore_fields = ('primary_ip4', 'primary_ip6') @classmethod def setUpTestData(cls): @@ -5401,15 +5409,22 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): VirtualDeviceContext.objects.bulk_create(vdcs) interfaces = ( - Interface(device=devices[0], name='Interface 1', type='virtual'), - Interface(device=devices[0], name='Interface 2', type='virtual'), + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL), + Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL), ) Interface.objects.bulk_create(interfaces) - interfaces[0].vdcs.set([vdcs[0]]) interfaces[1].vdcs.set([vdcs[1]]) + interfaces[2].vdcs.set([vdcs[2]]) + interfaces[3].vdcs.set([vdcs[3]]) + interfaces[4].vdcs.set([vdcs[4]]) + interfaces[5].vdcs.set([vdcs[5]]) - addresses = ( + ip_addresses = ( IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'), IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'), IPAddress(assigned_object=None, address='10.1.1.3/24'), @@ -5417,13 +5432,12 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'), IPAddress(assigned_object=None, address='2001:db8::3/64'), ) - IPAddress.objects.bulk_create(addresses) - - vdcs[0].primary_ip4 = addresses[0] - vdcs[0].primary_ip6 = addresses[3] + IPAddress.objects.bulk_create(ip_addresses) + vdcs[0].primary_ip4 = ip_addresses[0] + vdcs[0].primary_ip6 = ip_addresses[3] vdcs[0].save() - vdcs[1].primary_ip4 = addresses[1] - vdcs[1].primary_ip6 = addresses[4] + vdcs[1].primary_ip4 = ip_addresses[1] + vdcs[1].primary_ip6 = ip_addresses[4] vdcs[1].save() def test_q(self): @@ -5431,8 +5445,11 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_device(self): - params = {'device': ['Device 1', 'Device 2']} + devices = Device.objects.filter(name__in=['Device 1', 'Device 2']) + params = {'device': [devices[0].name, devices[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) + params = {'device_id': [devices[0].pk, devices[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_status(self): params = {'status': ['active']} @@ -5442,10 +5459,10 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_device_id(self): - devices = Device.objects.filter(name__in=['Device 1', 'Device 2']) - params = {'device_id': [devices[0].pk, devices[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_interface(self): + interfaces = Interface.objects.filter(name__in=['Interface 1', 'Interface 3']) + params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_has_primary_ip(self): params = {'has_primary_ip': True} diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index d88b8c9b3..4674335c9 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -40,12 +40,14 @@ class ScriptFilterSet(BaseFilterSet): method='search', label=_('Search'), ) + module_id = django_filters.ModelMultipleChoiceFilter( + queryset=ScriptModule.objects.all(), + label=_('Script module (ID)'), + ) class Meta: model = Script - fields = [ - 'id', 'name', - ] + fields = ('id', 'name', 'is_executable') def search(self, queryset, name, value): if not value.strip(): @@ -69,10 +71,10 @@ class WebhookFilterSet(NetBoxModelFilterSet): class Meta: model = Webhook - fields = [ + fields = ( 'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', 'description', - ] + ) def search(self, queryset, name, value): if not value.strip(): @@ -89,8 +91,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - object_type_id = MultiValueNumberFilter( - field_name='object_types__id' + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' ) object_type = ContentTypeFilter( field_name='object_types' @@ -103,10 +106,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet): class Meta: model = EventRule - fields = [ + fields = ( 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'action_type', 'description', - ] + ) def search(self, queryset, name, value): if not value.strip(): @@ -118,7 +121,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet): ) -class CustomFieldFilterSet(BaseFilterSet): +class CustomFieldFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -126,14 +129,16 @@ class CustomFieldFilterSet(BaseFilterSet): type = django_filters.MultipleChoiceFilter( choices=CustomFieldTypeChoices ) - object_type_id = MultiValueNumberFilter( - field_name='object_types__id' + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' ) object_type = ContentTypeFilter( field_name='object_types' ) - related_object_type_id = MultiValueNumberFilter( - field_name='related_object_type__id' + related_object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='related_object_type' ) related_object_type = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( @@ -147,10 +152,11 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField - fields = [ - 'id', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', - 'weight', 'is_cloneable', 'description', - ] + fields = ( + 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', + 'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum', + 'validation_regex', + ) def search(self, queryset, name, value): if not value.strip(): @@ -163,7 +169,7 @@ class CustomFieldFilterSet(BaseFilterSet): ) -class CustomFieldChoiceSetFilterSet(BaseFilterSet): +class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -174,9 +180,9 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet): class Meta: model = CustomFieldChoiceSet - fields = [ + fields = ( 'id', 'name', 'description', 'base_choices', 'order_alphabetically', - ] + ) def search(self, queryset, name, value): if not value.strip(): @@ -191,13 +197,14 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet): return queryset.filter(extra_choices__overlap=value) -class CustomLinkFilterSet(BaseFilterSet): +class CustomLinkFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) - object_type_id = MultiValueNumberFilter( - field_name='object_types__id' + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' ) object_type = ContentTypeFilter( field_name='object_types' @@ -205,9 +212,9 @@ class CustomLinkFilterSet(BaseFilterSet): class Meta: model = CustomLink - fields = [ - 'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', - ] + fields = ( + 'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'button_class', + ) def search(self, queryset, name, value): if not value.strip(): @@ -220,13 +227,14 @@ class CustomLinkFilterSet(BaseFilterSet): ) -class ExportTemplateFilterSet(BaseFilterSet): +class ExportTemplateFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) - object_type_id = MultiValueNumberFilter( - field_name='object_types__id' + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' ) object_type = ContentTypeFilter( field_name='object_types' @@ -242,7 +250,10 @@ class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate - fields = ['id', 'name', 'description', 'data_synced'] + fields = ( + 'id', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment', 'auto_sync_enabled', + 'data_synced', + ) def search(self, queryset, name, value): if not value.strip(): @@ -253,13 +264,14 @@ class ExportTemplateFilterSet(BaseFilterSet): ) -class SavedFilterFilterSet(BaseFilterSet): +class SavedFilterFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) - object_type_id = MultiValueNumberFilter( - field_name='object_types__id' + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' ) object_type = ContentTypeFilter( field_name='object_types' @@ -280,7 +292,7 @@ class SavedFilterFilterSet(BaseFilterSet): class Meta: model = SavedFilter - fields = ['id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] + fields = ('id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight') def search(self, queryset, name, value): if not value.strip(): @@ -321,20 +333,19 @@ class BookmarkFilterSet(BaseFilterSet): class Meta: model = Bookmark - fields = ['id', 'object_id'] + fields = ('id', 'object_id') -class ImageAttachmentFilterSet(BaseFilterSet): +class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) - created = django_filters.DateTimeFilter() object_type = ContentTypeFilter() class Meta: model = ImageAttachment - fields = ['id', 'object_type_id', 'object_id', 'name'] + fields = ('id', 'object_type_id', 'object_id', 'name', 'image_width', 'image_height') def search(self, queryset, name, value): if not value.strip(): @@ -364,7 +375,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet): class Meta: model = JournalEntry - fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind'] + fields = ('id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind') def search(self, queryset, name, value): if not value.strip(): @@ -389,7 +400,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet): class Meta: model = Tag - fields = ['id', 'name', 'slug', 'color', 'description', 'object_types'] + fields = ('id', 'name', 'slug', 'color', 'description', 'object_types') def search(self, queryset, name, value): if not value.strip(): @@ -486,12 +497,12 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): queryset=DeviceType.objects.all(), label=_('Device type'), ) - role_id = django_filters.ModelMultipleChoiceFilter( + device_role_id = django_filters.ModelMultipleChoiceFilter( field_name='roles', queryset=DeviceRole.objects.all(), label=_('Role'), ) - role = django_filters.ModelMultipleChoiceFilter( + device_role = django_filters.ModelMultipleChoiceFilter( field_name='roles__slug', queryset=DeviceRole.objects.all(), to_field_name='slug', @@ -577,9 +588,13 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): label=_('Data file (ID)'), ) + # TODO: Remove in v4.1 + role = device_role + role_id = device_role_id + class Meta: model = ConfigContext - fields = ['id', 'name', 'is_active', 'data_synced', 'description'] + fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced') def search(self, queryset, name, value): if not value.strip(): @@ -591,7 +606,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): ) -class ConfigTemplateFilterSet(BaseFilterSet): +class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -608,7 +623,7 @@ class ConfigTemplateFilterSet(BaseFilterSet): class Meta: model = ConfigTemplate - fields = ['id', 'name', 'description', 'data_synced'] + fields = ('id', 'name', 'description', 'auto_sync_enabled', 'data_synced') def search(self, queryset, name, value): if not value.strip(): @@ -656,10 +671,10 @@ class ObjectChangeFilterSet(BaseFilterSet): class Meta: model = ObjectChange - fields = [ + fields = ( 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id', - 'object_repr', - ] + 'related_object_type', 'related_object_id', 'object_repr', + ) def search(self, queryset, name, value): if not value.strip(): @@ -682,7 +697,7 @@ class ObjectTypeFilterSet(django_filters.FilterSet): class Meta: model = ObjectType - fields = ['id', 'app_label', 'model'] + fields = ('id', 'app_label', 'model') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index bec62c688..b68c02efc 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -23,9 +23,10 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType User = get_user_model() -class CustomFieldTestCase(TestCase, BaseFilterSetTests): +class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CustomField.objects.all() filterset = CustomFieldFilterSet + ignore_fields = ('default',) @classmethod def setUpTestData(cls): @@ -155,9 +156,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests): +class CustomFieldChoiceSetTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CustomFieldChoiceSet.objects.all() filterset = CustomFieldChoiceSetFilterSet + ignore_fields = ('extra_choices',) @classmethod def setUpTestData(cls): @@ -188,6 +190,7 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests): class WebhookTestCase(TestCase, BaseFilterSetTests): queryset = Webhook.objects.all() filterset = WebhookFilterSet + ignore_fields = ('additional_headers', 'body_template') @classmethod def setUpTestData(cls): @@ -252,6 +255,7 @@ class WebhookTestCase(TestCase, BaseFilterSetTests): class EventRuleTestCase(TestCase, BaseFilterSetTests): queryset = EventRule.objects.all() filterset = EventRuleFilterSet + ignore_fields = ('action_data', 'conditions') @classmethod def setUpTestData(cls): @@ -405,7 +409,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class CustomLinkTestCase(TestCase, BaseFilterSetTests): +class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CustomLink.objects.all() filterset = CustomLinkFilterSet @@ -474,9 +478,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class SavedFilterTestCase(TestCase, BaseFilterSetTests): +class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = SavedFilter.objects.all() filterset = SavedFilterFilterSet + ignore_fields = ('parameters',) @classmethod def setUpTestData(cls): @@ -647,9 +652,10 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class ExportTemplateTestCase(TestCase, BaseFilterSetTests): +class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ExportTemplate.objects.all() filterset = ExportTemplateFilterSet + ignore_fields = ('template_code', 'data_path') @classmethod def setUpTestData(cls): @@ -683,9 +689,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): +class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ImageAttachment.objects.all() filterset = ImageAttachmentFilterSet + ignore_fields = ('image',) @classmethod def setUpTestData(cls): @@ -760,12 +767,6 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_created(self): - pk_list = self.queryset.values_list('pk', flat=True)[:2] - self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) - params = {'created': '2021-01-01T00:00:00'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = JournalEntry.objects.all() @@ -873,6 +874,7 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConfigContext.objects.all() filterset = ConfigContextFilterSet + ignore_fields = ('data', 'data_path') @classmethod def setUpTestData(cls): @@ -1041,11 +1043,11 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_role(self): + def test_device_role(self): device_roles = DeviceRole.objects.all()[:2] - params = {'role_id': [device_roles[0].pk, device_roles[1].pk]} + params = {'device_role_id': [device_roles[0].pk, device_roles[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'role': [device_roles[0].slug, device_roles[1].slug]} + params = {'device_role': [device_roles[0].slug, device_roles[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_platform(self): @@ -1096,9 +1098,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ConfigTemplateTestCase(TestCase, BaseFilterSetTests): +class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConfigTemplate.objects.all() filterset = ConfigTemplateFilterSet + ignore_fields = ('template_code', 'environment_params', 'data_path') @classmethod def setUpTestData(cls): @@ -1125,6 +1128,93 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests): class TagTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Tag.objects.all() filterset = TagFilterSet + ignore_fields = ( + 'object_types', + + # Reverse relationships (to tagged models) we can ignore + 'aggregate', + 'asn', + 'asnrange', + 'cable', + 'circuit', + 'circuittermination', + 'circuittype', + 'cluster', + 'clustergroup', + 'clustertype', + 'configtemplate', + 'consoleport', + 'consoleserverport', + 'contact', + 'contactassignment', + 'contactgroup', + 'contactrole', + 'datasource', + 'device', + 'devicebay', + 'devicerole', + 'devicetype', + 'dummymodel', # From dummy_plugin + 'eventrule', + 'fhrpgroup', + 'frontport', + 'ikepolicy', + 'ikeproposal', + 'interface', + 'inventoryitem', + 'inventoryitemrole', + 'ipaddress', + 'iprange', + 'ipsecpolicy', + 'ipsecprofile', + 'ipsecproposal', + 'journalentry', + 'l2vpn', + 'l2vpntermination', + 'location', + 'manufacturer', + 'module', + 'modulebay', + 'moduletype', + 'platform', + 'powerfeed', + 'poweroutlet', + 'powerpanel', + 'powerport', + 'prefix', + 'provider', + 'provideraccount', + 'providernetwork', + 'rack', + 'rackreservation', + 'rackrole', + 'rearport', + 'region', + 'rir', + 'role', + 'routetarget', + 'service', + 'servicetemplate', + 'site', + 'sitegroup', + 'tenant', + 'tenantgroup', + 'tunnel', + 'tunnelgroup', + 'tunneltermination', + 'virtualchassis', + 'virtualdevicecontext', + 'virtualdisk', + 'virtualmachine', + 'vlan', + 'vlangroup', + 'vminterface', + 'vrf', + 'webhook', + 'wirelesslan', + 'wirelesslangroup', + 'wirelesslink', + ) @classmethod def setUpTestData(cls): @@ -1193,6 +1283,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): class ObjectChangeTestCase(TestCase, BaseFilterSetTests): queryset = ObjectChange.objects.all() filterset = ObjectChangeFilterSet + ignore_fields = ('prechange_data', 'postchange_data') @classmethod def setUpTestData(cls): diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 404baf71b..d58f5bfc9 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -8,6 +8,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from netaddr.core import AddrFormatError +from circuits.models import Provider from dcim.models import Device, Interface, Region, Site, SiteGroup from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet @@ -75,7 +76,7 @@ class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = VRF - fields = ['id', 'name', 'rd', 'enforce_unique', 'description'] + fields = ('id', 'name', 'rd', 'enforce_unique', 'description') class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet): @@ -101,6 +102,28 @@ class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet): to_field_name='rd', label=_('Export VRF (RD)'), ) + importing_l2vpn_id = django_filters.ModelMultipleChoiceFilter( + field_name='importing_l2vpns', + queryset=L2VPN.objects.all(), + label=_('Importing L2VPN'), + ) + importing_l2vpn = django_filters.ModelMultipleChoiceFilter( + field_name='importing_l2vpns__identifier', + queryset=L2VPN.objects.all(), + to_field_name='identifier', + label=_('Importing L2VPN (identifier)'), + ) + exporting_l2vpn_id = django_filters.ModelMultipleChoiceFilter( + field_name='exporting_l2vpns', + queryset=L2VPN.objects.all(), + label=_('Exporting L2VPN'), + ) + exporting_l2vpn = django_filters.ModelMultipleChoiceFilter( + field_name='exporting_l2vpns__identifier', + queryset=L2VPN.objects.all(), + to_field_name='identifier', + label=_('Exporting L2VPN (identifier)'), + ) def search(self, queryset, name, value): if not value.strip(): @@ -112,14 +135,14 @@ class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = RouteTarget - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'description') class RIRFilterSet(OrganizationalModelFilterSet): class Meta: model = RIR - fields = ['id', 'name', 'slug', 'is_private', 'description'] + fields = ('id', 'name', 'slug', 'is_private', 'description') class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet): @@ -144,7 +167,7 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = Aggregate - fields = ['id', 'date_added', 'description'] + fields = ('id', 'date_added', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -183,7 +206,7 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): class Meta: model = ASNRange - fields = ['id', 'name', 'start', 'end', 'description'] + fields = ('id', 'name', 'slug', 'start', 'end', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -214,10 +237,21 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): to_field_name='slug', label=_('Site (slug)'), ) + provider_id = django_filters.ModelMultipleChoiceFilter( + field_name='providers', + queryset=Provider.objects.all(), + label=_('Provider (ID)'), + ) + provider = django_filters.ModelMultipleChoiceFilter( + field_name='providers__slug', + queryset=Provider.objects.all(), + to_field_name='slug', + label=_('Provider (slug)'), + ) class Meta: model = ASN - fields = ['id', 'asn', 'description'] + fields = ('id', 'asn', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -234,7 +268,7 @@ class RoleFilterSet(OrganizationalModelFilterSet): class Meta: model = Role - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description', 'weight') class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet): @@ -359,7 +393,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = Prefix - fields = ['id', 'is_pool', 'mark_utilized', 'description'] + fields = ('id', 'is_pool', 'mark_utilized', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -475,7 +509,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class Meta: model = IPRange - fields = ['id', 'mark_utilized', 'description'] + fields = ('id', 'mark_utilized', 'size', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -628,10 +662,20 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): role = django_filters.MultipleChoiceFilter( choices=IPAddressRoleChoices ) + service_id = django_filters.ModelMultipleChoiceFilter( + field_name='services', + queryset=Service.objects.all(), + label=_('Service (ID)'), + ) + nat_inside_id = django_filters.ModelMultipleChoiceFilter( + field_name='nat_inside', + queryset=IPAddress.objects.all(), + label=_('NAT inside IP address (ID)'), + ) class Meta: model = IPAddress - fields = ['id', 'dns_name', 'description'] + fields = ('id', 'dns_name', 'description', 'assigned_object_type', 'assigned_object_id') def search(self, queryset, name, value): if not value.strip(): @@ -758,7 +802,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet): class Meta: model = FHRPGroup - fields = ['id', 'group_id', 'name', 'auth_key', 'description'] + fields = ('id', 'group_id', 'name', 'auth_key', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -819,7 +863,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet): class Meta: model = FHRPGroupAssignment - fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority'] + fields = ('id', 'group_id', 'interface_type', 'interface_id', 'priority') def filter_device(self, queryset, name, value): devices = Device.objects.filter(**{f'{name}__in': value}) @@ -849,7 +893,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet): region = django_filters.NumberFilter( method='filter_scope' ) - sitegroup = django_filters.NumberFilter( + site_group = django_filters.NumberFilter( method='filter_scope' ) site = django_filters.NumberFilter( @@ -861,16 +905,20 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet): rack = django_filters.NumberFilter( method='filter_scope' ) - clustergroup = django_filters.NumberFilter( + cluster_group = django_filters.NumberFilter( method='filter_scope' ) cluster = django_filters.NumberFilter( method='filter_scope' ) + # TODO: Remove in v4.1 + sitegroup = site_group + clustergroup = cluster_group + class Meta: model = VLANGroup - fields = ['id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id'] + fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id') def search(self, queryset, name, value): if not value.strip(): @@ -882,8 +930,9 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet): return queryset.filter(qs_filter) def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') return queryset.filter( - scope_type=ContentType.objects.get(model=name), + scope_type=ContentType.objects.get(model=model_name), scope_id=value ) @@ -975,7 +1024,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = VLAN - fields = ['id', 'vid', 'name', 'description'] + fields = ('id', 'vid', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -1008,7 +1057,7 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet): class Meta: model = ServiceTemplate - fields = ['id', 'name', 'protocol', 'description'] + fields = ('id', 'name', 'protocol', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -1041,26 +1090,29 @@ class ServiceFilterSet(NetBoxModelFilterSet): to_field_name='name', label=_('Virtual machine (name)'), ) - ipaddress_id = django_filters.ModelMultipleChoiceFilter( + ip_address_id = django_filters.ModelMultipleChoiceFilter( field_name='ipaddresses', queryset=IPAddress.objects.all(), label=_('IP address (ID)'), ) - ipaddress = django_filters.ModelMultipleChoiceFilter( + ip_address = django_filters.ModelMultipleChoiceFilter( field_name='ipaddresses__address', queryset=IPAddress.objects.all(), to_field_name='address', label=_('IP address'), ) - port = NumericArrayFilter( field_name='ports', lookup_expr='contains' ) + # TODO: Remove in v4.1 + ipaddress = ip_address + ipaddress_id = ip_address_id + class Meta: model = Service - fields = ['id', 'name', 'protocol', 'description'] + fields = ('id', 'name', 'protocol', 'description') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 909de886f..cf2e4d46e 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -304,7 +304,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): 'placeholder': 'Prefix', } ), - label='Parent Prefix' + label=_('Parent Prefix') ) family = forms.ChoiceField( required=False, diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index bb4f50c21..3a46423a5 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from netaddr import IPNetwork +from circuits.models import Provider from dcim.choices import InterfaceTypeChoices from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup from ipam.choices import * @@ -10,6 +11,8 @@ from ipam.models import * from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface +from vpn.choices import L2VPNTypeChoices +from vpn.models import L2VPN class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests): @@ -110,13 +113,6 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): ] RIR.objects.bulk_create(rirs) - sites = [ - Site(name='Site 1', slug='site-1'), - Site(name='Site 2', slug='site-2'), - Site(name='Site 3', slug='site-3') - ] - Site.objects.bulk_create(sites) - tenants = [ Tenant(name='Tenant 1', slug='tenant-1'), Tenant(name='Tenant 2', slug='tenant-2'), @@ -136,6 +132,12 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): ) ASN.objects.bulk_create(asns) + sites = [ + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3') + ] + Site.objects.bulk_create(sites) asns[0].sites.set([sites[0]]) asns[1].sites.set([sites[1]]) asns[2].sites.set([sites[2]]) @@ -143,6 +145,16 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): asns[4].sites.set([sites[1]]) asns[5].sites.set([sites[2]]) + providers = ( + Provider(name='Provider 1', slug='provider-1'), + Provider(name='Provider 2', slug='provider-2'), + Provider(name='Provider 3', slug='provider-3'), + ) + Provider.objects.bulk_create(providers) + providers[0].asns.add(asns[0]) + providers[1].asns.add(asns[1]) + providers[2].asns.add(asns[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -176,11 +188,24 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_provider(self): + providers = Provider.objects.all()[:2] + params = {'provider_id': [providers[0].pk, providers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class VRFTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VRF.objects.all() filterset = VRFFilterSet + def get_m2m_filter_name(self, field): + # Override filter names for import & export RouteTargets + if field.name == 'import_targets': + return 'import_target' + if field.name == 'export_targets': + return 'export_target' + return ChangeLoggedFilterSetTests.get_m2m_filter_name(field) + @classmethod def setUpTestData(cls): @@ -277,6 +302,18 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RouteTarget.objects.all() filterset = RouteTargetFilterSet + def get_m2m_filter_name(self, field): + # Override filter names for import & export VRFs and L2VPNs + if field.name == 'importing_vrfs': + return 'importing_vrf' + if field.name == 'exporting_vrfs': + return 'exporting_vrf' + if field.name == 'importing_l2vpns': + return 'importing_l2vpn' + if field.name == 'exporting_l2vpns': + return 'exporting_l2vpn' + return ChangeLoggedFilterSetTests.get_m2m_filter_name(field) + @classmethod def setUpTestData(cls): @@ -322,6 +359,17 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests): vrfs[1].import_targets.add(route_targets[4], route_targets[5]) vrfs[1].export_targets.add(route_targets[6], route_targets[7]) + l2vpns = ( + L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=100), + L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=200), + L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=300), + ) + L2VPN.objects.bulk_create(l2vpns) + l2vpns[0].import_targets.add(route_targets[0], route_targets[1]) + l2vpns[0].export_targets.add(route_targets[2], route_targets[3]) + l2vpns[1].import_targets.add(route_targets[4], route_targets[5]) + l2vpns[1].export_targets.add(route_targets[6], route_targets[7]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -344,6 +392,20 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'exporting_vrf': [vrfs[0].rd, vrfs[1].rd]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_importing_l2vpn(self): + l2vpns = L2VPN.objects.all()[:2] + params = {'importing_l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'importing_l2vpn': [l2vpns[0].identifier, l2vpns[1].identifier]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_exporting_l2vpn(self): + l2vpns = L2VPN.objects.all()[:2] + params = {'exporting_l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'exporting_l2vpn': [l2vpns[0].identifier, l2vpns[1].identifier]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_tenant(self): tenants = Tenant.objects.all()[:2] params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} @@ -922,6 +984,7 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests): class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IPAddress.objects.all() filterset = IPAddressFilterSet + ignore_fields = ('fhrpgroup',) @classmethod def setUpTestData(cls): @@ -1092,6 +1155,16 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): ) IPAddress.objects.bulk_create(ipaddresses) + services = ( + Service(name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]), + Service(name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]), + Service(name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]), + ) + Service.objects.bulk_create(services) + services[0].ipaddresses.add(ipaddresses[0]) + services[1].ipaddresses.add(ipaddresses[1]) + services[2].ipaddresses.add(ipaddresses[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -1231,6 +1304,11 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_service(self): + services = Service.objects.all()[:2] + params = {'service_id': [services[0].pk, services[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FHRPGroup.objects.all() @@ -1475,6 +1553,7 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VLAN.objects.all() filterset = VLANFilterSet + ignore_fields = ('interfaces_as_tagged', 'vminterfaces_as_tagged') @classmethod def setUpTestData(cls): @@ -1733,6 +1812,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ServiceTemplate.objects.all() filterset = ServiceTemplateFilterSet + ignore_fields = ('ports',) @classmethod def setUpTestData(cls): @@ -1797,6 +1877,7 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Service.objects.all() filterset = ServiceFilterSet + ignore_fields = ('ports',) @classmethod def setUpTestData(cls): @@ -1883,9 +1964,9 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'virtual_machine': [vms[0].name, vms[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_ipaddress(self): + def test_ip_address(self): ips = IPAddress.objects.all()[:2] - params = {'ipaddress_id': [ips[0].pk, ips[1].pk]} + params = {'ip_address_id': [ips[0].pk, ips[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]} + params = {'ip_address': [str(ips[0].address), str(ips[1].address)]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 7af3dc082..a7c52d3fb 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -50,14 +50,14 @@ class ContactGroupFilterSet(OrganizationalModelFilterSet): class Meta: model = ContactGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class ContactRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = ContactRole - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class ContactFilterSet(NetBoxModelFilterSet): @@ -77,7 +77,7 @@ class ContactFilterSet(NetBoxModelFilterSet): class Meta: model = Contact - fields = ['id', 'name', 'title', 'phone', 'email', 'address', 'link', 'description'] + fields = ('id', 'name', 'title', 'phone', 'email', 'address', 'link', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -131,7 +131,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): class Meta: model = ContactAssignment - fields = ['id', 'object_type_id', 'object_id', 'priority', 'tag'] + fields = ('id', 'object_type_id', 'object_id', 'priority', 'tag') def search(self, queryset, name, value): if not value.strip(): @@ -192,7 +192,7 @@ class TenantGroupFilterSet(OrganizationalModelFilterSet): class Meta: model = TenantGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): @@ -212,7 +212,7 @@ class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index d320c1bb8..da0095a1c 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -4,8 +4,10 @@ from django.contrib.auth import get_user_model from django.db.models import Q from django.utils.translation import gettext as _ +from core.models import ObjectType from netbox.filtersets import BaseFilterSet from users.models import Group, ObjectPermission, Token +from utilities.filters import ContentTypeFilter __all__ = ( 'GroupFilterSet', @@ -20,10 +22,20 @@ class GroupFilterSet(BaseFilterSet): method='search', label=_('Search'), ) + user_id = django_filters.ModelMultipleChoiceFilter( + field_name='user', + queryset=get_user_model().objects.all(), + label=_('User (ID)'), + ) + permission_id = django_filters.ModelMultipleChoiceFilter( + field_name='object_permissions', + queryset=ObjectPermission.objects.all(), + label=_('Permission (ID)'), + ) class Meta: model = Group - fields = ['id', 'name'] + fields = ('id', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -47,10 +59,18 @@ class UserFilterSet(BaseFilterSet): to_field_name='name', label=_('Group (name)'), ) + permission_id = django_filters.ModelMultipleChoiceFilter( + field_name='object_permissions', + queryset=ObjectPermission.objects.all(), + label=_('Permission (ID)'), + ) class Meta: model = get_user_model() - fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'is_superuser'] + fields = ( + 'id', 'username', 'first_name', 'last_name', 'email', 'date_joined', 'last_login', 'is_staff', 'is_active', + 'is_superuser', + ) def search(self, queryset, name, value): if not value.strip(): @@ -100,7 +120,7 @@ class TokenFilterSet(BaseFilterSet): class Meta: model = Token - fields = ['id', 'key', 'write_enabled', 'description'] + fields = ('id', 'key', 'write_enabled', 'description', 'last_used') def search(self, queryset, name, value): if not value.strip(): @@ -116,6 +136,13 @@ class ObjectPermissionFilterSet(BaseFilterSet): method='search', label=_('Search'), ) + object_type_id = django_filters.ModelMultipleChoiceFilter( + queryset=ObjectType.objects.all(), + field_name='object_types' + ) + object_type = ContentTypeFilter( + field_name='object_types' + ) can_view = django_filters.BooleanFilter( method='_check_action' ) @@ -153,7 +180,7 @@ class ObjectPermissionFilterSet(BaseFilterSet): class Meta: model = ObjectPermission - fields = ['id', 'name', 'enabled', 'object_types', 'description'] + fields = ('id', 'name', 'enabled', 'object_types', 'description') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 5930285a9..2cef6954a 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -15,6 +15,7 @@ User = get_user_model() class UserTestCase(TestCase, BaseFilterSetTests): queryset = User.objects.all() filterset = filtersets.UserFilterSet + ignore_fields = ('config', 'dashboard', 'password', 'user_permissions') @classmethod def setUpTestData(cls): @@ -66,6 +67,16 @@ class UserTestCase(TestCase, BaseFilterSetTests): users[1].groups.set([groups[1]]) users[2].groups.set([groups[2]]) + object_permissions = ( + ObjectPermission(name='Permission 1', actions=['add']), + ObjectPermission(name='Permission 2', actions=['change']), + ObjectPermission(name='Permission 3', actions=['delete']), + ) + ObjectPermission.objects.bulk_create(object_permissions) + object_permissions[0].users.add(users[0]) + object_permissions[1].users.add(users[1]) + object_permissions[2].users.add(users[2]) + def test_q(self): params = {'q': 'user1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -105,10 +116,16 @@ class UserTestCase(TestCase, BaseFilterSetTests): params = {'group': [groups[0].name, groups[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_permission(self): + object_permissions = ObjectPermission.objects.all()[:2] + params = {'permission_id': [object_permissions[0].pk, object_permissions[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class GroupTestCase(TestCase, BaseFilterSetTests): queryset = Group.objects.all() filterset = filtersets.GroupFilterSet + ignore_fields = ('permissions',) @classmethod def setUpTestData(cls): @@ -120,6 +137,26 @@ class GroupTestCase(TestCase, BaseFilterSetTests): ) Group.objects.bulk_create(groups) + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + users[0].groups.set([groups[0]]) + users[1].groups.set([groups[1]]) + users[2].groups.set([groups[2]]) + + object_permissions = ( + ObjectPermission(name='Permission 1', actions=['add']), + ObjectPermission(name='Permission 2', actions=['change']), + ObjectPermission(name='Permission 3', actions=['delete']), + ) + ObjectPermission.objects.bulk_create(object_permissions) + object_permissions[0].groups.add(groups[0]) + object_permissions[1].groups.add(groups[1]) + object_permissions[2].groups.add(groups[2]) + def test_q(self): params = {'q': 'group 1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -128,10 +165,21 @@ class GroupTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Group 1', 'Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_user(self): + users = User.objects.all()[:2] + params = {'user_id': [users[0].pk, users[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_permission(self): + object_permissions = ObjectPermission.objects.all()[:2] + params = {'permission_id': [object_permissions[0].pk, object_permissions[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): queryset = ObjectPermission.objects.all() filterset = filtersets.ObjectPermissionFilterSet + ignore_fields = ('actions', 'constraints') @classmethod def setUpTestData(cls): @@ -226,6 +274,7 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): class TokenTestCase(TestCase, BaseFilterSetTests): queryset = Token.objects.all() filterset = filtersets.TokenFilterSet + ignore_fields = ('allowed_ips',) @classmethod def setUpTestData(cls): diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py index 00f3d9745..e58123f03 100644 --- a/netbox/utilities/testing/filtersets.py +++ b/netbox/utilities/testing/filtersets.py @@ -1,15 +1,91 @@ -from datetime import date, datetime, timezone +import django_filters +from datetime import datetime, timezone +from itertools import chain +from mptt.models import MPTTModel +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.models import ContentType +from django.db.models import ForeignKey, ManyToManyField, ManyToManyRel, ManyToOneRel, OneToOneRel +from django.utils.module_loading import import_string +from taggit.managers import TaggableManager + +from extras.filters import TagFilter +from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter + +from core.models import ObjectType __all__ = ( 'BaseFilterSetTests', 'ChangeLoggedFilterSetTests', ) +EXEMPT_MODEL_FIELDS = ( + 'comments', + 'custom_field_data', + 'level', # MPTT + 'lft', # MPTT + 'rght', # MPTT + 'tree_id', # MPTT +) + class BaseFilterSetTests: queryset = None filterset = None + ignore_fields = tuple() + + def get_m2m_filter_name(self, field): + """ + Given a ManyToManyField, determine the correct name for its corresponding Filter. Individual test + cases may override this method to prescribe deviations for specific fields. + """ + related_model_name = field.related_model._meta.verbose_name + return related_model_name.lower().replace(' ', '_') + + def get_filters_for_model_field(self, field): + """ + Given a model field, return an iterable of (name, class) for each filter that should be defined on + the model's FilterSet class. If the appropriate filter class cannot be determined, it will be None. + """ + # ForeignKey & OneToOneField + if issubclass(field.__class__, ForeignKey) or type(field) is OneToOneRel: + + # Relationships to ContentType (used as part of a GFK) do not need a filter + if field.related_model is ContentType: + return [(None, None)] + + # ForeignKeys to ObjectType need two filters: 'app.model' & PK + if field.related_model is ObjectType: + return [ + (field.name, ContentTypeFilter), + (f'{field.name}_id', django_filters.ModelMultipleChoiceFilter), + ] + + # ForeignKey to an MPTT-enabled model + if issubclass(field.related_model, MPTTModel) and field.model is not field.related_model: + return [(f'{field.name}_id', TreeNodeMultipleChoiceFilter)] + + return [(f'{field.name}_id', django_filters.ModelMultipleChoiceFilter)] + + # Many-to-many relationships (forward & backward) + elif type(field) in (ManyToManyField, ManyToManyRel): + filter_name = self.get_m2m_filter_name(field) + + # ManyToManyFields to ObjectType need two filters: 'app.model' & PK + if field.related_model is ObjectType: + return [ + (filter_name, ContentTypeFilter), + (f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter), + ] + + return [(f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter)] + + # Tag manager + if type(field) is TaggableManager: + return [('tag', TagFilter)] + + # Unable to determine the correct filter class + return [(field.name, None)] def test_id(self): """ @@ -19,6 +95,61 @@ class BaseFilterSetTests: self.assertGreater(self.queryset.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_missing_filters(self): + """ + Check for any model fields which do not have the required filter(s) defined. + """ + app_label = self.__class__.__module__.split('.')[0] + model = self.queryset.model + model_name = model.__name__ + + # Import the FilterSet class & sanity check it + filterset = import_string(f'{app_label}.filtersets.{model_name}FilterSet') + self.assertEqual(model, filterset.Meta.model, "FilterSet model does not match!") + + filters = filterset.get_filters() + + # Check for missing filters + for model_field in model._meta.get_fields(): + + # Skip private fields + if model_field.name.startswith('_'): + continue + + # Skip ignored fields + if model_field.name in chain(self.ignore_fields, EXEMPT_MODEL_FIELDS): + continue + + # Skip reverse ForeignKey relationships + if type(model_field) is ManyToOneRel: + continue + + # Skip generic relationships + if type(model_field) in (GenericForeignKey, GenericRelation): + continue + + for filter_name, filter_class in self.get_filters_for_model_field(model_field): + + if filter_name is None: + # Field is exempt + continue + + # Check that the filter is defined + self.assertIn( + filter_name, + filters.keys(), + f'No filter defined for {filter_name} ({model_field.name})!' + ) + + # Check that the filter class is correct + filter = filters[filter_name] + if filter_class is not None: + self.assertIs( + type(filter), + filter_class, + f"Invalid filter class {type(filter)} for {filter_name} (should be {filter_class})!" + ) + class ChangeLoggedFilterSetTests(BaseFilterSetTests): diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 78f6566d3..55fadd1af 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -27,14 +27,14 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet): class Meta: model = ClusterType - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class Meta: model = ClusterGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): @@ -101,7 +101,7 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte class Meta: model = Cluster - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -240,7 +240,7 @@ class VirtualMachineFilterSet( class Meta: model = VirtualMachine - fields = ['id', 'cluster', 'vcpus', 'memory', 'disk', 'description'] + fields = ('id', 'cluster', 'vcpus', 'memory', 'disk', 'description', 'interface_count', 'virtual_disk_count') def search(self, queryset, name, value): if not value.strip(): @@ -299,7 +299,7 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet): class Meta: model = VMInterface - fields = ['id', 'name', 'enabled', 'mtu', 'description'] + fields = ('id', 'name', 'enabled', 'mtu', 'mode', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -325,7 +325,7 @@ class VirtualDiskFilterSet(NetBoxModelFilterSet): class Meta: model = VirtualDisk - fields = ['id', 'name', 'size', 'description'] + fields = ('id', 'name', 'size', 'description') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index 5c020e1b2..ff55aba10 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -522,6 +522,7 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests): class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VMInterface.objects.all() filterset = VMInterfaceFilterSet + ignore_fields = ('tagged_vlans', 'untagged_vlan',) @classmethod def setUpTestData(cls): diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py index 0647838a8..970f68795 100644 --- a/netbox/vpn/filtersets.py +++ b/netbox/vpn/filtersets.py @@ -29,7 +29,7 @@ class TunnelGroupFilterSet(OrganizationalModelFilterSet): class Meta: model = TunnelGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet): @@ -62,7 +62,7 @@ class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = Tunnel - fields = ['id', 'name', 'tunnel_id', 'description'] + fields = ('id', 'name', 'tunnel_id', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -120,10 +120,21 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet): class Meta: model = TunnelTermination - fields = ['id'] + fields = ('id', 'termination_id') class IKEProposalFilterSet(NetBoxModelFilterSet): + ike_policy_id = django_filters.ModelMultipleChoiceFilter( + field_name='ike_policies', + queryset=IKEPolicy.objects.all(), + label=_('IKE policy (ID)'), + ) + ike_policy = django_filters.ModelMultipleChoiceFilter( + field_name='ike_policies__name', + queryset=IKEPolicy.objects.all(), + to_field_name='name', + label=_('IKE policy (name)'), + ) authentication_method = django_filters.MultipleChoiceFilter( choices=AuthenticationMethodChoices ) @@ -139,7 +150,7 @@ class IKEProposalFilterSet(NetBoxModelFilterSet): class Meta: model = IKEProposal - fields = ['id', 'name', 'sa_lifetime', 'description'] + fields = ('id', 'name', 'sa_lifetime', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -158,16 +169,23 @@ class IKEPolicyFilterSet(NetBoxModelFilterSet): mode = django_filters.MultipleChoiceFilter( choices=IKEModeChoices ) - proposal_id = MultiValueNumberFilter( - field_name='proposals__id' + ike_proposal_id = django_filters.ModelMultipleChoiceFilter( + field_name='proposals', + queryset=IKEProposal.objects.all() ) - proposal = MultiValueCharFilter( - field_name='proposals__name' + ike_proposal = django_filters.ModelMultipleChoiceFilter( + field_name='proposals__name', + queryset=IKEProposal.objects.all(), + to_field_name='name' ) + # TODO: Remove in v4.1 + proposal = ike_proposal + proposal_id = ike_proposal_id + class Meta: model = IKEPolicy - fields = ['id', 'name', 'preshared_key', 'description'] + fields = ('id', 'name', 'preshared_key', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -180,6 +198,17 @@ class IKEPolicyFilterSet(NetBoxModelFilterSet): class IPSecProposalFilterSet(NetBoxModelFilterSet): + ipsec_policy_id = django_filters.ModelMultipleChoiceFilter( + field_name='ipsec_policies', + queryset=IPSecPolicy.objects.all(), + label=_('IPSec policy (ID)'), + ) + ipsec_policy = django_filters.ModelMultipleChoiceFilter( + field_name='ipsec_policies__name', + queryset=IPSecPolicy.objects.all(), + to_field_name='name', + label=_('IPSec policy (name)'), + ) encryption_algorithm = django_filters.MultipleChoiceFilter( choices=EncryptionAlgorithmChoices ) @@ -189,7 +218,7 @@ class IPSecProposalFilterSet(NetBoxModelFilterSet): class Meta: model = IPSecProposal - fields = ['id', 'name', 'sa_lifetime_seconds', 'sa_lifetime_data', 'description'] + fields = ('id', 'name', 'sa_lifetime_seconds', 'sa_lifetime_data', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -205,16 +234,23 @@ class IPSecPolicyFilterSet(NetBoxModelFilterSet): pfs_group = django_filters.MultipleChoiceFilter( choices=DHGroupChoices ) - proposal_id = MultiValueNumberFilter( - field_name='proposals__id' + ipsec_proposal_id = django_filters.ModelMultipleChoiceFilter( + field_name='proposals', + queryset=IPSecProposal.objects.all() ) - proposal = MultiValueCharFilter( - field_name='proposals__name' + ipsec_proposal = django_filters.ModelMultipleChoiceFilter( + field_name='proposals__name', + queryset=IPSecProposal.objects.all(), + to_field_name='name' ) + # TODO: Remove in v4.1 + proposal = ipsec_proposal + proposal_id = ipsec_proposal_id + class Meta: model = IPSecPolicy - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -253,7 +289,7 @@ class IPSecProfileFilterSet(NetBoxModelFilterSet): class Meta: model = IPSecProfile - fields = ['id', 'name', 'description'] + fields = ('id', 'name', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -295,7 +331,7 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = L2VPN - fields = ['id', 'identifier', 'name', 'slug', 'type', 'description'] + fields = ('id', 'identifier', 'name', 'slug', 'type', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -402,7 +438,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): class Meta: model = L2VPNTermination - fields = ('id', 'assigned_object_type_id') + fields = ('id', 'assigned_object_id') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py index d4e80750d..d2b893766 100644 --- a/netbox/vpn/tests/test_filtersets.py +++ b/netbox/vpn/tests/test_filtersets.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase from dcim.choices import InterfaceTypeChoices @@ -331,6 +330,16 @@ class IKEProposalTestCase(TestCase, ChangeLoggedFilterSetTests): ) IKEProposal.objects.bulk_create(ike_proposals) + ike_policies = ( + IKEPolicy(name='IKE Policy 1'), + IKEPolicy(name='IKE Policy 2'), + IKEPolicy(name='IKE Policy 3'), + ) + IKEPolicy.objects.bulk_create(ike_policies) + ike_policies[0].proposals.add(ike_proposals[0]) + ike_policies[1].proposals.add(ike_proposals[1]) + ike_policies[2].proposals.add(ike_proposals[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -343,6 +352,13 @@ class IKEProposalTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ike_policy(self): + ike_policies = IKEPolicy.objects.all()[:2] + params = {'ike_policy_id': [ike_policies[0].pk, ike_policies[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'ike_policy': [ike_policies[0].name, ike_policies[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_authentication_method(self): params = {'authentication_method': [ AuthenticationMethodChoices.PRESHARED_KEYS, AuthenticationMethodChoices.CERTIFICATES @@ -446,11 +462,11 @@ class IKEPolicyTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'mode': [IKEModeChoices.MAIN]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_proposal(self): + def test_ike_proposal(self): proposals = IKEProposal.objects.all()[:2] - params = {'proposal_id': [proposals[0].pk, proposals[1].pk]} + params = {'ike_proposal_id': [proposals[0].pk, proposals[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'proposal': [proposals[0].name, proposals[1].name]} + params = {'ike_proposal': [proposals[0].name, proposals[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -488,6 +504,16 @@ class IPSecProposalTestCase(TestCase, ChangeLoggedFilterSetTests): ) IPSecProposal.objects.bulk_create(ipsec_proposals) + ipsec_policies = ( + IPSecPolicy(name='IPSec Policy 1'), + IPSecPolicy(name='IPSec Policy 2'), + IPSecPolicy(name='IPSec Policy 3'), + ) + IPSecPolicy.objects.bulk_create(ipsec_policies) + ipsec_policies[0].proposals.add(ipsec_proposals[0]) + ipsec_policies[1].proposals.add(ipsec_proposals[1]) + ipsec_policies[2].proposals.add(ipsec_proposals[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -500,6 +526,13 @@ class IPSecProposalTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ipsec_policy(self): + ipsec_policies = IPSecPolicy.objects.all()[:2] + params = {'ipsec_policy_id': [ipsec_policies[0].pk, ipsec_policies[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_encryption_algorithm(self): params = {'encryption_algorithm': [ EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC, EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC @@ -584,11 +617,11 @@ class IPSecPolicyTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'pfs_group': [DHGroupChoices.GROUP_1, DHGroupChoices.GROUP_2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_proposal(self): + def test_ipsec_proposal(self): proposals = IPSecProposal.objects.all()[:2] - params = {'proposal_id': [proposals[0].pk, proposals[1].pk]} + params = {'ipsec_proposal_id': [proposals[0].pk, proposals[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'proposal': [proposals[0].name, proposals[1].name]} + params = {'ipsec_proposal': [proposals[0].name, proposals[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -710,6 +743,14 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = L2VPN.objects.all() filterset = L2VPNFilterSet + def get_m2m_filter_name(self, field): + # Override filter names for import & export RouteTargets + if field.name == 'import_targets': + return 'import_target' + if field.name == 'export_targets': + return 'export_target' + return ChangeLoggedFilterSetTests.get_m2m_filter_name(field) + @classmethod def setUpTestData(cls): @@ -848,8 +889,8 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'l2vpn': [l2vpns[0].slug, l2vpns[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) - def test_content_type(self): - params = {'assigned_object_type_id': ContentType.objects.get(model='vlan').pk} + def test_termination_type(self): + params = {'assigned_object_type': 'ipam.vlan'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_interface(self): diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 50b1f78b1..da66df144 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -2,6 +2,7 @@ import django_filters from django.db.models import Q from dcim.choices import LinkStatusChoices +from dcim.models import Interface from ipam.models import VLAN from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet @@ -39,7 +40,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): class Meta: model = WirelessLANGroup - fields = ['id', 'name', 'slug', 'description'] + fields = ('id', 'name', 'slug', 'description') class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): @@ -60,6 +61,10 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): vlan_id = django_filters.ModelMultipleChoiceFilter( queryset=VLAN.objects.all() ) + interface_id = django_filters.ModelMultipleChoiceFilter( + queryset=Interface.objects.all(), + field_name='interfaces' + ) auth_type = django_filters.MultipleChoiceFilter( choices=WirelessAuthTypeChoices ) @@ -69,7 +74,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = WirelessLAN - fields = ['id', 'ssid', 'auth_psk', 'description'] + fields = ('id', 'ssid', 'auth_psk', 'description') def search(self, queryset, name, value): if not value.strip(): @@ -82,8 +87,12 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet): - interface_a_id = MultiValueNumberFilter() - interface_b_id = MultiValueNumberFilter() + interface_a_id = django_filters.ModelMultipleChoiceFilter( + queryset=Interface.objects.all() + ) + interface_b_id = django_filters.ModelMultipleChoiceFilter( + queryset=Interface.objects.all() + ) status = django_filters.MultipleChoiceFilter( choices=LinkStatusChoices ) @@ -96,7 +105,7 @@ class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = WirelessLink - fields = ['id', 'ssid', 'auth_psk', 'description'] + fields = ('id', 'ssid', 'auth_psk', 'description') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index 78e50edb7..72264a158 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -153,6 +153,17 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): ) WirelessLAN.objects.bulk_create(wireless_lans) + device = create_test_device('Device 1') + interfaces = ( + Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N), + Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_80211N), + Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_80211N), + ) + Interface.objects.bulk_create(interfaces) + interfaces[0].wireless_lans.add(wireless_lans[0]) + interfaces[1].wireless_lans.add(wireless_lans[1]) + interfaces[2].wireless_lans.add(wireless_lans[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -200,6 +211,11 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'tenant': [tenants[0].slug, tenants[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_interface(self): + interfaces = Interface.objects.all()[:2] + params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = WirelessLink.objects.all()