Repository: netbox-community/netbox

Issue:
Enable filtering for null numeric values using the `empty` lookup
### NetBox version

v3.5.8

### Feature type

New functionality

### Proposed functionality

NetBox currently supports checking for empty string fields using the `empty` lookup (e.g. `?description__empty=true`). This FR proposes extending that functionality to numeric fields as well, to match null/non-null values.

### Use case

This enables a user to query numeric fields generally for null or non-null values. For example, `?position__empty=true` would return devices which do not have a rack position defined.

### Database changes

_No response_

### External dependencies

_No response_


Discussion:

Resolving patch:
diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py
--- a/netbox/utilities/constants.py
+++ b/netbox/utilities/constants.py
@@ -20,7 +20,8 @@
     lte='lte',
     lt='lt',
     gte='gte',
-    gt='gt'
+    gt='gt',
+    empty='isnull',
 )
 
 FILTER_NEGATION_LOOKUP_MAP = dict(


Test patch:
diff --git a/netbox/utilities/tests/test_filters.py b/netbox/utilities/tests/test_filters.py
--- a/netbox/utilities/tests/test_filters.py
+++ b/netbox/utilities/tests/test_filters.py
@@ -86,6 +86,10 @@ class DummyModel(models.Model):
     charfield = models.CharField(
         max_length=10
     )
+    numberfield = models.IntegerField(
+        blank=True,
+        null=True
+    )
     choicefield = models.IntegerField(
         choices=(('A', 1), ('B', 2), ('C', 3))
     )
@@ -108,6 +112,7 @@ class BaseFilterSetTest(TestCase):
     """
     class DummyFilterSet(BaseFilterSet):
         charfield = django_filters.CharFilter()
+        numberfield = django_filters.NumberFilter()
         macaddressfield = MACAddressFilter()
         modelchoicefield = django_filters.ModelChoiceFilter(
             field_name='integerfield',  # We're pretending this is a ForeignKey field
@@ -132,6 +137,7 @@ class Meta:
             model = DummyModel
             fields = (
                 'charfield',
+                'numberfield',
                 'choicefield',
                 'datefield',
                 'datetimefield',
@@ -171,6 +177,25 @@ def test_char_filter(self):
         self.assertEqual(self.filters['charfield__iew'].exclude, False)
         self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['charfield__niew'].exclude, True)
+        self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
+        self.assertEqual(self.filters['charfield__empty'].exclude, False)
+
+    def test_number_filter(self):
+        self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
+        self.assertEqual(self.filters['numberfield'].lookup_expr, 'exact')
+        self.assertEqual(self.filters['numberfield'].exclude, False)
+        self.assertEqual(self.filters['numberfield__n'].lookup_expr, 'exact')
+        self.assertEqual(self.filters['numberfield__n'].exclude, True)
+        self.assertEqual(self.filters['numberfield__lt'].lookup_expr, 'lt')
+        self.assertEqual(self.filters['numberfield__lt'].exclude, False)
+        self.assertEqual(self.filters['numberfield__lte'].lookup_expr, 'lte')
+        self.assertEqual(self.filters['numberfield__lte'].exclude, False)
+        self.assertEqual(self.filters['numberfield__gt'].lookup_expr, 'gt')
+        self.assertEqual(self.filters['numberfield__gt'].exclude, False)
+        self.assertEqual(self.filters['numberfield__gte'].lookup_expr, 'gte')
+        self.assertEqual(self.filters['numberfield__gte'].exclude, False)
+        self.assertEqual(self.filters['numberfield__empty'].lookup_expr, 'isnull')
+        self.assertEqual(self.filters['numberfield__empty'].exclude, False)
 
     def test_mac_address_filter(self):
         self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)