Source code for localflavor.fr.forms

"""FR-specific Form helpers"""
import re
from datetime import date

from django.forms import ValidationError
from django.forms.fields import CharField, RegexField, Select
from django.utils.translation import gettext_lazy as _
from stdnum import luhn

from .fr_department import DEPARTMENT_CHOICES_PER_REGION
from .fr_region import REGION_2016_CHOICES, REGION_CHOICES

nin_re = re.compile(
    r'^(?P<gender>[1278])(?P<year_of_birth>\d{2})(?P<month_of_birth>0[1-9]|1[0-2]|20|3[0-9]|4[0-2]|[5-9][0-9])'
    r'(?P<department_of_origin>\d{2}|2[AB])(?P<commune_of_origin>\d{3})(?P<person_unique_number>\d{3})'
    r'(?P<control_key>\d{2})$')


[docs]class FRZipCodeField(RegexField): """ Validate local French zip code. The correct format is 'XXXXX'. """ default_error_messages = { 'invalid': _('Enter a zip code in the format XXXXX.'), } def __init__(self, **kwargs): kwargs.setdefault('label', _('Zip code')) kwargs['max_length'] = 5 kwargs['min_length'] = 5 super().__init__(r'^\d{5}$', **kwargs)
[docs]class FRDepartmentSelect(Select): """A Select widget that uses a list of FR departments as its choices.""" def __init__(self, attrs=None): choices = [ (dep[0], '%s - %s' % (dep[0], dep[1])) for dep in DEPARTMENT_CHOICES_PER_REGION ] super().__init__(attrs, choices=choices)
[docs]class FRRegionSelect(Select): """A Select widget that uses a list of FR Regions as its choices.""" def __init__(self, attrs=None): choices = [ (dep[0], '%s - %s' % (dep[0], dep[1])) for dep in REGION_CHOICES ] super().__init__(attrs, choices=choices)
[docs]class FRRegion2016Select(Select): """ A Select widget that uses a list of France's New Regions as its choices. """ def __init__(self, attrs=None): choices = [ (reg[0], '%s - %s' % (reg[0], reg[1])) for reg in REGION_2016_CHOICES ] super().__init__(attrs, choices=choices)
[docs]class FRDepartmentField(CharField): """A Select Field that uses a FRDepartmentSelect widget.""" widget = FRDepartmentSelect def __init__(self, **kwargs): kwargs.setdefault('label', _('Select Department')) super().__init__(**kwargs)
[docs]class FRRegionField(CharField): """A Select Field that uses a FRRegionSelect widget.""" widget = FRRegionSelect def __init__(self, **kwargs): kwargs.setdefault('label', _('Select Region')) super().__init__(**kwargs)
[docs]class FRNationalIdentificationNumber(CharField): """ Validates input as a French National Identification number. Validation of the Number, and checksum calculation is detailed at http://en.wikipedia.org/wiki/INSEE_code Complete spec of the codification is detailed here: - https://fr.scribd.com/document/456848429/INSEE-Guide-Identification - https://fr.scribd.com/document/456848431/INSEE-Codes-Pays .. versionadded:: 1.1 """ default_error_messages = { 'invalid': _('Enter a valid French National Identification number.'), }
[docs] def clean(self, value): value = super().clean(value) if value in self.empty_values: return value value = value.replace(' ', '').replace('-', '') match = nin_re.match(value) if not match: raise ValidationError(self.error_messages['invalid'], code='invalid') # Extract all parts of social number gender = match.group('gender') year_of_birth = match.group('year_of_birth') month_of_birth = match.group('month_of_birth') department_of_origin = match.group('department_of_origin') commune_of_origin = match.group('commune_of_origin') person_unique_number = match.group('person_unique_number') control_key = int(match.group('control_key')) # Get current year current_year = int(str(date.today().year)[2:]) commune_of_origin, department_of_origin = self._clean_department_and_commune(commune_of_origin, current_year, department_of_origin, year_of_birth) if person_unique_number == '000': raise ValidationError(self.error_messages['invalid'], code='invalid') if control_key > 97: raise ValidationError(self.error_messages['invalid'], code='invalid') control_number = int(gender + year_of_birth + month_of_birth + department_of_origin.replace('A', '0').replace('B', '0') + commune_of_origin + person_unique_number) if (97 - control_number % 97) == control_key: return value else: raise ValidationError(self.error_messages['invalid'], code='invalid')
def _clean_department_and_commune(self, commune_of_origin, current_year, department_of_origin, year_of_birth): if department_of_origin in ['20', '2A', '2B']: self._check_corsica(commune_of_origin, current_year, department_of_origin, year_of_birth) elif department_of_origin in ['97', '98']: self._check_overseas(commune_of_origin, current_year, department_of_origin, year_of_birth) elif department_of_origin == '99': self._check_foreign_countries(commune_of_origin, current_year, department_of_origin, year_of_birth) return commune_of_origin, department_of_origin def _check_corsica(self, commune_of_origin, current_year, department_of_origin, year_of_birth): """Departments number 20, 2A and 2B represent Corsica""" # For people born before 1976, Corsica number was 20 if current_year < int(year_of_birth) < 76 and department_of_origin != '20': raise ValidationError(self.error_messages['invalid'], code='invalid') # For people born from 1976, Corsica dep number is either 2A or 2B if (int(year_of_birth) > 75 and department_of_origin not in ['2A', '2B']): raise ValidationError(self.error_messages['invalid'], code='invalid') def _check_overseas(self, commune_of_origin, current_year, department_of_origin, year_of_birth): """Overseas department numbers starts with 97 or 98 and are 3 digits long""" overseas_department_of_origin = department_of_origin + commune_of_origin[:1] overseas_commune_of_origin = commune_of_origin[1:] if department_of_origin == '97' and int(overseas_department_of_origin) not in range(971, 978): raise ValidationError(self.error_messages['invalid'], code='invalid') elif department_of_origin == '98' and int(overseas_department_of_origin) not in range(984, 989): raise ValidationError(self.error_messages['invalid'], code='invalid') if int(overseas_commune_of_origin) < 1 or int(overseas_commune_of_origin) > 90: raise ValidationError(self.error_messages['invalid'], code='invalid') def _check_foreign_countries(self, commune_of_origin, current_year, department_of_origin, year_of_birth): """ The department_of_origin '99' is reserved for people born in a foreign country. In this case, commune_of_origin is the INSEE country code, must be [001-990] """ if int(commune_of_origin) < 1 or int(commune_of_origin) > 990: raise ValidationError(self.error_messages['invalid'], code='invalid')
class FRSIRENENumberMixin: """Abstract class for SIREN and SIRET numbers, from the SIRENE register.""" def clean(self, value): value = super().clean(value) if value in self.empty_values: return value value = value.replace(' ', '').replace('-', '') if not self.r_valid.match(value) or not luhn.is_valid(value): raise ValidationError(self.error_messages['invalid'], code='invalid') return value
[docs]class FRSIRENField(FRSIRENENumberMixin, CharField): """ SIREN stands for "Système d'identification du répertoire des entreprises". It's under authority of the INSEE. See http://fr.wikipedia.org/wiki/Système_d'identification_du_répertoire_des_entreprises for more information. .. versionadded:: 1.1 """ r_valid = re.compile(r'^\d{9}$') default_error_messages = { 'invalid': _('Enter a valid French SIREN number.'), } def prepare_value(self, value): if value is None: return value value = value.replace(' ', '').replace('-', '') return ' '.join((value[:3], value[3:6], value[6:]))
[docs]class FRSIRETField(FRSIRENENumberMixin, CharField): """ SIRET stands for "Système d'identification du répertoire des établissements". It's under authority of the INSEE. See http://fr.wikipedia.org/wiki/Système_d'identification_du_répertoire_des_établissements for more information. .. versionadded:: 1.1 """ r_valid = re.compile(r'^\d{14}$') default_error_messages = { 'invalid': _('Enter a valid French SIRET number.'), }
[docs] def clean(self, value): value = super().clean(value) if value in self.empty_values: return value value = value.replace(' ', '').replace('-', '') if not luhn.is_valid(value[:9]): raise ValidationError(self.error_messages['invalid'], code='invalid') return value
def prepare_value(self, value): if value is None: return value value = value.replace(' ', '').replace('-', '') return ' '.join((value[:3], value[3:6], value[6:9], value[9:]))
[docs]class FRRNAField(CharField): """ RNA Stands for "Répertoire National des Associations" It's under the authority of the French Minister of the Interior. See https://fr.wikipedia.org/wiki/R%C3%A9pertoire_national_des_associations for more information. .. versionadded:: 4.0 """ default_error_messages = { 'invalid': _('Enter a valid French RNA number.'), } regex = re.compile(r'^W\d{9}$')
[docs] def clean(self, value): value = super().clean(value) if value in self.empty_values: return self.empty_value value = value.replace(' ', '').replace('-', '').replace('.', '') if not self.regex.match(value): raise ValidationError(self.error_messages['invalid'], code=['invalid']) return value