From dd7561baa04a236b302346f72e0b1018078989fb Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Wed, 10 Sep 2025 16:56:19 -0300 Subject: [PATCH] fix(extras): Django model field `RutField` does not validate DV of `Rut` In Django model field `RutField`, when field option `validate_dv` is enabled, the `to_python()` method does not validate the "digito verificador" if the input value is already an instance of `Rut`. This commit changes the implementation of `to_python()` so that the "digito verificador" is also validated for `Rut` instances when the `validate_dv` field option is enabled. --- src/cl_sii/extras/dj_model_fields.py | 44 ++++++++++++++---------- src/tests/test_extras_dj_model_fields.py | 37 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/cl_sii/extras/dj_model_fields.py b/src/cl_sii/extras/dj_model_fields.py index 8013c73d..a03b9f12 100644 --- a/src/cl_sii/extras/dj_model_fields.py +++ b/src/cl_sii/extras/dj_model_fields.py @@ -153,29 +153,37 @@ def to_python(self, value: Optional[object]) -> Optional[Rut]: if the data can't be converted """ - if value is None or isinstance(value, Rut): + if value is None: converted_value = value - else: - try: - converted_value = Rut(value, validate_dv=self.validate_dv) # type: ignore - except (AttributeError, TypeError, ValueError) as exc: - if ( - isinstance(exc, ValueError) - and exc.args - and exc.args[0] == Rut.INVALID_DV_ERROR_MESSAGE - ): - raise django.core.exceptions.ValidationError( - self.error_messages['invalid_dv'], - code='invalid_dv', - params={'value': value}, - ) - + return converted_value + elif isinstance(value, Rut): + if not self.validate_dv: + converted_value = value + return converted_value + elif self.validate_dv and value.validate_dv(raise_exception=False): + converted_value = value + return converted_value + + try: + converted_value = Rut(value, validate_dv=self.validate_dv) # type: ignore + except (AttributeError, TypeError, ValueError) as exc: + if ( + isinstance(exc, ValueError) + and exc.args + and exc.args[0] == Rut.INVALID_DV_ERROR_MESSAGE + ): raise django.core.exceptions.ValidationError( - self.error_messages['invalid'], - code='invalid', + self.error_messages['invalid_dv'], + code='invalid_dv', params={'value': value}, ) + raise django.core.exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) + return converted_value def value_to_string(self, obj: django.db.models.Model) -> str: diff --git a/src/tests/test_extras_dj_model_fields.py b/src/tests/test_extras_dj_model_fields.py index a62e0a3a..5f7d8c11 100644 --- a/src/tests/test_extras_dj_model_fields.py +++ b/src/tests/test_extras_dj_model_fields.py @@ -11,6 +11,7 @@ class RutFieldTest(unittest.TestCase): valid_rut_canonical: str valid_rut_instance: Rut valid_rut_canonical_with_invalid_dv: str + valid_rut_canonical_instance_with_invalid_dv: Rut valid_rut_verbose_leading_zero_lowercase: str mock_model_instance: django.db.models.Model @@ -19,6 +20,10 @@ def setUpClass(cls) -> None: cls.valid_rut_canonical = '60803000-K' cls.valid_rut_instance = Rut(cls.valid_rut_canonical) cls.valid_rut_canonical_with_invalid_dv = '60803000-0' + cls.valid_rut_canonical_instance_with_invalid_dv = Rut( + cls.valid_rut_canonical_with_invalid_dv + ) + assert not cls.valid_rut_canonical_instance_with_invalid_dv.validate_dv() cls.valid_rut_verbose_leading_zero_lowercase = '060.803.000-k' cls.mock_model_instance = unittest.mock.create_autospec( django.db.models.Model, instance=True @@ -43,6 +48,14 @@ def test_get_prep_value_of_None(self) -> None: prepared_value = RutField().get_prep_value(None) self.assertIsNone(prepared_value) + def test_clean_value_with_invalid_type(self) -> None: + for value_with_invalid_type in [12345678, 12.34, [], {}, object()]: + with self.subTest(value=value_with_invalid_type): + rut_field = RutField() + with self.assertRaises(django.core.exceptions.ValidationError) as cm: + rut_field.clean(value_with_invalid_type, self.mock_model_instance) + self.assertEqual(cm.exception.code, 'invalid') + def test_clean_value_of_rut_str_with_invalid_dv_if_validated(self) -> None: rut_field = RutField(validate_dv=True) with self.assertRaises(django.core.exceptions.ValidationError) as cm: @@ -57,6 +70,30 @@ def test_clean_value_of_rut_str_with_invalid_dv_if_not_validated(self) -> None: self.assertIsInstance(cleaned_value, Rut) self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical_with_invalid_dv) + def test_clean_value_of_rut_instance_with_valid_dv(self) -> None: + for validate_dv in [True, False]: + with self.subTest(validate_dv=validate_dv): + rut_field = RutField(validate_dv=validate_dv) + cleaned_value = rut_field.clean(self.valid_rut_instance, self.mock_model_instance) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value, self.valid_rut_instance) + + def test_clean_value_of_rut_instance_with_invalid_dv_if_validated(self) -> None: + rut_field = RutField(validate_dv=True) + with self.assertRaises(django.core.exceptions.ValidationError) as cm: + rut_field.clean( + self.valid_rut_canonical_instance_with_invalid_dv, self.mock_model_instance + ) + self.assertEqual(cm.exception.code, 'invalid_dv') + + def test_clean_value_of_rut_instance_with_invalid_dv_if_not_validated(self) -> None: + rut_field = RutField(validate_dv=False) + cleaned_value = rut_field.clean( + self.valid_rut_canonical_instance_with_invalid_dv, self.mock_model_instance + ) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value, self.valid_rut_canonical_instance_with_invalid_dv) + def test_deconstruct_without_options(self) -> None: name, path, args, kwargs = RutField().deconstruct() self.assertEqual(path, 'cl_sii.extras.dj_model_fields.RutField')