diff --git a/src/cl_sii/extras/dj_model_fields.py b/src/cl_sii/extras/dj_model_fields.py index cc2ee719..8013c73d 100644 --- a/src/cl_sii/extras/dj_model_fields.py +++ b/src/cl_sii/extras/dj_model_fields.py @@ -8,7 +8,7 @@ except ImportError as exc: # pragma: no cover raise ImportError("Package 'Django' is required to use this module.") from exc -from typing import Any, Optional, Tuple +from typing import Any, ClassVar, Optional, Tuple import django.core.exceptions import django.db.models @@ -41,7 +41,6 @@ class RutField(django.db.models.Field): """ - # TODO: add option to validate that "digito verificador" is correct. # TODO: implement method 'formfield'. Probably a copy of 'CharField.formfield' is fine. description = 'RUT for SII (Chile)' @@ -50,8 +49,18 @@ class RutField(django.db.models.Field): 'invalid_dv': "\"digito verificador\" of RUT '%(value)s' is incorrect.", } empty_strings_allowed = False + validate_dv_by_default: ClassVar[bool] = False + + def __init__( + self, + verbose_name: Optional[str] = None, + name: Optional[str] = None, + validate_dv: bool = validate_dv_by_default, + *args: Any, + **kwargs: Any, + ) -> None: + self.validate_dv = validate_dv - def __init__(self, *args: Any, **kwargs: Any) -> None: # note: the value saved to the DB will always be in canonical format. db_column_max_length = cl_sii.rut.constants.RUT_CANONICAL_MAX_LENGTH @@ -62,7 +71,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: raise ValueError("This field does not allow customization of 'max_length'.") kwargs['max_length'] = db_column_max_length - super().__init__(*args, **kwargs) + super().__init__(verbose_name, name, *args, **kwargs) def deconstruct(self) -> Tuple[str, str, Any, Any]: """ @@ -71,6 +80,10 @@ def deconstruct(self) -> Tuple[str, str, Any, Any]: # note: this override is necessary because we have a custom constructor. name, path, args, kwargs = super().deconstruct() + + if self.validate_dv != self.validate_dv_by_default: + kwargs['validate_dv'] = self.validate_dv + del kwargs['max_length'] return name, path, args, kwargs @@ -144,8 +157,19 @@ def to_python(self, value: Optional[object]) -> Optional[Rut]: converted_value = value else: try: - converted_value = Rut(value, validate_dv=False) # type: ignore - except (AttributeError, TypeError, ValueError): + 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}, + ) + raise django.core.exceptions.ValidationError( self.error_messages['invalid'], code='invalid', diff --git a/src/tests/test_extras_dj_model_fields.py b/src/tests/test_extras_dj_model_fields.py index 00dbfb7b..a62e0a3a 100644 --- a/src/tests/test_extras_dj_model_fields.py +++ b/src/tests/test_extras_dj_model_fields.py @@ -1,6 +1,8 @@ import unittest +import unittest.mock -import django.db.models # noqa: F401 +import django.core.exceptions +import django.db.models from cl_sii.extras.dj_model_fields import Rut, RutField @@ -8,13 +10,19 @@ class RutFieldTest(unittest.TestCase): valid_rut_canonical: str valid_rut_instance: Rut + valid_rut_canonical_with_invalid_dv: str valid_rut_verbose_leading_zero_lowercase: str + mock_model_instance: django.db.models.Model @classmethod 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_verbose_leading_zero_lowercase = '060.803.000-k' + cls.mock_model_instance = unittest.mock.create_autospec( + django.db.models.Model, instance=True + ) def test_get_prep_value_of_canonical_str(self) -> None: prepared_value = RutField().get_prep_value(self.valid_rut_canonical) @@ -34,3 +42,33 @@ def test_get_prep_value_of_Rut(self) -> None: def test_get_prep_value_of_None(self) -> None: prepared_value = RutField().get_prep_value(None) self.assertIsNone(prepared_value) + + 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: + rut_field.clean(self.valid_rut_canonical_with_invalid_dv, self.mock_model_instance) + self.assertEqual(cm.exception.code, 'invalid_dv') + + def test_clean_value_of_rut_str_with_invalid_dv_if_not_validated(self) -> None: + rut_field = RutField(validate_dv=False) + cleaned_value = rut_field.clean( + self.valid_rut_canonical_with_invalid_dv, self.mock_model_instance + ) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical_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') + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_deconstruct_with_option_validate_dv_enabled(self) -> None: + name, path, args, kwargs = RutField(validate_dv=True).deconstruct() + self.assertEqual(args, []) + self.assertEqual(kwargs, {'validate_dv': True}) + + def test_deconstruct_with_option_validate_dv_disabled(self) -> None: + name, path, args, kwargs = RutField(validate_dv=False).deconstruct() + self.assertEqual(args, []) + self.assertEqual(kwargs, {})