diff --git a/AUTHORS b/AUTHORS index a2cf8c68cc..ec4a222a4e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -44,6 +44,7 @@ answer newbie questions, and generally made Django that much better: adurdin@gmail.com Andreas andy@jadedplanet.net + Fabrice Aneche <akh@nobugware.com> ant9000@netwise.it David Ascher <http://ascher.ca/> Arthur <avandorp@gmail.com> diff --git a/django/contrib/localflavor/fr/__init__.py b/django/contrib/localflavor/fr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/fr/forms.py b/django/contrib/localflavor/fr/forms.py new file mode 100644 index 0000000000..ee87c5cda2 --- /dev/null +++ b/django/contrib/localflavor/fr/forms.py @@ -0,0 +1,44 @@ +""" +FR-specific Form helpers +""" + +from django.newforms import ValidationError +from django.newforms.fields import Field, RegexField, Select, EMPTY_VALUES +from django.newforms.util import smart_unicode +from django.utils.translation import gettext +import re + +phone_digits_re = re.compile(r'^0\d(\s|\.)?(\d{2}(\s|\.)?){3}\d{2}$') + +class FRZipCodeField(RegexField): + def __init__(self, *args, **kwargs): + super(FRZipCodeField, self).__init__(r'^\d{5}$', + max_length=None, min_length=None, + error_message=gettext(u'Enter a zip code in the format XXXXX.'), + *args, **kwargs) + +class FRPhoneNumberField(Field): + """ + Validate local French phone number (not international ones) + The correct format is '0X XX XX XX XX'. + '0X.XX.XX.XX.XX' and '0XXXXXXXXX' validate but are corrected to + '0X XX XX XX XX'. + """ + def clean(self, value): + super(FRPhoneNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + value = re.sub('(\.|\s)', '', smart_unicode(value)) + m = phone_digits_re.search(value) + if m: + return u'%s %s %s %s %s' % (value[0:2], value[2:4], value[4:6], value[6:8], value[8:10]) + raise ValidationError(u'Phone numbers must be in 0X XX XX XX XX format.') + +class FRDepartmentSelect(Select): + """ + A Select widget that uses a list of FR departments as its choices. + """ + def __init__(self, attrs=None): + from fr_department import DEPARTMENT_ASCII_CHOICES # relative import + super(FRDepartmentSelect, self).__init__(attrs, choices=DEPARTMENT_ASCII_CHOICES) + diff --git a/django/contrib/localflavor/fr/fr_department.py b/django/contrib/localflavor/fr/fr_department.py new file mode 100644 index 0000000000..726cc7b13a --- /dev/null +++ b/django/contrib/localflavor/fr/fr_department.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +DEPARTMENT_ASCII_CHOICES = ( +('01', '01 - Ain'), +('02', '02 - Aisne'), +('03', '03 - Allier'), +('04', '04 - Alpes-de-Haute-Provence'), +('05', '05 - Hautes-Alpes'), +('06', '06 - Alpes-Maritimes'), +('07', '07 - Ardeche'), +('08', '08 - Ardennes'), +('09', '09 - Ariege'), +('10', '10 - Aube'), +('11', '11 - Aude'), +('12', '12 - Aveyron'), +('13', '13 - Bouches-du-Rhone'), +('14', '14 - Calvados'), +('15', '15 - Cantal'), +('16', '16 - Charente'), +('17', '17 - Charente-Maritime'), +('18', '18 - Cher'), +('19', '19 - Correze'), +('21', '21 - Cote-d\'Or'), +('22', '22 - Cotes-d\'Armor'), +('23', '23 - Creuse'), +('24', '24 - Dordogne'), +('25', '25 - Doubs'), +('26', '26 - Drome'), +('27', '27 - Eure'), +('28', '28 - Eure-et-Loire'), +('29', '29 - Finistere'), +('2A', '2A - Corse-du-Sud'), +('2B', '2B - Haute-Corse'), +('30', '30 - Gard'), +('31', '31 - Haute-Garonne'), +('32', '32 - Gers'), +('33', '33 - Gironde'), +('34', '34 - Herault'), +('35', '35 - Ille-et-Vilaine'), +('36', '36 - Indre'), +('37', '37 - Indre-et-Loire'), +('38', '38 - Isere'), +('39', '39 - Jura'), +('40', '40 - Landes'), +('41', '41 - Loir-et-Cher'), +('42', '42 - Loire'), +('43', '43 - Haute-Loire'), +('44', '44 - Loire-Atlantique'), +('45', '45 - Loiret'), +('46', '46 - Lot'), +('47', '47 - Lot-et-Garonne'), +('48', '48 - Lozere'), +('49', '49 - Maine-et-Loire'), +('50', '50 - Manche'), +('51', '51 - Marne'), +('52', '52 - Haute-Marne'), +('53', '53 - Mayenne'), +('54', '54 - Meurthe-et-Moselle'), +('55', '55 - Meuse'), +('56', '56 - Morbihan'), +('57', '57 - Moselle'), +('58', '58 - Nievre'), +('59', '59 - Nord'), +('60', '60 - Oise'), +('61', '61 - Orne'), +('62', '62 - Pas-de-Calais'), +('63', '63 - Puy-de-Dome'), +('64', '64 - Pyrenees-Atlantiques'), +('65', '65 - Hautes-Pyrenees'), +('66', '66 - Pyrenees-Orientales'), +('67', '67 - Bas-Rhin'), +('68', '68 - Haut-Rhin'), +('69', '69 - Rhone'), +('70', '70 - Haute-Saone'), +('71', '71 - Saone-et-Loire'), +('72', '72 - Sarthe'), +('73', '73 - Savoie'), +('74', '74 - Haute-Savoie'), +('75', '75 - Paris'), +('76', '76 - Seine-Maritime'), +('77', '77 - Seine-et-Marne'), +('78', '78 - Yvelines'), +('79', '79 - Deux-Sevres'), +('80', '80 - Somme'), +('81', '81 - Tarn'), +('82', '82 - Tarn-et-Garonne'), +('83', '83 - Var'), +('84', '84 - Vaucluse'), +('85', '85 - Vendee'), +('86', '86 - Vienne'), +('87', '87 - Haute-Vienne'), +('88', '88 - Vosges'), +('89', '89 - Yonne'), +('90', '90 - Territoire de Belfort'), +('91', '91 - Essonne'), +('92', '92 - Hauts-de-Seine'), +('93', '93 - Seine-Saint-Denis'), +('94', '94 - Val-de-Marne'), +('95', '95 - Val-d\'Oise'), +('2A', '2A - Corse du sud'), +('2B', '2B - Haute Corse'), +('971', '971 - Guadeloupe'), +('972', '972 - Martinique'), +('973', '973 - Guyane'), +('974', '974 - La Reunion'), +('975', '975 - Saint-Pierre-et-Miquelon'), +('976', '976 - Mayotte'), +('984', '984 - Terres Australes et Antarctiques'), +('986', '986 - Wallis et Futuna'), +('987', '987 - Polynesie Francaise'), +('988', '988 - Nouvelle-Caledonie'), +) + diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index a9ce8d23b3..ab10d2f3e3 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -3556,6 +3556,226 @@ u'' >>> f.clean('') u'' +# FRZipCodeField ############################################################# + +FRZipCodeField validates that the data is a valid FR zipcode. +>>> from django.contrib.localflavor.fr.forms import FRZipCodeField +>>> f = FRZipCodeField() +>>> f.clean('75001') +u'75001' +>>> f.clean('93200') +u'93200' +>>> f.clean('2A200') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXXX.'] +>>> f.clean('980001') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXXX.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = FRZipCodeField(required=False) +>>> f.clean('75001') +u'75001' +>>> f.clean('93200') +u'93200' +>>> f.clean('2A200') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXXX.'] +>>> f.clean('980001') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXXX.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' + + +# FRPhoneNumberField ########################################################## + +FRPhoneNumberField validates that the data is a valid french phone number. +It's normalized to 0X XX XX XX XX format. Dots are valid too. +>>> from django.contrib.localflavor.fr.forms import FRPhoneNumberField +>>> f = FRPhoneNumberField() +>>> f.clean('01 55 44 58 64') +u'01 55 44 58 64' +>>> f.clean('0155445864') +u'01 55 44 58 64' +>>> f.clean('01 5544 5864') +u'01 55 44 58 64' +>>> f.clean('01 55.44.58.64') +u'01 55 44 58 64' +>>> f.clean('01.55.44.58.64') +u'01 55 44 58 64' +>>> f.clean('01,55,44,58,64') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in 0X XX XX XX XX format.'] +>>> f.clean('555 015 544') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in 0X XX XX XX XX format.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = FRPhoneNumberField(required=False) +>>> f.clean('01 55 44 58 64') +u'01 55 44 58 64' +>>> f.clean('0155445864') +u'01 55 44 58 64' +>>> f.clean('01 5544 5864') +u'01 55 44 58 64' +>>> f.clean('01 55.44.58.64') +u'01 55 44 58 64' +>>> f.clean('01.55.44.58.64') +u'01 55 44 58 64' +>>> f.clean('01,55,44,58,64') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in 0X XX XX XX XX format.'] +>>> f.clean('555 015 544') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in 0X XX XX XX XX format.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' + +# FRDepartmentSelect ############################################################### + +FRDepartmentSelect is a Select widget that uses a list of french departments +including DOM TOM +>>> from django.contrib.localflavor.fr.forms import FRDepartmentSelect +>>> w = FRDepartmentSelect() +>>> print w.render('dep', 'Paris') +<select name="dep"> +<option value="01">01 - Ain</option> +<option value="02">02 - Aisne</option> +<option value="03">03 - Allier</option> +<option value="04">04 - Alpes-de-Haute-Provence</option> +<option value="05">05 - Hautes-Alpes</option> +<option value="06">06 - Alpes-Maritimes</option> +<option value="07">07 - Ardeche</option> +<option value="08">08 - Ardennes</option> +<option value="09">09 - Ariege</option> +<option value="10">10 - Aube</option> +<option value="11">11 - Aude</option> +<option value="12">12 - Aveyron</option> +<option value="13">13 - Bouches-du-Rhone</option> +<option value="14">14 - Calvados</option> +<option value="15">15 - Cantal</option> +<option value="16">16 - Charente</option> +<option value="17">17 - Charente-Maritime</option> +<option value="18">18 - Cher</option> +<option value="19">19 - Correze</option> +<option value="21">21 - Cote-d'Or</option> +<option value="22">22 - Cotes-d'Armor</option> +<option value="23">23 - Creuse</option> +<option value="24">24 - Dordogne</option> +<option value="25">25 - Doubs</option> +<option value="26">26 - Drome</option> +<option value="27">27 - Eure</option> +<option value="28">28 - Eure-et-Loire</option> +<option value="29">29 - Finistere</option> +<option value="2A">2A - Corse-du-Sud</option> +<option value="2B">2B - Haute-Corse</option> +<option value="30">30 - Gard</option> +<option value="31">31 - Haute-Garonne</option> +<option value="32">32 - Gers</option> +<option value="33">33 - Gironde</option> +<option value="34">34 - Herault</option> +<option value="35">35 - Ille-et-Vilaine</option> +<option value="36">36 - Indre</option> +<option value="37">37 - Indre-et-Loire</option> +<option value="38">38 - Isere</option> +<option value="39">39 - Jura</option> +<option value="40">40 - Landes</option> +<option value="41">41 - Loir-et-Cher</option> +<option value="42">42 - Loire</option> +<option value="43">43 - Haute-Loire</option> +<option value="44">44 - Loire-Atlantique</option> +<option value="45">45 - Loiret</option> +<option value="46">46 - Lot</option> +<option value="47">47 - Lot-et-Garonne</option> +<option value="48">48 - Lozere</option> +<option value="49">49 - Maine-et-Loire</option> +<option value="50">50 - Manche</option> +<option value="51">51 - Marne</option> +<option value="52">52 - Haute-Marne</option> +<option value="53">53 - Mayenne</option> +<option value="54">54 - Meurthe-et-Moselle</option> +<option value="55">55 - Meuse</option> +<option value="56">56 - Morbihan</option> +<option value="57">57 - Moselle</option> +<option value="58">58 - Nievre</option> +<option value="59">59 - Nord</option> +<option value="60">60 - Oise</option> +<option value="61">61 - Orne</option> +<option value="62">62 - Pas-de-Calais</option> +<option value="63">63 - Puy-de-Dome</option> +<option value="64">64 - Pyrenees-Atlantiques</option> +<option value="65">65 - Hautes-Pyrenees</option> +<option value="66">66 - Pyrenees-Orientales</option> +<option value="67">67 - Bas-Rhin</option> +<option value="68">68 - Haut-Rhin</option> +<option value="69">69 - Rhone</option> +<option value="70">70 - Haute-Saone</option> +<option value="71">71 - Saone-et-Loire</option> +<option value="72">72 - Sarthe</option> +<option value="73">73 - Savoie</option> +<option value="74">74 - Haute-Savoie</option> +<option value="75">75 - Paris</option> +<option value="76">76 - Seine-Maritime</option> +<option value="77">77 - Seine-et-Marne</option> +<option value="78">78 - Yvelines</option> +<option value="79">79 - Deux-Sevres</option> +<option value="80">80 - Somme</option> +<option value="81">81 - Tarn</option> +<option value="82">82 - Tarn-et-Garonne</option> +<option value="83">83 - Var</option> +<option value="84">84 - Vaucluse</option> +<option value="85">85 - Vendee</option> +<option value="86">86 - Vienne</option> +<option value="87">87 - Haute-Vienne</option> +<option value="88">88 - Vosges</option> +<option value="89">89 - Yonne</option> +<option value="90">90 - Territoire de Belfort</option> +<option value="91">91 - Essonne</option> +<option value="92">92 - Hauts-de-Seine</option> +<option value="93">93 - Seine-Saint-Denis</option> +<option value="94">94 - Val-de-Marne</option> +<option value="95">95 - Val-d'Oise</option> +<option value="2A">2A - Corse du sud</option> +<option value="2B">2B - Haute Corse</option> +<option value="971">971 - Guadeloupe</option> +<option value="972">972 - Martinique</option> +<option value="973">973 - Guyane</option> +<option value="974">974 - La Reunion</option> +<option value="975">975 - Saint-Pierre-et-Miquelon</option> +<option value="976">976 - Mayotte</option> +<option value="984">984 - Terres Australes et Antarctiques</option> +<option value="986">986 - Wallis et Futuna</option> +<option value="987">987 - Polynesie Francaise</option> +<option value="988">988 - Nouvelle-Caledonie</option> +</select> + ################################# # Tests of underlying functions # #################################