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