Login

UKPhoneNumberField

Author:
nathan-reynolds
Posted:
November 20, 2008
Language:
Python
Version:
1.0
Score:
1 (after 1 ratings)

Validates and cleans UK telephone numbers. Number length is checked, and numbers are cleaned into a common format. For example, "+44 (0)1234 567890" will be stored as "01234 567890"

Can reject premium numbers (09123 123123) or service numbers (1471, 118 118) with UKPhoneNumberField(reject=('premium', 'service'))

Uses info from Wikipedia

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
from django.forms import ValidationError
from django.forms.fields import Field, EMPTY_VALUES
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
import re
import types

class UKPhoneNumberField(Field):
    default_error_messages = {
        'partial': _('Phone number must include an area code.'),
        'non_uk': _('Phone number must be a UK number.'),
        'length_range': _('Phone number must be between %d and %d digits'),
        'length': _('Phone number must be %d digits'),
        'reject_premium': _('Phone number can\'t be premium rate.'),
        'reject_service': _('Phone number can\'t be a service number.')
    }
    
    number_specs = (
        (r'^01(1[^1]|[^1]1)',      None,     (4, 3, 4)),
        (r'^01',                   None,     (5, (5, 6))),
        (r'^0500',                 None,     (4, 6)),
        (r'^0[235]',               None,     (3, 4, 4)),
        (r'^07',                   None,     (5, 6)),
        (r'^(08001111|08454647)$', None,     (4, 4)),
        (r'^08',                   None,     (4, 7)),
        (r'^09',                  'premium', (4, 6)),
        (r'^118',                 'service', (3, 3)),
        (r'^999$',                'service', (3,)),
        (r'^1',                   'service', None),
    )
    
    def __init__(self, *args, **kwargs):
        self.reject = set(kwargs.pop('reject', ()))
        super(UKPhoneNumberField, self).__init__(*args, **kwargs)
    
    def clean(self, value):
        super(UKPhoneNumberField, self).clean(value)
        
        value = smart_unicode(value)
        
        if value in EMPTY_VALUES:
            return u''
        
        value = re.sub(r'[^0-9+]',        r'',  value)
        value = re.sub(r'(?<!^)\+',       r'',  value)
        value = re.sub(r'^\+44(?=[1-9])', r'0', value)
        value = re.sub(r'^\+44(?=0)',     r'',  value)
        
        if re.match(r'^(\+(?!44)|00)', value):
            raise ValidationError(self.error_messages['non_uk'])
        
        number_spec = self.get_number_spec(value)
        
        if not number_spec:
            raise ValidationError(self.error_messages['partial'])
        
        if number_spec[0] in self.reject:
            raise ValidationError(self.error_messages['reject_%s' % number_spec[0]])
        
        if not self.valid_length(value, number_spec):
            min_length, max_length = self.spec_lengths(number_spec)
            if min_length == max_length:
                raise ValidationError(self.error_messages['length']
                    % min_length)
            else:
                raise ValidationError(self.error_messages['length_range']
                    % (min_length, max_length))
        
        return self.format_number(value, number_spec)
    
    def get_number_spec(self, value):
        for number_spec in self.number_specs:
            if re.match(number_spec[0], value):
                return number_spec[1:]
        return None
    
    def spec_lengths(self, number_spec):
        if not number_spec[1]:
            return None, None
        if type(number_spec[1][-1]) == types.TupleType:
            min_length, max_length = number_spec[1][-1]
            total = sum(number_spec[1][:-1])
            min_length += total
            max_length += total
        else:
            min_length = max_length = sum(number_spec[1])
        return min_length, max_length
    
    def valid_length(self, value, number_spec):
        min_length, max_length = self.spec_lengths(number_spec)
        if min_length is not None and len(value) < min_length: return False
        if max_length is not None and len(value) > max_length: return False
        return True
    
    def format_number(self, value, number_spec):
        if number_spec[1] is None:
            components = (value,)
        else:
            components = []
            position = 0
            last_index = len(number_spec) - 1
            for index, chunk in enumerate(number_spec[1]):
                if index == last_index:
                    components.append(value[position:])
                else:
                    components.append(value[position:position+chunk])
                    position += chunk
        return ' '.join(components)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 3 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 3 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 10 months, 2 weeks ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 11 months ago
  5. Help text hyperlinks by sa2812 12 months ago

Comments

Tarken (on November 20, 2008):

This is quite impressive. I would recommend posting this to a Django ticket for inclusion in django.contrib.localflavor.uk.forms

I'm sure this would be a welcome addition for many, and would help bring the UK localflavor more into line with the US

#

aj (on December 9, 2008):

I added:

    'reject_geo': _('Phone number can\'t be a landline number.'),
    'reject_nongeo': _('Phone number can\'t be a none-geographic number.'),
    'reject_mobile': _('Phone number can\'t be mobile number.')
}

number_specs = (
    (r'^01(1[^1]|[^1]1)',      'geo',     (4, 3, 4)),
    (r'^01',                   'geo',     (5, (5, 6))),
    (r'^0500',                 'nongeo',  (4, 6)),
    (r'^0[235]',               'geo',     (3, 4, 4)),
    (r'^07',                   'mobile',  (5, 6)),
    (r'^(08001111|08454647)$', 'nongeo',  (4, 4)),
    (r'^08',                   'nongeo',  (4, 7)),

So I could do things like:

reject=('premium', 'service', 'geo', 'nongeo')

to only get mobiles. Not perfect but serviceable. :)

Excellent snippet - thanks!

#

vmagamedov (on October 12, 2009):

If you will try to use custom error messages, exception could be raised because of hardcoded string formatting. You should change this "default_error_messages":

'length_range': _('Phone number must be between %(min)d and %(max)d digits'),

'length': _('Phone number must be %(min)d digits'),

and in the "clean" method:

raise ValidationError(self.error_messages['length'] % {'min':min_length})

and:

raise ValidationError(self.error_messages['length_range'] % {'min':min_length, 'max':max_length})

#

Please login first before commenting.