Bladeren bron

17170 Add ability to add contacts to multiple contact groups (#18885)

* 17170 Allow multiple Group assignments for Contacts

* 17170 update docs

* 17170 update api, detail view, graphql

* 17170 fixes

* 17170 fixes

* 17170 fixes

* 17170 fixes

* 17170 fixes

* 17170 fixes

* 17170 fix bulk import

* 17170 test fixes

* 17170 test fixes

* 17170 test fixes

* 17178 review changes

* 17178 review changes

* 17178 review changes

* 17178 review changes

* 17178 review changes

* 17178 review changes

* 17170 update migration

* 17170 bulk edit form
Arthur Hanson 11 maanden geleden
bovenliggende
commit
af5ec19430

+ 4 - 2
docs/models/tenancy/contact.md

@@ -4,9 +4,11 @@ A contact represents an individual or group that has been associated with an obj
 
 ## Fields
 
-### Group
+### Groups
 
-The [contact group](./contactgroup.md) to which this contact is assigned (if any).
+The [contact groups](./contactgroup.md) to which this contact is assigned (if any).
+
+!!! info "This field was renamed from `group` to `groups` in NetBox v4.3, and now supports the assignment of a contact to more than one group."
 
 ### Name
 

+ 12 - 2
netbox/templates/tenancy/contact.html

@@ -18,8 +18,18 @@
         <h2 class="card-header">{% trans "Contact" %}</h2>
         <table class="table table-hover attr-table">
           <tr>
-            <th scope="row">{% trans "Group" %}</th>
-            <td>{{ object.group|linkify|placeholder }}</td>
+            <th scope="row">{% trans "Groups" %}</th>
+            <td>
+              {% if object.groups.all|length > 0 %}
+                <ol class="list-unstyled mb-0">
+                  {% for group in object.groups.all %}
+                    <li>{{ group|linkify|placeholder }}</li>
+                  {% endfor %}
+                </ol>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
           </tr>
           <tr>
             <th scope="row">{% trans "Name" %}</th>

+ 8 - 3
netbox/tenancy/api/serializers_/contacts.py

@@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
-from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from tenancy.choices import ContactPriorityChoices
 from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole
@@ -43,12 +43,17 @@ class ContactRoleSerializer(NetBoxModelSerializer):
 
 
 class ContactSerializer(NetBoxModelSerializer):
-    group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None)
+    groups = SerializedPKRelatedField(
+        queryset=ContactGroup.objects.all(),
+        serializer=ContactGroupSerializer,
+        required=False,
+        many=True
+    )
 
     class Meta:
         model = Contact
         fields = [
-            'id', 'url', 'display_url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link',
+            'id', 'url', 'display_url', 'display', 'groups', 'name', 'title', 'phone', 'email', 'address', 'link',
             'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 1 - 1
netbox/tenancy/api/views.py

@@ -44,7 +44,7 @@ class ContactGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
     queryset = ContactGroup.objects.add_related_count(
         ContactGroup.objects.all(),
         Contact,
-        'group',
+        'groups',
         'contact_count',
         cumulative=True
     )

+ 11 - 6
netbox/tenancy/filtersets.py

@@ -46,6 +46,11 @@ class ContactGroupFilterSet(OrganizationalModelFilterSet):
         to_field_name='slug',
         label=_('Contact group (slug)'),
     )
+    contact_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='contact',
+        queryset=Contact.objects.all(),
+        label=_('Contact (ID)'),
+    )
 
     class Meta:
         model = ContactGroup
@@ -62,15 +67,15 @@ class ContactRoleFilterSet(OrganizationalModelFilterSet):
 class ContactFilterSet(NetBoxModelFilterSet):
     group_id = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        field_name='group',
+        field_name='groups',
         lookup_expr='in',
         label=_('Contact group (ID)'),
     )
     group = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        field_name='group',
-        lookup_expr='in',
+        field_name='groups',
         to_field_name='slug',
+        lookup_expr='in',
         label=_('Contact group (slug)'),
     )
 
@@ -105,13 +110,13 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
     )
     group_id = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        field_name='contact__group',
