diff --git a/src/cl_sii/extras/dj_filters.py b/src/cl_sii/extras/dj_filters.py index 20ec8f36..9cde5986 100644 --- a/src/cl_sii/extras/dj_filters.py +++ b/src/cl_sii/extras/dj_filters.py @@ -12,8 +12,9 @@ except ImportError as exc: # pragma: no cover raise ImportError("Package 'django-filter' is required to use this module.") from exc +from collections.abc import Sequence from copy import deepcopy -from typing import ClassVar, Mapping, Tuple, Type +from typing import Any, ClassVar, Mapping, Type import django.db.models import django.forms @@ -37,9 +38,37 @@ class RutFilter(django_filters.filters.CharFilter): - https://github.com/carltongibson/django-filter/blob/24.2/docs/ref/filters.txt """ - field_class: ClassVar[Type[django.forms.Field]] + field_class: Type[django.forms.Field] field_class = cl_sii.extras.dj_form_fields.RutField + field_class_for_substrings: Type[django.forms.Field] + field_class_for_substrings = django_filters.filters.CharFilter.field_class + + lookup_expressions_for_substrings: Sequence[str] = [ + 'contains', + 'icontains', + 'startswith', + 'istartswith', + 'endswith', + 'iendswith', + ] + + def __init__( + self, + field_name: Any = None, + lookup_expr: Any = None, + *args: Any, + **kwargs: Any, + ) -> None: + if lookup_expr in self.lookup_expressions_for_substrings: + # Lookups that can be used to search for substrings will not always + # work with the default field class because some substrings cannot + # be converted to instances of class `Rut`. For example, + # `…__contains="803"` fails because `Rut("803")` raises a `ValueError`. + self.field_class = self.field_class_for_substrings + + super().__init__(field_name, lookup_expr, *args, **kwargs) + FILTER_FOR_DBFIELD_DEFAULTS = { **FILTER_FOR_DBFIELD_DEFAULTS, @@ -62,18 +91,3 @@ class SiiFilterSet(django_filters.filterset.FilterSet): FILTER_DEFAULTS: ClassVar[Mapping[Type[django.db.models.Field], Mapping[str, object]]] FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS - - @classmethod - def filter_for_lookup( - cls, field: django.db.models.Field, lookup_type: str - ) -> Tuple[Type[django_filters.filters.Filter], Mapping[str, object]]: - filter_class, params = super().filter_for_lookup(field, lookup_type) - - # Override RUT containment lookups. - if isinstance(field, cl_sii.extras.dj_model_fields.RutField) and lookup_type in ( - 'contains', - 'icontains', - ): - filter_class, params = django_filters.filters.CharFilter, {} - - return filter_class, params diff --git a/src/tests/test_extras_dj_filters.py b/src/tests/test_extras_dj_filters.py index 1f83e209..1531ca90 100644 --- a/src/tests/test_extras_dj_filters.py +++ b/src/tests/test_extras_dj_filters.py @@ -4,6 +4,7 @@ import django_filters +from cl_sii.extras import dj_form_fields, dj_model_fields from cl_sii.extras.dj_filters import RutFilter, SiiFilterSet @@ -22,6 +23,34 @@ def test_new_instance(self) -> None: self.assertIsInstance(filter, RutFilter) self.assertIsInstance(filter, django_filters.filters.Filter) + def test_filter_class_lookup_expressions(self) -> None: + expected_field_class = dj_form_fields.RutField + for lookup_expr in [ + 'exact', + 'iexact', + 'in', + 'gt', + 'gte', + 'lt', + 'lte', + ]: + with self.subTest(field_class=expected_field_class, lookup_expr=lookup_expr): + filter_instance = RutFilter(lookup_expr=lookup_expr) + self.assertIs(filter_instance.field_class, expected_field_class) + + expected_field_class = django_filters.CharFilter.field_class + for lookup_expr in [ + 'contains', + 'icontains', + 'startswith', + 'istartswith', + 'endswith', + 'iendswith', + ]: + with self.subTest(field_class=expected_field_class, lookup_expr=lookup_expr): + filter_instance = RutFilter(lookup_expr=lookup_expr) + self.assertIs(filter_instance.field_class, expected_field_class) + # TODO: Add tests. @@ -34,4 +63,43 @@ class SiiFilterSetTest(unittest.TestCase): def test_filter_for_lookup(self) -> None: assert SiiFilterSet.filter_for_lookup() + def test_filter_for_lookup_types(self) -> None: + field = dj_model_fields.RutField() + + expected_field_class = dj_form_fields.RutField + for lookup_type in [ + 'exact', + 'iexact', + 'gt', + 'gte', + 'lt', + 'lte', + ]: + with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): + filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) + filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) + self.assertIs(filter_instance.field_class, expected_field_class) + + for lookup_type in [ + 'in', + ]: + with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): + filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) + filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) + self.assertTrue(issubclass(filter_instance.field_class, expected_field_class)) + + expected_field_class = django_filters.CharFilter.field_class + for lookup_type in [ + 'contains', + 'icontains', + 'startswith', + 'istartswith', + 'endswith', + 'iendswith', + ]: + with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): + filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) + filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) + self.assertIs(filter_instance.field_class, expected_field_class) + # TODO: Add tests.