Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Ticket 18903: GBPhoneNumberField #356

Closed
wants to merge 48 commits into from

4 participants

Brad Pitcher g1smd Florian Apolloner Adrian Holovaty
Brad Pitcher

This pull adds support for fields representing UK phone numbers, as well as tests and documentation.

g1smd

Your fork didn't pick up the vital change in
g1smd@d3bbcc1
from September 7th as I added that change a bit later in the day. Please add it to your repo. :-)

Thanks for your input so far. The conversion is fantastic. I have this code working and tested in Java and PHP already so I know the RegEx patterns used for those are 100% correct.

On September 10th I imported your four changes from September 7th and 8th into my repo.
On September 11th I made seven further commits to my repo g1smd/django but have no idea how to get them across to your stream.

I have changed the two RegEx patterns to multi-line format as that makes them much easier to read. I also made several small but vital changes to those patterns. (I'll update the wiki examples later).

Note that it is the valid_format pattern that stops numbers with incorrect NSN length making it through to later stages. This one allows a very wide range of input formats but chucks out a lot of garbage.

By splitting input format checking, NSN extraction, number range and length checking, and the output number formatting into separate processes much more detailed checks can be made at each stage.

I have also added a comment block listing accepted number formats for entry.

The final commits add a space after the +44 prefix, and fixes multiple identical cut and paste typos made in an earlier commit and some other typos and fixes.

Please update your repo to include all of this new code before making further changes, as I can then easily track the diffs after that point.

Thanks again for your fantastic effort in fixing up my broken python guesswork - and please check that I haven't introduced a silly syntax typo somewhere in the new code. In particular I don't know when to use ' or """ around values.

Florian Apolloner
Owner

@brad: You probably want to read https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/working-with-git/ and given https://groups.google.com/forum/?fromgroups=#!topic/django-developers/4EvlunsJ0LY I suggest you delay your work to after 1.5 since I highly doubt we'd merge your stuff before moving localflavor out. Btw why do you need such crazy checking at all? Even if the regex says a phone number is valid it doesn't mean it actually exists, so you have to call them to verify…

g1smd

Unlike the US and Canada where phone numbers always have 10 digits including a 3 digit area code, GB numbers have area codes from 2 to 5 digits in length and total number length of 9 or 10 digits - and sometimes mixed length numbers within the same area code. There's a lot more scope for making an error, but a few RegEx patterns can eliminate the vast majority of issues. The idea here is to eliminate the worst of the errors.

Additionally, many users don't always format their number correctly. Someone might write the London number 020 3000 5555 as 02030 005 555 or as +44 203 000 5555 akin to someone writing the New York number 212-555-7777 as 2125-557-777 or as +12-125-557-777. The user should be allowed to do that and then a few lines of simple code can reformat the number correctly for display.

International numbers are also often misunderstood, with people writing +44 20 3000 5555 as 00 44 20 3000 5555 (which won't work from the US or Canada) or as 011 44 20 3000 5555 (which won't work from Europe). These issues are also easily solved and the number can then be presented in the correct format.

Brad Pitcher

@apollo13 Thanks for pointing that out. I am aware that localflavors is getting moved out and decided to take my chances anyway. It's not the end of the world to me if I have to reapply the patch there.

@g1smd Can you please work on your changes and make sure they apply cleanly to my ticket_18903 branch? I'm getting conflicts and it is not clear the best way to resolve them.

g1smd

I'm not sure what to do. There's thousands of changes made by other people. I've no idea which apply or not.

Brad Pitcher

Yes, I've got the latest changes from django master in my branch. A merge should automatically accept all those thousands of changes without issue.

g1smd

All done.

I had to manually edit gb/forms.py again (actually just replaced it with a copy I already had as a backup in another folder).

g1smd

I have also refactored the RegEx patterns slightly and made them more efficient.

Brad Pitcher

That is much better, except those regexes aren't working for me (the unit tests fail). Can you explain what the differences are?

g1smd

If they all fail, I assume a typo. The new patterns are more strict and reject various area codes, prefixes and number ranges that do not exist and allow a range of dial prefixes with a variety of punctuation in various places.

Some of the RegEx patterns begin r' and others begin r""". I don't understand the difference. These RegEx patterns are working in another application, so I guess it's a python syntax problem/typo. I have now changed the ' to """ in several places.

Are you aware that the output is now formatted, rather than just a string of digits? The number format conforms to that listed at: http://www.aa-asterisk.org.uk/index.php/Number_format

Updated tests. Any further problems?

Brad Pitcher

Yeah, same problem as before, error: bad character range when parsing the gb_number_parts regex.
The """ just allows for multi-line strings. I don't think inline comments can be used in these expressions, but even with them removed I see the same error.

g1smd

OK. That's something concrete to go on.

Found a likely candidate, and fixed it. The '[\s-\d]' should probably be '[\s\d-]' here.

Inline comments should be usable with the 're.X' flag if it works anything like the PHP RegEx '/x' flag. Just be sure there are no slashes in the comments.

Brad Pitcher

Good work, the tests are now passing!

g1smd

Hurrah!

I'm not fazed by RegEx issues.

I have no idea what a pep8 is, but thanks for taking the time to do that too. :-) Good stuff.

