Source code for people.models.person

from onegov.core.orm import Base
from onegov.core.orm.mixins import ContentMixin
from onegov.core.orm.mixins import TimestampMixin
from onegov.core.orm.mixins import UTCPublicationMixin
from onegov.core.orm.types import UUID
from onegov.people.models import AgencyMembership
from onegov.search import ORMSearchable
from sqlalchemy import Column
from sqlalchemy import Text
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import relationship
from uuid import uuid4
from vobject import vCard
from vobject.vcard import Address
from vobject.vcard import Name


from typing import TYPE_CHECKING
if TYPE_CHECKING:
    import uuid
    from collections.abc import Collection
    from onegov.core.types import AppenderQuery
    from vobject.base import Component


[docs] class Person(Base, ContentMixin, TimestampMixin, ORMSearchable, UTCPublicationMixin): """ A person. """
[docs] __tablename__ = 'people'
#: the type of the item, this can be used to create custom polymorphic #: subclasses of this class. See #: `<https://docs.sqlalchemy.org/en/improve_toc/\ #: orm/extensions/declarative/inheritance.html>`_.
[docs] type: 'Column[str]' = Column( Text, nullable=False, default=lambda: 'generic' )
[docs] __mapper_args__ = { 'polymorphic_on': type, 'polymorphic_identity': 'generic', }
[docs] es_public = True
[docs] es_properties = { 'title': {'type': 'text'}, 'function': {'type': 'localized'}, 'email': {'type': 'text'}, }
@property
[docs] def es_suggestion(self) -> tuple[str, ...]: return (self.title, f'{self.first_name} {self.last_name}')
@property
[docs] def title(self) -> str: """ Returns the Eastern-ordered name. """ return self.last_name + ' ' + self.first_name
@property
[docs] def spoken_title(self) -> str: """ Returns the Western-ordered name. Includes the academic title if available. """ parts = [] if self.academic_title: parts.append(self.academic_title) parts.append(self.first_name) parts.append(self.last_name) return ' '.join(parts)
#: the unique id, part of the url
[docs] id: 'Column[uuid.UUID]' = Column( UUID, # type:ignore[arg-type] primary_key=True, default=uuid4 )
#: the salutation used for the person
[docs] salutation: 'Column[str | None]' = Column(Text, nullable=True)
#: the academic title of the person
[docs] academic_title: 'Column[str | None]' = Column(Text, nullable=True)
#: the first name of the person
[docs] first_name: 'Column[str]' = Column(Text, nullable=False)
#: the last name of the person
[docs] last_name: 'Column[str]' = Column(Text, nullable=False)
#: when the person was born
[docs] born: 'Column[str | None]' = Column(Text, nullable=True)
#: the profession of the person
[docs] profession: 'Column[str | None]' = Column(Text, nullable=True)
#: the function of the person
[docs] function: 'Column[str | None]' = Column(Text, nullable=True)
#: an organisation the person belongs to
[docs] organisation: 'Column[str | None]' = Column(Text, nullable=True)
# a sub organisation the person belongs to
[docs] sub_organisation: 'Column[str | None]' = Column(Text, nullable=True)
#: the political party the person belongs to
[docs] political_party: 'Column[str | None]' = Column(Text, nullable=True)
#: the parliamentary group the person belongs to
[docs] parliamentary_group: 'Column[str | None]' = Column(Text, nullable=True)
#: an URL leading to a picture of the person
[docs] picture_url: 'Column[str | None]' = Column(Text, nullable=True)
#: the email of the person
[docs] email: 'Column[str | None]' = Column(Text, nullable=True)
#: the phone number of the person
[docs] phone: 'Column[str | None]' = Column(Text, nullable=True)
#: the direct phone number of the person
[docs] phone_direct: 'Column[str | None]' = Column(Text, nullable=True)
#: the website related to the person
[docs] website: 'Column[str | None]' = Column(Text, nullable=True)
#: a second website related to the person
[docs] website_2: 'Column[str | None]' = Column(Text, nullable=True)
# agency does not use 'address' anymore. Instead, the 4 following items # are being used. The 'address' field is still used in org, town6, # volunteers and others #: the address of the person
[docs] address: 'Column[str | None]' = Column(Text, nullable=True)
#: the location address (street name and number) of the person
[docs] location_address: 'Column[str | None]' = Column(Text, nullable=True)
#: postal code of location and city of the person
[docs] location_code_city: 'Column[str | None]' = Column(Text, nullable=True)
#: the postal address (street name and number) of the person
[docs] postal_address: 'Column[str | None]' = Column(Text, nullable=True)
#: postal code and city of the person
[docs] postal_code_city: 'Column[str | None]' = Column(Text, nullable=True)
#: some remarks about the person
[docs] notes: 'Column[str | None]' = Column(Text, nullable=True)
[docs] memberships: 'relationship[AppenderQuery[AgencyMembership]]'
memberships = relationship( AgencyMembership, back_populates='person', cascade='all, delete-orphan', lazy='dynamic', )
[docs] def vcard_object( self, exclude: 'Collection[str] | None' = None, include_memberships: bool = True ) -> 'Component': """ Returns the person as vCard (3.0) object. Allows to specify the included attributes, provides a reasonable default if none are specified. Always includes the first and last name. """ def split_code_from_city(code_city: str) -> tuple[str, str]: """ Splits a postal code and city into two parts. Supported are formats like '1234 City Name' and '12345 City Name'. """ import re match = re.match(r'(\d{4,5})\s+(.*)', code_city) if match: code, city = match.groups() else: # assume no code is present code, city = '', code_city return code, city exclude = exclude or ['notes'] result = vCard() prefix = '' if 'academic_title' not in exclude and self.academic_title: prefix = self.academic_title # mandatory fields line = result.add('n') line.value = Name( prefix=prefix, given=self.first_name, family=self.last_name, ) line.charset_param = 'utf-8' line = result.add('fn') line.value = f'{prefix} {self.first_name} {self.last_name}'.strip() line.charset_param = 'utf-8' # optional fields if 'function' not in exclude and self.function: line = result.add('title') line.value = self.function line.charset_param = 'utf-8' if 'picture_url' not in exclude and self.picture_url: line = result.add('photo') line.value = self.picture_url if 'email' not in exclude and self.email: line = result.add('email') line.value = self.email if 'phone' not in exclude and self.phone: line = result.add('tel;type=work') line.value = self.phone if 'phone_direct' not in exclude and self.phone_direct: line = result.add('tel;type=work;type=pref') line.value = self.phone_direct if 'organisation' not in exclude and self.organisation: line = result.add('org') line.value = [ '; '.join( o for o in (self.organisation, self.sub_organisation) if o ) ] line.charset_param = 'utf-8' if 'website' not in exclude and self.website: line = result.add('url') line.value = self.website if ( 'postal_address' not in exclude and self.postal_address and 'postal_code_city' not in exclude and self.postal_code_city ): line = result.add('adr') code, city = split_code_from_city(self.postal_code_city) line.value = Address(street=self.postal_address, code=code, city=city) line.charset_param = 'utf-8' if ( 'location_address' not in exclude and self.location_address and 'location_code_city' not in exclude and self.location_code_city ): line = result.add('adr') code, city = split_code_from_city(self.location_code_city) line.value = Address(street=self.location_address, code=code, city=city) line.charset_param = 'utf-8' if 'notes' not in exclude and self.notes: line = result.add('note') line.value = self.notes line.charset_param = 'utf-8' if include_memberships and (memberships := [ f'{m.agency.title}, {m.title}' for m in self.memberships.options( # eagerly load the agency along with the membership joinedload(AgencyMembership.agency) ) ]): line = result.add('org') line.value = ['; '.join(memberships)] line.charset_param = 'utf-8' return result
[docs] def vcard(self, exclude: 'Collection[str] | None' = None) -> str: """ Returns the person as vCard (3.0). Allows to specify the included attributes, provides a reasonable default if none are specified. Always includes the first and last name. """ return self.vcard_object(exclude).serialize()
@property
[docs] def memberships_by_agency(self) -> list[AgencyMembership]: """ Returns the memberships sorted alphabetically by the agency. """ def sortkey(membership: AgencyMembership) -> int: return membership.order_within_person return sorted(self.memberships, key=sortkey)