validators.py 6.3 KB

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