Source code for rattail.db.model.people

# -*- coding: utf-8; -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-20234 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 People
"""

import datetime
import warnings

import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.orderinglist import ordering_list

from .core import Base, uuid_column, Note
from .contact import PhoneNumber, EmailAddress, MailingAddress, ContactMixin
from rattail.db.util import normalize_full_name


# TODO: deprecate/remove this
def get_person_display_name(first_name, last_name):
    return normalize_full_name(first_name, last_name)

# TODO: rename this?
def get_person_display_name_from_context(context):
    first_name = context.current_parameters.get('first_name')
    last_name = context.current_parameters.get('last_name')
    return normalize_full_name(first_name, last_name)


[docs] class Person(ContactMixin, Base): """ Represents a real, living and breathing person. (Or, at least was previously living and breathing, in the case of the deceased.) """ __tablename__ = 'person' __versioned__ = { 'exclude': ['modified'], } uuid = uuid_column() first_name = sa.Column(sa.String(length=50), nullable=True, doc=""" Person's "true" first name. """) preferred_first_name = sa.Column(sa.String(length=50), nullable=True, doc=""" Preferred first name for the person, if applicable. When present, this may be used *instead of* the ``first_name`` value, when displaying the person's full name etc. """) middle_name = sa.Column(sa.String(length=50), nullable=True, doc=""" Person's middle name (or initial), if applicable/known. """) last_name = sa.Column(sa.String(length=50), nullable=True, doc=""" Person's last name. """) # TODO: rename this to 'full_name' at some point...? display_name = sa.Column(sa.String(length=100), nullable=True) local_only = sa.Column(sa.Boolean(), nullable=True, doc=""" Flag indicating the person is somehow specific to the "local" app node etc. and should not be synced elsewhere. """) invalid_address = sa.Column(sa.Boolean(), nullable=True, doc=""" Flag indicating the person's mailing address(es) on file are invalid. """) modified = sa.Column(sa.DateTime(), nullable=True, onupdate=datetime.datetime.utcnow) def __str__(self): return self.display_name or "" @property def user(self): if self.users: return self.users[0] # TODO: deprecate / remove this def add_email_address(self, address, type='Home', flush=False): email = self.add_email(type=type, address=address, flush=flush) return email # TODO: deprecate / remove this def add_phone_number(self, number, type='Home', flush=False): phone = self.add_phone(type=type, number=number, flush=flush) return phone # TODO: deprecate / remove this
[docs] def first_valid_email(self): """ Returns the first :class:`Email` which has not been marked invalid, or ``None``. """ for email in self.emails: if not email.invalid: return email
# TODO: deprecate / remove this
[docs] def first_valid_email_address(self): """ Returns the first email address which has not been marked invalid, or ``None``. """ email = self.first_valid_email() if email: return email.address
[docs] def only_customer(self, require=True): """ DEPRECATED Convenience method to retrieve the one and only ``Customer`` record which is associated with this person. An error will be raised if there is not exactly one customer associated. """ warnings.warn("person.only_customer() is deprecated; please " "use app.get_customer(person) instead", DeprecationWarning, stacklevel=2) # TODO: all 3 options below are indeterminate, since it's # *possible* for a person to hold multiple accounts # etc. but not sure how to fix in a generic way? maybe # just everyone must override as needed if self.customer_accounts: return self.customer_accounts[0] for shopper in self.customer_shoppers: if shopper.shopper_number == 1: return shopper.customer # legacy fallback if self.customers: return self.customers[0] if require: raise ValueError(f"person {self.uuid} has no customer")
[docs] class PersonContactInfoMixin(object): """ Base mixin class for person contact info models. """ Parent = Person @declared_attr def parent(cls): return orm.synonym('person')
[docs] class PersonPhoneNumber(PersonContactInfoMixin, PhoneNumber): """ Represents a phone (or fax) number associated with a person. """ __mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_phone_model = PersonPhoneNumber Person.phones = orm.relationship( PersonPhoneNumber, primaryjoin=PersonPhoneNumber.parent_uuid == Person.uuid, foreign_keys=[PersonPhoneNumber.parent_uuid], collection_class=ordering_list('preference', count_from=1), order_by=PersonPhoneNumber.preference, cascade='save-update, merge, delete, delete-orphan', doc=""" Sequence of :class:`PersonPhoneNUmber` instances which belong to the person. """, backref=orm.backref( 'person', cascade_backrefs=False, doc=""" Reference to the :class:`Person` instance to which the phone number belongs. """), ) Person.phone = orm.relationship( PersonPhoneNumber, primaryjoin=sa.and_( PersonPhoneNumber.parent_uuid == Person.uuid, PersonPhoneNumber.preference == 1, ), foreign_keys=[PersonPhoneNumber.parent_uuid], uselist=False, viewonly=True)
[docs] class PersonEmailAddress(PersonContactInfoMixin, EmailAddress): """ Represents an email address associated with a person. """ __mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_email_model = PersonEmailAddress Person.emails = orm.relationship( PersonEmailAddress, primaryjoin=PersonEmailAddress.parent_uuid == Person.uuid, foreign_keys=[PersonEmailAddress.parent_uuid], collection_class=ordering_list('preference', count_from=1), order_by=PersonEmailAddress.preference, cascade='save-update, merge, delete, delete-orphan', doc=""" Sequence of :class:`PersonEmailAddress` instances which belong to the person. """, backref=orm.backref( 'person', cascade_backrefs=False, doc=""" Reference to the :class:`Person` instance to which the email address belongs. """), ) Person.email = orm.relationship( PersonEmailAddress, primaryjoin=sa.and_( PersonEmailAddress.parent_uuid == Person.uuid, PersonEmailAddress.preference == 1, ), foreign_keys=[PersonEmailAddress.parent_uuid], uselist=False, viewonly=True)
[docs] class PersonMailingAddress(PersonContactInfoMixin, MailingAddress): """ Represents a physical / mailing address associated with a person. """ __mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_address_model = PersonMailingAddress Person.addresses = orm.relationship( PersonMailingAddress, backref='person', primaryjoin=PersonMailingAddress.parent_uuid == Person.uuid, foreign_keys=[PersonMailingAddress.parent_uuid], collection_class=ordering_list('preference', count_from=1), order_by=PersonMailingAddress.preference, cascade='save-update, merge, delete, delete-orphan') Person.address = orm.relationship( PersonMailingAddress, primaryjoin=sa.and_( PersonMailingAddress.parent_uuid == Person.uuid, PersonMailingAddress.preference == 1, ), foreign_keys=[PersonMailingAddress.parent_uuid], uselist=False, viewonly=True)
[docs] class PersonNote(Note): """ Represents a note attached to a person. """ __mapper_args__ = {'polymorphic_identity': 'Person'}
Person.notes = orm.relationship( PersonNote, primaryjoin=PersonNote.parent_uuid == Person.uuid, foreign_keys=[PersonNote.parent_uuid], order_by=PersonNote.created, cascade='all, delete-orphan', doc=""" Sequence of notes which belong to the person. """, backref=orm.backref( 'person', cascade_backrefs=False, doc=""" Reference to the person to which the note is attached. """))
[docs] class MergePeopleRequest(Base): """ Represents a *request* to merge 2 people. If the request is no longer wanted then it should be deleted. Otherwise it should be updated to reflect who satisfies the request, by performing the merge. """ __tablename__ = 'merge_request_people' __table_args__ = ( sa.ForeignKeyConstraint(['requested_by_uuid'], ['user.uuid'], name='merge_request_people_fk_requested_by'), sa.ForeignKeyConstraint(['merged_by_uuid'], ['user.uuid'], name='merge_request_people_fk_merged_by'), ) uuid = uuid_column() removing_uuid = sa.Column(sa.String(length=32), nullable=False, doc=""" UUID of the "unwanted" person, which is to be merged into the "preserved" person. """) keeping_uuid = sa.Column(sa.String(length=32), nullable=False, doc=""" UUID of the person to be "preserved", into which the "unwanted" person is to be merged. """) requested = sa.Column(sa.DateTime(), nullable=False, default=datetime.datetime.utcnow, doc=""" Date and time when the request was made. """) requested_by_uuid = sa.Column(sa.String(length=32), nullable=False) requested_by = orm.relationship( 'User', foreign_keys=[requested_by_uuid], doc=""" Reference to the User who created the request. """) merged = sa.Column(sa.DateTime(), nullable=True, doc=""" Date and time when the merge was performed. """) merged_by_uuid = sa.Column(sa.String(length=32), nullable=True) merged_by = orm.relationship( 'User', foreign_keys=[merged_by_uuid], doc=""" Reference to the User who performed the merge. """) def __str__(self): return "Person Merge Request: {} -> {}".format( self.removing_uuid, self.keeping_uuid)