diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 6c7b45164..3335e77a9 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -9,7 +9,7 @@ from ipam.models import ASN from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import ( - ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter, + MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter, ) from utilities.filtersets import register_filterset from .choices import * @@ -281,7 +281,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet): queryset=Circuit.objects.all(), label=_('Circuit'), ) - termination_type = ContentTypeFilter() + termination_type = MultiValueContentTypeFilter() region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='_region', @@ -381,7 +381,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - member_type = ContentTypeFilter() + member_type = MultiValueContentTypeFilter() circuit = MultiValueCharFilter( method='filter_circuit', field_name='cid', diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index 13f046b5b..3d391bd97 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext as _ from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet from netbox.utils import get_data_backend_choices from users.models import User -from utilities.filters import ContentTypeFilter +from utilities.filters import MultiValueContentTypeFilter from utilities.filtersets import register_filterset from .choices import * from .models import * @@ -88,7 +88,7 @@ class JobFilterSet(BaseFilterSet): queryset=ObjectType.objects.with_feature('jobs'), field_name='object_type_id', ) - object_type = ContentTypeFilter() + object_type = MultiValueContentTypeFilter() created = django_filters.DateTimeFilter() created__before = django_filters.DateTimeFilter( field_name='created', @@ -180,11 +180,11 @@ class ObjectChangeFilterSet(BaseFilterSet): label=_('Search'), ) time = django_filters.DateTimeFromToRangeFilter() - changed_object_type = ContentTypeFilter() + changed_object_type = MultiValueContentTypeFilter() changed_object_type_id = django_filters.ModelMultipleChoiceFilter( queryset=ContentType.objects.all() ) - related_object_type = ContentTypeFilter() + related_object_type = MultiValueContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( queryset=User.objects.all(), label=_('User (ID)'), diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py index 50441cf62..613833192 100644 --- a/netbox/core/tests/test_filtersets.py +++ b/netbox/core/tests/test_filtersets.py @@ -237,9 +237,9 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_changed_object_type(self): - params = {'changed_object_type': 'dcim.site'} + params = {'changed_object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]} + params = {'changed_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) diff --git a/netbox/dcim/base_filtersets.py b/netbox/dcim/base_filtersets.py index df2a6b650..68ad1b5d4 100644 --- a/netbox/dcim/base_filtersets.py +++ b/netbox/dcim/base_filtersets.py @@ -2,7 +2,7 @@ import django_filters from django.utils.translation import gettext as _ from netbox.filtersets import BaseFilterSet -from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter from .models import * __all__ = ( @@ -14,7 +14,7 @@ class ScopedFilterSet(BaseFilterSet): """ Provides additional filtering functionality for location, site, etc.. for Scoped models. """ - scope_type = ContentTypeFilter() + scope_type = MultiValueContentTypeFilter() region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='_region', diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index acc117398..5554732a3 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -21,8 +21,8 @@ from tenancy.models import * from users.filterset_mixins import OwnerFilterMixin from users.models import User from utilities.filters import ( - ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, - NumericArrayFilter, TreeNodeMultipleChoiceFilter, + MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, + MultiValueWWNFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter, ) from utilities.filtersets import register_filterset from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -977,7 +977,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo to_field_name='slug', label=_('Role (slug)'), ) - component_type = ContentTypeFilter() + component_type = MultiValueContentTypeFilter() component_id = MultiValueNumberFilter() class Meta: @@ -1822,7 +1822,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe @register_filterset class MACAddressFilterSet(PrimaryModelFilterSet): mac_address = MultiValueMACAddressFilter() - assigned_object_type = ContentTypeFilter() + assigned_object_type = MultiValueContentTypeFilter() device = MultiValueCharFilter( method='filter_device', field_name='name', @@ -2267,7 +2267,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet): to_field_name='slug', label=_('Role (slug)'), ) - component_type = ContentTypeFilter() + component_type = MultiValueContentTypeFilter() component_id = MultiValueNumberFilter() serial = MultiValueCharFilter( lookup_expr='iexact' @@ -2381,14 +2381,14 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet): @register_filterset class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): - termination_a_type = ContentTypeFilter( + termination_a_type = MultiValueContentTypeFilter( field_name='terminations__termination_type' ) termination_a_id = MultiValueNumberFilter( method='filter_by_cable_end_a', field_name='terminations__termination_id' ) - termination_b_type = ContentTypeFilter( + termination_b_type = MultiValueContentTypeFilter( field_name='terminations__termination_type' ) termination_b_id = MultiValueNumberFilter( @@ -2554,7 +2554,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): @register_filterset class CableTerminationFilterSet(ChangeLoggedModelFilterSet): - termination_type = ContentTypeFilter() + termination_type = MultiValueContentTypeFilter() class Meta: model = CableTermination diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 96a0f14fb..3c1d529be 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -6251,7 +6251,7 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_component_type(self): - params = {'component_type': 'dcim.interface'} + params = {'component_type': ['dcim.interface']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_status(self): @@ -6723,10 +6723,8 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_termination_types(self): - params = {'termination_a_type': 'dcim.consoleport'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - # params = {'termination_b_type': 'dcim.consoleserverport'} - # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'termination_a_type': ['dcim.consoleport', 'dcim.consoleserverport']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_termination_ids(self): interface_ids = CableTermination.objects.filter( @@ -6734,7 +6732,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): cable_end='A' ).values_list('termination_id', flat=True) params = { - 'termination_a_type': 'dcim.interface', + 'termination_a_type': ['dcim.interface'], 'termination_a_id': list(interface_ids), } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index cdc0c75c5..a5f23feec 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -10,7 +10,7 @@ from tenancy.models import Tenant, TenantGroup from users.filterset_mixins import OwnerFilterMixin from users.models import Group, User from utilities.filters import ( - ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter + MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter ) from utilities.filtersets import register_filterset from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -104,7 +104,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) event_type = MultiValueCharFilter( @@ -113,7 +113,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): action_type = django_filters.MultipleChoiceFilter( choices=EventRuleActionChoices ) - action_object_type = ContentTypeFilter() + action_object_type = MultiValueContentTypeFilter() action_object_id = MultiValueNumberFilter() class Meta: @@ -148,14 +148,14 @@ class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) related_object_type_id = django_filters.ModelMultipleChoiceFilter( queryset=ObjectType.objects.all(), field_name='related_object_type' ) - related_object_type = ContentTypeFilter() + related_object_type = MultiValueContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) @@ -224,7 +224,7 @@ class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) @@ -255,7 +255,7 @@ class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) data_source_id = django_filters.ModelMultipleChoiceFilter( @@ -294,7 +294,7 @@ class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) user_id = django_filters.ModelMultipleChoiceFilter( @@ -347,7 +347,7 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet): queryset=ObjectType.objects.all(), field_name='object_type' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_type' ) user_id = django_filters.ModelMultipleChoiceFilter( @@ -395,7 +395,7 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet): class BookmarkFilterSet(BaseFilterSet): created = django_filters.DateTimeFilter() object_type_id = MultiValueNumberFilter() - object_type = ContentTypeFilter() + object_type = MultiValueContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( queryset=User.objects.all(), label=_('User (ID)'), @@ -462,7 +462,7 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet): method='search', label=_('Search'), ) - object_type = ContentTypeFilter() + object_type = MultiValueContentTypeFilter() class Meta: model = ImageAttachment @@ -481,7 +481,7 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet): @register_filterset class JournalEntryFilterSet(NetBoxModelFilterSet): created = django_filters.DateTimeFromToRangeFilter() - assigned_object_type = ContentTypeFilter() + assigned_object_type = MultiValueContentTypeFilter() assigned_object_type_id = django_filters.ModelMultipleChoiceFilter( queryset=ContentType.objects.all() ) @@ -576,7 +576,7 @@ class TaggedItemFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='content_type' ) object_type_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py index 236c53eaa..d705ba6ae 100644 --- a/netbox/extras/tests/test_conditions.py +++ b/netbox/extras/tests/test_conditions.py @@ -304,7 +304,7 @@ class ConditionSetTest(TestCase): Test Event Rule with incorrect condition (key "foo" is wrong). Must return false. """ - ct = ContentType.objects.get(app_label='extras', model='webhook') + ct = ContentType.objects.get_by_natural_key('extras', 'webhook') site_ct = ContentType.objects.get_for_model(Site) webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST') form = EventRuleForm({ diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 3b9a65dec..04b348e5f 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -111,13 +111,13 @@ class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_related_object_type(self): - params = {'related_object_type': 'dcim.site'} + params = {'related_object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -348,7 +348,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.region'} + params = {'object_type': ['dcim.region']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'object_type_id': [ObjectType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -417,7 +417,7 @@ class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -508,7 +508,7 @@ class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -600,7 +600,7 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests): Bookmark.objects.bulk_create(bookmarks) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) params = {'object_type_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -663,7 +663,7 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -697,8 +697,8 @@ class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get(app_label='dcim', model='site') - rack_ct = ContentType.objects.get(app_label='dcim', model='rack') + site_ct = ContentType.objects.get_by_natural_key('dcim', 'site') + rack_ct = ContentType.objects.get_by_natural_key('dcim', 'rack') sites = ( Site(name='Site 1', slug='site-1'), @@ -757,12 +757,12 @@ class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type(self): - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_type_id_and_object_id(self): params = { - 'object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, + 'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site').pk, 'object_id': [Site.objects.first().pk], } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -845,14 +845,14 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_assigned_object_type(self): - params = {'assigned_object_type': 'dcim.site'} + params = {'assigned_object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - params = {'assigned_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]} + params = {'assigned_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_assigned_object(self): params = { - 'assigned_object_type': 'dcim.site', + 'assigned_object_type': ['dcim.site'], 'assigned_object_id': [Site.objects.first().pk], } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1426,15 +1426,15 @@ class TaggedItemFilterSetTestCase(TestCase): def test_object_type(self): object_type = ObjectType.objects.get_for_model(Site) - params = {'object_type': 'dcim.site'} + params = {'object_type': ['dcim.site']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) params = {'object_type_id': [object_type.pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - def test_object_id(self): + def test_object(self): site_ids = Site.objects.values_list('pk', flat=True) params = { - 'object_type': 'dcim.site', + 'object_type': ['dcim.site'], 'object_id': site_ids[:2], } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index 3156e7a1b..a7de50623 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -17,7 +17,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac class ImageAttachmentTests(TestCase): @classmethod def setUpTestData(cls): - cls.ct_rack = ContentType.objects.get(app_label='dcim', model='rack') + cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack') cls.image_content = b'' def _stub_image_attachment(self, object_id, image_filename, name=None): diff --git a/netbox/extras/tests/test_utils.py b/netbox/extras/tests/test_utils.py index ec0102887..540c64701 100644 --- a/netbox/extras/tests/test_utils.py +++ b/netbox/extras/tests/test_utils.py @@ -27,7 +27,7 @@ class ImageUploadTests(TestCase): def setUpTestData(cls): # We only need a ContentType with model="rack" for the prefix; # this doesn't require creating a Rack object. - cls.ct_rack = ContentType.objects.get(app_label='dcim', model='rack') + cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack') def _stub_instance(self, object_id=12, name=None): """ diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 5063b6a9c..d2168a6ce 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -16,7 +16,8 @@ from netbox.filtersets import ( ) from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import ( - ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter, + MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter, NumericArrayFilter, + TreeNodeMultipleChoiceFilter, ) from utilities.filtersets import register_filterset from virtualization.models import VirtualMachine, VMInterface @@ -607,7 +608,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi to_field_name='rd', label=_('VRF (RD)'), ) - assigned_object_type = ContentTypeFilter() + assigned_object_type = MultiValueContentTypeFilter() device = MultiValueCharFilter( method='filter_device', field_name='name', @@ -846,7 +847,7 @@ class FHRPGroupFilterSet(PrimaryModelFilterSet): @register_filterset class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet): - interface_type = ContentTypeFilter() + interface_type = MultiValueContentTypeFilter() group_id = django_filters.ModelMultipleChoiceFilter( queryset=FHRPGroup.objects.all(), label=_('Group (ID)'), @@ -901,7 +902,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet): @register_filterset class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): - scope_type = ContentTypeFilter() + scope_type = MultiValueContentTypeFilter() region = django_filters.NumberFilter( method='filter_scope' ) @@ -1173,7 +1174,7 @@ class ServiceTemplateFilterSet(PrimaryModelFilterSet): @register_filterset class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet): - parent_object_type = ContentTypeFilter() + parent_object_type = MultiValueContentTypeFilter() device = MultiValueCharFilter( method='filter_device', field_name='name', diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index d2cd13dce..c77b05dcf 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1572,12 +1572,12 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_interface_type(self): - params = {'interface_type': 'dcim.interface'} + params = {'interface_type': ['dcim.interface']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_interface(self): interfaces = Interface.objects.all()[:2] - params = {'interface_type': 'dcim.interface', 'interface_id': [interfaces[0].pk, interfaces[1].pk]} + params = {'interface_type': ['dcim.interface'], 'interface_id': [interfaces[0].pk, interfaces[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_priority(self): diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index aa463ef50..4db66691d 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from netbox.filtersets import ( NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet, ) -from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter from utilities.filtersets import register_filterset from .models import * @@ -110,7 +110,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - object_type = ContentTypeFilter() + object_type = MultiValueContentTypeFilter() contact_id = django_filters.ModelMultipleChoiceFilter( queryset=Contact.objects.all(), label=_('Contact (ID)'), diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index fcb354079..04d80104f 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -355,6 +355,8 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): ContactAssignment.objects.bulk_create(assignments) def test_object_type(self): + params = {'object_type': ['dcim.site']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) params = {'object_type_id': ObjectType.objects.get_by_natural_key('dcim', 'site')} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index c9587cca1..56727cabb 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -6,7 +6,7 @@ from core.models import ObjectType from extras.models import NotificationGroup from netbox.filtersets import BaseFilterSet from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User -from utilities.filters import ContentTypeFilter +from utilities.filters import MultiValueContentTypeFilter from utilities.filtersets import register_filterset __all__ = ( @@ -194,7 +194,7 @@ class ObjectPermissionFilterSet(BaseFilterSet): queryset=ObjectType.objects.all(), field_name='object_types' ) - object_type = ContentTypeFilter( + object_type = MultiValueContentTypeFilter( field_name='object_types' ) can_view = django_filters.BooleanFilter( diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 05454543e..b2a75d554 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,6 +1,7 @@ import django_filters from django import forms from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django_filters.constants import EMPTY_VALUES from drf_spectacular.types import OpenApiTypes @@ -10,6 +11,7 @@ __all__ = ( 'ContentTypeFilter', 'MultiValueArrayFilter', 'MultiValueCharFilter', + 'MultiValueContentTypeFilter', 'MultiValueDateFilter', 'MultiValueDateTimeFilter', 'MultiValueDecimalFilter', @@ -171,3 +173,27 @@ class ContentTypeFilter(django_filters.CharFilter): f'{self.field_name}__model': model } ) + + +class MultiValueContentTypeFilter(MultiValueCharFilter): + """ + A multi-value version of ContentTypeFilter. + """ + def filter(self, qs, value): + if value in EMPTY_VALUES: + return qs + + content_types = [] + for key in value: + try: + app_label, model = key.lower().split('.') + ct = ContentType.objects.get_by_natural_key(app_label, model) + content_types.append(ct) + except (ValueError, ContentType.DoesNotExist): + continue + + return qs.filter( + **{ + f'{self.field_name}__in': content_types, + } + ) diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py index b8fb62821..adb0bc6fa 100644 --- a/netbox/utilities/testing/filtersets.py +++ b/netbox/utilities/testing/filtersets.py @@ -10,7 +10,7 @@ from mptt.models import MPTTModel from taggit.managers import TaggableManager from extras.filters import TagFilter -from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter __all__ = ( 'BaseFilterSetTests', @@ -75,7 +75,7 @@ class BaseFilterSetTests: # Standardize on object_type for filter name even though it's technically a ContentType filter_name = 'object_type' return [ - (filter_name, ContentTypeFilter), + (filter_name, MultiValueContentTypeFilter), (f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter), ] diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py index e6fcd87fc..e7bd95be1 100644 --- a/netbox/vpn/filtersets.py +++ b/netbox/vpn/filtersets.py @@ -7,7 +7,7 @@ from dcim.models import Device, Interface from ipam.models import IPAddress, RouteTarget, VLAN from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet -from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter +from utilities.filters import MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter from utilities.filtersets import register_filterset from virtualization.models import VirtualMachine, VMInterface from .choices import * @@ -94,7 +94,7 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet): role = django_filters.MultipleChoiceFilter( choices=TunnelTerminationRoleChoices ) - termination_type = ContentTypeFilter() + termination_type = MultiValueContentTypeFilter() interface = django_filters.ModelMultipleChoiceFilter( field_name='interface__name', queryset=Interface.objects.all(), @@ -445,7 +445,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): queryset=ObjectType.objects.all(), field_name='assigned_object_type' ) - assigned_object_type = ContentTypeFilter() + assigned_object_type = MultiValueContentTypeFilter() class Meta: model = L2VPNTermination diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py index ee1f9ca72..e024cd2a0 100644 --- a/netbox/vpn/tests/test_filtersets.py +++ b/netbox/vpn/tests/test_filtersets.py @@ -268,9 +268,9 @@ class TunnelTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_termination_type(self): - params = {'termination_type': 'dcim.interface'} + params = {'termination_type': ['dcim.interface']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - params = {'termination_type': 'virtualization.vminterface'} + params = {'termination_type': ['virtualization.vminterface']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_interface(self): @@ -902,7 +902,7 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_termination_type(self): - params = {'assigned_object_type': 'ipam.vlan'} + 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/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index 9e8905d4a..443418bd8 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -305,7 +305,7 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_scope_type(self): - params = {'scope_type': 'dcim.location'} + params = {'scope_type': ['dcim.location']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)