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_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)