From 29860aaf764531df7d7d3a1b6438e77a26c08a83 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Wed, 29 Jul 2026 15:07:42 +0200 Subject: [PATCH] fix(circuits): Add member_type_id filter to CircuitGroupAssignment Adds `member_type_id` filter field to support filtering by ContentType ID alongside existing `member_type` filter. Includes test coverage verifying correct filtering when member IDs overlap across different Content Types. Fixes #22800 --- netbox/circuits/filtersets.py | 7 ++++++- netbox/circuits/tests/test_filtersets.py | 25 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 3ed9145be..51cb1687b 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -405,6 +405,11 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet): label=_('Search'), ) member_type = MultiValueContentTypeFilter() + member_type_id = django_filters.ModelMultipleChoiceFilter( + field_name='member_type', + queryset=ContentType.objects.all(), + distinct=False, + ) circuit = MultiValueCharFilter( method='filter_circuit', field_name='cid', @@ -450,7 +455,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet): class Meta: model = CircuitGroupAssignment - fields = ('id', 'member_id', 'priority') + fields = ('id', 'member_type_id', 'member_id', 'priority') 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 92111d4c4..b0e46324a 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.test import TestCase from circuits.choices import * @@ -779,6 +780,30 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_member_type(self): + params = {'member_type': ['circuits.circuit']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'member_type_id': [ContentType.objects.get_for_model(Circuit).pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'member_type_id': [ContentType.objects.get_for_model(VirtualCircuit).pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_member(self): + """The member type and ID are matched together, so a matching ID of another type is excluded.""" + circuit = Circuit.objects.first() + circuit_type = ContentType.objects.get_for_model(Circuit) + virtual_circuit_type = ContentType.objects.get_for_model(VirtualCircuit) + expected = self.queryset.get(member_type=circuit_type, member_id=circuit.pk) + + # A virtual circuit assignment sharing the circuit's object ID must not match + group = CircuitGroup.objects.create(name='Circuit Group 4', slug='circuit-group-4') + CircuitGroupAssignment.objects.create( + group=group, member_type=virtual_circuit_type, member_id=circuit.pk + ) + + params = {'member_type_id': [circuit_type.pk], 'member_id': [circuit.pk]} + self.assertEqual(list(self.filterset(params, self.queryset).qs), [expected]) + def test_provider(self): providers = Provider.objects.all()[:2] params = {'provider_id': [providers[0].pk, providers[1].pk]}