diff --git a/CHANGES.rst b/CHANGES.rst index c1cf89c..2ce07d9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. +0.32.3 (2016-04-20) +^^^^^^^^^^^^^^^^^^^ + +- Added support for PhoneNumber objects as composites + + 0.32.2 (2016-04-20) ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/data_types.rst b/docs/data_types.rst index 87fc895..887f1d2 100644 --- a/docs/data_types.rst +++ b/docs/data_types.rst @@ -108,6 +108,8 @@ PhoneNumberType .. module:: sqlalchemy_utils.types.phone_number +.. autoclass:: PhoneNumber + .. autoclass:: PhoneNumberType diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 436eff8..126bd4c 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -93,4 +93,4 @@ from .types import ( # noqa WeekDaysType ) -__version__ = '0.32.2' +__version__ = '0.32.3' diff --git a/sqlalchemy_utils/types/phone_number.py b/sqlalchemy_utils/types/phone_number.py index 3d23eb5..02b6606 100644 --- a/sqlalchemy_utils/types/phone_number.py +++ b/sqlalchemy_utils/types/phone_number.py @@ -25,18 +25,46 @@ class PhoneNumber(BasePhoneNumber): .. _Python phonenumbers library: https://github.com/daviddrysdale/python-phonenumbers + + :: + + from sqlalchemy_utils import PhoneNumber + + + class User(self.Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + _phone_number = sa.Column(sa.Unicode(20)) + country_code = sa.Column(sa.Unicode(8)) + + phonenumber = sa.orm.composite( + PhoneNumber, + _phone_number, + country_code + ) + + + user = User(phone_number=PhoneNumber('0401234567', 'FI')) + + user.phone_number.e164 # u'+358401234567' + user.phone_number.international # u'+358 40 1234567' + user.phone_number.national # u'040 1234567' + user.country_code # 'FI' + + :param raw_number: String representation of the phone number. - :param country_code: - Country code of the phone number. + :param region: + Region of the phone number. ''' - def __init__(self, raw_number, country_code=None): + def __init__(self, raw_number, region=None): # Bail if phonenumbers is not found. if phonenumbers is None: raise ImproperlyConfigured( "'phonenumbers' is required to use 'PhoneNumber'") - self._phone_number = phonenumbers.parse(raw_number, country_code) + self._phone_number = phonenumbers.parse(raw_number, region) super(PhoneNumber, self).__init__( country_code=self._phone_number.country_code, national_number=self._phone_number.national_number, @@ -48,6 +76,7 @@ class PhoneNumber(BasePhoneNumber): self._phone_number.preferred_domestic_carrier_code ) ) + self.region = region self.national = phonenumbers.format_number( self._phone_number, phonenumbers.PhoneNumberFormat.NATIONAL @@ -61,6 +90,9 @@ class PhoneNumber(BasePhoneNumber): phonenumbers.PhoneNumberFormat.E164 ) + def __composite_values__(self): + return self.national, self.region + def is_valid_number(self): return phonenumbers.is_valid_number(self._phone_number) @@ -96,20 +128,20 @@ class PhoneNumberType(types.TypeDecorator, ScalarCoercible): def python_type(self, text): return self._coerce(text) - def __init__(self, country_code='US', max_length=20, *args, **kwargs): + def __init__(self, region='US', max_length=20, *args, **kwargs): # Bail if phonenumbers is not found. if phonenumbers is None: raise ImproperlyConfigured( "'phonenumbers' is required to use 'PhoneNumberType'") super(PhoneNumberType, self).__init__(*args, **kwargs) - self.country_code = country_code + self.region = region self.impl = types.Unicode(max_length) def process_bind_param(self, value, dialect): if value: if not isinstance(value, PhoneNumber): - value = PhoneNumber(value, country_code=self.country_code) + value = PhoneNumber(value, region=self.region) if self.STORE_FORMAT == 'e164' and value.extension: return '%s;ext=%s' % (value.e164, value.extension) @@ -120,11 +152,11 @@ class PhoneNumberType(types.TypeDecorator, ScalarCoercible): def process_result_value(self, value, dialect): if value: - return PhoneNumber(value, self.country_code) + return PhoneNumber(value, self.region) return value def _coerce(self, value): if value and not isinstance(value, PhoneNumber): - value = PhoneNumber(value, country_code=self.country_code) + value = PhoneNumber(value, region=self.region) return value or None diff --git a/tests/types/test_phonenumber.py b/tests/types/test_phonenumber.py index 6e3e719..ddc7284 100644 --- a/tests/types/test_phonenumber.py +++ b/tests/types/test_phonenumber.py @@ -148,3 +148,40 @@ class TestPhoneNumberType(object): user = User(phone_number='050111222') assert isinstance(user.phone_number, PhoneNumber) + + +@pytest.mark.skipif('types.phone_number.phonenumbers is None') +class TestPhoneNumberComposite(object): + @pytest.fixture + def User(self, Base): + class User(Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.String(255)) + _phone_number = sa.Column(sa.String(255)) + country = sa.Column(sa.String(255)) + phone_number = sa.orm.composite( + PhoneNumber, + _phone_number, + country + ) + return User + + @pytest.fixture + def user(self, session, User): + user = User() + user.name = u'Someone' + user.phone_number = PhoneNumber('+35840111222', 'FI') + session.add(user) + session.commit() + return user + + def test_query_returns_phone_number_object( + self, + session, + User, + user + ): + queried_user = session.query(User).first() + assert queried_user.phone_number.national == '040 111222' + assert queried_user.phone_number.region == 'FI'