1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

188 lines
6.6 KiB
Python

import inspect
import operator
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
# NOTE: As this module may be imported by configuration.py, we cannot import
# anything from NetBox itself.
class IsEqualValidator(validators.BaseValidator):
"""
Employed by CustomValidator to require a specific value.
"""
message = _("Ensure this value is equal to %(limit_value)s.")
code = "is_equal"
def compare(self, a, b):
return a != b
class IsNotEqualValidator(validators.BaseValidator):
"""
Employed by CustomValidator to exclude a specific value.
"""
message = _("Ensure this value does not equal %(limit_value)s.")
code = "is_not_equal"
def compare(self, a, b):
return a == b
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:
"""
This class enables the application of user-defined validation rules to NetBox models. It can be instantiated by
passing a dictionary of validation rules in the form {attribute: rules}, where 'rules' is a dictionary mapping
descriptors (e.g. min_length or regex) to values.
A CustomValidator instance is applied by calling it with the instance being validated:
validator = CustomValidator({'name': {'min_length: 10}})
site = Site(name='abcdef')
validator(site) # Raises ValidationError
:param validation_rules: A dictionary mapping object attributes to validation rules
"""
REQUEST_TOKEN = 'request'
VALIDATORS = {
'eq': IsEqualValidator,
'neq': IsNotEqualValidator,
'min': validators.MinValueValidator,
'max': validators.MaxValueValidator,
'min_length': validators.MinLengthValidator,
'max_length': validators.MaxLengthValidator,
'regex': validators.RegexValidator,
'required': IsNotEmptyValidator,
'prohibited': IsEmptyValidator,
}
def __init__(self, validation_rules=None):
self.validation_rules = validation_rules or {}
if type(self.validation_rules) is not dict:
raise ValueError(_("Validation rules must be passed as a dictionary"))
def __call__(self, instance, request=None):
"""
Validate the instance and (optional) request against the validation rule(s).
"""
for attr_path, rules in self.validation_rules.items():
# The rule applies to the current request
if attr_path.split('.')[0] == self.REQUEST_TOKEN:
# Skip if no request has been provided (we can't validate)
if request is None:
continue
attr = self._get_request_attr(request, attr_path)
# The rule applies to the instance
else:
attr = self._get_instance_attr(instance, attr_path)
# Validate the attribute's value against each of the rules defined for it
for descriptor, value in rules.items():
validator = self.get_validator(descriptor, value)
try:
validator(attr)
except ValidationError as exc:
raise ValidationError(
_("Custom validation failed for {attribute}: {exception}").format(
attribute=attr_path, exception=exc
)
)
# Execute custom validation logic (if any)
# TODO: Remove in v4.1
# Inspect the validate() method, which may have been overridden, to determine
# whether we should pass the request (maintains backward compatibility for pre-v4.0)
if 'request' in inspect.signature(self.validate).parameters:
self.validate(instance, request)
else:
self.validate(instance)
@staticmethod
def _get_request_attr(request, name):
name = name.split('.', maxsplit=1)[1] # Remove token
try:
return operator.attrgetter(name)(request)
except AttributeError:
raise ValidationError(_('Invalid attribute "{name}" for request').format(name=name))
@staticmethod
def _get_instance_attr(instance, name):
# Attempt to resolve many-to-many fields to their stored values
m2m_fields = [f.name for f in instance._meta.local_many_to_many]
if name in m2m_fields:
if name in getattr(instance, '_m2m_values', []):
return instance._m2m_values[name]
if instance.pk:
return list(getattr(instance, name).all())
return []
# Raise a ValidationError for unknown attributes
try:
return operator.attrgetter(name)(instance)
except AttributeError:
raise ValidationError(_('Invalid attribute "{name}" for {model}').format(
name=name,
model=instance.__class__.__name__
))
def get_validator(self, descriptor, value):
"""
Instantiate and return the appropriate validator based on the descriptor given. For
example, 'min' returns MinValueValidator(value).
"""
if descriptor not in self.VALIDATORS:
raise NotImplementedError(
f"Unknown validation type for {self.__class__.__name__}: '{descriptor}'"
)
validator_cls = self.VALIDATORS.get(descriptor)
return validator_cls(value)
def validate(self, instance, request):
"""
Custom validation method, to be overridden by the user. Validation failures should
raise a ValidationError exception.
"""
return
def fail(self, message, field=None):
"""
Raise a ValidationError exception. Associate the provided message with a form/serializer field if specified.
"""
if field is not None:
raise ValidationError({field: message})
raise ValidationError(message)