diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 32713dcb..89f6363c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.54.0 +current_version = 0.55.0 commit = True tag = False message = chore: Bump version from {current_version} to {new_version} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b196e0f7..5662ce99 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ jobs: - name: Set Up Python ${{ matrix.python_version }} id: set_up_python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "${{ matrix.python_version }}" check-latest: true @@ -92,7 +92,7 @@ jobs: - name: Set Up Python ${{ matrix.python_version }} id: set_up_python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "${{ matrix.python_version }}" check-latest: true @@ -139,7 +139,7 @@ jobs: make test-coverage-report - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./test-reports/coverage/ diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index b0abcc88..9c6b33d8 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -42,7 +42,7 @@ jobs: - name: Set Up Python id: set_up_python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.10" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e98b01c1..edd933ea 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -39,7 +39,7 @@ jobs: - name: Set Up Python id: set_up_python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.10" diff --git a/HISTORY.md b/HISTORY.md index f6c1a494..9fd382db 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # History +## 0.55.0 (2025-09-10) + +- (PR #878, 2025-09-10) extras: Improve Django filter for `Rut` +- (PR #879, 2025-09-10) chore(deps): Bump the github-actions-production group with 2 updates + ## 0.54.0 (2025-09-09) - (PR #871, 2025-09-09) rut: Use class variables for exception messages raised by `Rut` diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index 852c8b84..9b305a08 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.54.0' +__version__ = '0.55.0' 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.