misc: rely on phonenumbers library for is_french_mobile_phone_number (#72793) #1037

Open
fpeters wants to merge 1 commits from wip/72793-french-mobile-number-templatetag into main
3 changed files with 65 additions and 51 deletions

View File

@ -864,6 +864,9 @@ def test_is_french_mobile_phone_number(pub):
assert t.render({'number': '06.23.45.67.89'}) == 'True'
assert t.render({'number': '0 6 2 3 45 67 89'}) == 'True'
assert t.render({'number': '+33 6 23 45 67 89'}) == 'True'
assert t.render({'number': '+262 6 92 11 22 33'}) == 'True'
t = Template('{% if number|is_french_mobile_phone_number %}ok{% endif %}')
assert t.render({'number': '06 23 45 67 8989'}) == ''
assert t.render({'number': '06 23 45 67 89'}) == 'ok'

View File

@ -853,15 +853,19 @@ def is_ascii_digit(string_value):
return string_value and all(x in '0123456789' for x in string_value)
fpeters marked this conversation as resolved Outdated
Outdated
Review

Dans les commentaires, quelque part, ici ou ailleurs, je verrais bien une copie de ce que tu as écrit à côté, parce que c'est utile de se le rappeler dans ce fratas de code:

glossaire : code région, c'est BE pour la Belgique, RE pour la Réunion, etc. code pays c'est l'indicatif, 32 pour la Belgique, 262 pour la Réunion, etc.

Dans les commentaires, quelque part, ici ou ailleurs, je verrais bien une copie de ce que tu as écrit à côté, parce que c'est utile de se le rappeler dans ce fratas de code: > glossaire : code région, c'est BE pour la Belgique, RE pour la Réunion, etc. code pays c'est l'indicatif, 32 pour la Belgique, 262 pour la Réunion, etc.
def validate_phone_fr(string_value):
if not re.match(r'^[0\+][\d\.\s]+$', string_value):
def get_valid_phone_number(string_value, region_codes=None, country_codes=None):
# get string_value as a valid phonenumber in default or specified region_codes,
# with additional check against country_codes if given.
#
# region_codes are strings like BE for Belgium, RE for La Reunion, etc.
# country_codes are numeric codes like 32 for Belgium, 262 for La Reunion, etc.
if not re.match(r'^[0\+][\(\)\d\.\s]+$', string_value or ''):
# leading zero or +, then digits, dots, or spaces
return False
return None
french_country_codes = [33, 262, 508, 590, 594, 596]
french_region_codes = [phonenumbers.region_code_for_country_code(x) for x in french_country_codes]
for region_code in french_region_codes:
region_codes = region_codes or [get_publisher().get_phone_local_region_code()]
for region_code in region_codes:
pn = None
try:
pn = phonenumbers.parse(string_value, region_code)
@ -869,65 +873,73 @@ def validate_phone_fr(string_value):
continue
if not phonenumbers.is_valid_number(pn):
continue
fpeters marked this conversation as resolved Outdated
Outdated
Review

Pour éviter de rechercher l'info, je proposerais ici de rappeler la signification de ces 6 indicatifs:

country_codes = [
   # France has 6 country codes in E.164 system :
    33,  # Metropolitan France (FR)
    262,  #  Réunion (RE)
    508,  #  Saint-Pierre-et-Miquelon (PM)
    590,  #  Guadeloupe, Saint-Barthélemy, Saint-Martin (GP)
    594,  #  Guyanne (GF)
    596,  #  Martinique (MQ)
]

éventuellement pour rappeler/sous-entendre que ça ne gère pas Nouvelle-Calédonie, Wallis-et-Futuna, Polynésie française et autres endroits qui ne sont pas partie du "plan de numérotation français".

Pour éviter de rechercher l'info, je proposerais ici de rappeler la signification de ces 6 indicatifs: ``` country_codes = [ # France has 6 country codes in E.164 system : 33, # Metropolitan France (FR) 262, # Réunion (RE) 508, # Saint-Pierre-et-Miquelon (PM) 590, # Guadeloupe, Saint-Barthélemy, Saint-Martin (GP) 594, # Guyanne (GF) 596, # Martinique (MQ) ] ``` éventuellement pour rappeler/sous-entendre que ça ne gère pas Nouvelle-Calédonie, Wallis-et-Futuna, Polynésie française et autres endroits qui ne sont pas partie du "plan de numérotation français".
if pn.country_code not in french_country_codes:
if country_codes and pn.country_code not in country_codes:
continue
return True
return False
return pn
return None
def validate_mobile_phone_local(string_value):
if not re.match(r'^[0\+][\d\.\s]+$', string_value):
# leading zero or +, then digits, dots, or spaces
return False
region_code = get_publisher().get_phone_local_region_code()
country_codes = []
def get_french_country_and_region_codes():
country_codes = [
# France has 6 country codes in E.164 system :
33, # Metropolitan France (FR)
262, # Réunion (RE)
508, # Saint-Pierre-et-Miquelon (PM)
590, # Guadeloupe, Saint-Barthélemy, Saint-Martin (GP)
594, # Guyanne (GF)
596, # Martinique (MQ)
]
region_codes = [phonenumbers.region_code_for_country_code(x) for x in country_codes]
local_region_code = get_publisher().get_phone_local_region_code()
if local_region_code in region_codes:
# set parsing preference to configured local region code
region_codes = [local_region_code] + [x for x in region_codes if x != local_region_code]
return country_codes, region_codes
def validate_phone_fr(string_value):
country_codes, region_codes = get_french_country_and_region_codes()
pn = get_valid_phone_number(string_value, region_codes=region_codes, country_codes=country_codes)
return bool(pn)
def validate_mobile_phone_local(string_value, region_code=None):
region_code = region_code or get_publisher().get_phone_local_region_code()
if region_code == 'FR':
country_codes = [33, 262, 508, 590, 594, 596]
# in case of France extend list of country/region codes, so it's not just
# metropolitan France.
country_codes, region_codes = get_french_country_and_region_codes()
else:
country_codes = []
for country_code, region_codes in phonenumbers.COUNTRY_CODE_TO_REGION_CODE.items():
if region_code in region_codes:
country_codes = [country_code]
break
pn = None
try:
pn = phonenumbers.parse(string_value, region_code)
except phonenumbers.NumberParseException:
return False
region_codes = [region_code]
pn = get_valid_phone_number(string_value, region_codes, country_codes)
return bool(
phonenumbers.is_valid_number(pn)
pn
and phonenumbers.number_type(pn)
in (phonenumbers.PhoneNumberType.MOBILE, phonenumbers.PhoneNumberType.FIXED_LINE_OR_MOBILE)
and (pn.country_code in country_codes or not country_codes)
)
def get_formatted_phone(number, country_code=None):
if not country_code:
country_code = get_publisher().get_phone_local_region_code()
try:
pn = phonenumbers.parse(number)
except phonenumbers.NumberParseException:
try:
pn = phonenumbers.parse(number, country_code)
except phonenumbers.NumberParseException:
return number
if phonenumbers.is_valid_number(pn):
def get_formatted_phone(number, region_code=None):
if not region_code:
region_code = get_publisher().get_phone_local_region_code()
pn = get_valid_phone_number(number, [region_code])
if pn:
return phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.NATIONAL)
else:
return number
def normalize_phone_number_for_fts(value):
country_code = get_publisher().get_phone_local_region_code()
try:
pn = phonenumbers.parse(value)
except phonenumbers.NumberParseException:
try:
pn = phonenumbers.parse(value, country_code)
except phonenumbers.NumberParseException:
return value
return phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.E164)
def normalize_phone_number_for_fts(number):
pn = get_valid_phone_number(number)
if pn:
return phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.E164)
else:
return number
def validate_siren(string_value):

View File

@ -59,7 +59,7 @@ from django.utils.timezone import is_naive, localtime, make_aware, make_naive
from wcs.qommon import _, calendar, evalutils, upload_storage
from wcs.qommon.admin.texts import TextsDirectory
from wcs.qommon.humantime import seconds2humanduration
from wcs.qommon.misc import parse_decimal, strip_some_tags, unlazy, validate_phone_fr
from wcs.qommon.misc import parse_decimal, strip_some_tags, unlazy, validate_mobile_phone_local
from wcs.qommon.template import TemplateError
register = template.Library()
@ -1052,16 +1052,15 @@ def phonenumber_fr(value, separator=' '):
@register.filter
def is_french_mobile_phone_number(value):
# check the given value is a valid French mobile phone number (Metropolitan
# or overseas, with support for local prefixes).
value = unlazy(value)
if not value:
return False
value = value.strip().replace(' ', '')
if not validate_phone_fr(value):
return False
return value.startswith('06') or value.startswith('07')
return validate_mobile_phone_local(value, region_code='FR')
@register.filter