Repository: netbox-community/netbox
Pull request: Fixes #11459 - Allow using null in conditions
### Fixes: #11459

Issue here was two-fold I believe. First issue is that primary_ip is always None when evaluating the conditions as computed properties are not serialized when queuing webhooks.

The other issue is that comparing "" with None is False. Allowing conditions to contain `null`/`None` allows to compare against None instead of empty string which was not functional.

Problem statement:
Webhook condition for having a primary IP doesn't appear to be working
### NetBox version

v3.4.2

### Python version

3.10

### Steps to Reproduce

1. Create a webhook, content type 'DCIM > Device', all events, with the condition:
`{
    "and": [
        {
            "attr": "primary_ip",
            "value": "",
            "negate": true
        }
    ]
}`
2. Create a site, all defaults.
3. Create a device role, all defaults.
4. Create a manufacturer, all defaults.
5. Create a device type, all defaults.
6. Create a device.
7. Go to the admin panel, check the jobs in the default queue.

### Expected Behavior

The webhook does not fire due to the condition of requiring a primary IP.

### Observed Behavior

The webhook fires (despite the condition).

Hints from issue discussion:
Seems to be similar/related to #10519 . We see this issue on our prod instance as well (v3.3.5, python 3.8).

Commit message: Fixes #11459 - Allow using null in conditions
- Update docs to reflect this
- Change docs example from primary_ip to primary_ip4 as computed properties are not serialized when queuing webhooks

Patch to review:
diff --git a/docs/reference/conditions.md b/docs/reference/conditions.md
index fb8b6613964..514006b0174 100644
--- a/docs/reference/conditions.md
+++ b/docs/reference/conditions.md
@@ -97,7 +97,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
 
 ### Examples
 
-`status` is "active" and `primary_ip` is defined _or_ the "exempt" tag is applied.
+`status` is "active" and `primary_ip4` is defined _or_ the "exempt" tag is applied.
 
 ```json
 {
@@ -109,8 +109,8 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
           "value": "active"
         },
         {
-          "attr": "primary_ip",
-          "value": "",
+          "attr": "primary_ip4",
+          "value": null,
           "negate": true
         }
       ]
diff --git a/netbox/extras/conditions.py b/netbox/extras/conditions.py
index 965488c3a04..067cf44ee36 100644
--- a/netbox/extras/conditions.py
+++ b/netbox/extras/conditions.py
@@ -44,7 +44,8 @@ class Condition:
         bool: (EQ, CONTAINS),
         int: (EQ, GT, GTE, LT, LTE, CONTAINS),
         float: (EQ, GT, GTE, LT, LTE, CONTAINS),
-        list: (EQ, IN, CONTAINS)
+        list: (EQ, IN, CONTAINS),
+        type(None): (EQ)
     }
 
     def __init__(self, attr, value, op=EQ, negate=False):
diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py
index 8e02eb75d47..e7275482afd 100644
--- a/netbox/extras/tests/test_conditions.py
+++ b/netbox/extras/tests/test_conditions.py
@@ -126,6 +126,16 @@ def test_invalid_logic(self):
         with self.assertRaises(ValueError):
             ConditionSet({'foo': []})
 
+    def test_null_value(self):
+        cs = ConditionSet({
+            'and': [
+                {'attr': 'a', 'value': None, 'op': 'eq', 'negate': True},
+            ]
+        })
+        self.assertFalse(cs.eval({'a': None}))
+        self.assertTrue(cs.eval({'a': "string"}))
+        self.assertTrue(cs.eval({'a': {"key": "value"}}))
+
     def test_and_single_depth(self):
         cs = ConditionSet({
             'and': [


Code review comments:
diff_hunk: @@ -44,7 +44,8 @@ class Condition:
         bool: (EQ, CONTAINS),
         int: (EQ, GT, GTE, LT, LTE, CONTAINS),
         float: (EQ, GT, GTE, LT, LTE, CONTAINS),
-        list: (EQ, IN, CONTAINS)
+        list: (EQ, IN, CONTAINS),
+        type(None): (EQ)
path: netbox/extras/conditions.py
text: @user1:
I think this needs the comma to be evaluated as a tuple. It's not causing a problem right now because `'eq' in 'eq'` evaluates to True.
```suggestion
        type(None): (EQ,)
```