Skip to content

Commit a6cf02c

Browse files
committed
chore: bump version 0.38.0 → 0.38.1
1 parent 97b1c31 commit a6cf02c

File tree

2 files changed

+88
-36
lines changed

2 files changed

+88
-36
lines changed

django_forms_workflows/forms.py

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,79 @@ def clean(self, data, initial=None):
136136
raise forms.ValidationError(self.error_messages["required"])
137137

138138

139+
# ---------------------------------------------------------------------------
140+
# Phone number normalisation
141+
# ---------------------------------------------------------------------------
142+
143+
144+
def _normalize_phone_number(raw: str) -> str:
145+
"""Parse *raw* and return a consistently formatted phone string.
146+
147+
Accepted inputs (all produce the same normalised output):
148+
2065551234 → (206) 555-1234
149+
206 555 1234 → (206) 555-1234
150+
206-555-1234 → (206) 555-1234
151+
206.555.1234 → (206) 555-1234
152+
(206) 555-1234 → (206) 555-1234 (pass-through)
153+
12065551234 → +1 (206) 555-1234
154+
+12065551234 → +1 (206) 555-1234
155+
+1 206 555 1234 → +1 (206) 555-1234
156+
+1 (206) 555-1234 → +1 (206) 555-1234 (pass-through)
157+
+44 7911 123456 → +44 (791) 111-2345 (best-effort for non-NANP)
158+
159+
Raises ``forms.ValidationError`` when the input cannot be normalised.
160+
"""
161+
raw = raw.strip()
162+
163+
has_plus = raw.startswith("+")
164+
digits = re.sub(r"\D", "", raw)
165+
166+
country_code: str | None = None
167+
local_digits: str
168+
169+
if has_plus and len(digits) > 10:
170+
# Everything before the last 10 digits is the country code.
171+
cc_len = len(digits) - 10
172+
country_code = digits[:cc_len]
173+
local_digits = digits[cc_len:]
174+
elif len(digits) == 11 and digits[0] == "1":
175+
# NANP with implicit country code: 1XXXXXXXXXX
176+
country_code = "1"
177+
local_digits = digits[1:]
178+
elif len(digits) == 10:
179+
local_digits = digits
180+
else:
181+
raise forms.ValidationError(
182+
"Enter a valid phone number. "
183+
"Examples: 2065551234, (206) 555-1234, or +1 (206) 555-1234."
184+
)
185+
186+
if len(local_digits) != 10:
187+
raise forms.ValidationError(
188+
"Phone number must contain 10 local digits. "
189+
"Examples: 2065551234 or +1 (206) 555-1234."
190+
)
191+
192+
local_fmt = f"({local_digits[0:3]}) {local_digits[3:6]}-{local_digits[6:10]}"
193+
return f"+{country_code} {local_fmt}" if country_code else local_fmt
194+
195+
196+
class PhoneFormField(forms.CharField):
197+
"""CharField that auto-normalises phone numbers via ``_normalize_phone_number``.
198+
199+
Users may type in any common format (digits only, dashes, dots, spaces,
200+
parentheses, with or without a country code). The value stored in the
201+
submission is always the canonical ``(NXX) NXX-XXXX`` or
202+
``+CC (NXX) NXX-XXXX`` form.
203+
"""
204+
205+
def clean(self, value):
206+
value = super().clean(value)
207+
if not value:
208+
return value
209+
return _normalize_phone_number(value)
210+
211+
139212
class DynamicForm(forms.Form):
140213
"""
141214
Dynamically generated form based on FormDefinition.
@@ -200,7 +273,7 @@ def __init__(self, form_definition, user=None, initial_data=None, *args, **kwarg
200273
Field(next_field.field_name),
201274
css_class=f"col-md-6 field-wrapper field-{next_field.field_name}",
202275
),
203-
css_class="align-items-end",
276+
css_class="align-items-start",
204277
)
205278
)
206279
i += 2
@@ -235,7 +308,7 @@ def __init__(self, form_definition, user=None, initial_data=None, *args, **kwarg
235308
)
236309
for f in group
237310
],
238-
css_class="align-items-end",
311+
css_class="align-items-start",
239312
)
240313
)
241314
else:
@@ -353,32 +426,21 @@ def add_field(self, field_def, initial_data):
353426
)
354427

355428
elif field_def.field_type == "phone":
356-
# Format: optional country code (+## ) then (###) ###-####
357-
# Examples: (555) 867-5309 | +1 (555) 867-5309 | +44 (555) 867-5309
358-
_phone_pattern = r"(\+[0-9]{1,3} )?\([0-9]{3}\) [0-9]{3}-[0-9]{4}"
429+
# Accepts any common format; normalised to (NXX) NXX-XXXX on clean().
359430
widget_attrs.update(
360431
{
361432
"type": "tel",
362433
"inputmode": "tel",
363-
"pattern": _phone_pattern,
364-
"placeholder": widget_attrs.get("placeholder", "(555) 867-5309"),
434+
"placeholder": widget_attrs.get(
435+
"placeholder", "e.g. 2065551234 or (206) 555-1234"
436+
),
365437
}
366438
)
367-
field = forms.CharField(
368-
max_length=20,
439+
self.fields[field_def.field_name] = PhoneFormField(
440+
max_length=25,
369441
widget=forms.TextInput(attrs=widget_attrs),
370442
**field_args,
371443
)
372-
field.validators.append(
373-
RegexValidator(
374-
regex=r"^(\+[0-9]{1,3} )?\([0-9]{3}\) [0-9]{3}-[0-9]{4}$",
375-
message=(
376-
"Enter a phone number in the format (555) 867-5309 "
377-
"or +1 (555) 867-5309 for international numbers."
378-
),
379-
)
380-
)
381-
self.fields[field_def.field_name] = field
382444

383445
elif field_def.field_type == "textarea":
384446
widget_attrs["rows"] = 4
@@ -1159,31 +1221,21 @@ def _create_field(self, field_def, field_args, widget_attrs, is_editable):
11591221
)
11601222

11611223
elif field_def.field_type == "phone":
1162-
# Format: optional country code (+## ) then (###) ###-####
1163-
_phone_pattern = r"(\+[0-9]{1,3} )?\([0-9]{3}\) [0-9]{3}-[0-9]{4}"
1224+
# Accepts any common format; normalised to (NXX) NXX-XXXX on clean().
11641225
widget_attrs.update(
11651226
{
11661227
"type": "tel",
11671228
"inputmode": "tel",
1168-
"pattern": _phone_pattern,
1169-
"placeholder": widget_attrs.get("placeholder", "(555) 867-5309"),
1229+
"placeholder": widget_attrs.get(
1230+
"placeholder", "e.g. 2065551234 or (206) 555-1234"
1231+
),
11701232
}
11711233
)
1172-
field = forms.CharField(
1173-
max_length=20,
1234+
self.fields[field_def.field_name] = PhoneFormField(
1235+
max_length=25,
11741236
widget=forms.TextInput(attrs=widget_attrs),
11751237
**field_args,
11761238
)
1177-
field.validators.append(
1178-
RegexValidator(
1179-
regex=r"^(\+[0-9]{1,3} )?\([0-9]{3}\) [0-9]{3}-[0-9]{4}$",
1180-
message=(
1181-
"Enter a phone number in the format (555) 867-5309 "
1182-
"or +1 (555) 867-5309 for international numbers."
1183-
),
1184-
)
1185-
)
1186-
self.fields[field_def.field_name] = field
11871239

11881240
elif field_def.field_type == "textarea":
11891241
widget_attrs["rows"] = 4

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-forms-workflows"
3-
version = "0.38.0"
3+
version = "0.38.1"
44
description = "Enterprise-grade, database-driven form builder with approval workflows and external data integration"
55
license = "LGPL-3.0-only"
66
readme = "README.md"

0 commit comments

Comments
 (0)