filters.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import django_filters
  2. from django import forms
  3. from django.conf import settings
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.exceptions import ValidationError
  6. from django.utils.translation import gettext as _
  7. from django_filters.constants import EMPTY_VALUES
  8. from django_filters.utils import get_model_field
  9. from drf_spectacular.types import OpenApiTypes
  10. from drf_spectacular.utils import extend_schema_field
  11. from .forms.fields import BigIntegerField
  12. __all__ = (
  13. 'ContentTypeFilter',
  14. 'MultiValueArrayFilter',
  15. 'MultiValueBigNumberFilter',
  16. 'MultiValueCharFilter',
  17. 'MultiValueContentTypeFilter',
  18. 'MultiValueDateFilter',
  19. 'MultiValueDateTimeFilter',
  20. 'MultiValueDecimalFilter',
  21. 'MultiValueMACAddressFilter',
  22. 'MultiValueNumberFilter',
  23. 'MultiValueTimeFilter',
  24. 'MultiValueTimeZoneFilter',
  25. 'MultiValueWWNFilter',
  26. 'NullableCharFieldFilter',
  27. 'NumericArrayFilter',
  28. 'TreeNodeMultipleChoiceFilter',
  29. )
  30. def multivalue_field_factory(field_class):
  31. """
  32. Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
  33. filter values while maintaining the field's built-in validation. Example: GET /api/dcim/devices/?name=foo&name=bar
  34. """
  35. class NewField(field_class):
  36. widget = forms.SelectMultiple
  37. def to_python(self, value):
  38. if not value:
  39. return []
  40. field = field_class()
  41. return [
  42. # Only append non-empty values (this avoids e.g. trying to cast '' as an integer)
  43. field.to_python(v) for v in value if v
  44. ]
  45. def run_validators(self, value):
  46. for v in value:
  47. super().run_validators(v)
  48. def validate(self, value):
  49. for v in value:
  50. super().validate(v)
  51. return type(f'MultiValue{field_class.__name__}', (NewField,), dict())
  52. #
  53. # Filters
  54. #
  55. @extend_schema_field(OpenApiTypes.STR)
  56. class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
  57. field_class = multivalue_field_factory(forms.CharField)
  58. @extend_schema_field(OpenApiTypes.DATE)
  59. class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
  60. field_class = multivalue_field_factory(forms.DateField)
  61. @extend_schema_field(OpenApiTypes.DATETIME)
  62. class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
  63. field_class = multivalue_field_factory(forms.DateTimeField)
  64. @extend_schema_field(OpenApiTypes.INT32)
  65. class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
  66. field_class = multivalue_field_factory(forms.IntegerField)
  67. @extend_schema_field(OpenApiTypes.INT64)
  68. class MultiValueBigNumberFilter(MultiValueNumberFilter):
  69. field_class = multivalue_field_factory(BigIntegerField)
  70. @extend_schema_field(OpenApiTypes.DECIMAL)
  71. class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
  72. field_class = multivalue_field_factory(forms.DecimalField)
  73. @extend_schema_field(OpenApiTypes.TIME)
  74. class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
  75. field_class = multivalue_field_factory(forms.TimeField)
  76. @extend_schema_field(OpenApiTypes.STR)
  77. class MultiValueArrayFilter(django_filters.MultipleChoiceFilter):
  78. field_class = multivalue_field_factory(forms.CharField)
  79. def __init__(self, *args, lookup_expr='contains', **kwargs):
  80. # Set default lookup_expr to 'contains'
  81. super().__init__(*args, lookup_expr=lookup_expr, **kwargs)
  82. def get_filter_predicate(self, v):
  83. # If filtering for null values, ignore lookup_expr
  84. if v is None:
  85. return {self.field_name: None}
  86. return super().get_filter_predicate(v)
  87. class InvalidValueFilterMixin:
  88. """
  89. Validate values with the model field for lookups that prepare their inputs.
  90. Partial-match lookups bypass this validation.
  91. """
  92. @property
  93. def field(self):
  94. field = super().field
  95. # The model is assigned only once the filter has been bound to a FilterSet instance
  96. if getattr(self, 'model', None) is not None and not getattr(self, '_model_field_validated', False):
  97. self._model_field_validated = True
  98. model_field = get_model_field(self.model, self.field_name)
  99. lookup = model_field.get_lookup(self.lookup_expr) if model_field is not None else None
  100. if lookup is not None and lookup.prepare_rhs:
  101. field.validators.append(self._get_validator(model_field))
  102. return field
  103. def _get_validator(self, model_field):
  104. null_value = self.null_value
  105. def validator(value):
  106. # The null choice value is translated to None when filtering
  107. if value == null_value:
  108. return
  109. try:
  110. model_field.get_prep_value(value)
  111. except (OSError, TypeError, ValueError):
  112. # Some values are rejected with an exception other than ValidationError (e.g. zoneinfo raises a
  113. # ValueError for a time zone key which is not a normalized path)
  114. raise ValidationError(_('Invalid value: {value}').format(value=value))
  115. return validator
  116. @extend_schema_field(OpenApiTypes.STR)
  117. class MultiValueMACAddressFilter(InvalidValueFilterMixin, MultiValueCharFilter):
  118. pass
  119. @extend_schema_field(OpenApiTypes.STR)
  120. class MultiValueTimeZoneFilter(InvalidValueFilterMixin, MultiValueCharFilter):
  121. pass
  122. @extend_schema_field(OpenApiTypes.STR)
  123. class MultiValueWWNFilter(InvalidValueFilterMixin, MultiValueCharFilter):
  124. pass
  125. @extend_schema_field(OpenApiTypes.STR)
  126. class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
  127. """
  128. Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
  129. """
  130. def get_filter_predicate(self, v):
  131. # Null value filtering
  132. if v is None:
  133. return {f"{self.field_name}__isnull": True}
  134. return super().get_filter_predicate(v)
  135. def filter(self, qs, value):
  136. value = [node.get_descendants(include_self=True) if not isinstance(node, str) else node for node in value]
  137. return super().filter(qs, value)
  138. class NullableCharFieldFilter(django_filters.CharFilter):
  139. """
  140. Allow matching on null field values by passing a special string used to signify NULL.
  141. """
  142. def filter(self, qs, value):
  143. if value != settings.FILTERS_NULL_CHOICE_VALUE:
  144. return super().filter(qs, value)
  145. qs = self.get_method(qs)(**{'{}__isnull'.format(self.field_name): True})
  146. return qs.distinct() if self.distinct else qs
  147. class NumericArrayFilter(django_filters.NumberFilter):
  148. """
  149. Filter based on the presence of an integer within an ArrayField.
  150. """
  151. field_class = forms.IntegerField
  152. def filter(self, qs, value):
  153. if value is None:
  154. return qs
  155. return super().filter(qs, [value])
  156. class ContentTypeFilter(django_filters.CharFilter):
  157. """
  158. Allow specifying a ContentType by <app_label>.<model> (e.g. "dcim.site").
  159. """
  160. def filter(self, qs, value):
  161. if value in EMPTY_VALUES:
  162. return qs
  163. try:
  164. app_label, model = value.lower().split('.')
  165. content_type = ContentType.objects.get_by_natural_key(app_label, model)
  166. except (ValueError, ContentType.DoesNotExist):
  167. return qs.none()
  168. return qs.filter(
  169. **{
  170. f'{self.field_name}': content_type,
  171. }
  172. )
  173. class MultiValueContentTypeFilter(MultiValueCharFilter):
  174. """
  175. A multi-value version of ContentTypeFilter.
  176. """
  177. def filter(self, qs, value):
  178. if value in EMPTY_VALUES:
  179. return qs
  180. content_types = []
  181. for key in value:
  182. try:
  183. app_label, model = key.lower().split('.')
  184. ct = ContentType.objects.get_by_natural_key(app_label, model)
  185. content_types.append(ct)
  186. except (ValueError, ContentType.DoesNotExist):
  187. continue
  188. return qs.filter(
  189. **{
  190. f'{self.field_name}__in': content_types,
  191. }
  192. )