validators.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import operator
  2. from django.core import validators
  3. from django.core.exceptions import ValidationError
  4. from django.utils.translation import gettext_lazy as _
  5. # NOTE: As this module may be imported by configuration.py, we cannot import
  6. # anything from NetBox itself.
  7. class IsEqualValidator(validators.BaseValidator):
  8. """
  9. Employed by CustomValidator to require a specific value.
  10. """
  11. message = _("Ensure this value is equal to %(limit_value)s.")
  12. code = "is_equal"
  13. def compare(self, a, b):
  14. return a != b
  15. class IsNotEqualValidator(validators.BaseValidator):
  16. """
  17. Employed by CustomValidator to exclude a specific value.
  18. """
  19. message = _("Ensure this value does not equal %(limit_value)s.")
  20. code = "is_not_equal"
  21. def compare(self, a, b):
  22. return a == b
  23. class IsEmptyValidator:
  24. """
  25. Employed by CustomValidator to enforce required fields.
  26. """
  27. message = _("This field must be empty.")
  28. code = 'is_empty'
  29. def __init__(self, enforce=True):
  30. self._enforce = enforce
  31. def __call__(self, value):
  32. if self._enforce and value not in validators.EMPTY_VALUES:
  33. raise ValidationError(self.message, code=self.code)
  34. class IsNotEmptyValidator:
  35. """
  36. Employed by CustomValidator to enforce prohibited fields.
  37. """
  38. message = _("This field must not be empty.")
  39. code = 'not_empty'
  40. def __init__(self, enforce=True):
  41. self._enforce = enforce
  42. def __call__(self, value):
  43. if self._enforce and value in validators.EMPTY_VALUES:
  44. raise ValidationError(self.message, code=self.code)
  45. class CustomValidator:
  46. """
  47. This class enables the application of user-defined validation rules to NetBox models. It can be instantiated by
  48. passing a dictionary of validation rules in the form {attribute: rules}, where 'rules' is a dictionary mapping
  49. descriptors (e.g. min_length or regex) to values.
  50. A CustomValidator instance is applied by calling it with the instance being validated:
  51. validator = CustomValidator({'name': {'min_length: 10}})
  52. site = Site(name='abcdef')
  53. validator(site) # Raises ValidationError
  54. :param validation_rules: A dictionary mapping object attributes to validation rules
  55. """
  56. REQUEST_TOKEN = 'request'
  57. VALIDATORS = {
  58. 'eq': IsEqualValidator,
  59. 'neq': IsNotEqualValidator,
  60. 'min': validators.MinValueValidator,
  61. 'max': validators.MaxValueValidator,
  62. 'min_length': validators.MinLengthValidator,
  63. 'max_length': validators.MaxLengthValidator,
  64. 'regex': validators.RegexValidator,
  65. 'required': IsNotEmptyValidator,
  66. 'prohibited': IsEmptyValidator,
  67. }
  68. def __init__(self, validation_rules=None):
  69. self.validation_rules = validation_rules or {}
  70. if type(self.validation_rules) is not dict:
  71. raise ValueError(_("Validation rules must be passed as a dictionary"))
  72. def __call__(self, instance, request=None):
  73. """
  74. Validate the instance and (optional) request against the validation rule(s).
  75. """
  76. for attr_path, rules in self.validation_rules.items():
  77. # The rule applies to the current request
  78. if attr_path.split('.')[0] == self.REQUEST_TOKEN:
  79. # Skip if no request has been provided (we can't validate)
  80. if request is None:
  81. continue
  82. attr = self._get_request_attr(request, attr_path)
  83. # The rule applies to the instance
  84. else:
  85. attr = self._get_instance_attr(instance, attr_path)
  86. # Validate the attribute's value against each of the rules defined for it
  87. for descriptor, value in rules.items():
  88. validator = self.get_validator(descriptor, value)
  89. try:
  90. validator(attr)
  91. except ValidationError as exc:
  92. raise ValidationError(
  93. _("Custom validation failed for {attribute}: {exception}").format(
  94. attribute=attr_path, exception=exc
  95. )
  96. )
  97. # Execute custom validation logic (if any)
  98. self.validate(instance, request)
  99. @staticmethod
  100. def _get_request_attr(request, name):
  101. name = name.split('.', maxsplit=1)[1] # Remove token
  102. try:
  103. return operator.attrgetter(name)(request)
  104. except AttributeError:
  105. raise ValidationError(_('Invalid attribute "{name}" for request').format(name=name))
  106. @staticmethod
  107. def _get_instance_attr(instance, name):
  108. # Attempt to resolve many-to-many fields to their stored values
  109. m2m_fields = [f.name for f in instance._meta.local_many_to_many]
  110. if name in m2m_fields:
  111. if name in getattr(instance, '_m2m_values', []):
  112. return instance._m2m_values[name]
  113. if instance.pk:
  114. return list(getattr(instance, name).all())
  115. return []
  116. # Raise a ValidationError for unknown attributes
  117. try:
  118. return operator.attrgetter(name)(instance)
  119. except AttributeError:
  120. raise ValidationError(_('Invalid attribute "{name}" for {model}').format(
  121. name=name,
  122. model=instance.__class__.__name__
  123. ))
  124. def get_validator(self, descriptor, value):
  125. """
  126. Instantiate and return the appropriate validator based on the descriptor given. For
  127. example, 'min' returns MinValueValidator(value).
  128. """
  129. if descriptor not in self.VALIDATORS:
  130. raise NotImplementedError(
  131. f"Unknown validation type for {self.__class__.__name__}: '{descriptor}'"
  132. )
  133. validator_cls = self.VALIDATORS.get(descriptor)
  134. return validator_cls(value)
  135. def validate(self, instance, request):
  136. """
  137. Custom validation method, to be overridden by the user. Validation failures should
  138. raise a ValidationError exception.
  139. """
  140. return
  141. def fail(self, message, field=None):
  142. """
  143. Raise a ValidationError exception. Associate the provided message with a form/serializer field if specified.
  144. """
  145. if field is not None:
  146. raise ValidationError({field: message})
  147. raise ValidationError(message)