From ef3db07d81099e0a10b330750fb39ec0e544b0d8 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 23 Jul 2019 17:40:14 +0200 Subject: [PATCH] fields: add Luhn algorithm to string field validation (#35013) --- tests/test_widgets.py | 38 ++++++++++++++++++++++++++++++++++++++ wcs/qommon/form.py | 5 +++++ wcs/qommon/misc.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index d1481182..03ad0ccd 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -630,6 +630,44 @@ def test_wcsextrastringwidget_builtin_validation(): mock_form_submission(req, widget, {'test': '1234'}) assert widget.has_error() +def test_wcsextrastringwidget_siren_validation(): + class FakeField: pass + fakefield = FakeField() + fakefield.validation = {'type': 'siren-fr'} + + widget = WcsExtraStringWidget('test', value='foo', required=False) + widget.field = fakefield + mock_form_submission(req, widget, {'test': '443170139'}) + assert not widget.has_error() + + widget = WcsExtraStringWidget('test', value='foo', required=False) + widget.field = fakefield + mock_form_submission(req, widget, {'test': '443170130'}) + assert widget.has_error() + +def test_wcsextrastringwidget_siret_validation(): + class FakeField: pass + fakefield = FakeField() + fakefield.validation = {'type': 'siret-fr'} + + # regular case + widget = WcsExtraStringWidget('test', value='foo', required=False) + widget.field = fakefield + mock_form_submission(req, widget, {'test': '44317013900036'}) + assert not widget.has_error() + + # special case la poste + widget = WcsExtraStringWidget('test', value='foo', required=False) + widget.field = fakefield + mock_form_submission(req, widget, {'test': '35600000000048'}) + assert not widget.has_error() + + # failing case + widget = WcsExtraStringWidget('test', value='foo', required=False) + widget.field = fakefield + mock_form_submission(req, widget, {'test': '44317013900037'}) + assert widget.has_error() + def test_wcsextrastringwidget_django_validation(): class FakeField: pass fakefield = FakeField() diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index 2a7702f9..86021aa5 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -906,6 +906,8 @@ class ValidationWidget(CompositeWidget): ('digits', {'title': N_('Digits'), 'regex': '\d+'}), ('phone-fr', {'title': N_('Phone Number (France)'), 'regex': '0[\d\.\s]{9}'}), ('zipcode-fr', {'title': N_('Zip Code (France)'), 'regex': '\d{5}'}), + ('siren-fr', {'title': N_('SIREN Code (France)'), 'function': 'validate_siren'}), + ('siret-fr', {'title': N_('SIRET Code (France)'), 'function': 'validate_siret'}), ('regex', {'title': N_('Regular Expression')}), ('django', {'title': N_('Django Condition')}), ]) @@ -965,6 +967,9 @@ class ValidationWidget(CompositeWidget): condition = ValidationCondition(validation['value'], value=value) return condition.evaluate() return django_validation + validation_method = cls.validation_methods.get(validation['type']) + if 'function' in validation_method: + return getattr(misc, validation_method['function']) @classmethod def get_validation_pattern(cls, validation): diff --git a/wcs/qommon/misc.py b/wcs/qommon/misc.py index 3a441bf2..3c4bcf7d 100644 --- a/wcs/qommon/misc.py +++ b/wcs/qommon/misc.py @@ -639,3 +639,36 @@ def html2text(text): if isinstance(text, (htmltext, str)): text = unicode(str(text), get_publisher().site_charset) return site_encode(HTMLParser().unescape(strip_tags(text))) + + +def validate_luhn(string_value, length=None): + '''Verify Luhn checksum on a string representing a number''' + if not string_value: + return False + if length is not None and len(string_value) != length: + return False + + # take all digits counting from the right, double value for digits pair + # index (counting from 1), if double has 2 digits take their sum + checksum = 0 + for i, x in enumerate(reversed(string_value)): + if i % 2 == 0: + checksum += int(x) + else: + checksum += sum(int(y) for y in str(2 * int(x))) + if checksum % 10 != 0: + return False + return True + + +def validate_siren(string_value): + return validate_luhn(string_value, length=9) + + +def validate_siret(string_value): + # special case : La Poste + if (string_value.startswith('356000000') + and len(string_value) == 14 + and sum(int(x) for x in string_value) % 5 == 0): + return True + return validate_luhn(string_value, length=14) -- 2.23.0.rc1