Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/cl_sii/extras/dj_model_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)'
Expand All @@ -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

Expand All @@ -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]:
"""
Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down
40 changes: 39 additions & 1 deletion src/tests/test_extras_dj_model_fields.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
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


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)
Expand All @@ -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, {})
Loading