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 def set_country_and_region_code(self): # Try hinting with the shipping country if we can determine one. = self.get_country() if self.region_code = 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.") %, ) 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