diff --git a/changes/205.fixed b/changes/205.fixed new file mode 100644 index 00000000..7d94982d --- /dev/null +++ b/changes/205.fixed @@ -0,0 +1 @@ +Added support for validating custom fields by referencing them as cf_ in ComplianceError exceptions \ No newline at end of file diff --git a/docs/user/app_data_compliance.md b/docs/user/app_data_compliance.md index 5d7aeaae..06e51701 100644 --- a/docs/user/app_data_compliance.md +++ b/docs/user/app_data_compliance.md @@ -18,6 +18,9 @@ Any `DataComplianceRule` class can have a `name` defined to provide a friendly n > > For example, if a user fixes an object attribute that was incompliant with a built-in rule and then navigates to its `Data Compliance` tab, the object will still show as invalid for that built-in rule. This will remain the case until the job is ran again with the `Run built-in validation rules?` option checked. +!!! note + When raising a ComplianceError, the attribute must exist on the object. To raise errors for custom fields, use cf_custom_field_name as the attribute name. + ## How to Use ### Step 1. Create Data Compliance Rules diff --git a/nautobot_data_validation_engine/custom_validators.py b/nautobot_data_validation_engine/custom_validators.py index c653bc52..9e5157d3 100644 --- a/nautobot_data_validation_engine/custom_validators.py +++ b/nautobot_data_validation_engine/custom_validators.py @@ -283,7 +283,10 @@ def compliance_result(self, message, attribute=None, valid=True): """Generate a DataCompliance object based on the given parameters.""" instance = self.context["object"] attribute_value = None - if attribute: + if attribute and attribute.startswith("cf_"): + # Custom field attributes are prefixed with 'cf_' + attribute_value = instance.cf.get(attribute[3:], None) + elif attribute: attribute_value = getattr(instance, attribute) else: attribute = "__all__" @@ -295,7 +298,7 @@ def compliance_result(self, message, attribute=None, valid=True): defaults={ "last_validation_date": self.result_date, "validated_object_str": str(instance), - "validated_attribute_value": str(attribute_value) if attribute_value else "", + "validated_attribute_value": str(attribute_value)[:254] if attribute_value else "", "message": message, "valid": valid, }, diff --git a/nautobot_data_validation_engine/tests/test_data_compliance_rules.py b/nautobot_data_validation_engine/tests/test_data_compliance_rules.py index 016a2e5d..554902d7 100644 --- a/nautobot_data_validation_engine/tests/test_data_compliance_rules.py +++ b/nautobot_data_validation_engine/tests/test_data_compliance_rules.py @@ -78,3 +78,34 @@ def test_validate_replaces_results(self): len(DataCompliance.objects.filter(compliance_class_name=TestFailedDataComplianceRule.__name__)), 5, ) + + def test_custom_field_attribute_value(self): + # Simulate a Location with a custom field value + self.s.cf = {"foo": "bar"} + # Patch DataComplianceRule.context to include our instance + rule = TestPassedDataComplianceRule(self.s) + rule.context = {"object": self.s} + + # Call _create_data_compliance_object with a custom field attribute + obj = rule._create_data_compliance_object(attribute="cf_foo", valid=True, message="msg") + self.assertEqual(obj.validated_attribute, "cf_foo") + self.assertEqual(obj.validated_attribute_value, "bar") + + def test_custom_field_attribute_value_missing(self): + # Simulate a Location with no custom field value + self.s.cf = {} + rule = TestPassedDataComplianceRule(self.s) + rule.context = {"object": self.s} + + obj = rule._create_data_compliance_object(attribute="cf_missing", valid=True, message="msg") + self.assertEqual(obj.validated_attribute, "cf_missing") + self.assertIsNone(obj.validated_attribute_value) + + def test_regular_attribute_value(self): + # Test with a regular attribute + rule = TestPassedDataComplianceRule(self.s) + rule.context = {"object": self.s} + + obj = rule._create_data_compliance_object(attribute="name", valid=True, message="msg") + self.assertEqual(obj.validated_attribute, "name") + self.assertEqual(obj.validated_attribute_value, self.s.name)