What happens next?

g1smd

Is it worth adding more test data?

0114 223 4567 => Valid: +44 114 223 4567
01145 345 567 => Valid: +44 114 534 5567 <= typo (fixed in later commit)
+44 1213 456 789 => Valid: +44 121 345 6789
00 44 (0) 1697 73555 => Valid: +44 16977 3555
011 44 11 4890 2345 => Valid: +44 114 890 2345 <= typo (fixed in later commit)
025 4555 6777 => NOT VALID
0119 456 4567 => NOT VALID
0623 111 3456 => NOT VALID
0756 334556 => NOT VALID

Brad Pitcher

Yes, ideally we could cover all cases covered by the regular expressions.

g1smd

With optional spaces, hyphens and brackets, and the various area code and number lengths, there's over 400 valid combinations.

Brad Pitcher

I'm not too concerned about space/hyphen variations, since those cases are covered simply by removing those characters from the string. I'm more concerned with different area code lengths which I did not cover in my original test set.

g1smd

Those listed above, plus these:

020 3000 5000 => Valid: +44 20 3000 5000
0121 555 7777 => Valid: +44 121 555 7777
01750 615777 => Valid: +44 1750 615777
019467 55555 => Valid: +44 19467 55555
01750 62555 => Valid: +44 1750 62555
016977 3555 => Valid: +44 16977 3555
0500 777888 => Valid: +44 500 777888
020 5000 5000 => NOT VALID
0171 555 7777 => NOT VALID
01999 777888 => NOT VALID
01750 61777 => NOT VALID

Brad Pitcher

Can you please specify what the formatted result should be for each valid number?

g1smd

Amended list above. Also added several 'non-valid' numbers. Added to file and pushed.

Brad Pitcher

The test doesn't pass. Unfortunately it doesn't inform me which number(s) failed. I'll use the process of elimination I guess.

g1smd

Ack. It'll be a silly typo.

Brad Pitcher

'01145 345 567': '+44 114 534 5567'
and
'011 44 11 4890 2345': '+44 114 890 2345'
both fail at the valid_gb_phone_range step. I printed out number_parts for each number:
{u'prefix': u'+44 ', u'extension': None, u'NSN': u'1145345567'}
and
{u'prefix': u'+44 ', u'extension': None, u'NSN': u'1148902345'}

does that look right? Is the problem in the test or the validation?

g1smd

Yes, stupid typo. I had intended to use the 0141 area code, but typed 0114 which has less valid ranges.

Fixed and pushed. Apologies!

Brad Pitcher

Looks good, I think this is ready for checkin.

g1smd

Does anything else need amending on the ticket?

Brad Pitcher

I don't think so, now we just wait for it to catch someone's eye and mark it "Ready for Checkin". I don't think either of us should mark it so, since we were both involved in the patch.

Brad Pitcher

I'm going to go ahead and add @apollo13 's comment here since it seems important but it was deleted:

Given this crazy regex I suggest you test if it's vulnerable to backtracking attack (http://www.regular-expressions.info/catastrophic.html -- yes you actually can DOS a server with something like this, we had this in URLField a while ago)

g1smd

I've not come across that before, but I don't think it applies here because the patterns to match are always a fixed number of digits, and designed to parse left to right - and fail without much in the way of backtracking. There's very limited use of + or * quantifiers which seem to be the main part of the problem described in the article.

However, I'll look into it a bit more.

Florian Apolloner
Owner

@brad githubs ui fooled me :/

Brad Pitcher

@apollo13 hehe, it happens to all of us

g1smd

Are there any improvements that can be made to error messages?

"Not a valid format and/or number length" for non-matching valid_gb_format.

"Not a valid range or not a valid number length for this range" for non-matching valid_gb_range.

Brad Pitcher

OK, I have added both messages and the tests now check that the error message is correct.

g1smd

Thanks!

I don't know how deep to take the error checking.

I'm tempted to also add another simple code chunk right near the beginning:

