Login

UKPhoneNumberField GB v3 (improved)

Author:
g1smd
Posted:
September 6, 2012
Language:
Python
Version:
1.4
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 (0912 312 3123) or service numbers (1471, 118 118) with UKPhoneNumberField(reject=('premium', 'service'))

Can reject multiple number types so you can tune the form input to accept only landline or only mobile, or whatever combination you want.

Corrects the errors found in http://djangosnippets.org/snippets/1207/ and adds extra functionality and detail to the code found at http://djangosnippets.org/snippets/2809/

In particular, this version rejects individual invalid area codes and caters for area codes with mixed-length numbering in fine-grained detail.

Uses info from: here

  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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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 a premium rate number.'),
        'reject_service': _('Phone number can\'t be a service number.'),
        'reject_geo': _('Phone number can\'t be a geographic landline number.'),
        'reject_georate': _('Phone number can\'t be a geographic-rate non-geographic number.'),
        'reject_voip': _('Phone number can\'t be VoIP number.')
        'reject_freefone': _('Phone number can\'t be a non-geographic freefone number.'),
        'reject_business': _('Phone number can\'t be a non-geographic business number.'),
        'reject_mobile': _('Phone number can\'t be mobile number.')
    }
    
    number_specs = (
        (r'^01((1(3[0-48]|[46][0-4]|5[012789]|7[0-49]|8[01349])|21[0-7]|31[0-8]|[459]1\d|61[0-46-9]))\d{6}$',   'geo',   (4, 3, 4)),
        (r'^016977[23]\d{3}$',                                        'geo',         (6, 4)),
        (r'^01(3873|5(242|39[456])|697[347]|768[347]|9467)\d{5}$',    'geo',         (6, 5)),
        (r'^0176888[234678]\d{2}$',                                   'geo',         (5, 5)),
        (r'^01(2(0(46[1-4]|87[2-9])|545[1-79]|76(2\d|3[1-8]|6[1-6])|9(7(2[0-4]|3[2-5])|8(2[2-8]|7[0-4789]|8[345])))|3(638[2-5]|647[23]|8(47[04-9]|64[015789]))|4(044[1-7]|20(2[23]|8\d)|6(0(30|5[2-57]|6[1-8]|7[2-8])|140)|8(052|87[123]))|5(24(3[2-79]|6\d)|276\d|6(26[06-9]|686))|6(06(4\d|7[4-79])|295[567]|35[34]\d|47(24|61)|59(5[08]|6[67]|74)|955[0-4])|7(26(6[13-9]|7[0-7])|442\d|50(2[0-3]|[3-68]2|76))|8(27[56]\d|37(5[2-5]|8[239])|84(3[2-58]))|9(0(0(6[1-8]|85)|52\d)|3583|4(66[1-8]|9(2[01]|81))|63(23|3[1-4])|9561))\d{3}$',   'geo',   (5, 5)),
        (r'^01(2(0[024-9]|2[3-9]|3[3-79]|4[1-689]|[58][02-9]|6[0-4789]|7[013-9]|9\d)|3(0\d|[25][02-9]|3[02-579]|[468][0-46-9]|7[1235679]|9[24578])|4(0[03-9]|2[02-5789]|[37]\d|4[02-69]|5[0-8]|[69][0-79]|8[0-5789])|5(0[1235-9]|2[024-9]|3[0145689]|4[02-9]|5[03-9]|6\d|7[0-35-9]|8[0-468]|9[0-5789])|6(0[034689]|2[0-689]|[38][013-9]|4[1-467]|5[0-69]|6[13-9]|7[0-8]|9[0124578])|7(0[0246-9]|2\d|3[023678]|4[03-9]|5[0-46-9]|6[013-9]|7[0-35-9]|8[024-9]|9[02-9])|8(0[35-9]|2[1-5789]|3[02-578]|4[0-578]|5[124-9]|6[2-69]|7\d|8[02-9]|9[02569])|9(0[02-589]|2[02-689]|3[1-5789]|4[2-9]|5[0-579]|6[234789]|7[0124578]|8\d|9[2-57]))\d{6}$',   'geo',   (5, 6)),
        (r'^02(0[01378]|3[0189]|4[017]|8[0-46-9]|9[012])\d{7}$',      'geo',         (3, 4, 4)),
        (r'^03[0347]\d{8}$',                                          'georate',     (4, 3, 4)),
        (r'^0500\d{6}$',                                              'freefone',    (4, 6)),
        (r'^05[56]\d{8}$',                                            'voip',        (3, 4, 4)),
        (r'^07([1-5789]\d{2}|624)\d{6}$',                             'mobile',      (5, 6)),
        (r'^070\d{8}$',                                               'premium',     (3, 4, 4)),
        (r'^08(001111|45464\d)$',                                     'freefone',    (4, 4)),
        (r'^0800\d{6}$',                                              'freefone',    (4, 6)),
        (r'^080[08]\d{7}$',                                           'freefone',    (4, 3, 4)),
        (r'^08(4[2-5]|70)\d{7}$',                                     'business',    (4, 3, 4)),
        (r'^0(87[123]|9([01]\d|8[0-3]))\d{7}$',                       'premium',     (4, 3, 4)),
        (r'^11[68]',                                                  '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

g1smd (on October 11, 2012):

Continued in https://github.com/g1smd/django-localflavor-UK-forms

#

Please login first before commenting.