Permalink
Browse files

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...
1 parent cdf5ad4 commit d320deef25059d1f349c386453f228debcf4cfe7 @jezdez jezdez committed Dec 22, 2009
View
@@ -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
@@ -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(' ', '')
@@ -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')),
+)
@@ -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
+
@@ -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``)
====================
Oops, something went wrong.

0 comments on commit d320dee

Please sign in to comment.