If number matches [^\+\(\)\dext.\#\s-]

with error message

"Input contains invalid characters or punctuation. Use only digits and '+', '(', ')', 'ext.', 'x' and '#' with optional spaces and/or hyphens."

with test number 020@7000 8000 (preferred);

or even simply automatically clear all those unwanted junk characters from the input before processing (not preferred).

Additionally, it would be possible to add

If NOT match (r"""^\(?(?:(?:0(?:0|11)\)?[\s-]?\(?|\+)44\)?[\s-]?\(?(?:0\)?[\s-]?\(?)?|0).*$""")

with error message:

"Number must begin 0, +44, 00 44 or 011 44 followed by 9 or 10 digits and optional 'ext.', 'x' or '#' extension.

with test number 00 33 1 44 55 66 77 (preferred);

or else amend the existing 'Not a valid format and/or number length.' error message to instead read 'Not a valid format and/or number length. Number must begin 0, +44, 00 44 or 011 44 followed by 9 or 10 digits and optional 'ext.', 'x' or '#' extension.' (not preferred).

We also need a test number like 020 7000 9000 x4567 to test extension handling; result +44 20 7000 9000 #4567.

I don't know how much feedback to give the user. I want to keep things simple, but also feel that more specific error messages are a good idea where possible.

It's not easy to give specific warnings about too many digits or too few digits. That will have to be skipped here, relying only on the current "non valid length" message to cover both cases.

There are loads of other checks that could be done - such as warning when multiple contiguous spaces or hyphens or multiple unwanted punctuation is used (or automatically removing it), but there's diminishing returns on module speed. I'm not too bothered about checking for stuff like ((020))---7000---8000 as it should already be obvious to the user that they've already typed way too much extra crud.

Where's the dividing line between interpreting what the user wanted and just getting on with it (hence allowing 00 44 and 011 44 in addition to the usual +44) or saying 'you've botched the input, do it again'? I tend to write more detailed code to make a simpler or more intuitive user experience, but I know that not everyone holds that view perhaps preferring the bare minimum code for improved speed.

Brad Pitcher

I added tests for numbers with extensions. Please review my changes. I think this is as far as we should take the validation. While I appreciate the complexities of these numbers, there is a balance like you say between too much and too little validation. I fear it will be too much if it is taken any further, I think we have struck a good balance here, you are welcome to keep adding to it if you like.

g1smd

Looks OK.

I'll hold off until there is some user feedback. If users want more detailed error messages they can be added later.

g1smd

I added a comment to the ticket... ""We think this is 'Ready for Checkin'""" asking for others to take a look.

Adrian Holovaty

Hey there -- django.contrib.localflavor is now deprecated, and we're not making any more changes to it. Could you reopen this pull request for the shiny new package django-localflavor-gb? Here's the link: https://github.com/django/django-localflavor-gb

Sorry we didn't get to this pull request before the deprecation. I hope it's not too much of a pain to migrate this to the new package.

Adrian Holovaty adrianholovaty closed this October 15, 2012
g1smd

@brad has the latest version. My copy is out of sync.

Brad Pitcher

No problem, I'll take care of it. Thanks for the heads up, Adrian.

Brad Pitcher brad referenced this pull request in django/django-localflavor-gb October 15, 2012
Closed

Ticket 18903: GBPhoneNumberField #1

g1smd

Looks good. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 48 unique commits by 2 authors.

Sep 02, 2012
g1smd Outline for validating GB telephone numbers. e6e603e
g1smd Comments. d9da9c6
Sep 04, 2012
g1smd Merge branch 'master' of https://github.com/g1smd/django 5d910fc
Sep 05, 2012
g1smd Syntax fixes. be7ac29
g1smd More python code. f01b24e
g1smd Add more detailed code comments. 51a13a7
g1smd Removing rogue punctuation. b5a7b61
g1smd Fix RegEx inline comment. c2b0570
g1smd Rogue comment markers. a68c33c
g1smd More code snippets. d1d522a
Sep 07, 2012
g1smd Typo in comments. abcb036
Brad Pitcher Merge remote-tracking branch 'g1smd/master' into ticket_18903 95527eb
g1smd Add three missing "mixed [4+5]" area codes: 1524, 1768, 1946. d3bbcc1
Brad Pitcher working GBPhoneNumberField d362314
Brad Pitcher test_GBPhoneNumberField 88a0fc1
Sep 08, 2012
Brad Pitcher GBPhoneNumberField: more pythonic and simplified 1f93998
Brad Pitcher GBPhoneNumberField docs 8c08fca
Sep 11, 2012
g1smd Code tidied (from brad/django branch) 18b8822
g1smd Add three missing "mixed [4+5]" area codes: 1524, 1768, 1946. 66eea2a
g1smd Code tidied (from brad/django branch) 88a4626
g1smd [Revert to 66eea2a] Add three missing "mixed [4+5]" area codes: 1524,…
… 1768, 1946.
d811fd7
g1smd test_GBPhoneNumberField. (from brad/django branch) ebc5b7b
g1smd GBPhoneNumberField: more pythonic and simplified. (from brad/django b…
…ranch)
2e6561a
g1smd Add three missing "mixed [4+5]" area codes: 1524, 1768, 1946. e813ffb
g1smd GBPhoneNumberField docs. (from brad/django branch) 2829cda
g1smd Improved RegEx patterns for GB phone numbers. 39586c5
g1smd Insert spaces in formatted numbers. 33a48e5
g1smd Multi-line format RegEx patterns with improved comments. 985a131
g1smd Improved module documentation detailing accepted input formats. ba197fe
g1smd Add space after +44 prefix. Fix multiple identical cut and paste typos. 0827a51
g1smd Fix one cut and paste typo in RegEx pattern. a473705
g1smd Correct handling of # for extension. 77ae68f
Sep 12, 2012
Brad Pitcher Merge branch 'master' of github.com:brad/django into ticket_18903 d85f73a
g1smd Merge remote branch 'remotes/ticket_18903/ticket_18903' 0b562f6
Sep 16, 2012
g1smd Minor refactor of RegEx pattern. Minor edit to comments. 75fa013
g1smd Minor RegEx refactor for efficiency. 5fc8283
Sep 17, 2012
g1smd Syntax change. 6902292
g1smd Swap RegEx order to match the order used in other projects. eddbf8f
g1smd Updated phone number tests. 2d38e65
g1smd Minor change to character group. f5f4d4b
Brad Pitcher pep8 fixes bf279d8
Sep 18, 2012
g1smd Additional tests for other number formats. 2b2f2a6
Brad Pitcher Merge branch 'master' of git://github.com/g1smd/django into ticket_18903 483c92d
g1smd Minor amendments to tests. 3926473
g1smd Minor amendments to tests. 685ed82
Brad Pitcher Merge branch 'master' of git://github.com/g1smd/django into ticket_18903 ec4c83a
Brad Pitcher use more precise error messages 4f7cd2b
Sep 22, 2012
Brad Pitcher add extension tests 37c6950
This page is out of date. Refresh to see the latest.
308  django/contrib/localflavor/gb/forms.py
@@ -7,6 +7,7 @@
7 7
 import re
8 8
 
9 9
 from django.contrib.localflavor.gb.gb_regions import GB_NATIONS_CHOICES, GB_REGION_CHOICES
  10
+from django.core.validators import EMPTY_VALUES
10 11
 from django.forms.fields import CharField, Select
11 12
 from django.forms import ValidationError
12 13
 from django.utils.translation import ugettext_lazy as _
@@ -31,8 +32,8 @@ class GBPostcodeField(CharField):
31 32
 
32 33
     def clean(self, value):
33 34
         value = super(GBPostcodeField, self).clean(value)
34  
-        if value == '':
35  
-            return value
  35
+        if value in EMPTY_VALUES:
  36
+            return ''
36 37
         postcode = value.upper().strip()
37 38
         # Put a single space before the incode (second part).
38 39
         postcode = self.space_regex.sub(r' \1', postcode)
@@ -40,6 +41,7 @@ def clean(self, value):
40 41
             raise ValidationError(self.error_messages['invalid'])
41 42
         return postcode
42 43
 
  44
+
43 45
 class GBCountySelect(Select):
44 46
     """
45 47
     A Select widget that uses a list of UK Counties/Regions as its choices.
@@ -47,9 +49,311 @@ class GBCountySelect(Select):
47 49
     def __init__(self, attrs=None):
48 50
         super(GBCountySelect, self).__init__(attrs, choices=GB_REGION_CHOICES)
49 51
 
  52
+
50 53
 class GBNationSelect(Select):
51 54
     """
52 55
     A Select widget that uses a list of UK Nations as its choices.
53 56
     """
54 57
     def __init__(self, attrs=None):
55 58
         super(GBNationSelect, self).__init__(attrs, choices=GB_NATIONS_CHOICES)
  59
+
  60
+
  61
+class GBPhoneNumberField(CharField):
  62
+    """
  63
+     ==ACCEPTS==
  64
+     This table lists the valid accepted input formats for GB numbers.
  65
+     International:
  66
+        +          44_        null      20_3000_5000        null
  67
+       (+          44_(       0)_       20)_3000_5000       #5555
  68
+        00_        44)_       0)_(      121_555_7777       _#5555
  69
+        00_(       44)_(      0_        121)_555_7777       #555
  70
+       (00)_                  0_(       1750_615_777       _#555
  71
+       (00)_(                           1750)_615_777
  72
+       (00_                             19467_55555
  73
+        011_                            19467)_55555
  74
+        011_(                           1750_62555
  75
+       (011)_                           1750)_62555
  76
+       (011)_(                          16977_3555
  77
+       (011_                            16977)_3555
  78
+                                        500_777_888
  79
+                                        500)_777_888
  80
+     National:
  81
+        ->         ->         0         ^as above           ^as above
  82
+        ->         ->        (0         ^                   ^
  83
+     Pick one item from each column. Underscores represent spaces or hyphens.
  84
+     All number formats can also be matched without spaces or hyphens. The
  85
+     '#' character can also be an 'x'.
  86
+
  87
+     "Be conservative in what you do, be liberal in what you accept from
  88
+      others."
  89
+     (Postel's Law)
  90
+
  91
+     ==REJECTS==
  92
+     The following inputs are rejected:
  93
+     - international format numbers that do not begin with an item from column
  94
+       1 above,
  95
+     - international format numbers with country code other than 44,
  96
+     - national format numbers that do not begin with item in column 3 above,
  97
+     - numbers with more than 10 digits in NSN part,
  98
+     - numbers with less than 9 digits in NSN part (except for two special
  99
+       cases),
  100
+     - numbers with incorrect number of digits in NSN for number range,
  101
+     - numbers in ranges with NSN beginning 4 or 6 and other such non-valid
  102
+       ranges,
  103
+     - numbers with obviously non-GB formatting,
  104
+     - numbers with multiple contiguous spaces,
  105
+     - entries with letters and/or punctuation other than hyphens or brackets,
  106
+     - 116xxx, 118xxx, 1xx, 999.
  107
+
  108
+     ==OUTPUTS==
  109
+     Irrespective of the format used for input, all valid numbers are output
  110
+     with a +44 prefix followed by a space and the 10 or 9 digit national
  111
+     number arranged in the correct 2+8, 3+7, 4+6, 4+5, 5+5, 5+4 or 3+6 format.
  112
+    """
  113
+    default_error_messages = {
  114
+        'number_format': _('Not a valid format and/or number length.'),
  115
+        'number_range': _('Not a valid range or not a valid number length for '
  116
+                         'this range')
  117
+    }
  118
+
  119
+    def clean(self, value):
  120
+        super(GBPhoneNumberField, self).clean(value)
  121
+        if value in EMPTY_VALUES:
  122
+            return ''
  123
+
  124
+        number_parts = {'prefix': '+44 ', 'NSN': '', 'extension': None}
  125
+
  126
+        valid_gb_pattern = re.compile(r"""
  127
+        ^\(?
  128
+            (?:         # leading 00, 011 or + before 44 with optional (0)
  129
+                        # parentheses, hyphens and spaces optional
  130
+                (?:0(?:0|11)\)?[\s-]?\(?|\+)44\)?[\s-]?\(?(?:0\)?[\s-]?\(?)?
  131
+                |
  132
+                0                                            # leading 0
  133
+            )
  134
+            (?:
  135
+                \d{5}\)?[\s-]?\d{4,5}                        # [5+4][5+5]
  136
+                |
  137
+                \d{4}\)?[\s-]?(?:\d{5}|\d{3}[\s-]?\d{3})     # [4+5][4+6]
  138
+                |
  139
+                \d{3}\)?[\s-]?\d{3}[\s-]?\d{3,4}             # [3+6][3+7]
  140
+                |
  141
+                \d{2}\)?[\s-]?\d{4}[\s-]?\d{4}               # [2+8]
  142
+                |
  143
+                8(?:00[\s-]?11[\s-]?11|45[\s-]?46[\s-]?4\d)  # [0+7]
  144
+            )
  145
+            (?:
  146
+                (?:[\s-]?(?:x|ext\.?|\#)\d+)?    # optional extension number
  147
+            )
  148
+        $""", re.X)
  149
+
  150
+        gb_number_parts = re.compile(r"""
  151
+        ^\(?
  152
+            (?:         # leading 00, 011 or + before 44 with optional (0)
  153
+                        # parentheses, hyphens and spaces optional
  154
+                (?:0(?:0|11)\)?[\s-]?\(?|\+)(44)\)?[\s-]?\(?(?:0\)?[\s-]?\(?)?
  155
+                |
  156
+                0                           # leading 0
  157
+            )
  158
+            (
  159
+                [1-9]\d{1,4}\)?[\s\d-]+     # NSN
  160
+            )
  161
+            (?:
  162
+                ((?:x|ext\.?|\#)\d+)?       # optional extension number
  163
+            )
  164
+        $""", re.X)
  165
+
  166
+        # Check if number entered matches a valid format
  167
+        if not re.search(valid_gb_pattern, value):
  168
+            raise ValidationError(self.default_error_messages['number_format'])
  169
+
  170
+        # Extract number parts: prefix, NSN, extension
  171
+        # group(1) contains "44" or None depending on whether number entered in
  172
+        # international or national format
  173
+        # group(2) contains NSN
  174
+        # group(3) contains extension
  175
+        m = re.search(gb_number_parts, value)
  176
+        if m.group:
  177
+            # Extract NSN part of GB number
  178
+            if m.group(2):
  179
+                # Trim NSN and remove space, hyphen or ')' if present
  180
+                translate_table = dict((ord(char), u'') for char in u')- ')
  181
+                number_parts['NSN'] = m.group(2).translate(
  182
+                    translate_table).strip()
  183
+
  184
+                # Extract extension
  185
+                if m.group(3):
  186
+                    # Add a # and remove the x
  187
+                    number_parts['extension'] = '#' + m.group(3)[1:]
  188
+        if not number_parts:
  189
+            raise ValidationError(self.default_error_messages['number_format'])
  190
+
  191
+        phone_number_nsn = number_parts['NSN']
  192
+        # Check if NSN entered is in a valid range
  193
+        if not valid_gb_phone_range(phone_number_nsn):
  194
+            raise ValidationError(self.default_error_messages['number_range'])
  195
+
  196
+        return format_gb_phone_number(number_parts)
  197
+
  198
+
  199
+def valid_gb_phone_range(phone_number_nsn):
  200
+    """
  201
+    Verifies that phone_number_nsn is a valid UK phone number range by initial
  202
+    digits and length. Tests the NSN part for length and number range. Based on
  203
+    http://www.aa-asterisk.org.uk/index.php/Number_format
  204
+    http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_UK_Telephone_Numbers
  205
+    @param string phone_number_nsn
  206
+    @return boolean Returns boolean False if the phone number is not valid.
  207
+    """
  208
+    return re.match(re.compile(r"""
  209
+    ^(       # 2d with 10 digits [2+8] Landlines
  210
+        2(?:0[01378]|3[0189]|4[017]|8[0-46-9]|9[012])\d{7}
  211
+        |    # 11d, 1d1 with 10 digits [3+7] Landlines
  212
+        1(?:(?: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}
  213
+        |    # 1ddd (and 1dddd) with 10 digits [4+6][5+5] Landlines
  214
+        1(?: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}
  215
+        |    # 1ddd with 9 digits [4+5] Landlines
  216
+        1(?: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}
  217
+        |    # 1ddd with 9 digits [4+5] Landlines (special case)
  218
+        176888[234678]\d{2}
  219
+        |    # 1dddd with 9 digits [5+4] Landlines
  220
+        16977[23]\d{3}
  221
+        |    # 7ddd (including 7624) (not 70, 76) with 10 digits [4+6] Mobile phones
  222
+        7(?:[1-4]\d\d|5(?:0[0-8]|[13-9]\d|2[0-35-9])|624|7(?:0[1-9]|[1-7]\d|8[02-9]|9[0-689])|8(?:[014-9]\d|[23][0-8])|9(?:[04-9]\d|1[02-9]|2[0-35-9]|3[0-689]))\d{6}
  223
+        |    # 76 (excluding 7624) with 10 digits [2+8] Pagers
  224
+        76(?:0[012]|2[356]|4[0134]|5[49]|6[0-369]|77|81|9[39])\d{6}
  225
+        |    # 800 with 9 or 10 digits, 808 with 10 digits, 500 with 9 digits [3+7][3+6] Freephone
  226
+        80(?:0\d{6,7}|8\d{7})|500\d{6}
  227
+        |    # 871, 872, 873, 90d, 91d, 980, 981, 982, 983 with 10 digits [3+7] Premium rate
  228
+        (?:87[123]|9(?:[01]\d|8[0-3]))\d{7}
  229
+        |     # 842, 843, 844, 845, 870 with 10 digits [3+7] Business rate
  230
+        8(?:4[2-5]|70)\d{7}
  231
+        |    # 70 with 10 digits [2+8] Personal numbers
  232
+        70\d{8}
  233
+        |    # 56 with 10 digits [2+8] LIECS&VoIP
  234
+        56\d{8}
  235
+        |    # 30d, 33d, 34d, 37d, 55 with 10 digits [3+7] UAN and [2+8] Corporate
  236
+        (?:3[0347]|55)\d{8}
  237
+        |    # 800 1111, 845 46 4d with 7 digits [3+4] Freephone helplines
  238
+        8(?:001111|45464\d)
  239
+    )$
  240
+    """, re.X), phone_number_nsn)
  241
+
  242
+
  243
+def format_gb_nsn(phone_number_nsn):
  244
+    """
  245
+    Format GB phone numbers in correct format per number range. Based on
  246
+    http://www.aa-asterisk.org.uk/index.php/Number_format
  247
+    http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_UK_Telephone_Numbers
  248
+    created by @g1smd
  249
+    @param string phone_number_nsn Must be the 10 or 9 digit NSN part of the
  250
+        number.
  251
+    @return string phone_number_nsn Returns correctly formatted NSN by length
  252
+        and range.
  253
+    """
  254
+    nsn_length = len(phone_number_nsn)
  255
+    # RegEx patterns to define formatting by length and initial digits
  256
+    # [2+8] 2d, 55, 56, 70, 76 (not 7624) with 10 digits
  257
+    pattern28 = re.compile(r"^(?:2|5[56]|7(?:0|6(?:[013-9]|2[0-35-9])))")
  258
+    capture28 = re.compile(r"^(\d{2})(\d{4})(\d{4})$")
  259
+    # [3+7] 11d, 1d1, 3dd, 80d, 84d, 87d, 9dd with 10 digits
  260
+    pattern37 = re.compile(r"^(?:1(?:1|\d1)|3|8(?:0[08]|4[2-5]|7[0-3])|9[018])")
  261
+    capture37 = re.compile(r"^(\d{3})(\d{3})(\d{4})$")
  262
+    # [5+5] 1dddd (12 areas) with 10 digits
  263
+    pattern55 = re.compile(r"^(?:1(?:3873|5(?:242|39[456])|697[347]|768[347]|9467))")
  264
+    capture55 = re.compile(r"^(\d{5})(\d{5})")
  265
+    # [5+4] 1dddd (1 area) with 9 digits
  266
+    pattern54 = re.compile(r"^(?:16977[23])")
  267
+    capture54 = re.compile(r"^(\d{5})(\d{4})$")
  268
+    # [4+6] 1ddd, 7ddd (inc 7624) (not 70, 76) with 10 digits
  269
+    pattern46 = re.compile(r"^(?:1|7(?:[1-5789]|624))")
  270
+    capture46 = re.compile(r"^(\d{4})(\d{6})$")
  271
+    # [4+5] 1ddd (40 areas) with 9 digits
  272
+    pattern45 = re.compile(r"^(?:1(?:2(?:0[48]|54|76|9[78])|3(?:6[34]|8[46])|4(?:04|20|6[01]|8[08])|5(?:2[47]|6[26])|6(?:06|29|35|47|59|95)|7(?:26|44|50|68)|8(?:27|37|84)|9(?:0[05]|35|4[69]|63|95)))")
  273
+    capture45 = re.compile(r"^(\d{4})(\d{5})$")
  274
+    # [3+6] 500, 800 with 9 digits
  275
+    pattern36 = re.compile(r"^(?:[58]00)")
  276
+    capture36 = re.compile(r"^(\d{3})(\d{6})$")
  277
+    # [3+4] 8001111, 845464d with 7 digits
  278
+    pattern34 = re.compile(r"^(?:8(?:001111|45464\d))")
  279
+    capture34 = re.compile(r"^(\d{3})(\d{4})$")
  280
+    # Format numbers by leading digits and length
  281
+    if nsn_length is 10 and re.match(pattern28, phone_number_nsn):
  282
+        m = (re.search(capture28, phone_number_nsn))
  283
+        if m.group:
  284
+            phone_number_nsn = m.group(1) + ' ' + m.group(2) + ' ' + m.group(3)
  285
+    elif nsn_length is 10 and re.match(pattern37, phone_number_nsn):
  286
+        m = (re.search(capture37, phone_number_nsn))
  287
+        if m.group:
  288
+            phone_number_nsn = m.group(1) + ' ' + m.group(2) + ' ' + m.group(3)
  289
+    elif nsn_length is 10 and re.match(pattern55, phone_number_nsn):
  290
+        m = (re.search(capture55, phone_number_nsn))
  291
+        if m.group:
  292
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  293
+    elif nsn_length is 9 and re.match(pattern54, phone_number_nsn):
  294
+        m = (re.search(capture54, phone_number_nsn))
  295
+        if m.group:
  296
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  297
+    elif nsn_length is 10 and re.match(pattern46, phone_number_nsn):
  298
+        m = (re.search(capture46, phone_number_nsn))
  299
+        if m.group:
  300
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  301
+    elif nsn_length is 9 and re.match(pattern45, phone_number_nsn):
  302
+        m = (re.search(capture45, phone_number_nsn))
  303
+        if m.group:
  304
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  305
+    elif nsn_length is 9 and re.match(pattern36, phone_number_nsn):
  306
+        m = (re.search(capture36, phone_number_nsn))
  307
+        if m.group:
  308
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  309
+    elif nsn_length is 7 and re.match(pattern34, phone_number_nsn):
  310
+        m = (re.search(capture34, phone_number_nsn))
  311
+        if m.group:
  312
+            phone_number_nsn = m.group(1) + ' ' + m.group(2)
  313
+    elif nsn_length > 5:
  314
+        # Default format for non-valid numbers (shouldn't ever get here)
  315
+        m = (re.search("^(\d)(\d{4})(\d*)$", phone_number_nsn))
  316
+        if m.group:
  317
+            phone_number_nsn = m.group(1) + ' ' + m.group(2) + ' ' + m.group(3)
  318
+
  319
+    return phone_number_nsn
  320
+
  321
+
  322
+def format_gb_phone_number(number_parts):
  323
+    """
  324
+    Convert a valid United Kingdom phone number into standard +44 20 3000 5555
  325
+    #0001, +44 121 555 7788, +44 1970 223344, +44 1750 62555, +44 19467 55555
  326
+    or +44 16977 2333 international format or into national format with 0,
  327
+    according to entry format. Accepts a wide range of input formats and
  328
+    prefixes and re-formats the number taking into account the required 2+8,
  329
+    3+7, 4+6, 4+5, 5+5, 5+4 and 3+6 formats by number range.
  330
+
  331
+    @param dict number_parts must be a valid nine or ten-digit number split
  332
+        into its constituent parts
  333
+    @return string phone_number
  334
+    """
  335
+    phone_number = number_parts['prefix'] + number_parts['NSN'] + \
  336
+        str(number_parts['extension'])
  337
+
  338
+    if number_parts:
  339
+        # Grab the NSN part of GB number
  340
+        phone_number_nsn = number_parts['NSN']
  341
+        if not phone_number_nsn:
  342
+            return phone_number
  343
+
  344
+        # Set prefix (will be +44 or 0)
  345
+        if 'prefix' in number_parts and number_parts['prefix'] is not None:
  346
+            phone_number = number_parts['prefix']
  347
+
  348
+        # Remove spaces, hyphens, and brackets from NSN part of GB number
  349
+        translate_table = dict((ord(char), u'') for char in u')- ')
  350
+        phone_number_nsn = phone_number_nsn.translate(translate_table).strip()
  351
+        # Format NSN part of GB number
  352
+        phone_number += format_gb_nsn(phone_number_nsn)
  353
+
  354
+        # Grab extension and trim it
  355
+        if 'extension' in number_parts and \
  356
+                number_parts['extension'] is not None:
  357
+            phone_number += ' ' + number_parts['extension'].strip()
  358
+
  359
+    return phone_number
4  docs/ref/contrib/localflavor.txt
@@ -1276,6 +1276,10 @@ United Kingdom (``gb``)
1276 1276
     expression used is sourced from the schema for British Standard BS7666
1277 1277
     address types at http://www.cabinetoffice.gov.uk/media/291293/bs7666-v2-0.xml.
1278 1278
 
  1279
+.. class:: gb.forms.GBPhoneNumberField
  1280
+
  1281
+    A form field that validates input as a UK phone number.
  1282
+
1279 1283
 .. class:: gb.forms.GBCountySelect
1280 1284
 
1281 1285
     A ``Select`` widget that uses a list of UK counties/regions as its choices.
67  tests/regressiontests/localflavor/gb/tests.py
... ...
@@ -1,6 +1,8 @@
1 1
 from __future__ import unicode_literals
2 2
 
3  
-from django.contrib.localflavor.gb.forms import GBPostcodeField
  3
+from django.contrib.localflavor.gb.forms import (
  4
+    GBPostcodeField, GBPhoneNumberField
  5
+)
4 6
 
5 7
 from django.test import SimpleTestCase
6 8
 
@@ -30,3 +32,66 @@ def test_GBPostcodeField(self):
30 32
         }
