Repository: netbox-community/netbox

Issue:
Simple condition (without and/or) does not work in event rule
### Deployment Type

Self-hosted

### NetBox Version

v3.7.0

### Python Version

3.11

### Steps to Reproduce

1. Create webhook: Name = Test, URL = http://127.0.0.1:9000 (doesn't matter in this case, it won't be triggered but is required to configure event rule)
2. Go to **Event rules - Add**:
- Name = Test
- Content types = Circuit
- select Updates
- set Conditions:
```
{
    "attr": "status.value",
    "value": "active"
}
```

- Action type = Webhook
- Webhook = Test
- **Create**


### Expected Behavior

Event rule is created

### Observed Behavior

Error is shown about the condition:

**Ruleset must have exactly one logical operator (found 2)** 

The examples in https://docs.netbox.dev/en/stable/reference/conditions/ look the same: simple JSON object with attributes `attr` and `value`.


Discussion:
Please, someone could assign it to me?

Resolving patch:
diff --git a/netbox/extras/conditions.py b/netbox/extras/conditions.py
--- a/netbox/extras/conditions.py
+++ b/netbox/extras/conditions.py
@@ -135,23 +135,23 @@ class ConditionSet:
     def __init__(self, ruleset):
         if type(ruleset) is not dict:
             raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset)))
-        if len(ruleset) != 1:
-            raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format(
-                ruleset=len(ruleset)))
-
-        # Determine the logic type
-        logic = list(ruleset.keys())[0]
-        if type(logic) is not str or logic.lower() not in (AND, OR):
-            raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format(
-                logic=logic, op_and=AND, op_or=OR
-            ))
-        self.logic = logic.lower()
 
-        # Compile the set of Conditions
-        self.conditions = [
-            ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
-            for rule in ruleset[self.logic]
-        ]
+        if len(ruleset) == 1:
+            self.logic = (list(ruleset.keys())[0]).lower()
+            if self.logic not in (AND, OR):
+                raise ValueError(_("Invalid logic type: must be 'AND' or 'OR'. Please check documentation."))
+
+            # Compile the set of Conditions
+            self.conditions = [
+                ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
+                for rule in ruleset[self.logic]
+            ]
+        else:
+            try:
+                self.logic = None
+                self.conditions = [Condition(**ruleset)]
+            except TypeError:
+                raise ValueError(_("Incorrect key(s) informed. Please check documentation."))
 
     def eval(self, data):
         """


Test patch:
diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py
--- a/netbox/extras/tests/test_conditions.py
+++ b/netbox/extras/tests/test_conditions.py
@@ -1,6 +1,12 @@
+from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 
+from dcim.choices import SiteStatusChoices
+from dcim.models import Site
 from extras.conditions import Condition, ConditionSet
+from extras.events import serialize_for_event
+from extras.forms import EventRuleForm
+from extras.models import EventRule, Webhook
 
 
 class ConditionTestCase(TestCase):
@@ -217,3 +223,93 @@ def test_mixed_or(self):
         self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 9}))
         self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 9}))
         self.assertFalse(cs.eval({'a': 9, 'b': 9, 'c': 3}))
+
+    def test_event_rule_conditions_without_logic_operator(self):
+        """
+        Test evaluation of EventRule conditions without logic operator.
+        """
+        event_rule = EventRule(
+            name='Event Rule 1',
+            type_create=True,
+            type_update=True,
+            conditions={
+                'attr': 'status.value',
+                'value': 'active',
+            }
+        )
+
+        # Create a Site to evaluate - Status = active
+        site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
+        data = serialize_for_event(site)
+
+        # Evaluate the conditions (status='active')
+        self.assertTrue(event_rule.eval_conditions(data))
+
+    def test_event_rule_conditions_with_logical_operation(self):
+        """
+        Test evaluation of EventRule conditions without logic operator, but with logical operation (in).
+        """
+        event_rule = EventRule(
+            name='Event Rule 1',
+            type_create=True,
+            type_update=True,
+            conditions={
+                "attr": "status.value",
+                "value": ["planned", "staging"],
+                "op": "in",
+            }
+        )
+
+        # Create a Site to evaluate - Status = active
+        site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
+        data = serialize_for_event(site)
+
+        # Evaluate the conditions (status in ['planned, 'staging'])
+        self.assertFalse(event_rule.eval_conditions(data))
+
+    def test_event_rule_conditions_with_logical_operation_and_negate(self):
+        """
+        Test evaluation of EventRule with logical operation (in) and negate.
+        """
+        event_rule = EventRule(
+            name='Event Rule 1',
+            type_create=True,
+            type_update=True,
+            conditions={
+                "attr": "status.value",
+                "value": ["planned", "staging"],
+                "op": "in",
+                "negate": True,
+            }
+        )
+
+        # Create a Site to evaluate - Status = active
+        site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
+        data = serialize_for_event(site)
+
+        # Evaluate the conditions (status NOT in ['planned, 'staging'])
+        self.assertTrue(event_rule.eval_conditions(data))
+
+    def test_event_rule_conditions_with_incorrect_key_must_return_false(self):
+        """
+        Test Event Rule with incorrect condition (key "foo" is wrong). Must return false.
+        """
+
+        ct = ContentType.objects.get(app_label='extras', model='webhook')
+        site_ct = ContentType.objects.get_for_model(Site)
+        webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST')
+        form = EventRuleForm({
+            "name": "Event Rule 1",
+            "type_create": True,
+            "type_update": True,
+            "action_object_type": ct.pk,
+            "action_type": "webhook",
+            "action_choice": webhook.pk,
+            "content_types": [site_ct.pk],
+            "conditions": {
+                "foo": "status.value",
+                "value": "active"
+            }
+        })
+
+        self.assertFalse(form.is_valid())