Просмотр исходного кода

Extend CustomValidator to support required, prohibited fields

jeremystretch 4 лет назад
Родитель
Сommit
44c0dec68b

+ 6 - 1
docs/additional-features/custom-validation.md

@@ -28,8 +28,13 @@ The `CustomValidator` class supports several validation types:
 * `min_length`: Minimum string length
 * `max_length`: Maximum string length
 * `regex`: Application of a [regular expression](https://en.wikipedia.org/wiki/Regular_expression)
+* `required`: A value must be specified
+* `prohibited`: A value must _not_ be specified
 
-The `min` and `max` types should be defined for numeric values, whereas `min_length`, `max_length`, and `regex` are suitable for character strings (text values).
+The `min` and `max` types should be defined for numeric values, whereas `min_length`, `max_length`, and `regex` are suitable for character strings (text values). The `required` and `prohibited` validators may be used for any field, and should be passed a value of `True`.
+
+!!! warning
+    Bear in mind that these validators merely supplement NetBox's own validation: They will not override it. For example, if a certain model field is required by NetBox, setting a validator for it with `{'prohibited': True}` will not work.
 
 ### Custom Validation Logic
 

+ 66 - 20
netbox/extras/tests/test_customvalidator.py

@@ -13,15 +13,51 @@ class MyValidator(CustomValidator):
             self.fail("Name must be foo!")
 
 
-stock_validator = CustomValidator({
-    'name': {
-        'min_length': 5,
-        'max_length': 10,
-        'regex': r'\d{3}$',  # Ends with three digits
-    },
+min_validator = CustomValidator({
     'asn': {
-        'min': 65000,
-        'max': 65100,
+        'min': 65000
+    }
+})
+
+
+max_validator = CustomValidator({
+    'asn': {
+        'max': 65100
+    }
+})
+
+
+min_length_validator = CustomValidator({
+    'name': {
+        'min_length': 5
+    }
+})
+
+
+max_length_validator = CustomValidator({
+    'name': {
+        'max_length': 10
+    }
+})
+
+
+regex_validator = CustomValidator({
+    'name': {
+        'regex': r'\d{3}$'  # Ends with three digits
+    }
+})
+
+
+required_validator = CustomValidator({
+    'description': {
+        'required': True
+    }
+})
+
+
+prohibited_validator = CustomValidator({
+    'description': {
+        'prohibited': True
     }
 })
 
@@ -30,46 +66,56 @@ custom_validator = MyValidator()
 
 class CustomValidatorTest(TestCase):
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_validator]})
     def test_configuration(self):
         self.assertIn('dcim.site', settings.CUSTOM_VALIDATORS)
         validator = settings.CUSTOM_VALIDATORS['dcim.site'][0]
         self.assertIsInstance(validator, CustomValidator)
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_validator]})
     def test_min(self):
         with self.assertRaises(ValidationError):
             Site(name='abcdef123', slug='abcdefghijk', asn=1).clean()
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [max_validator]})
     def test_max(self):
         with self.assertRaises(ValidationError):
             Site(name='abcdef123', slug='abcdefghijk', asn=65535).clean()
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]})
     def test_min_length(self):
         with self.assertRaises(ValidationError):
             Site(name='abc', slug='abc', asn=65000).clean()
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [max_length_validator]})
     def test_max_length(self):
         with self.assertRaises(ValidationError):
-            Site(name='abcdefghijk', slug='abcdefghijk', asn=65000).clean()
+            Site(name='abcdefghijk', slug='abcdefghijk').clean()
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [regex_validator]})
     def test_regex(self):
         with self.assertRaises(ValidationError):
-            Site(name='abcdefgh', slug='abcdefgh', asn=65000).clean()
+            Site(name='abcdefgh', slug='abcdefgh').clean()
+
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [required_validator]})
+    def test_required(self):
+        with self.assertRaises(ValidationError):
+            Site(name='abcdefgh', slug='abcdefgh', description='').clean()
+
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [prohibited_validator]})
+    def test_prohibited(self):
+        with self.assertRaises(ValidationError):
+            Site(name='abcdefgh', slug='abcdefgh', description='ABC').clean()
 
-    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [stock_validator]})
+    @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]})
     def test_valid(self):
-        Site(name='abcdef123', slug='abcdef123', asn=65000).clean()
+        Site(name='abcdef123', slug='abcdef123').clean()
 
     @override_settings(CUSTOM_VALIDATORS={'dcim.site': [custom_validator]})
     def test_custom_invalid(self):
         with self.assertRaises(ValidationError):
-            Site(name='abc', slug='abc', asn=65000).clean()
+            Site(name='abc', slug='abc').clean()
 
     @override_settings(CUSTOM_VALIDATORS={'dcim.site': [custom_validator]})
     def test_custom_valid(self):
-        Site(name='foo', slug='foo', asn=65000).clean()
+        Site(name='foo', slug='foo').clean()

+ 35 - 0
netbox/extras/validators.py

@@ -1,6 +1,39 @@
 from django.core.exceptions import ValidationError
 from django.core import validators
 
+# NOTE: As this module may be imported by configuration.py, we cannot import
+# anything from NetBox itself.
+
+
+class IsEmptyValidator:
+    """
+    Employed by CustomValidator to enforce required fields.
+    """
+    message = "This field must be empty."
+    code = 'is_empty'
+
+    def __init__(self, enforce=True):
+        self._enforce = enforce
+
+    def __call__(self, value):
+        if self._enforce and value not in validators.EMPTY_VALUES:
+            raise ValidationError(self.message, code=self.code)
+
+
+class IsNotEmptyValidator:
+    """
+    Employed by CustomValidator to enforce prohibited fields.
+    """
+    message = "This field must not be empty."
+    code = 'not_empty'
+
+    def __init__(self, enforce=True):
+        self._enforce = enforce
+
+    def __call__(self, value):
+        if self._enforce and value in validators.EMPTY_VALUES:
+            raise ValidationError(self.message, code=self.code)
+
 
 class CustomValidator:
     """
@@ -22,6 +55,8 @@ class CustomValidator:
         'min_length': validators.MinLengthValidator,
         'max_length': validators.MaxLengthValidator,
         'regex': validators.RegexValidator,
+        'required': IsNotEmptyValidator,
+        'prohibited': IsEmptyValidator,
     }
 
     def __init__(self, validation_rules=None):