diff --git a/docs/changelog.rst b/docs/changelog.rst index 6778816fb..6fe95883e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,7 +13,7 @@ New fields for existing flavors: Modifications to existing flavors: -- None +- Deprecated `generic.checksums.luhn` and `generic.checksums.ean`. Please use the python-stdnum library instead. Other changes: diff --git a/localflavor/ca/forms.py b/localflavor/ca/forms.py index e61c6f151..555ccfd5e 100644 --- a/localflavor/ca/forms.py +++ b/localflavor/ca/forms.py @@ -8,8 +8,7 @@ from django.forms import ValidationError from django.forms.fields import CharField, Field, Select from django.utils.translation import ugettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn sin_re = re.compile(r"^(\d{3})-(\d{3})-(\d{3})$") @@ -113,6 +112,6 @@ def clean(self, value): match.group(1), match.group(2), match.group(3)) - if not luhn(check_number): + if not luhn.is_valid(check_number): raise ValidationError(self.error_messages['invalid']) return number diff --git a/localflavor/fr/forms.py b/localflavor/fr/forms.py index 566584191..c46d78d62 100644 --- a/localflavor/fr/forms.py +++ b/localflavor/fr/forms.py @@ -5,12 +5,10 @@ import re from datetime import date -from django.core.validators import EMPTY_VALUES from django.forms import ValidationError from django.forms.fields import CharField, RegexField, Select from django.utils.translation import ugettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn from .fr_department import DEPARTMENT_CHOICES_PER_REGION from .fr_region import REGION_2016_CHOICES, REGION_CHOICES @@ -189,7 +187,7 @@ def clean(self, value): return self.empty_value value = value.replace(' ', '').replace('-', '') - if not self.r_valid.match(value) or not luhn(value): + if not self.r_valid.match(value) or not luhn.is_valid(value): raise ValidationError(self.error_messages['invalid']) return value @@ -234,14 +232,15 @@ class FRSIRETField(FRSIRENENumberMixin, CharField): } def clean(self, value): - if value not in EMPTY_VALUES: - value = value.replace(' ', '').replace('-', '') + value = super(FRSIRETField, self).clean(value) + if value in self.empty_values: + return self.empty_value - ret = super(FRSIRETField, self).clean(value) + value = value.replace(' ', '').replace('-', '') - if ret is not None and not luhn(ret[:9]): + if not luhn.is_valid(value[:9]): raise ValidationError(self.error_messages['invalid']) - return ret + return value def prepare_value(self, value): if value is None: diff --git a/localflavor/generic/checksums.py b/localflavor/generic/checksums.py index 6272964fd..aedf3fa4d 100644 --- a/localflavor/generic/checksums.py +++ b/localflavor/generic/checksums.py @@ -1,10 +1,13 @@ """Common checksum routines.""" +import warnings + from django.utils import six +from stdnum import ean as stdnum_ean +from stdnum import luhn as stdnum_luhn -__all__ = ['luhn', 'ean'] +from localflavor.deprecation import RemovedInLocalflavor30Warning -LUHN_ODD_LOOKUP = (0, 2, 4, 6, 8, 1, 3, 5, 7, 9) # sum_of_digits(index * 2) -EAN_LOOKUP = (3, 1) +__all__ = ['luhn', 'ean'] def luhn(candidate): @@ -13,15 +16,25 @@ def luhn(candidate): Luhn algorithm is used in validation of, for example, credit cards. Both numeric and string candidates are accepted. + + .. deprecated:: 2.2 + Use the luhn function in the python-stdnum_ library instead. + + .. _python-stdnum: https://arthurdejong.org/python-stdnum/ """ + warnings.warn( + 'luhn is deprecated in favor of the luhn function in the python-stdnum library.', + RemovedInLocalflavor30Warning, + ) + if not isinstance(candidate, six.string_types): candidate = str(candidate) - try: - evens = sum(int(c) for c in candidate[-1::-2]) - odds = sum(LUHN_ODD_LOOKUP[int(c)] for c in candidate[-2::-2]) - return ((evens + odds) % 10 == 0) - except ValueError: # Raised if an int conversion fails - return False + + # Our version returned True for empty strings. + if candidate == '': + return True + + return stdnum_luhn.is_valid(candidate) def ean(candidate): @@ -31,16 +44,18 @@ def ean(candidate): Note that this validator does not enforce any length checks (usually 13 or 8). http://en.wikipedia.org/wiki/International_Article_Number_(EAN) + + .. deprecated:: 2.2 + Use the ean function in the python-stdnum_ library instead. + + .. _python-stdnum: https://arthurdejong.org/python-stdnum/ """ + warnings.warn( + 'ean is deprecated in favor of the ean function in the python-stdnum library.', + RemovedInLocalflavor30Warning, + ) + if not isinstance(candidate, six.string_types): candidate = str(candidate) - if len(candidate) <= 1: - return False - given_number, given_checksum = candidate[:-1], candidate[-1] - try: - calculated_checksum = sum( - int(digit) * EAN_LOOKUP[i % 2] for i, digit in enumerate(reversed(given_number))) - calculated_checksum = 9 - ((calculated_checksum - 1) % 10) - return str(calculated_checksum) == given_checksum - except ValueError: # Raised if an int conversion fails - return False + + return stdnum_ean.is_valid(candidate) diff --git a/localflavor/generic/validators.py b/localflavor/generic/validators.py index b70539a1b..a4d56ab5b 100644 --- a/localflavor/generic/validators.py +++ b/localflavor/generic/validators.py @@ -7,8 +7,8 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils.deconstruct import deconstructible from django.utils.translation import ugettext_lazy as _ +from stdnum import ean -from . import checksums from .countries.iso_3166 import ISO_3166_1_ALPHA2_COUNTRY_CODES # Dictionary of ISO country code to IBAN length. @@ -272,7 +272,7 @@ def __call__(self, value): return value if self.strip_nondigits: value = re.compile(r'[^\d]+').sub('', value) - if not checksums.ean(value): + if not ean.is_valid(value): raise ValidationError(self.message, code='invalid') diff --git a/localflavor/gr/forms.py b/localflavor/gr/forms.py index d2f97b6b7..9c9bb6e0d 100644 --- a/localflavor/gr/forms.py +++ b/localflavor/gr/forms.py @@ -6,8 +6,7 @@ from django.forms import Field, RegexField, ValidationError from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn class GRPostalCodeField(RegexField): @@ -100,7 +99,7 @@ def clean(self, value): raise ValidationError(self.error_messages['invalid']) self.check_date(val) - if not luhn(val): + if not luhn.is_valid(val): raise ValidationError(self.error_messages['invalid']) return val diff --git a/localflavor/hr/forms.py b/localflavor/hr/forms.py index 175f09869..f6829bcef 100644 --- a/localflavor/hr/forms.py +++ b/localflavor/hr/forms.py @@ -10,8 +10,7 @@ from django.forms.fields import Field, RegexField, Select from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn from .hr_choices import HR_COUNTY_CHOICES, HR_LICENSE_PLATE_PREFIX_CHOICES @@ -227,7 +226,7 @@ def clean(self, value): raise ValidationError(self.error_messages['copy']) # Validate checksum using Luhn algorithm. - if not luhn(value): + if not luhn.is_valid(value): raise ValidationError(self.error_messages['invalid']) return '%s' % value diff --git a/localflavor/il/forms.py b/localflavor/il/forms.py index 99021c1cc..b9e616d18 100644 --- a/localflavor/il/forms.py +++ b/localflavor/il/forms.py @@ -7,8 +7,7 @@ from django.core.validators import EMPTY_VALUES from django.forms.fields import Field, RegexField from django.utils.translation import ugettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn id_number_re = re.compile(r'^(?P\d{1,8})-?(?P\d)$') @@ -66,6 +65,6 @@ def clean(self, value): raise ValidationError(self.error_messages['invalid']) value = match.group('number') + match.group('check') - if not luhn(value): + if not luhn.is_valid(value): raise ValidationError(self.error_messages['invalid']) return value diff --git a/localflavor/za/forms.py b/localflavor/za/forms.py index 5b4133f1c..c639a16cf 100644 --- a/localflavor/za/forms.py +++ b/localflavor/za/forms.py @@ -7,8 +7,7 @@ from django.forms import ValidationError from django.forms.fields import CharField, RegexField, Select from django.utils.translation import gettext_lazy as _ - -from localflavor.generic.checksums import luhn +from stdnum import luhn id_re = re.compile(r'^(?P\d\d)(?P\d\d)(?P
\d\d)(?P\d{4})(?P\d{3})') @@ -49,7 +48,7 @@ def clean(self, value): except ValueError: raise ValidationError(self.error_messages['invalid']) - if not luhn(value): + if not luhn.is_valid(value): raise ValidationError(self.error_messages['invalid']) return value diff --git a/setup.py b/setup.py index 0f82e7f2d..b59970239 100644 --- a/setup.py +++ b/setup.py @@ -119,7 +119,10 @@ def find_package_data(where='.', package='', author_email='foundation@djangoproject.com', packages=find_packages(exclude=['tests', 'tests.*']), package_data=find_package_data(), - install_requires=['django>=1.11'], + install_requires=[ + 'django>=1.11', + 'python-stdnum>=1.0', + ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment',