Parcourir la source

Closes #13585: Introduce 'empty' lookup for numeric value filters

Jeremy Stretch il y a 2 ans
Parent
commit
480f83c42d

+ 20 - 19
docs/reference/filtering.md

@@ -61,13 +61,14 @@ These lookup expressions can be applied by adding a suffix to the desired field'
 
 
 Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
 Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
 
 
-| Filter | Description |
-|--------|-------------|
-| `n` | Not equal to |
-| `lt` | Less than |
-| `lte` | Less than or equal to |
-| `gt` | Greater than |
-| `gte` | Greater than or equal to |
+| Filter  | Description              |
+|---------|--------------------------|
+| `n`     | Not equal to             |
+| `lt`    | Less than                |
+| `lte`   | Less than or equal to    |
+| `gt`    | Greater than             |
+| `gte`   | Greater than or equal to |
+| `empty` | Is empty/null (boolean)  |
 
 
 Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
 Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
 
 
@@ -79,18 +80,18 @@ GET /api/ipam/vlans/?vid__gt=900
 
 
 String based (char) fields (Name, Address, etc) support these lookup expressions:
 String based (char) fields (Name, Address, etc) support these lookup expressions:
 
 
-| Filter | Description |
-|--------|-------------|
-| `n` | Not equal to |
-| `ic` | Contains (case-insensitive) |
-| `nic` | Does not contain (case-insensitive) |
-| `isw` | Starts with (case-insensitive) |
-| `nisw` | Does not start with (case-insensitive) |
-| `iew` | Ends with (case-insensitive) |
-| `niew` | Does not end with (case-insensitive) |
-| `ie` | Exact match (case-insensitive) |
-| `nie` | Inverse exact match (case-insensitive) |
-| `empty` | Is empty (boolean) |
+| Filter  | Description                            |
+|---------|----------------------------------------|
+| `n`     | Not equal to                           |
+| `ic`    | Contains (case-insensitive)            |
+| `nic`   | Does not contain (case-insensitive)    |
+| `isw`   | Starts with (case-insensitive)         |
+| `nisw`  | Does not start with (case-insensitive) |
+| `iew`   | Ends with (case-insensitive)           |
+| `niew`  | Does not end with (case-insensitive)   |
+| `ie`    | Exact match (case-insensitive)         |
+| `nie`   | Inverse exact match (case-insensitive) |
+| `empty` | Is empty/null (boolean)                |
 
 
 Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
 Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
 
 

+ 2 - 1
netbox/utilities/constants.py

@@ -20,7 +20,8 @@ FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
     lte='lte',
     lte='lte',
     lt='lt',
     lt='lt',
     gte='gte',
     gte='gte',
-    gt='gt'
+    gt='gt',
+    empty='isnull',
 )
 )
 
 
 FILTER_NEGATION_LOOKUP_MAP = dict(
 FILTER_NEGATION_LOOKUP_MAP = dict(

+ 25 - 0
netbox/utilities/tests/test_filters.py

@@ -86,6 +86,10 @@ class DummyModel(models.Model):
     charfield = models.CharField(
     charfield = models.CharField(
         max_length=10
         max_length=10
     )
     )
+    numberfield = models.IntegerField(
+        blank=True,
+        null=True
+    )
     choicefield = models.IntegerField(
     choicefield = models.IntegerField(
         choices=(('A', 1), ('B', 2), ('C', 3))
         choices=(('A', 1), ('B', 2), ('C', 3))
     )
     )
@@ -108,6 +112,7 @@ class BaseFilterSetTest(TestCase):
     """
     """
     class DummyFilterSet(BaseFilterSet):
     class DummyFilterSet(BaseFilterSet):
         charfield = django_filters.CharFilter()
         charfield = django_filters.CharFilter()
+        numberfield = django_filters.NumberFilter()
         macaddressfield = MACAddressFilter()
         macaddressfield = MACAddressFilter()
         modelchoicefield = django_filters.ModelChoiceFilter(
         modelchoicefield = django_filters.ModelChoiceFilter(
             field_name='integerfield',  # We're pretending this is a ForeignKey field
             field_name='integerfield',  # We're pretending this is a ForeignKey field
@@ -132,6 +137,7 @@ class BaseFilterSetTest(TestCase):
             model = DummyModel
             model = DummyModel
             fields = (
             fields = (
                 'charfield',
                 'charfield',
+                'numberfield',
                 'choicefield',
                 'choicefield',
                 'datefield',
                 'datefield',
                 'datetimefield',
                 'datetimefield',
@@ -171,6 +177,25 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['charfield__iew'].exclude, False)
         self.assertEqual(self.filters['charfield__iew'].exclude, False)
         self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['charfield__niew'].exclude, True)
         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):
     def test_mac_address_filter(self):
         self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)
         self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)