Просмотр исходного кода

Fixes #8872: Enable filtering by custom object fields

jeremystretch 3 лет назад
Родитель
Сommit
df2f6d4a7d

+ 1 - 0
docs/release-notes/version-3.2.md

@@ -150,6 +150,7 @@ Where it is desired to limit the range of available VLANs within a group, users
 * [#8838](https://github.com/netbox-community/netbox/issues/8838) - Fix FieldError exception during global search
 * [#8838](https://github.com/netbox-community/netbox/issues/8838) - Fix FieldError exception during global search
 * [#8845](https://github.com/netbox-community/netbox/issues/8845) - Correct default ASN formatting in table
 * [#8845](https://github.com/netbox-community/netbox/issues/8845) - Correct default ASN formatting in table
 * [#8869](https://github.com/netbox-community/netbox/issues/8869) - Fix NoReverseMatch exception when displaying tag w/assignments
 * [#8869](https://github.com/netbox-community/netbox/issues/8869) - Fix NoReverseMatch exception when displaying tag w/assignments
+* [#8872](https://github.com/netbox-community/netbox/issues/8872) - Enable filtering by custom object fields
 
 
 ### Other Changes
 ### Other Changes
 
 

+ 9 - 0
netbox/extras/models/customfields.py

@@ -430,6 +430,15 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
             filter_class = filters.MultiValueCharFilter
             filter_class = filters.MultiValueCharFilter
             kwargs['lookup_expr'] = 'has_key'
             kwargs['lookup_expr'] = 'has_key'
 
 
+        # Object
+        elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
+            filter_class = filters.MultiValueNumberFilter
+
+        # Multi-object
+        elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
+            filter_class = filters.MultiValueNumberFilter
+            kwargs['lookup_expr'] = 'contains'
+
         # Unsupported custom field type
         # Unsupported custom field type
         else:
         else:
             return None
             return None

+ 41 - 1
netbox/extras/tests/test_customfields.py

@@ -5,7 +5,7 @@ from rest_framework import status
 
 
 from dcim.filtersets import SiteFilterSet
 from dcim.filtersets import SiteFilterSet
 from dcim.forms import SiteCSVForm
 from dcim.forms import SiteCSVForm
-from dcim.models import Site, Rack
+from dcim.models import Manufacturer, Rack, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import CustomField
 from extras.models import CustomField
 from ipam.models import VLAN
 from ipam.models import VLAN
@@ -1022,6 +1022,13 @@ class CustomFieldModelFilterTest(TestCase):
     def setUpTestData(cls):
     def setUpTestData(cls):
         obj_type = ContentType.objects.get_for_model(Site)
         obj_type = ContentType.objects.get_for_model(Site)
 
 
+        manufacturers = Manufacturer.objects.bulk_create((
+            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+            Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+            Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
+            Manufacturer(name='Manufacturer 4', slug='manufacturer-4'),
+        ))
+
         # Integer filtering
         # Integer filtering
         cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
         cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
         cf.save()
         cf.save()
@@ -1071,6 +1078,24 @@ class CustomFieldModelFilterTest(TestCase):
         cf.save()
         cf.save()
         cf.content_types.set([obj_type])
         cf.content_types.set([obj_type])
 
 
+        # Object filtering
+        cf = CustomField(
+            name='cf10',
+            type=CustomFieldTypeChoices.TYPE_OBJECT,
+            object_type=ContentType.objects.get_for_model(Manufacturer)
+        )
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Multi-object filtering
+        cf = CustomField(
+            name='cf11',
+            type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
+            object_type=ContentType.objects.get_for_model(Manufacturer)
+        )
+        cf.save()
+        cf.content_types.set([obj_type])
+
         Site.objects.bulk_create([
         Site.objects.bulk_create([
             Site(name='Site 1', slug='site-1', custom_field_data={
             Site(name='Site 1', slug='site-1', custom_field_data={
                 'cf1': 100,
                 'cf1': 100,
@@ -1082,6 +1107,8 @@ class CustomFieldModelFilterTest(TestCase):
                 'cf7': 'http://a.example.com',
                 'cf7': 'http://a.example.com',
                 'cf8': 'Foo',
                 'cf8': 'Foo',
                 'cf9': ['A', 'X'],
                 'cf9': ['A', 'X'],
+                'cf10': manufacturers[0].pk,
+                'cf11': [manufacturers[0].pk, manufacturers[3].pk],
             }),
             }),
             Site(name='Site 2', slug='site-2', custom_field_data={
             Site(name='Site 2', slug='site-2', custom_field_data={
                 'cf1': 200,
                 'cf1': 200,
@@ -1093,6 +1120,8 @@ class CustomFieldModelFilterTest(TestCase):
                 'cf7': 'http://b.example.com',
                 'cf7': 'http://b.example.com',
                 'cf8': 'Bar',
                 'cf8': 'Bar',
                 'cf9': ['B', 'X'],
                 'cf9': ['B', 'X'],
+                'cf10': manufacturers[1].pk,
+                'cf11': [manufacturers[1].pk, manufacturers[3].pk],
             }),
             }),
             Site(name='Site 3', slug='site-3', custom_field_data={
             Site(name='Site 3', slug='site-3', custom_field_data={
                 'cf1': 300,
                 'cf1': 300,
@@ -1104,6 +1133,8 @@ class CustomFieldModelFilterTest(TestCase):
                 'cf7': 'http://c.example.com',
                 'cf7': 'http://c.example.com',
                 'cf8': 'Baz',
                 'cf8': 'Baz',
                 'cf9': ['C', 'X'],
                 'cf9': ['C', 'X'],
+                'cf10': manufacturers[2].pk,
+                'cf11': [manufacturers[2].pk, manufacturers[3].pk],
             }),
             }),
         ])
         ])
 
 
@@ -1163,3 +1194,12 @@ class CustomFieldModelFilterTest(TestCase):
     def test_filter_multiselect(self):
     def test_filter_multiselect(self):
         self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)
         self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)
+
+    def test_filter_object(self):
+        manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
+        self.assertEqual(self.filterset({'cf_cf10': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
+
+    def test_filter_multiobject(self):
+        manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
+        self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
+        self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)

+ 3 - 5
netbox/templates/inc/panels/custom_fields.html

@@ -29,11 +29,9 @@
                                 {% elif field.type == 'object' and value %}
                                 {% elif field.type == 'object' and value %}
                                     {{ value|linkify }}
                                     {{ value|linkify }}
                                 {% elif field.type == 'multiobject' and value %}
                                 {% elif field.type == 'multiobject' and value %}
-                                    <ul>
-                                      {% for obj in value %}
-                                        <li>{{ obj|linkify }}</li>
-                                      {% endfor %}
-                                    </ul>
+                                    {% for obj in value %}
+                                      {{ obj|linkify }}{% if not forloop.last %}<br />{% endif %}
+                                    {% endfor %}
                                 {% elif value %}
                                 {% elif value %}
                                     {{ value }}
                                     {{ value }}
                                 {% elif field.required %}
                                 {% elif field.required %}