Source code for rattail.db.model.contact

# -*- coding: utf-8; -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-2023 Lance Edgar
#
#  This file is part of Rattail.
#
#  Rattail is free software: you can redistribute it and/or modify it under the
#  terms of the GNU General Public License as published by the Free Software
#  Foundation, either version 3 of the License, or (at your option) any later
#  version.
#
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
#  details.
#
#  You should have received a copy of the GNU General Public License along with
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Data Models for Contact Info
"""

import sqlalchemy as sa
from sqlalchemy import orm

from .core import Base, uuid_column


[docs] class PhoneNumber(Base): """ Represents a phone (or fax) number associated with a contactable entity. """ __tablename__ = 'phone' __table_args__ = ( sa.Index('phone_ix_parent', 'parent_type', 'parent_uuid'), ) __versioned__= {} uuid = uuid_column() parent_type = sa.Column(sa.String(length=20), nullable=False) parent_uuid = sa.Column(sa.String(length=32), nullable=False) preference = sa.Column(sa.Integer(), nullable=False) type = sa.Column(sa.String(length=15)) number = sa.Column(sa.String(length=20), nullable=False) __mapper_args__ = {'polymorphic_on': parent_type} def __str__(self): return self.number or "" @property def preferred(self): return self.preference == 1
[docs] class EmailAddress(Base): """ Represents an email address associated with a contactable entity. """ __tablename__ = 'email' __table_args__ = ( sa.Index('email_ix_parent', 'parent_type', 'parent_uuid'), ) __versioned__= {} uuid = uuid_column() parent_type = sa.Column(sa.String(length=20), nullable=False) parent_uuid = sa.Column(sa.String(length=32), nullable=False) preference = sa.Column(sa.Integer(), nullable=False) type = sa.Column(sa.String(length=15)) address = sa.Column(sa.String(length=255), nullable=False) invalid = sa.Column(sa.Boolean(), nullable=True, doc=""" Flag indicating whether the email address is *known* to be invalid. Defaults to NULL, meaning the validity is "not known". """) __mapper_args__ = {'polymorphic_on': parent_type} def __str__(self): return self.address or "" @property def preferred(self): return self.preference == 1
[docs] class MailingAddress(Base): """ Represents a physical / mailing address associated with a contactable entity. """ __tablename__ = 'address' __table_args__ = ( sa.Index('address_ix_parent', 'parent_type', 'parent_uuid'), ) __versioned__= {} uuid = uuid_column() parent_type = sa.Column(sa.String(length=20), nullable=False) parent_uuid = sa.Column(sa.String(length=32), nullable=False) preference = sa.Column(sa.Integer(), nullable=False) type = sa.Column(sa.String(length=15), nullable=True) street = sa.Column(sa.String(length=100), nullable=True) street2 = sa.Column(sa.String(length=100), nullable=True) city = sa.Column(sa.String(length=60), nullable=True) state = sa.Column(sa.String(length=2), nullable=True) zipcode = sa.Column(sa.String(length=10), nullable=True) invalid = sa.Column(sa.Boolean(), nullable=True) __mapper_args__ = {'polymorphic_on': parent_type} def __str__(self): if self.street and self.street2: street = '{}, {}'.format(self.street, self.street2) else: street = self.street or '' if self.city and self.state: city = '{}, {}'.format(self.city, self.state) else: city = self.city or self.state or '' if street and city and self.zipcode: text = '{}, {} {}'.format(street, city, self.zipcode) elif street and city: text = '{}, {}'.format(street, city) elif street and self.zipcode: text = '{} {}'.format(street, self.zipcode) elif city and self.zipcode: text = '{} {}'.format(city, self.zipcode) else: text = city or self.zipcode or '' return text @property def preferred(self): return self.preference == 1
[docs] class ContactMixin(object): """ Mixin which provides some useful methods for "contact" models, i.e. those which can play "parent" to email, phone and address records. """ # TODO: subclass must set these _contact_email_model = None _contact_phone_model = None _contact_address_model = None
[docs] def first_email(self, invalid=False, **kwargs): """ Return the first available email record for the contact. :param invalid: If true, then this may return an email marked invalid; if false then only valid email will be returned. """ if invalid: emails = self.emails else: emails = [email for email in self.emails if not email.invalid] if emails: return emails[0]
[docs] def first_email_address(self, invalid=False, **kwargs): """ Return the first available email address for the contact. """ email = self.first_email(invalid=invalid) if email: return email.address
[docs] def make_email(self, **kwargs): """ Make a new "email" record for the contact. """ email = self._contact_email_model(**kwargs) return email
[docs] def add_email(self, **kwargs): """ Add a new "email" record to the contact. """ flush = kwargs.pop('flush', True) primary = kwargs.pop('primary', False) email = self.make_email(**kwargs) self.emails.append(email) if flush: session = orm.object_session(self) session.flush() if primary: self.set_primary_email(email, flush=flush) return email
[docs] def set_primary_email(self, email, flush=True): """ Will re-arrange the contact's email records as needed to ensure that the given ``email`` record is "primary" - i.e. first in the list. """ if email.preference != 1: session = orm.object_session(self) contact = email.parent if not contact: contact = session.get(email.Parent, email.parent_uuid) if not contact: raise ValueError("cannot locate parent {} contact for email: {}".format( email.Parent.__name__, email)) emails = contact.emails if email in emails: emails.remove(email) emails.insert(0, email) emails.reorder() if flush: session.flush()
[docs] def remove_email(self, email, **kwargs): """ Remove the given email record from the contact. """ flush = kwargs.pop('flush', True) self.emails.remove(email) if flush: session = orm.object_session(self) session.flush()
[docs] def first_phone(self, **kwargs): """ Return the first available phone record for the contact. """ if self.phones: return self.phones[0]
[docs] def first_phone_number(self, **kwargs): """ Return the first available phone number for the contact. """ phone = self.first_phone() if phone: return phone.number
[docs] def make_phone(self, **kwargs): """ Make a new "phone" record for the contact. """ # set some safe defaults in case session is flushed early kwargs.setdefault('number', '') phone = self._contact_phone_model(**kwargs) return phone
[docs] def add_phone(self, **kwargs): """ Add a new "phone" record to the contact. """ flush = kwargs.pop('flush', True) primary = kwargs.pop('primary', False) phone = self.make_phone(**kwargs) self.phones.append(phone) if flush: session = orm.object_session(self) session.flush() if primary: self.set_primary_phone(phone, flush=flush) return phone
[docs] def remove_phone(self, phone, **kwargs): """ Remove the given phone record from the contact. """ flush = kwargs.pop('flush', True) self.phones.remove(phone) if flush: session = orm.object_session(self) session.flush()
[docs] def set_primary_phone(self, phone, flush=True): """ Will re-arrange the contact's phone records as needed to ensure that the given ``phone`` record is "primary" - i.e. first in the list. """ if phone.preference != 1: session = orm.object_session(self) contact = phone.parent if not contact: contact = session.get(phone.Parent, phone.parent_uuid) if not contact: raise ValueError("cannot locate parent {} contact for phone: {}".format( phone.Parent.__name__, phone)) phones = contact.phones if phone in phones: phones.remove(phone) phones.insert(0, phone) phones.reorder() if flush: session.flush()
[docs] def first_address(self, **kwargs): """ Return the first available address record for the contact. """ if self.addresses: return self.addresses[0]
[docs] def make_address(self, **kwargs): """ Make a new "address" record for the contact. """ address = self._contact_address_model(**kwargs) return address
[docs] def add_address(self, **kwargs): """ Add a new "address" record to the contact. """ flush = kwargs.pop('flush', True) address = self.make_address(**kwargs) self.addresses.append(address) if flush: session = orm.object_session(self) session.flush() return address
[docs] def set_primary_address(self, address, flush=True): """ Will re-arrange the contact's address records as needed to ensure that the given ``address`` record is "primary" - i.e. first in the list. """ if address.preference != 1: session = orm.object_session(self) contact = address.parent if not contact: contact = session.get(address.Parent, address.parent_uuid) if not contact: raise ValueError("cannot locate parent {} contact for address: {}".format( address.Parent.__name__, address)) addresses = contact.addresses if address in addresses: addresses.remove(address) addresses.insert(0, address) addresses.reorder() if flush: session.flush()
[docs] def remove_address(self, address, **kwargs): """ Remove the given address record from the contact. """ flush = kwargs.pop('flush', True) self.addresses.remove(address) if flush: session = orm.object_session(self) session.flush()