diff --git a/AUTHORS b/AUTHORS
index d0bf6502de..027dbc39ba 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -225,6 +225,7 @@ answer newbie questions, and generally made Django that much better:
wam-djangobug@wamber.net
Dan Watson
Chris Wesseling
+ charly.wilhelm@gmail.com
Rachel Willmer
Gary Wilson
wojtek
diff --git a/django/contrib/localflavor/ch/__init__.py b/django/contrib/localflavor/ch/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/django/contrib/localflavor/ch/ch_states.py b/django/contrib/localflavor/ch/ch_states.py
new file mode 100644
index 0000000000..e9bbcc6268
--- /dev/null
+++ b/django/contrib/localflavor/ch/ch_states.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*
+from django.utils.translation import gettext_lazy as _
+
+STATE_CHOICES = (
+ ('AG', _('Aargau')),
+ ('AI', _('Appenzell Innerrhoden')),
+ ('AR', _('Appenzell Ausserrhoden')),
+ ('BS', _('Basel-Stadt')),
+ ('BL', _('Basel-Land')),
+ ('BE', _('Berne')),
+ ('FR', _('Fribourg')),
+ ('GE', _('Geneva')),
+ ('GL', _('Glarus')),
+ ('GR', _('Graubuenden')),
+ ('JU', _('Jura')),
+ ('LU', _('Lucerne')),
+ ('NE', _('Neuchatel')),
+ ('NW', _('Nidwalden')),
+ ('OW', _('Obwalden')),
+ ('SH', _('Schaffhausen')),
+ ('SZ', _('Schwyz')),
+ ('SO', _('Solothurn')),
+ ('SG', _('St. Gallen')),
+ ('TG', _('Thurgau')),
+ ('TI', _('Ticino')),
+ ('UR', _('Uri')),
+ ('VS', _('Valais')),
+ ('VD', _('Vaud')),
+ ('ZG', _('Zug')),
+ ('ZH', _('Zurich'))
+)
diff --git a/django/contrib/localflavor/ch/forms.py b/django/contrib/localflavor/ch/forms.py
new file mode 100644
index 0000000000..51e52dc0e9
--- /dev/null
+++ b/django/contrib/localflavor/ch/forms.py
@@ -0,0 +1,109 @@
+"""
+Swiss-specific Form helpers
+"""
+
+from django.newforms import ValidationError
+from django.newforms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.utils.encoding import smart_unicode
+from django.utils.translation import gettext
+import re
+
+id_re = re.compile(r"^(?P\w{8})(?P(\d{1}|<))(?P\d{1})$")
+phone_digits_re = re.compile(r'^0([1-9]{1})\d{8}$')
+
+class CHZipCodeField(RegexField):
+ def __init__(self, *args, **kwargs):
+ super(CHZipCodeField, self).__init__(r'^\d{4}$',
+ max_length=None, min_length=None,
+ error_message=gettext('Enter a zip code in the format XXXX.'),
+ *args, **kwargs)
+
+class CHPhoneNumberField(Field):
+ """
+ Validate local Swiss phone number (not international ones)
+ The correct format is '0XX XXX XX XX'.
+ '0XX.XXX.XX.XX' and '0XXXXXXXXX' validate but are corrected to
+ '0XX XXX XX XX'.
+ """
+ def clean(self, value):
+ super(CHPhoneNumberField, 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' % (value[0:3], value[3:6], value[6:8], value[8:10])
+ raise ValidationError('Phone numbers must be in 0XX XXX XX XX format.')
+
+class CHStateSelect(Select):
+ """
+ A Select widget that uses a list of CH states as its choices.
+ """
+ def __init__(self, attrs=None):
+ from ch_states import STATE_CHOICES # relative import
+ super(CHStateSelect, self).__init__(attrs, choices=STATE_CHOICES)
+
+class CHIdentityCardNumberField(Field):
+ """
+ A Swiss identity card number.
+
+ Checks the following rules to determine whether the number is valid:
+
+ * Conforms to the X1234567<0 or 1234567890 format.
+ * Included checksums match calculated checksums
+
+ Algorithm is documented at http://adi.kousz.ch/artikel/IDCHE.htm
+ """
+ def has_valid_checksum(self, number):
+ given_number, given_checksum = number[:-1], number[-1]
+ new_number = given_number
+ calculated_checksum = 0
+ fragment = ""
+ parameter = 7
+
+ first = str(number[:1])
+ if first.isalpha():
+ num = ord(first.upper()) - 65
+ if num < 0 or num > 8:
+ return False
+ new_number = str(num) + new_number[1:]
+ new_number = new_number[:8] + '0'
+
+ if not new_number.isdigit():
+ return False
+
+ for i in range(len(new_number)):
+ fragment = int(new_number[i])*parameter
+ calculated_checksum += fragment
+
+ if parameter == 1:
+ parameter = 7
+ elif parameter == 3:
+ parameter = 1
+ elif parameter ==7:
+ parameter = 3
+
+ return str(calculated_checksum)[-1] == given_checksum
+
+ def clean(self, value):
+ super(CHIdentityCardNumberField, self).clean(value)
+ error_msg = gettext('Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.')
+ if value in EMPTY_VALUES:
+ return u''
+
+ match = re.match(id_re, value)
+ if not match:
+ raise ValidationError(error_msg)
+
+ idnumber, pos9, checksum = match.groupdict()['idnumber'], match.groupdict()['pos9'], match.groupdict()['checksum']
+
+ if idnumber == '00000000' or \
+ idnumber == 'A0000000':
+ raise ValidationError(error_msg)
+
+ all_digits = "%s%s%s" % (idnumber, pos9, checksum)
+ if not self.has_valid_checksum(all_digits):
+ raise ValidationError(error_msg)
+
+ return u'%s%s%s' % (idnumber, pos9, checksum)
+
diff --git a/tests/regressiontests/forms/localflavor.py b/tests/regressiontests/forms/localflavor.py
index f725fb38b7..ede89de2a0 100644
--- a/tests/regressiontests/forms/localflavor.py
+++ b/tests/regressiontests/forms/localflavor.py
@@ -1011,6 +1011,60 @@ Traceback (most recent call last):
...
ValidationError: [u'Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X format.']
+# CHZipCodeField ############################################################
+
+>>> from django.contrib.localflavor.ch.forms import CHZipCodeField
+>>> f = CHZipCodeField()
+>>> f.clean('800x')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXX.']
+>>> f.clean('80 00')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXX.']
+>>> f.clean('8000')
+u'8000'
+
+# CHPhoneNumberField ########################################################
+
+>>> from django.contrib.localflavor.ch.forms import CHPhoneNumberField
+>>> f = CHPhoneNumberField()
+>>> f.clean('01234567890')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in 0XX XXX XX XX format.']
+>>> f.clean('1234567890')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in 0XX XXX XX XX format.']
+>>> f.clean('0123456789')
+u'012 345 67 89'
+
+# CHIdentityCardNumberField #################################################
+
+>>> from django.contrib.localflavor.ch.forms import CHIdentityCardNumberField
+>>> f = CHIdentityCardNumberField()
+>>> f.clean('C1234567<0')
+u'C1234567<0'
+>>> f.clean('C1234567<1')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.']
+>>> f.clean('2123456700')
+u'2123456700'
+>>> f.clean('2123456701')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.']
+
+# CHStateSelect #############################################################
+
+>>> from django.contrib.localflavor.ch.forms import CHStateSelect
+>>> w = CHStateSelect()
+>>> w.render('state', 'AG')
+u''
+
## AUPostCodeField ##########################################################
A field that accepts a four digit Australian post code.