Fixed #9289 - Added Swedish localflavor. Thanks to Andreas Pelme, Ludvig Ericson and Filip Noetzel for working on a patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11969 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2009-12-22 21:10:40 +00:00
parent cdf5ad4217
commit d320deef25
8 changed files with 669 additions and 0 deletions

View File

@ -328,6 +328,7 @@ answer newbie questions, and generally made Django that much better:
Gopal Narayanan <gopastro@gmail.com>
Fraser Nevett <mail@nevett.org>
Sam Newman <http://www.magpiebrain.com/>
Filip Noetzel <http://filip.noetzel.co.uk/>
Afonso Fernández Nogueira <fonzzo.django@gmail.com>
Neal Norwitz <nnorwitz@google.com>
Todd O'Bryan <toddobryan@mac.com>
@ -338,6 +339,7 @@ answer newbie questions, and generally made Django that much better:
Carlos Eduardo de Paula <carlosedp@gmail.com>
pavithran s <pavithran.s@gmail.com>
Barry Pederson <bp@barryp.org>
Andreas Pelme <andreas@pelme.se>
permonik@mesias.brnonet.cz
peter@mymart.com
pgross@thoughtworks.com

View File

@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
"""
Swedish specific Form helpers
"""
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.forms.fields import EMPTY_VALUES
from django.contrib.localflavor.se.utils import (id_number_checksum,
validate_id_birthday, format_personal_id_number, valid_organisation,
format_organisation_number)
__all__ = ('SECountySelect', 'SEOrganisationNumberField',
'SEPersonalIdentityNumberField', 'SEPostalCodeField')
SWEDISH_ID_NUMBER = re.compile(r'^(?P<century>\d{2})?(?P<year>\d{2})(?P<month>\d{2})(?P<day>\d{2})(?P<sign>[\-+])?(?P<serial>\d{3})(?P<checksum>\d)$')
SE_POSTAL_CODE = re.compile(r'^[1-9]\d{2} ?\d{2}$')
class SECountySelect(forms.Select):
"""
A Select form widget that uses a list of the Swedish counties (län) as its
choices.
The cleaned value is the official county code -- see
http://en.wikipedia.org/wiki/Counties_of_Sweden for a list.
"""
def __init__(self, attrs=None):
from se_counties import COUNTY_CHOICES
super(SECountySelect, self).__init__(attrs=attrs,
choices=COUNTY_CHOICES)
class SEOrganisationNumberField(forms.CharField):
"""
A form field that validates input as a Swedish organisation number
(organisationsnummer).
It accepts the same input as SEPersonalIdentityField (for sole
proprietorships (enskild firma). However, co-ordination numbers are not
accepted.
It also accepts ordinary Swedish organisation numbers with the format
NNNNNNNNNN.
The return value will be YYYYMMDDXXXX for sole proprietors, and NNNNNNNNNN
for other organisations.
"""
default_error_messages = {
'invalid': _('Enter a valid Swedish organisation number.'),
}
def clean(self, value):
value = super(SEOrganisationNumberField, self).clean(value)
if value in EMPTY_VALUES:
return u''
match = SWEDISH_ID_NUMBER.match(value)
if not match:
raise forms.ValidationError(self.error_messages['invalid'])
gd = match.groupdict()
# Compare the calculated value with the checksum
if id_number_checksum(gd) != int(gd['checksum']):
raise forms.ValidationError(self.error_messages['invalid'])
# First: check if this is a real organisation_number
if valid_organisation(gd):
return format_organisation_number(gd)
# Is this a single properitor (enskild firma)?
try:
birth_day = validate_id_birthday(gd, False)
return format_personal_id_number(birth_day, gd)
except ValueError:
raise forms.ValidationError(self.error_messages['invalid'])
class SEPersonalIdentityNumberField(forms.CharField):
"""
A form field that validates input as a Swedish personal identity number
(personnummer).
The correct formats are YYYYMMDD-XXXX, YYYYMMDDXXXX, YYMMDD-XXXX,
YYMMDDXXXX and YYMMDD+XXXX.
A + indicates that the person is older than 100 years, which will be taken
into consideration when the date is validated.
The checksum will be calculated and checked. The birth date is checked to
be a valid date.
By default, co-ordination numbers (samordningsnummer) will be accepted. To
only allow real personal identity numbers, pass the keyword argument
coordination_number=False to the constructor.
The cleaned value will always have the format YYYYMMDDXXXX.
"""
def __init__(self, coordination_number=True, *args, **kwargs):
self.coordination_number = coordination_number
super(SEPersonalIdentityNumberField, self).__init__(*args, **kwargs)
default_error_messages = {
'invalid': _('Enter a valid Swedish personal identity number.'),
'coordination_number': _('Co-ordination numbers are not allowed.'),
}
def clean(self, value):
value = super(SEPersonalIdentityNumberField, self).clean(value)
if value in EMPTY_VALUES:
return u''
match = SWEDISH_ID_NUMBER.match(value)
if match is None:
raise forms.ValidationError(self.error_messages['invalid'])
gd = match.groupdict()
# compare the calculated value with the checksum
if id_number_checksum(gd) != int(gd['checksum']):
raise forms.ValidationError(self.error_messages['invalid'])
# check for valid birthday
try:
birth_day = validate_id_birthday(gd)
except ValueError:
raise forms.ValidationError(self.error_messages['invalid'])
# make sure that co-ordination numbers do not pass if not allowed
if not self.coordination_number and int(gd['day']) > 60:
raise forms.ValidationError(self.error_messages['coordination_number'])
return format_personal_id_number(birth_day, gd)
class SEPostalCodeField(forms.RegexField):
"""
A form field that validates input as a Swedish postal code (postnummer).
Valid codes consist of five digits (XXXXX). The number can optionally be
formatted with a space after the third digit (XXX XX).
The cleaned value will never contain the space.
"""
default_error_messages = {
'invalid': _('Enter a Swedish postal code in the format XXXXX.'),
}
def __init__(self, *args, **kwargs):
super(SEPostalCodeField, self).__init__(SE_POSTAL_CODE, *args, **kwargs)
def clean(self, value):
return super(SEPostalCodeField, self).clean(value).replace(' ', '')

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""
An alphabetical list of Swedish counties, sorted by codes.
http://en.wikipedia.org/wiki/Counties_of_Sweden
This exists in this standalone file so that it's only imported into memory
when explicitly needed.
"""
from django.utils.translation import ugettext_lazy as _
COUNTY_CHOICES = (
('AB', _(u'Stockholm')),
('AC', _(u'Västerbotten')),
('BD', _(u'Norrbotten')),
('C', _(u'Uppsala')),
('D', _(u'Södermanland')),
('E', _(u'Östergötland')),
('F', _(u'Jönköping')),
('G', _(u'Kronoberg')),
('H', _(u'Kalmar')),
('I', _(u'Gotland')),
('K', _(u'Blekinge')),
('M', _(u'Skåne')),
('N', _(u'Halland')),
('O', _(u'Västra Götaland')),
('S', _(u'Värmland')),
('T', _(u'Örebro')),
('U', _(u'Västmanland')),
('W', _(u'Dalarna')),
('X', _(u'Gävleborg')),
('Y', _(u'Västernorrland')),
('Z', _(u'Jämtland')),
)

View File

@ -0,0 +1,84 @@
import re
import datetime
def id_number_checksum(gd):
"""
Calculates a Swedish ID number checksum, using the
"Luhn"-algoritm
"""
n = s = 0
for c in (gd['year'] + gd['month'] + gd['day'] + gd['serial']):
tmp = ((n % 2) and 1 or 2) * int(c)
if tmp > 9:
tmp = sum([int(i) for i in str(tmp)])
s += tmp
n += 1
if (s % 10) == 0:
return 0
return (((s / 10) + 1) * 10) - s
def validate_id_birthday(gd, fix_coordination_number_day=True):
"""
Validates the birth_day and returns the datetime.date object for
the birth_day.
If the date is an invalid birth day, a ValueError will be raised.
"""
today = datetime.date.today()
day = int(gd['day'])
if fix_coordination_number_day and day > 60:
day -= 60
if gd['century'] is None:
# The century was not specified, and need to be calculated from todays date
current_year = today.year
year = int(today.strftime('%Y')) - int(today.strftime('%y')) + int(gd['year'])
if ('%s%s%02d' % (gd['year'], gd['month'], day)) > today.strftime('%y%m%d'):
year -= 100
# If the person is older than 100 years
if gd['sign'] == '+':
year -= 100
else:
year = int(gd['century'] + gd['year'])
# Make sure the year is valid
# There are no swedish personal identity numbers where year < 1800
if year < 1800:
raise ValueError
# ValueError will be raise for invalid dates
birth_day = datetime.date(year, int(gd['month']), day)
# birth_day must not be in the future
if birth_day > today:
raise ValueError
return birth_day
def format_personal_id_number(birth_day, gd):
# birth_day.strftime cannot be used, since it does not support dates < 1900
return unicode(str(birth_day.year) + gd['month'] + gd['day'] + gd['serial'] + gd['checksum'])
def format_organisation_number(gd):
if gd['century'] is None:
century = ''
else:
century = gd['century']
return unicode(century + gd['year'] + gd['month'] + gd['day'] + gd['serial'] + gd['checksum'])
def valid_organisation(gd):
return gd['century'] in (None, 16) and \
int(gd['month']) >= 20 and \
gd['sign'] in (None, '-') and \
gd['year'][0] in ('2', '5', '7', '8', '9') # group identifier

View File

@ -61,6 +61,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are:
* Slovakia_
* `South Africa`_
* Spain_
* Sweden_
* Switzerland_
* `United Kingdom`_
* `United States of America`_
@ -101,6 +102,7 @@ Here's an example of how to use them::
.. _Slovakia: `Slovakia (sk)`_
.. _South Africa: `South Africa (za)`_
.. _Spain: `Spain (es)`_
.. _Sweden: `Sweden (se)`_
.. _Switzerland: `Switzerland (ch)`_
.. _United Kingdom: `United Kingdom (uk)`_
.. _United States of America: `United States of America (us)`_
@ -596,6 +598,60 @@ Spain (``es``)
A ``Select`` widget that uses a list of Spanish regions as its choices.
Sweden (``se``)
===============
.. class:: se.forms.SECountySelect
A Select form widget that uses a list of the Swedish counties (län) as its
choices.
The cleaned value is the official county code -- see
http://en.wikipedia.org/wiki/Counties_of_Sweden for a list.
.. class:: se.forms.SEOrganisationNumber
A form field that validates input as a Swedish organisation number
(organisationsnummer).
It accepts the same input as SEPersonalIdentityField (for sole
proprietorships (enskild firma). However, co-ordination numbers are not
accepted.
It also accepts ordinary Swedish organisation numbers with the format
NNNNNNNNNN.
The return value will be YYYYMMDDXXXX for sole proprietors, and NNNNNNNNNN
for other organisations.
.. class:: se.forms.SEPersonalIdentityNumber
A form field that validates input as a Swedish personal identity number
(personnummer).
The correct formats are YYYYMMDD-XXXX, YYYYMMDDXXXX, YYMMDD-XXXX,
YYMMDDXXXX and YYMMDD+XXXX.
A \+ indicates that the person is older than 100 years, which will be taken
into consideration when the date is validated.
The checksum will be calculated and checked. The birth date is checked
to be a valid date.
By default, co-ordination numbers (samordningsnummer) will be accepted. To
only allow real personal identity numbers, pass the keyword argument
coordination_number=False to the constructor.
The cleaned value will always have the format YYYYMMDDXXXX.
.. class:: se.forms.SEPostalCodeField
A form field that validates input as a Swedish postal code (postnummer).
Valid codes consist of five digits (XXXXX). The number can optionally be
formatted with a space after the third digit (XXX XX).
The cleaned value will never contain the space.
Switzerland (``ch``)
====================

View File

@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
# Tests for the contrib/localflavor/se form fields.
tests = r"""
# Monkey-patch datetime.date
>>> import datetime
>>> class MockDate(datetime.date):
... def today(cls):
... return datetime.date(2008, 5, 14)
... today = classmethod(today)
...
>>> olddate = datetime.date
>>> datetime.date = MockDate
>>> datetime.date.today()
MockDate(2008, 5, 14)
# SECountySelect #####################################################
>>> from django.contrib.localflavor.se.forms import SECountySelect
>>> w = SECountySelect()
>>> w.render('swedish_county', 'E')
u'<select name="swedish_county">\n<option value="AB">Stockholm</option>\n<option value="AC">V\xe4sterbotten</option>\n<option value="BD">Norrbotten</option>\n<option value="C">Uppsala</option>\n<option value="D">S\xf6dermanland</option>\n<option value="E" selected="selected">\xd6sterg\xf6tland</option>\n<option value="F">J\xf6nk\xf6ping</option>\n<option value="G">Kronoberg</option>\n<option value="H">Kalmar</option>\n<option value="I">Gotland</option>\n<option value="K">Blekinge</option>\n<option value="M">Sk\xe5ne</option>\n<option value="N">Halland</option>\n<option value="O">V\xe4stra G\xf6taland</option>\n<option value="S">V\xe4rmland</option>\n<option value="T">\xd6rebro</option>\n<option value="U">V\xe4stmanland</option>\n<option value="W">Dalarna</option>\n<option value="X">G\xe4vleborg</option>\n<option value="Y">V\xe4sternorrland</option>\n<option value="Z">J\xe4mtland</option>\n</select>'
# SEOrganisationNumberField #######################################
>>> from django.contrib.localflavor.se.forms import SEOrganisationNumberField
>>> f = SEOrganisationNumberField()
# Ordinary personal identity numbers for sole proprietors
# The same rules as for SEPersonalIdentityField applies here
>>> f.clean('870512-1989')
u'198705121989'
>>> f.clean('19870512-1989')
u'198705121989'
>>> f.clean('870512-2128')
u'198705122128'
>>> f.clean('081015-6315')
u'190810156315'
>>> f.clean('081015+6315')
u'180810156315'
>>> f.clean('0810156315')
u'190810156315'
>>> f.clean('081015 6315')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('950231-4496')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('6914104499')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('950d314496')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('invalid!!!')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('870514-1111')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
# Empty values
>>> f.clean('')
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean(None)
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
# Co-ordination number checking
# Co-ordination numbers are not valid organisation numbers
>>> f.clean('870574-1315')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
>>> f.clean('870573-1311')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
# Test some different organisation numbers
>>> f.clean('556074-7569') # IKEA Linköping
u'5560747569'
>>> f.clean('556074-3089') # Volvo Personvagnar
u'5560743089'
>>> f.clean('822001-5476') # LJS (organisation)
u'8220015476'
>>> f.clean('8220015476') # LJS (organisation)
u'8220015476'
>>> f.clean('2120000449') # Katedralskolan Linköping (school)
u'2120000449'
# Faux organisation number, which tests that the checksum can be 0
>>> f.clean('232518-5060')
u'2325185060'
>>> f.clean('556074+3089') # Volvo Personvagnar, bad format
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
# Invalid checksum
>>> f.clean('2120000441')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
# Valid checksum but invalid organisation type
f.clean('1120000441')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish organisation number.']
# Empty values with required=False
>>> f = SEOrganisationNumberField(required=False)
>>> f.clean(None)
u''
>>> f.clean('')
u''
# SEPersonalIdentityNumberField #######################################
>>> from django.contrib.localflavor.se.forms import SEPersonalIdentityNumberField
>>> f = SEPersonalIdentityNumberField()
# Valid id numbers
>>> f.clean('870512-1989')
u'198705121989'
>>> f.clean('870512-2128')
u'198705122128'
>>> f.clean('19870512-1989')
u'198705121989'
>>> f.clean('198705121989')
u'198705121989'
>>> f.clean('081015-6315')
u'190810156315'
>>> f.clean('0810156315')
u'190810156315'
# This is a "special-case" in the checksum calculation,
# where the sum is divisible by 10 (the checksum digit == 0)
>>> f.clean('8705141060')
u'198705141060'
# + means that the person is older than 100 years
>>> f.clean('081015+6315')
u'180810156315'
# Bogus values
>>> f.clean('081015 6315')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
>>> f.clean('950d314496')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
>>> f.clean('invalid!!!')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Invalid dates
# February 31st does not exist
>>> f.clean('950231-4496')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Month 14 does not exist
>>> f.clean('6914104499')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# There are no Swedish personal id numbers where year < 1800
>>> f.clean('17430309-7135')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Invalid checksum
>>> f.clean('870514-1111')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Empty values
>>> f.clean('')
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean(None)
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
# Co-ordination number checking
>>> f.clean('870574-1315')
u'198705741315'
>>> f.clean('870574+1315')
u'188705741315'
>>> f.clean('198705741315')
u'198705741315'
# Co-ordination number with bad checksum
>>> f.clean('870573-1311')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Check valid co-ordination numbers, that should not be accepted
# because of coordination_number=False
>>> f = SEPersonalIdentityNumberField(coordination_number=False)
>>> f.clean('870574-1315')
Traceback (most recent call last):
...
ValidationError: [u'Co-ordination numbers are not allowed.']
>>> f.clean('870574+1315')
Traceback (most recent call last):
...
ValidationError: [u'Co-ordination numbers are not allowed.']
>>> f.clean('8705741315')
Traceback (most recent call last):
...
ValidationError: [u'Co-ordination numbers are not allowed.']
# Invalid co-ordination numbers should be treated as invalid, and not
# as co-ordination numbers
>>> f.clean('870573-1311')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid Swedish personal identity number.']
# Empty values with required=False
>>> f = SEPersonalIdentityNumberField(required=False)
>>> f.clean(None)
u''
>>> f.clean('')
u''
# SEPostalCodeField ###############################################
>>> from django.contrib.localflavor.se.forms import SEPostalCodeField
>>> f = SEPostalCodeField()
>>>
Postal codes can have spaces
>>> f.clean('589 37')
u'58937'
... but the dont have to
>>> f.clean('58937')
u'58937'
>>> f.clean('abcasfassadf')
Traceback (most recent call last):
...
ValidationError: [u'Enter a Swedish postal code in the format XXXXX.']
# Only one space is allowed for separation
>>> f.clean('589 37')
Traceback (most recent call last):
...
ValidationError: [u'Enter a Swedish postal code in the format XXXXX.']
# The postal code must not start with 0
>>> f.clean('01234')
Traceback (most recent call last):
...
ValidationError: [u'Enter a Swedish postal code in the format XXXXX.']
# Empty values
>>> f.clean('')
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean(None)
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
# Empty values, required=False
>>> f = SEPostalCodeField(required=False)
>>> f.clean('')
u''
>>> f.clean(None)
u''
# Revert the monkey patching
>>> datetime.date = olddate
"""

View File

@ -21,6 +21,7 @@ from localflavor.jp import tests as localflavor_jp_tests
from localflavor.nl import tests as localflavor_nl_tests
from localflavor.pl import tests as localflavor_pl_tests
from localflavor.ro import tests as localflavor_ro_tests
from localflavor.se import tests as localflavor_se_tests
from localflavor.sk import tests as localflavor_sk_tests
from localflavor.uk import tests as localflavor_uk_tests
from localflavor.us import tests as localflavor_us_tests
@ -56,6 +57,7 @@ __test__ = {
'localflavor_nl_tests': localflavor_nl_tests,
'localflavor_pl_tests': localflavor_pl_tests,
'localflavor_ro_tests': localflavor_ro_tests,
'localflavor_se_tests': localflavor_se_tests,
'localflavor_sk_tests': localflavor_sk_tests,
'localflavor_uk_tests': localflavor_uk_tests,
'localflavor_us_tests': localflavor_us_tests,