-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed #9289 - Added Swedish localflavor. Thanks to Andreas Pelme, Lud…
…vig 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
- Loading branch information
Showing
8 changed files
with
669 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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(' ', '') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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')), | |||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.