Index: django/contrib/localflavor/ca/ca_provinces.py
===================================================================
--- django/contrib/localflavor/ca/ca_provinces.py	(revision 0)
+++ django/contrib/localflavor/ca/ca_provinces.py	(revision 0)
@@ -0,0 +1,57 @@
+""" 
+An alphabetical list of provinces and territories for use as `choices` 
+in a formfield., and a mapping of province misspellings/abbreviations to 
+normalized abbreviations
+
+Source: http://www.canada.gc.ca/othergov/prov_e.html 
+
+This exists in this standalone file so that it's only imported into memory 
+when explicitly needed. 
+""" 
+
+PROVINCE_CHOICES = ( 
+    ('AB', 'Alberta'), 
+    ('BC', 'British Columbia'), 
+    ('MB', 'Manitoba'), 
+    ('NB', 'New Brunswick'), 
+    ('NF', 'Newfoundland and Labrador'), 
+    ('NT', 'Northwest Territories'), 
+    ('NS', 'Nova Scotia'), 
+    ('NU', 'Nunavut'), 
+    ('ON', 'Ontario'), 
+    ('PE', 'Prince Edward Island'), 
+    ('QC', 'Quebec'), 
+    ('SK', 'Saskatchewan'), 
+    ('YK', 'Yukon') 
+)
+
+PROVINCES_NORMALIZED = {
+    'ab': 'AB',
+    'alberta': 'AB',
+    'bc': 'BC',
+    'b.c.': 'BC',
+    'british columbia': 'BC',
+    'mb': 'MB',
+    'manitoba': 'MB',
+    'nf': 'NF',
+    'newfoundland': 'NF',
+    'newfoundland and labrador': 'NF',
+    'nt': 'NT',
+    'northwest territories': 'NT',
+    'ns': 'NS',
+    'nova scotia': 'NS',
+    'nu': 'NU',
+    'nunavut': 'NU',
+    'on': 'ON',
+    'ontario': 'ON',
+    'pe': 'PE',
+    'pei': 'PE',
+    'p.e.i.': 'PE',
+    'prince edward island': 'PE',
+    'qc': 'QC',
+    'quebec': 'QC',
+    'sk': 'SK',
+    'saskatchewan': 'SK',
+    'yk': 'YK',
+    'yukon': 'YK',
+}
\ No newline at end of file
Index: django/contrib/localflavor/ca/__init__.py
===================================================================
Index: django/contrib/localflavor/ca/forms.py
===================================================================
--- django/contrib/localflavor/ca/forms.py	(revision 0)
+++ django/contrib/localflavor/ca/forms.py	(revision 0)
@@ -0,0 +1,112 @@
+""" 
+Canada-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, ugettext
+import re 
+ 
+phone_digits_re = re.compile(r'^(?:1-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$')
+sin_re = re.compile(r"^(\d{3})-(\d{3})-(\d{3})$")
+ 
+class CAPostalCodeField(RegexField): 
+    """Canadian postal code field.""" 
+    def __init__(self, *args, **kwargs): 
+        super(CAPostalCodeField, self).__init__(r'^[ABCEGHJKLMNPRSTVXYZ]\d[A-Z] \d[A-Z]\d$', 
+            max_length=None, min_length=None, 
+            error_message=gettext(u'Enter a postal code in the format XXX XXX.'), 
+            *args, **kwargs) 
+ 
+class CAPhoneNumberField(Field): 
+    """Canadian phone number field.""" 
+    def clean(self, value): 
+        """Validate a phone number. 
+        """ 
+        super(CAPhoneNumberField, 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' % (m.group(1), m.group(2), m.group(3))
+        raise ValidationError(u'Phone numbers must be in XXX-XXX-XXXX format.') 
+
+class CAProvinceField(Field):
+    """
+    A form field that validates its input is a Canadian province name or abbreviation.
+    It normalizes the input to the standard two-leter postal service
+    abbreviation for the given province.
+    """
+    def clean(self, value):
+        from ca_provinces import PROVINCES_NORMALIZED
+        super(CAProvinceField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        try:
+            value = value.strip().lower()
+        except AttributeError:
+            pass
+        else:
+            try:
+                return PROVINCES_NORMALIZED[value.strip().lower()].decode('ascii')
+            except KeyError:
+                pass
+        raise ValidationError(u'Enter a Canadian province or territory.')
+ 
+class CAProvinceSelect(Select): 
+    """ 
+    A Select widget that uses a list of Canadian provinces and 
+    territories as its choices. 
+    """ 
+    def __init__(self, attrs=None): 
+        from ca_provinces import PROVINCE_CHOICES # relative import 
+        super(CAProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES)
+            
+class CASocialInsuranceNumberField(Field):
+    """
+    A Canadian Social Insurance Number (SIN).
+
+    Checks the following rules to determine whether the number is valid:
+
+        * Conforms to the XXX-XXX-XXXX format.
+        * Passes the check digit process "Luhn Algorithm"
+             See: http://en.wikipedia.org/wiki/Social_Insurance_Number
+    """
+    def clean(self, value):
+        super(CASocialInsuranceNumberField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        msg = ugettext('Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.')
+        match = re.match(sin_re, value)
+        if not match:
+            raise ValidationError(msg)
+            
+        number = u'%s-%s-%s' % (match.group(1), match.group(2), match.group(3))    
+        check_number = u'%s%s%s' % (match.group(1), match.group(2), match.group(3))
+        if not self.luhn_checksum_is_valid(check_number):
+            raise ValidationError(msg)
+        return number
+        
+    def luhn_checksum_is_valid(self, number):
+        """
+        Checks to make sure that the SIN passes a luhn mod-10 checksum 
+        See: http://en.wikipedia.org/wiki/Luhn_algorithm
+        """
+
+        sum = 0
+        num_digits = len(number)
+        oddeven = num_digits & 1
+
+        for count in range(0, num_digits):
+            digit = int(number[count])
+
+            if not (( count & 1 ) ^ oddeven ):
+                digit = digit * 2
+            if digit > 9:
+                digit = digit - 9
+
+            sum = sum + digit
+
+        return ( (sum % 10) == 0 )
\ No newline at end of file
Index: tests/regressiontests/forms/localflavor.py
===================================================================
--- tests/regressiontests/forms/localflavor.py	(revision 6194)
+++ tests/regressiontests/forms/localflavor.py	(working copy)
@@ -1818,4 +1818,222 @@
 u''
 >>> f.clean(u'')
 u''
+
+# CAPostalCodeField ##############################################################
+
+CAPostalCodeField validates that the data is a six-character Canadian postal code.
+>>> from django.contrib.localflavor.ca.forms import CAPostalCodeField
+>>> f = CAPostalCodeField()
+>>> f.clean('T2S 2H7')
+u'T2S 2H7'
+>>> f.clean('T2S 2H')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean('2T6 H8I')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean('T2S2H')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean(90210)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> 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 = CAPostalCodeField(required=False)
+>>> f.clean('T2S 2H7')
+u'T2S 2H7'
+>>> f.clean('T2S2H7')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean('T2S 2H')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean('2T6 H8I')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean('T2S2H')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean(90210)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postal code in the format XXX XXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# CAPhoneNumberField ##########################################################
+
+CAPhoneNumberField validates that the data is a valid Canadian phone number,
+including the area code. It's normalized to XXX-XXX-XXXX format.
+Note: This test is exactly the same as the USPhoneNumberField except using a real
+Candian area code
+
+>>> from django.contrib.localflavor.ca.forms import CAPhoneNumberField
+>>> f = CAPhoneNumberField()
+>>> f.clean('403-555-1212')
+u'403-555-1212'
+>>> f.clean('4035551212')
+u'403-555-1212'
+>>> f.clean('403 555-1212')
+u'403-555-1212'
+>>> f.clean('(403) 555-1212')
+u'403-555-1212'
+>>> f.clean('403 555 1212')
+u'403-555-1212'
+>>> f.clean('403.555.1212')
+u'403-555-1212'
+>>> f.clean('403.555-1212')
+u'403-555-1212'
+>>> f.clean(' (403) 555.1212 ')
+u'403-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('403-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX 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 = CAPhoneNumberField(required=False)
+>>> f.clean('403-555-1212')
+u'403-555-1212'
+>>> f.clean('4035551212')
+u'403-555-1212'
+>>> f.clean('403 555-1212')
+u'403-555-1212'
+>>> f.clean('(403) 555-1212')
+u'403-555-1212'
+>>> f.clean('403 555 1212')
+u'403-555-1212'
+>>> f.clean('403.555.1212')
+u'403-555-1212'
+>>> f.clean('403.555-1212')
+u'403-555-1212'
+>>> f.clean(' (403) 555.1212 ')
+u'403-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('403-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# CAProvinceField ################################################################
+
+CAProvinceField validates that the data is either an abbreviation or name of a
+Canadian province.
+>>> from django.contrib.localflavor.ca.forms import CAProvinceField
+>>> f = CAProvinceField()
+>>> f.clean('ab')
+u'AB'
+>>> f.clean('BC')
+u'BC'
+>>> f.clean('nova scotia')
+u'NS'
+>>> f.clean('  manitoba ')
+u'MB'
+>>> f.clean('T2S 2H7')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a Canadian province or territory.']
+>>> 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 = CAProvinceField(required=False)
+>>> f.clean('ab')
+u'AB'
+>>> f.clean('BC')
+u'BC'
+>>> f.clean('nova scotia')
+u'NS'
+>>> f.clean('  manitoba ')
+u'MB'
+>>> f.clean('T2S 2H7')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a Canadian province or territory.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# CAProvinceSelect ###############################################################
+
+CAProvinceSelect is a Select widget that uses a list of Canadian provinces/territories
+as its choices.
+>>> from django.contrib.localflavor.ca.forms import CAProvinceSelect
+>>> w = CAProvinceSelect()
+>>> print w.render('province', 'AB')
+<select name="province">
+<option value="AB" selected="selected">Alberta</option>
+<option value="BC">British Columbia</option>
+<option value="MB">Manitoba</option>
+<option value="NB">New Brunswick</option>
+<option value="NF">Newfoundland and Labrador</option>
+<option value="NT">Northwest Territories</option>
+<option value="NS">Nova Scotia</option>
+<option value="NU">Nunavut</option>
+<option value="ON">Ontario</option>
+<option value="PE">Prince Edward Island</option>
+<option value="QC">Quebec</option>
+<option value="SK">Saskatchewan</option>
+<option value="YK">Yukon</option>
+</select>
+
+# CASocialInsuranceNumberField #################################################
+>>> from django.contrib.localflavor.ca.forms import CASocialInsuranceNumberField
+>>> f = CASocialInsuranceNumberField()
+>>> f.clean('046-454-286')
+u'046-454-286'
+>>> f.clean('046-454-287')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.']
+>>> f.clean('046 454 286')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.']
+>>> f.clean('046-44-286')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.']
+
 """
Index: AUTHORS
===================================================================
--- AUTHORS	(revision 6194)
+++ AUTHORS	(working copy)
@@ -274,6 +274,7 @@
     Swaroop C H <http://www.swaroopch.info>
     Aaron Swartz <http://www.aaronsw.com/>
     Ville Säävuori <http://www.unessa.net/>
+    Tyler Tarabula <tyler.tarabula@gmail.com>
     Tyson Tate <tyson@fallingbullets.com>
     Frank Tegtmeyer <fte@fte.to>
     thebjorn <bp@datakortet.no>
