diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 82491226..5132f359 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.12.2 +current_version = 0.12.3 commit = True tag = True diff --git a/HISTORY.rst b/HISTORY.rst index 56999351..2c796c37 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ------- +0.12.3 (2021-02-26) ++++++++++++++++++++++++ + +* (PR #193, 2021-02-16) requirements: Update dependency graph of base requirements +* (PR #197, 2021-02-26) extras: add 'RutField' for Django forms + 0.12.2 (2021-02-16) +++++++++++++++++++++++ diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py index ccfe723c..0f05af73 100644 --- a/cl_sii/__init__.py +++ b/cl_sii/__init__.py @@ -5,4 +5,4 @@ """ -__version__ = '0.12.2' +__version__ = '0.12.3' diff --git a/cl_sii/extras/dj_form_fields.py b/cl_sii/extras/dj_form_fields.py new file mode 100644 index 00000000..bb3c1bfb --- /dev/null +++ b/cl_sii/extras/dj_form_fields.py @@ -0,0 +1,73 @@ +""" +cl_sii "extras" / Django form fields. + +""" +try: + import django +except ImportError as exc: # pragma: no cover + raise ImportError("Package 'Django' is required to use this module.") from exc + +from typing import Any, Optional + +import django.core.exceptions +import django.forms +from django.utils.translation import gettext_lazy as _ + +from cl_sii.rut import Rut + + +class RutField(django.forms.CharField): + + """ + Django form field for RUT. + + * Python data type: :class:`cl_sii.rut.Rut` + + .. seealso:: + :class:`.dj_model_fields.RutField` + + """ + + default_error_messages = { + 'invalid': _('Enter a valid RUT.'), + 'invalid_dv': _('RUT\'s "digito verificador" is incorrect.'), + } + + def __init__(self, *, validate_dv: bool = False, **kwargs: Any) -> None: + """ + :param validate_dv: Boolean that specifies whether to validate that + the RUT's "digito verificador" is correct. False by default. + """ + + self.validate_dv = validate_dv + super().__init__(strip=True, **kwargs) + + def to_python(self, value: Optional[object]) -> Optional[Rut]: + """ + Validate that the input can be converted to a Python object (:class:`Rut`). + + :raises django.core.exceptions.ValidationError: + if the input can't be converted + """ + + if value in self.empty_values: + converted_value = None + elif isinstance(value, Rut): + converted_value = value + else: + try: + converted_value = Rut(value) # type: ignore[arg-type] + except (AttributeError, TypeError, ValueError): + raise django.core.exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + ) + + if (converted_value is not None and self.validate_dv + and Rut.calc_dv(converted_value.digits) != converted_value.dv): + raise django.core.exceptions.ValidationError( + self.error_messages['invalid_dv'], + code='invalid_dv', + ) + + return converted_value diff --git a/requirements/base.txt b/requirements/base.txt index 04ec48eb..e36a92c6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -17,21 +17,23 @@ signxml==2.8.1 # - cffi: # - pycparser # - six -# - jsonschema +# - jsonschema: # - attrs # - importlib-metadata (python_version<'3.8') # - zipp # - pyrsistent # - setuptools # - six +# - pyOpenSSL: +# - cryptography +# - six # - signxml: # - certifi # - cryptography -# - eight +# - eight: # - future # - lxml # - pyOpenSSL -# - six attrs==20.3.0 certifi==2020.12.5 cffi==1.14.0 @@ -42,4 +44,4 @@ pycparser==2.20 pyrsistent==0.17.3 # setuptools six==1.15.0 -zipp==3.4.0 +zipp==3.4.0 ; python_version < "3.8" diff --git a/tests/test_extras_dj_form_fields.py b/tests/test_extras_dj_form_fields.py new file mode 100644 index 00000000..5f217683 --- /dev/null +++ b/tests/test_extras_dj_form_fields.py @@ -0,0 +1,71 @@ +import unittest + +import django.core.exceptions + +from cl_sii.extras.dj_form_fields import Rut, RutField + + +class RutFieldTest(unittest.TestCase): + valid_rut_canonical: str + valid_rut_instance: Rut + valid_rut_verbose_leading_zero_lowercase: str + + @classmethod + def setUpClass(cls) -> None: + cls.invalid_rut_canonical = '60803000K' + 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 + ) + cls.valid_rut_verbose_leading_zero_lowercase = '060.803.000-k' + + def test_clean_value_of_invalid_canonical_str(self) -> None: + rut_field = RutField() + with self.assertRaises(django.core.exceptions.ValidationError) as cm: + rut_field.clean(self.invalid_rut_canonical) + self.assertEqual(cm.exception.code, 'invalid') + + def test_clean_value_of_canonical_str(self) -> None: + rut_field = RutField() + cleaned_value = rut_field.clean(self.valid_rut_canonical) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) + + def test_clean_value_of_non_canonical_str(self) -> None: + rut_field = RutField() + cleaned_value = rut_field.clean(self.valid_rut_verbose_leading_zero_lowercase) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) + + def test_clean_value_of_Rut(self) -> None: + rut_field = RutField() + cleaned_value = rut_field.clean(self.valid_rut_instance) + self.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) + + 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.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.assertIsInstance(cleaned_value, Rut) + self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical_with_invalid_dv) + + def test_clean_of_empty_value_if_not_required(self) -> None: + rut_field = RutField(required=False) + for value in RutField.empty_values: + cleaned_value = rut_field.clean(value) + self.assertIsNone(cleaned_value) + + def test_clean_of_empty_value_if_required(self) -> None: + rut_field = RutField() + for value in RutField.empty_values: + with self.assertRaises(django.core.exceptions.ValidationError) as cm: + rut_field.clean(value) + self.assertEqual(cm.exception.code, 'required')