views:

58

answers:

1

Hi I've created my first custom django field. Its a social security number field (Cpr) following danish syntax. As I'm new to both python and django i would love to get some feedback on the code!

import django.forms as f
from datetime import datetime
import json
from django.utils.safestring import mark_safe
from django.core.exceptions import ValidationError

# Globals
YEAR = datetime.now().year          
FIELDS = (
    {'name' : "cpr_date",   'size': 2, 'maxlength': 2, 'renderfunc': None, 
        'field': f.IntegerField(min_value=1, max_value=31)
    },
    {'name' : "cpr_month",  'size': 2, 'maxlength': 2, 'renderfunc': None,
        'field': f.IntegerField(min_value=1, max_value=12),
    },    
    {'name' : "cpr_year",   'size': 4, 'maxlength': 4, 
        'renderfunc': 'render_age',
        'field': f.IntegerField(min_value=YEAR-120, max_value=YEAR),
    },
    {'name' : "cpr_digits", 'size': 4, 'maxlength': 4, 
        'renderfunc': 'render_gender',
        'field': f.IntegerField(min_value=1000, max_value=9999),
    },
)

class CprWidget(f.widgets.MultiWidget):
    """Widget showing date, month, year and 4-last-digit fields for a danish cpr 
    number.
    """
    def __init__(self, attrs=None):
        widgets = [f.widgets.TextInput(field) for field in FIELDS]
        super(CprWidget, self).__init__(widgets, attrs)

    def decompress(self, v):
        """Decompresses widget values to json before save"""
        if v: return json.loads(v)
        return [None, None, None, None]

    def render(self, name, value, attrs=None):
        """Extends with details rendered from parts of Cpr number"""
        values = self.decompress_value(value)
        output = super(CprWidget, self).render(name, value, attrs)
        extra_output = ""
        all_is_valid = True
        i = 0
        for field in FIELDS:
            try:
                value = values[i]
                if value == None:
                    raise ValueError
                fieldinst = field['field']
                fieldinst.validate(value)
                fieldinst.run_validators(value)
                if field['renderfunc']:
                    func = getattr(self, field['renderfunc'])
                    extra_output = extra_output + func(value)
            except (IndexError, ValueError, ValidationError):
                all_is_valid = False
            finally:
                i = i + 1
        if all_is_valid:
            extra_output = extra_output + self.render_cpr(values)
        return output + mark_safe(extra_output)

    def render_gender(self, value):
        """Renders male or female based on last digit of Cpr"""
        last_digit = int(str(value)[3])
        gender = (('Male','Female')[last_digit % 2 == 0])
        return self.render_line("Gender", gender)

    def render_age(self, value):
        """Renders age"""
        return self.render_line("Age", YEAR-value)

    def render_cpr(self, values):
        """Renders full Cpr number"""
        cpr = "%02d%02d%s-%s" % (
            int(values[0]), int(values[1]), str(values[2])[2:4], values[3])
        return self.render_line("Cpr", cpr)

    def render_line(self, title, value):
        """Renders a line with title and value"""
        return "<br /><b>%s: </b>%s" % (title, value)

    def decompress_value(self, value):
        """Decompress values from value if needed"""
        if not isinstance(value, list):
            value = self.decompress(value)
        return value

    def format_output(self, rendered_widgets):
        """Format html for widgets"""
        labels = [u'Date', u'Month', u'Year', u'Digits']
        labels.reverse()
        html = u''
        for w in rendered_widgets: html += labels.pop() + ': ' + str(w)
        return html

class CprField(f.MultiValueField):
    """Field extending MultiValueField with date, month, year and 4-last-digit 
    fields for a danish cpr number.
    """
    widget = CprWidget
    def __init__(self, required=False, widget=None, label=None, initial=None):
        fields = [field['field'] for field in FIELDS]
        super(CprField, self).__init__(fields=fields, widget=widget, label=label
            ,initial=initial, required=required)

    def compress(self, data_list):
        """Compress field values to json"""
        return json.dumps(data_list)
A: 

Is there some particular reason you are combining a Date widget with the Cpr number? Seems that they are two separate things, and Django already has a decent SelectDateWidget. I'm not familiar with the Danish Cpr number, but unless the date is part of the number, they should really be two separate fields.

Chris Lawlor
Hi Chris, thx for the reply! Date _is_ part of the number and im using a simple textfield widget to render it, so thats why.
runish
In that case I'd say your code looks fine. You might consider contributing this to contrib.localflavor(http://docs.djangoproject.com/en/dev/ref/contrib/localflavor/)
Chris Lawlor
Ah wauw, good thing a danish entry did not already exist :) Will see if I can get it committed there. Thx a lot for that link!
runish