Source code for oscar.forms.mixins
import phonenumbers
from django import forms
from django.core import validators
from django.utils.translation import gettext_lazy as _
from phonenumber_field.phonenumber import PhoneNumber
[docs]class PhoneNumberMixin(object):
"""Validation mixin for forms with a phone numbers, and optionally a country.
It tries to validate the phone numbers, and on failure tries to validate
them using a hint (the country provided), and treating it as a local number.
Specify which fields to treat as phone numbers by specifying them in
`phone_number_fields`, a dictionary of fields names and default kwargs
for instantiation of the field.
"""
country = None
region_code = None
# Since this mixin will be used with `ModelForms`, names of phone number
# fields should match names of the related Model field
phone_number_fields = {
"phone_number": {
"required": False,
"help_text": "",
"max_length": 32,
"label": _("Phone number"),
},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# We can't use the PhoneNumberField here since we want validate the
# phonenumber based on the selected country as a fallback when a local
# number is entered. We add the fields in the init since on Python 2
# using forms.Form as base class results in errors when using this
# class as mixin.
# If the model field already exists, copy existing properties from it
for field_name, field_kwargs in self.phone_number_fields.items():
for key in field_kwargs:
try:
field_kwargs[key] = getattr(self.fields[field_name], key)
except (KeyError, AttributeError):
pass
self.fields[field_name] = forms.CharField(**field_kwargs)
def get_country(self):
# If the form data contains valid country information, we use that.
if hasattr(self, "cleaned_data") and "country" in self.cleaned_data:
return self.cleaned_data["country"]
# Oscar hides the field if there's only one country. Then (and only
# then!) we can consider a country on the model instance.
elif "country" not in self.fields and hasattr(self.instance, "country"):
return self.instance.country
def set_country_and_region_code(self):
# Try hinting with the shipping country if we can determine one.
self.country = self.get_country()
if self.country:
self.region_code = self.country.iso_3166_1_a2
def clean_phone_number_field(self, field_name):
number = self.cleaned_data.get(field_name)
# Empty
if number in validators.EMPTY_VALUES:
return ""
# Check for an international phone format
try:
phone_number = PhoneNumber.from_string(number)
except phonenumbers.NumberParseException:
if not self.region_code:
# There is no shipping country, not a valid international number
self.add_error(
field_name, _("This is not a valid international phone format.")
)
return number
# The PhoneNumber class does not allow specifying
# the region. So we drop down to the underlying phonenumbers
# library, which luckily allows parsing into a PhoneNumber
# instance.
try:
phone_number = PhoneNumber.from_string(number, region=self.region_code)
if not phone_number.is_valid():
self.add_error(
field_name,
_("This is not a valid local phone format for %s.")
% self.country,
)
except phonenumbers.NumberParseException:
# Not a valid local or international phone number
self.add_error(
field_name,
_("This is not a valid local or international phone format."),
)
return number
return phone_number
def clean(self):
self.set_country_and_region_code()
cleaned_data = super().clean()
for field_name in self.phone_number_fields:
cleaned_data[field_name] = self.clean_phone_number_field(field_name)
return cleaned_data