From 6085e0bb0bdefc3c7bc7e586607bee4699f2ea03 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Mar 2024 14:59:41 -0500 Subject: [PATCH] Test for missing ManyToManyField filters --- netbox/dcim/filtersets.py | 12 +++++++++- netbox/dcim/tests/test_filtersets.py | 2 +- netbox/extras/filtersets.py | 8 +++++-- netbox/extras/tests/test_filtersets.py | 7 +++--- netbox/ipam/filtersets.py | 8 +++++-- netbox/ipam/tests/test_filtersets.py | 15 +++++++++--- netbox/users/filtersets.py | 7 ++++++ netbox/users/tests/test_filtersets.py | 3 ++- netbox/utilities/testing/filtersets.py | 32 ++++++++++++++++++++------ netbox/vpn/filtersets.py | 16 +++++++++---- netbox/vpn/tests/test_filtersets.py | 22 ++++++++++++------ 11 files changed, 101 insertions(+), 31 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index f0fc4ac60..0e22f613f 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -23,6 +23,7 @@ from utilities.filters import ( 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 * @@ -1637,13 +1638,22 @@ class InterfaceFilterSet( to_field_name='name', 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 = ( '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', 'mark_connected', - 'cable_id', 'cable_end', 'wireless_link_id', + 'cable_id', 'cable_end', ) def filter_virtual_chassis_member(self, queryset, name, value): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index e9bee9322..d844cfd6b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -3235,7 +3235,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): queryset = Interface.objects.all() filterset = InterfaceFilterSet - ignore_fields = ('untagged_vlan',) + ignore_fields = ('untagged_vlan', 'vdcs') @classmethod def setUpTestData(cls): diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 1d833bd28..0be2fde28 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -491,12 +491,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', @@ -582,6 +582,10 @@ 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', 'description', 'weight', 'auto_sync_enabled', 'data_synced') diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index ccccaa793..d7d9e6ca2 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1043,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): @@ -1128,6 +1128,7 @@ class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): class TagTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Tag.objects.all() filterset = TagFilterSet + ignore_fields = ('object_types',) @classmethod def setUpTestData(cls): diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index c95863be8..84d507e44 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -1046,12 +1046,12 @@ 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', @@ -1062,6 +1062,10 @@ class ServiceFilterSet(NetBoxModelFilterSet): 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') diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index f69e2d2f4..0b3c92b3e 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -181,6 +181,15 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VRF.objects.all() filterset = VRFFilterSet + @staticmethod + def get_m2m_filter_name(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 super().get_m2m_filter_name(field) + @classmethod def setUpTestData(cls): @@ -1886,9 +1895,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/users/filtersets.py b/netbox/users/filtersets.py index 2f0bb068a..5ad8b6476 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext as _ from netbox.filtersets import BaseFilterSet from users.models import Group, ObjectPermission, Token +from utilities.filters import ContentTypeFilter, MultiValueNumberFilter __all__ = ( 'GroupFilterSet', @@ -118,6 +119,12 @@ class ObjectPermissionFilterSet(BaseFilterSet): method='search', label=_('Search'), ) + object_type_id = MultiValueNumberFilter( + field_name='object_types__id' + ) + object_type = ContentTypeFilter( + field_name='object_types' + ) can_view = django_filters.BooleanFilter( method='_check_action' ) diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 7f41746fd..d7a352793 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -15,7 +15,7 @@ User = get_user_model() class UserTestCase(TestCase, BaseFilterSetTests): queryset = User.objects.all() filterset = filtersets.UserFilterSet - ignore_fields = ('config', 'dashboard', 'password') + ignore_fields = ('config', 'dashboard', 'password', 'user_permissions') @classmethod def setUpTestData(cls): @@ -110,6 +110,7 @@ class UserTestCase(TestCase, BaseFilterSetTests): class GroupTestCase(TestCase, BaseFilterSetTests): queryset = Group.objects.all() filterset = filtersets.GroupFilterSet + ignore_fields = ('permissions',) @classmethod def setUpTestData(cls): diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py index 4d8025672..898669cde 100644 --- a/netbox/utilities/testing/filtersets.py +++ b/netbox/utilities/testing/filtersets.py @@ -43,6 +43,15 @@ class BaseFilterSetTests: filterset = None ignore_fields = tuple() + @staticmethod + def get_m2m_filter_name(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 test_id(self): """ Test filtering for two PKs from a set of >2 objects. @@ -94,13 +103,22 @@ class BaseFilterSetTests: filter_name = model_field.name else: filter_name = f'{model_field.name}_id' - self.assertIn(filter_name, filterset_fields, f'No filter found for {filter_name}!') + self.assertIn( + filter_name, + filterset_fields, + f'No filter defined for {filter_name} ({model_field.name})!' + ) + + elif type(model_field) is ManyToManyField: + filter_name = self.get_m2m_filter_name(model_field) + filter_name = f'{filter_name}_id' + self.assertIn( + filter_name, + filterset_fields, + f'No filter defined for {filter_name} ({model_field.name})!' + ) # TODO: Many-to-many relationships - elif type(model_field) is ManyToManyField: - related_model = model_field.related_model._meta.model_name - filter_name = f'{related_model}_id' - self.assertIn(filter_name, filterset_fields, f'M2M: No filter found for {filter_name}!') elif type(model_field) is ManyToManyRel: continue @@ -110,14 +128,14 @@ class BaseFilterSetTests: # Tags elif type(model_field) is TaggableManager: - self.assertIn('tag', filterset_fields, f'No filter found for {model_field.name}!') + self.assertIn('tag', filterset_fields, f'No filter defined for {model_field.name}!') # All other fields else: self.assertIn( model_field.name, filterset_fields, - f'No filter found for {model_field.name} ({type(model_field)})!' + f'No defined found for {model_field.name} ({type(model_field)})!' ) diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py index 9a1c328a9..03eb0ee67 100644 --- a/netbox/vpn/filtersets.py +++ b/netbox/vpn/filtersets.py @@ -158,13 +158,17 @@ class IKEPolicyFilterSet(NetBoxModelFilterSet): mode = django_filters.MultipleChoiceFilter( choices=IKEModeChoices ) - proposal_id = MultiValueNumberFilter( + ike_proposal_id = MultiValueNumberFilter( field_name='proposals__id' ) - proposal = MultiValueCharFilter( + ike_proposal = MultiValueCharFilter( field_name='proposals__name' ) + # TODO: Remove in v4.1 + proposal = ike_proposal + proposal_id = ike_proposal_id + class Meta: model = IKEPolicy fields = ['id', 'name', 'preshared_key', 'description'] @@ -205,13 +209,17 @@ class IPSecPolicyFilterSet(NetBoxModelFilterSet): pfs_group = django_filters.MultipleChoiceFilter( choices=DHGroupChoices ) - proposal_id = MultiValueNumberFilter( + ipsec_proposal_id = MultiValueNumberFilter( field_name='proposals__id' ) - proposal = MultiValueCharFilter( + ipsec_proposal = MultiValueCharFilter( field_name='proposals__name' ) + # TODO: Remove in v4.1 + proposal = ipsec_proposal + proposal_id = ipsec_proposal_id + class Meta: model = IPSecPolicy fields = ['id', 'name', 'description'] diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py index 4d099a065..26fd79854 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 @@ -446,11 +445,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) @@ -584,11 +583,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 +709,15 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = L2VPN.objects.all() filterset = L2VPNFilterSet + @staticmethod + def get_m2m_filter_name(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 super().get_m2m_filter_name(field) + @classmethod def setUpTestData(cls):