31 33
         kwargs = {'error_messages': {'invalid': 'Enter a bloody postcode!'}}
32 34
         self.assertFieldOutput(GBPostcodeField, valid, invalid, field_kwargs=kwargs)
  35
+
  36
+    def test_GBPhoneNumberField(self):
  37
+        valid = {
  38
+            '020 3000 5555': '+44 20 3000 5555',
  39
+            '(020) 3000 5555': '+44 20 3000 5555',
  40
+            '+44 20 3000 5555': '+44 20 3000 5555',
  41
+            '0203 000 5555': '+44 20 3000 5555',
  42
+            '(0203) 000 5555': '+44 20 3000 5555',
  43
+            '02030 005 555': '+44 20 3000 5555',
  44
+            '+44 (0) 20 3000 5555': '+44 20 3000 5555',
  45
+            '+44(0)203 000 5555': '+44 20 3000 5555',
  46
+            '00 (44) 2030 005 555': '+44 20 3000 5555',
  47
+            '(+44 203) 000 5555': '+44 20 3000 5555',
  48
+            '(+44) 203 000 5555': '+44 20 3000 5555',
  49
+            '011 44 203 000 5555': '+44 20 3000 5555',
  50
+            '020-3000-5555': '+44 20 3000 5555',
  51
+            '(020)-3000-5555': '+44 20 3000 5555',
  52
+            '+44-20-3000-5555': '+44 20 3000 5555',
  53
+            '0203-000-5555': '+44 20 3000 5555',
  54
+            '(0203)-000-5555': '+44 20 3000 5555',
  55
+            '02030-005-555': '+44 20 3000 5555',
  56
+            '+44-(0)-20-3000-5555': '+44 20 3000 5555',
  57
+            '+44(0)203-000-5555': '+44 20 3000 5555',
  58
+            '00-(44)-2030-005-555': '+44 20 3000 5555',
  59
+            '(+44-203)-000-5555': '+44 20 3000 5555',
  60
+            '(+44)-203-000-5555': '+44 20 3000 5555',
  61
+            '011-44-203-000-5555': '+44 20 3000 5555',
  62
+            '0114 223 4567': '+44 114 223 4567',
  63
+            '01142 345 567': '+44 114 234 5567',
  64
+            '01415 345 567': '+44 141 534 5567',
  65
+            '+44 1213 456 789': '+44 121 345 6789',
  66
+            '00 44 (0) 1697 73555': '+44 16977 3555',
  67
+            '011 44 14 1890 2345': '+44 141 890 2345',
  68
+            '011 44 11 4345 2345': '+44 114 345 2345',
  69
+            '020 3000 5000': '+44 20 3000 5000',
  70
+            '0121 555 7777': '+44 121 555 7777',
  71
+            '01750 615777': '+44 1750 615777',
  72
+            '019467 55555': '+44 19467 55555',
  73
+            '01750 62555': '+44 1750 62555',
  74
+            '016977 3555': '+44 16977 3555',
  75
+            '0500 777888': '+44 500 777888',
  76
+            '020 7000 9000 x4567': '+44 20 7000 9000 #4567',
  77
+            '020 7000 9000 #4567': '+44 20 7000 9000 #4567'
  78
+        }
  79
+        errors = GBPhoneNumberField.default_error_messages
  80
+        messages = {
  81
+            'number_format': [errors['number_format'].translate('gb')],
  82
+            'number_range': [errors['number_range'].translate('gb')]
  83
+        }
  84
+        invalid = {
  85
+            '011 44 203 000 5555 5': messages['number_format'],
  86
+            '+44 20 300 5555': messages['number_format'],
  87
+            '025 4555 6777': messages['number_range'],
  88
+            '0119 456 4567': messages['number_range'],
  89
+            '0623 111 3456': messages['number_range'],
  90
+            '0756 334556': messages['number_range'],
  91
+            '020 5000 5000': messages['number_range'],
  92
+            '0171 555 7777': messages['number_range'],
  93
+            '01999 777888': messages['number_range'],
  94
+            '01750 61777': messages['number_range'],
  95
+            '020 7000 9000 ext. 4567': messages['number_format']
  96
+        }
  97
+        self.assertFieldOutput(GBPhoneNumberField, valid, invalid)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.