+        field_name='contact__groups',
         lookup_expr='in',
         label=_('Contact group (ID)'),
     )
     group = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        field_name='contact__group',
+        field_name='contact__groups',
         lookup_expr='in',
         to_field_name='slug',
         label=_('Contact group (slug)'),
@@ -153,7 +158,7 @@ class ContactModelFilterSet(django_filters.FilterSet):
     )
     contact_group = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        field_name='contacts__contact__group',
+        field_name='contacts__contact__groups',
         lookup_expr='in',
         label=_('Contact group'),
     )

+ 14 - 5
netbox/tenancy/forms/bulk_edit.py

@@ -5,7 +5,7 @@ from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.choices import ContactPriorityChoices
 from tenancy.models import *
 from utilities.forms import add_blank_choice
-from utilities.forms.fields import CommentField, DynamicModelChoiceField
+from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.rendering import FieldSet
 
 __all__ = (
@@ -90,8 +90,13 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 
 class ContactBulkEditForm(NetBoxModelBulkEditForm):
-    group = DynamicModelChoiceField(
-        label=_('Group'),
+    add_groups = DynamicModelMultipleChoiceField(
+        label=_('Add groups'),
+        queryset=ContactGroup.objects.all(),
+        required=False
+    )
+    remove_groups = DynamicModelMultipleChoiceField(
+        label=_('Remove groups'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
@@ -127,9 +132,13 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
 
     model = Contact
     fieldsets = (
-        FieldSet('group', 'title', 'phone', 'email', 'address', 'link', 'description'),
+        FieldSet('title', 'phone', 'email', 'address', 'link', 'description'),
+        FieldSet('add_groups', 'remove_groups', name=_('Groups')),
+    )
+
+    nullable_fields = (
+        'add_groups', 'remove_groups', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments'
     )
-    nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments')
 
 
 class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):

+ 4 - 5
netbox/tenancy/forms/bulk_import.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
 
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import *
-from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, SlugField
+from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
 
 __all__ = (
     'ContactAssignmentImportForm',
@@ -77,17 +77,16 @@ class ContactRoleImportForm(NetBoxModelImportForm):
 
 
 class ContactImportForm(NetBoxModelImportForm):
-    group = CSVModelChoiceField(
-        label=_('Group'),
+    groups = CSVModelMultipleChoiceField(
         queryset=ContactGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text=_('Assigned group')
+        help_text=_('Group names separated by commas, encased with double quotes (e.g. "Group 1,Group 2")')
     )
 
     class Meta:
         model = Contact
-        fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments', 'tags')
+        fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'groups', 'description', 'comments', 'tags')
 
 
 class ContactAssignmentImportForm(NetBoxModelImportForm):

+ 1 - 1
netbox/tenancy/forms/filtersets.py

@@ -75,7 +75,7 @@ class ContactFilterForm(NetBoxModelFilterSetForm):
         queryset=ContactGroup.objects.all(),
         required=False,
         null_option='None',
-        label=_('Group')
+        label=_('Groups')
     )
     tag = TagFilterField(model)
 

+ 6 - 6
netbox/tenancy/forms/model_forms.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
 
 from netbox.forms import NetBoxModelForm
 from tenancy.models import *
-from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
+from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
 from utilities.forms.rendering import FieldSet, ObjectAttribute
 
 __all__ = (
@@ -93,8 +93,8 @@ class ContactRoleForm(NetBoxModelForm):
 
 
 class ContactForm(NetBoxModelForm):
-    group = DynamicModelChoiceField(
-        label=_('Group'),
+    groups = DynamicModelMultipleChoiceField(
+        label=_('Groups'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
@@ -102,7 +102,7 @@ class ContactForm(NetBoxModelForm):
 
     fieldsets = (
         FieldSet(
-            'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags',
+            'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags',
             name=_('Contact')
         ),
     )
@@ -110,7 +110,7 @@ class ContactForm(NetBoxModelForm):
     class Meta:
         model = Contact
         fields = (
-            'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags',
+            'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags',
         )
         widgets = {
             'address': forms.Textarea(attrs={'rows': 3}),
@@ -123,7 +123,7 @@ class ContactAssignmentForm(NetBoxModelForm):
         queryset=ContactGroup.objects.all(),
         required=False,
         initial_params={
-            'contacts': '$contact'
+            'contact': '$contact'
         }
     )
     contact = DynamicModelChoiceField(

+ 1 - 1
netbox/tenancy/graphql/types.py

@@ -97,7 +97,7 @@ class TenantGroupType(OrganizationalObjectType):
 
 @strawberry_django.type(models.Contact, fields='__all__', filters=ContactFilter)
 class ContactType(ContactAssignmentsMixin, NetBoxObjectType):
-    group: Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')] | None
+    groups: List[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]]
 
 
 @strawberry_django.type(models.ContactRole, fields='__all__', filters=ContactRoleFilter)

+ 68 - 0
netbox/tenancy/migrations/0018_contact_groups.py

@@ -0,0 +1,68 @@
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+def migrate_contact_groups(apps, schema_editor):
+    Contacts = apps.get_model('tenancy', 'Contact')
+
+    qs = Contacts.objects.filter(group__isnull=False)
+    for contact in qs:
+        contact.groups.add(contact.group)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0017_natural_ordering'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ContactGroupMembership',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+            ],
+            options={
+                'verbose_name': 'contact group membership',
+                'verbose_name_plural': 'contact group memberships',
+            },
+        ),
+        migrations.RemoveConstraint(
+            model_name='contact',
+            name='tenancy_contact_unique_group_name',
+        ),
+        migrations.AddField(
+            model_name='contactgroupmembership',
+            name='contact',
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, related_name='+', to='tenancy.contact'
+            ),
+        ),
+        migrations.AddField(
+            model_name='contactgroupmembership',
+            name='group',
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, related_name='+', to='tenancy.contactgroup'
+            ),
+        ),
+        migrations.AddField(
+            model_name='contact',
+            name='groups',
+            field=models.ManyToManyField(
+                blank=True,
+                related_name='contacts',
+                related_query_name='contact',
+                through='tenancy.ContactGroupMembership',
+                to='tenancy.contactgroup',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='contactgroupmembership',
+            constraint=models.UniqueConstraint(fields=('group', 'contact'), name='unique_group_name'),
+        ),
+        migrations.RunPython(code=migrate_contact_groups, reverse_code=migrations.RunPython.noop),
+        migrations.RemoveField(
+            model_name='contact',
+            name='group',
+        ),
+    ]

+ 18 - 11
netbox/tenancy/models/contacts.py

@@ -13,6 +13,7 @@ __all__ = (
     'ContactAssignment',
     'Contact',
     'ContactGroup',
+    'ContactGroupMembership',
     'ContactRole',
 )
 
@@ -47,12 +48,12 @@ class Contact(PrimaryModel):
     """
     Contact information for a particular object(s) in NetBox.
     """
-    group = models.ForeignKey(
+    groups = models.ManyToManyField(
         to='tenancy.ContactGroup',
-        on_delete=models.SET_NULL,
         related_name='contacts',
-        blank=True,
-        null=True
+        through='tenancy.ContactGroupMembership',
+        related_query_name='contact',
+        blank=True
     )
     name = models.CharField(
         verbose_name=_('name'),
@@ -84,17 +85,11 @@ class Contact(PrimaryModel):
     )
 
     clone_fields = (
-        'group', 'name', 'title', 'phone', 'email', 'address', 'link',
+        'groups', 'name', 'title', 'phone', 'email', 'address', 'link',
     )
 
     class Meta:
         ordering = ['name']
-        constraints = (
-            models.UniqueConstraint(
-                fields=('group', 'name'),
-                name='%(app_label)s_%(class)s_unique_group_name'
-            ),
-        )
         verbose_name = _('contact')
         verbose_name_plural = _('contacts')
 
@@ -102,6 +97,18 @@ class Contact(PrimaryModel):
         return self.name
 
 
+class ContactGroupMembership(models.Model):
+    group = models.ForeignKey(ContactGroup, related_name="+", on_delete=models.CASCADE)
+    contact = models.ForeignKey(Contact, related_name="+", on_delete=models.CASCADE)
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(fields=['group', 'contact'], name='unique_group_name')
+        ]
+        verbose_name = _('contact group membership')
+        verbose_name_plural = _('contact group memberships')
+
+
 class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
     object_type = models.ForeignKey(
         to='contenttypes.ContentType',

+ 5 - 5
netbox/tenancy/tables/contacts.py

@@ -56,9 +56,9 @@ class ContactTable(NetBoxTable):
         verbose_name=_('Name'),
         linkify=True
     )
-    group = tables.Column(
-        verbose_name=_('Group'),
-        linkify=True
+    groups = columns.ManyToManyColumn(
+        verbose_name=_('Groups'),
+        linkify_item=('tenancy:contactgroup', {'pk': tables.A('pk')})
     )
     phone = tables.Column(
         verbose_name=_('Phone'),
@@ -79,10 +79,10 @@ class ContactTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Contact
         fields = (
-            'pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments',
+            'pk', 'name', 'groups', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments',
             'assignment_count', 'tags', 'created', 'last_updated',
         )
-        default_columns = ('pk', 'name', 'group', 'assignment_count', 'title', 'phone', 'email')
+        default_columns = ('pk', 'name', 'groups', 'assignment_count', 'title', 'phone', 'email')
 
 
 class ContactAssignmentTable(NetBoxTable):

+ 8 - 6
netbox/tenancy/tests/test_api.py

@@ -170,7 +170,7 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
     model = Contact
     brief_fields = ['description', 'display', 'id', 'name', 'url']
     bulk_update_data = {
-        'group': None,
+        'groups': [],
         'comments': 'New comments',
     }
 
@@ -183,20 +183,22 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
         )
 
         contacts = (
-            Contact(name='Contact 1', group=contact_groups[0]),
-            Contact(name='Contact 2', group=contact_groups[0]),
-            Contact(name='Contact 3', group=contact_groups[0]),
+            Contact(name='Contact 1'),
+            Contact(name='Contact 2'),
+            Contact(name='Contact 3'),
         )
         Contact.objects.bulk_create(contacts)
+        contacts[0].groups.add(contact_groups[0])
+        contacts[1].groups.add(contact_groups[0])
+        contacts[2].groups.add(contact_groups[0])
 
         cls.create_data = [
             {
                 'name': 'Contact 4',
-                'group': contact_groups[1].pk,
+                'groups': [contact_groups[1].pk],
             },
             {
                 'name': 'Contact 5',
-                'group': contact_groups[1].pk,
             },
             {
                 'name': 'Contact 6',

+ 13 - 6
netbox/tenancy/tests/test_filtersets.py

@@ -241,6 +241,7 @@ class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
 class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = Contact.objects.all()
     filterset = ContactFilterSet
+    ignore_fields = ('groups',)
 
     @classmethod
     def setUpTestData(cls):
@@ -254,11 +255,14 @@ class ContactTestCase(TestCase, ChangeLoggedFilterSetTests):
             contactgroup.save()
 
         contacts = (
-            Contact(name='Contact 1', group=contact_groups[0], description='foobar1'),
-            Contact(name='Contact 2', group=contact_groups[1], description='foobar2'),
-            Contact(name='Contact 3', group=contact_groups[2], description='foobar3'),
+            Contact(name='Contact 1', description='foobar1'),
+            Contact(name='Contact 2', description='foobar2'),
+            Contact(name='Contact 3', description='foobar3'),
         )
         Contact.objects.bulk_create(contacts)
+        contacts[0].groups.add(contact_groups[0])
+        contacts[1].groups.add(contact_groups[1])
+        contacts[2].groups.add(contact_groups[2])
 
     def test_q(self):
         params = {'q': 'foobar1'}
@@ -311,11 +315,14 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
         ContactRole.objects.bulk_create(contact_roles)
 
         contacts = (
-            Contact(name='Contact 1', group=contact_groups[0]),
-            Contact(name='Contact 2', group=contact_groups[1]),
-            Contact(name='Contact 3', group=contact_groups[2]),
+            Contact(name='Contact 1'),
+            Contact(name='Contact 2'),
+            Contact(name='Contact 3'),
         )
         Contact.objects.bulk_create(contacts)
+        contacts[0].groups.add(contact_groups[0])
+        contacts[1].groups.add(contact_groups[1])
+        contacts[2].groups.add(contact_groups[2])
 
         assignments = (
             ContactAssignment(object=sites[0], contact=contacts[0], role=contact_roles[0]),

+ 16 - 13
netbox/tenancy/tests/test_views.py

@@ -196,37 +196,40 @@ class ContactTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             contactgroup.save()
 
         contacts = (
-            Contact(name='Contact 1', group=contact_groups[0]),
-            Contact(name='Contact 2', group=contact_groups[0]),
-            Contact(name='Contact 3', group=contact_groups[0]),
+            Contact(name='Contact 1'),
+            Contact(name='Contact 2'),
+            Contact(name='Contact 3'),
         )
         Contact.objects.bulk_create(contacts)
+        contacts[0].groups.add(contact_groups[0])
+        contacts[1].groups.add(contact_groups[1])
 
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Contact X',
-            'group': contact_groups[1].pk,
+            'groups': [contact_groups[1].pk],
             'comments': 'Some comments',
             'tags': [t.pk for t in tags],
         }
 
         cls.csv_data = (
-            "group,name",
-            "Contact Group 1,Contact 4",
-            "Contact Group 1,Contact 5",
-            "Contact Group 1,Contact 6",
+            "name",
+            "groups",
+            "Contact 4",
+            "Contact 5",
+            "Contact 6",
         )
 
         cls.csv_update_data = (
-            "id,name,comments",
-            f"{contacts[0].pk},Contact Group 7,New comments 7",
-            f"{contacts[1].pk},Contact Group 8,New comments 8",
-            f"{contacts[2].pk},Contact Group 9,New comments 9",
+            "id,name,groups,comments",
+            f'{contacts[0].pk},Contact 7,"Contact Group 1,Contact Group 2",New comments 7',
+            f'{contacts[1].pk},Contact 8,"Contact Group 1",New comments 8',
+            f'{contacts[2].pk},Contact 9,"Contact Group 1",New comments 9',
         )
 
         cls.bulk_edit_data = {
-            'group': contact_groups[1].pk,
+            'description':  "New description",
         }
 
 

+ 12 - 3
netbox/tenancy/views.py

@@ -170,7 +170,7 @@ class ContactGroupListView(generic.ObjectListView):
     queryset = ContactGroup.objects.add_related_count(
         ContactGroup.objects.all(),
         Contact,
-        'group',
+        'groups',
         'contact_count',
         cumulative=True
     )
@@ -214,7 +214,7 @@ class ContactGroupBulkEditView(generic.BulkEditView):
     queryset = ContactGroup.objects.add_related_count(
         ContactGroup.objects.all(),
         Contact,
-        'group',
+        'groups',
         'contact_count',
         cumulative=True
     )
@@ -228,7 +228,7 @@ class ContactGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = ContactGroup.objects.add_related_count(
         ContactGroup.objects.all(),
         Contact,
-        'group',
+        'groups',
         'contact_count',
         cumulative=True
     )
@@ -337,6 +337,15 @@ class ContactBulkEditView(generic.BulkEditView):
     table = tables.ContactTable
     form = forms.ContactBulkEditForm
 
+    def post_save_operations(self, form, obj):
+        super().post_save_operations(form, obj)
+
+        # Add/remove groups
+        if form.cleaned_data.get('add_groups', None):
+            obj.groups.add(*form.cleaned_data['add_groups'])
+        if form.cleaned_data.get('remove_groups', None):
+            obj.groups.remove(*form.cleaned_data['remove_groups'])
+
 
 @register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
 class ContactBulkDeleteView(generic.BulkDeleteView):

+ 2 - 2
netbox/utilities/testing/filtersets.py

@@ -144,8 +144,8 @@ class BaseFilterSetTests:
                 # Check that the filter class is correct
                 filter = filters[filter_name]
                 if filter_class is not None:
-                    self.assertIs(
-                        type(filter),
+                    self.assertIsInstance(
+                        filter,
                         filter_class,
                         f"Invalid filter class {type(filter)} for {filter_name} (should be {filter_class})!"
                     )