Source code for flexicon.code.Notebook.PersonOperations

#
#   PersonOperations.py
#
#   Class: PersonOperations
#          Person operations for FieldWorks Language Explorer
#          projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

# Import FLEx LCM types
from SIL.LCModel import (
    ICmPerson,
    ICmPersonFactory,
    ICmPersonRepository,
    ICmPossibility,
    ICmPossibilityRepository,
    ICmLocation,
    ILangProject,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_match_key


[docs] class PersonOperations(BaseOperations): """ This class provides operations for managing people (consultants, speakers, researchers) in a FieldWorks project. People are stored as ICmPerson objects and can represent consultants, native speakers, researchers, or any other individuals associated with the linguistic data collection and analysis. This class should be accessed via FLExProject.Person property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all people for person in project.Person.GetAll(): name = project.Person.GetName(person) print(name) # Create a new person person = project.Person.Create("John Smith") # Set properties project.Person.SetEmail(person, "john.smith@example.com") project.Person.SetDateOfBirth(person, "1985-03-15") project.Person.SetGender(person, "Male") # Add contact information project.Person.SetPhone(person, "+1-555-123-4567") project.Person.SetAddress(person, "123 Main St, City, Country") # Add notes project.Person.AddNote(person, "Primary consultant for dialect study") project.CloseProject() """ def __init__(self, project): """ Initialize PersonOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all people in the project. This method returns an iterator over all ICmPerson objects in the project database, allowing iteration over all registered people. Yields: ICmPerson: Each person object in the project Example: >>> for person in project.Person.GetAll(): ... name = project.Person.GetName(person) ... email = project.Person.GetEmail(person) ... print(f"{name}: {email}") John Smith: john.smith@example.com Maria Garcia: maria.garcia@example.com Ahmed Hassan: ahmed.hassan@example.com Notes: - Returns an iterator for memory efficiency - People are returned in database order - Includes all person types (consultants, researchers, speakers) - Use GetName() to access the display name See Also: Find, Create, GetName """ return self.project.ObjectsIn(ICmPersonRepository) @OperationsMethod def Create(self, name, wsHandle=None): """ Create a new person in the FLEx project. Args: name (str): The full name of the person wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: ICmPerson: The newly created person object Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If name is None FP_ParameterError: If name is empty Example: >>> # Create a basic person >>> person = project.Person.Create("John Smith") >>> print(project.Person.GetName(person)) John Smith >>> # Create with specific writing system >>> person = project.Person.Create("María García", ... project.WSHandle('es')) >>> # Create and set additional properties >>> consultant = project.Person.Create("Ahmed Hassan") >>> project.Person.SetEmail(consultant, "ahmed@example.com") >>> project.Person.SetGender(consultant, "Male") Notes: - The person is added to the project's people collection - Only name is required; other properties can be set later - Person GUID is auto-generated - Use Set* methods to add contact information and other details See Also: Delete, Exists, Find, SetName """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") wsHandle = self.__WSHandle(wsHandle) # Create the new person using the factory with self._TransactionCM(f"Create person '{name}'"): factory = self.project.project.ServiceLocator.GetService(ICmPersonFactory) new_person = factory.Create() # Add person to the language project's people collection (must be done before setting properties) self.project.lp.PeopleOA.PossibilitiesOS.Add(new_person) # Set the name mkstr = TsStringUtils.MakeString(name, wsHandle) new_person.Name.set_String(wsHandle, mkstr) return new_person @OperationsMethod def Delete(self, person_or_hvo): """ Delete a person from the FLEx project. Args: person_or_hvo: Either an ICmPerson object or its HVO (database ID) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo is None FP_ParameterError: If person doesn't exist Example: >>> person = project.Person.Find("John Smith") >>> if person: ... project.Person.Delete(person) >>> # Delete by HVO >>> project.Person.Delete(12345) Warning: - This is a destructive operation - All associated data (positions, notes, etc.) will be deleted - References from texts or lexical entries may become invalid - Cannot be undone - Person will be removed from all linked records Notes: - Deletion cascades to all owned objects - Consider archiving data before deletion - Use with caution in production databases See Also: Create, Exists """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") # Resolve to person object person = self.__ResolveObject(person_or_hvo) # Remove from people collection self.project.lp.PeopleOA.PossibilitiesOS.Remove(person) @OperationsMethod def Exists(self, name, wsHandle=None): """ Check if a person with the given name exists. Args: name (str): The name to search for wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: bool: True if a person exists with this name, False otherwise Raises: FP_NullParameterError: If name is None Example: >>> if not project.Person.Exists("John Smith"): ... person = project.Person.Create("John Smith") >>> # Check in specific writing system >>> if project.Person.Exists("María García", project.WSHandle('es')): ... print("Spanish name exists") Notes: - Search is case-sensitive - Search is writing-system specific - Returns False for empty or whitespace-only names - Use Find() to get the actual person object See Also: Find, Create """ self._ValidateParam(name, "name") if not name or not name.strip(): return False return self.Find(name, wsHandle) is not None @OperationsMethod def Find(self, name, wsHandle=None): """ Find a person by name. Args: name (str): The name to search for wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: ICmPerson or None: The person object if found, None otherwise Raises: FP_NullParameterError: If name is None Example: >>> person = project.Person.Find("John Smith") >>> if person: ... email = project.Person.GetEmail(person) ... print(f"Found: {email}") Found: john.smith@example.com >>> # Search in specific writing system >>> person = project.Person.Find("María García", ... project.WSHandle('es')) Notes: - Returns first match only - Search is case-sensitive - Search is writing-system specific - Returns None if not found (doesn't raise exception) - For partial name search, iterate GetAll() and filter See Also: Exists, GetAll, GetName """ self._ValidateParam(name, "name") if not name or not name.strip(): return None wsHandle = self.__WSHandle(wsHandle) # Search through all people target = normalize_match_key(name, casefold=False) for person in self.GetAll(): person_name = ITsString(person.Name.get_String(wsHandle)).Text if normalize_match_key(person_name, casefold=False) == target: return person return None # --- Name Management --- @OperationsMethod def GetName(self, person_or_hvo, wsHandle=None): """ Get the name of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: str: The person's name (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> name = project.Person.GetName(person) >>> print(name) John Smith >>> # Get in specific writing system >>> name_es = project.Person.GetName(person, project.WSHandle('es')) Notes: - Returns empty string if name not set - Returns empty string if not set in specified writing system - Name can be set in multiple writing systems See Also: SetName, Create """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandle(wsHandle) name = ITsString(person.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetName(self, person_or_hvo, name, wsHandle=None): """ Set the name of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO name (str): The new name wsHandle: Optional writing system handle. Defaults to vernacular WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or name is None FP_ParameterError: If name is empty Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetName(person, "John Robert Smith") >>> print(project.Person.GetName(person)) John Robert Smith >>> # Set in specific writing system >>> project.Person.SetName(person, "Juan Smith", ... project.WSHandle('es')) Notes: - Name should be the full name - Can be set in multiple writing systems - Empty names are not allowed See Also: GetName, Create """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) person.Name.set_String(wsHandle, mkstr) # --- Gender --- @OperationsMethod def GetGender(self, person_or_hvo, wsHandle=None): """ Get the gender of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The gender (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> gender = project.Person.GetGender(person) >>> print(gender) Male Notes: - Returns empty string if gender not set - Gender is stored as a string field - Common values: "Male", "Female", "Other", but can be any text See Also: SetGender """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) gender = ITsString(person.Gender.get_String(wsHandle)).Text return gender or "" @OperationsMethod def SetGender(self, person_or_hvo, gender, wsHandle=None): """ Set the gender of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO gender (str): The gender to set wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or gender is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetGender(person, "Male") >>> # Clear gender >>> project.Person.SetGender(person, "") Notes: - Gender can be any text value - Common values: "Male", "Female", "Other" - Can be empty string to clear See Also: GetGender """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(gender, "gender") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(gender, wsHandle) person.Gender.set_String(wsHandle, mkstr) # --- Date of Birth --- @OperationsMethod def GetDateOfBirth(self, person_or_hvo): """ Get the date of birth of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: str: Date of birth as string (empty if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> dob = project.Person.GetDateOfBirth(person) >>> print(dob) 1985-03-15 Notes: - Returns empty string if date not set - Date format depends on how it was stored - Typically stored as ISO format (YYYY-MM-DD) or locale format See Also: SetDateOfBirth """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) # DateOfBirth is stored as a GenDate field if person.DateOfBirth: return str(person.DateOfBirth) return "" @OperationsMethod def SetDateOfBirth(self, person_or_hvo, date_str): """ Set the date of birth of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO date_str (str): Date of birth as string (e.g., "1985-03-15") Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or date_str is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetDateOfBirth(person, "1985-03-15") >>> # Clear date >>> project.Person.SetDateOfBirth(person, "") Notes: - Accepts various date formats - ISO format (YYYY-MM-DD) recommended - Can be empty string to clear - Invalid dates may raise FLEx internal errors See Also: GetDateOfBirth """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(date_str, "date_str") person = self.__ResolveObject(person_or_hvo) # DateOfBirth is a GenDate field - set as string person.DateOfBirth = date_str # --- Contact Information --- @OperationsMethod def GetEmail(self, person_or_hvo, wsHandle=None): """ Get the email address of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Email address (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> email = project.Person.GetEmail(person) >>> print(email) john.smith@example.com Notes: - Returns empty string if email not set - No email validation is performed - Can store multiple emails as comma-separated values See Also: SetEmail, GetPhone """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) email = ITsString(person.Email.get_String(wsHandle)).Text return email or "" @OperationsMethod def SetEmail(self, person_or_hvo, email, wsHandle=None): """ Set the email address of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO email (str): Email address to set wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or email is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetEmail(person, "john.smith@example.com") >>> # Multiple emails >>> project.Person.SetEmail(person, ... "john.smith@example.com, j.smith@work.org") >>> # Clear email >>> project.Person.SetEmail(person, "") Notes: - No email validation is performed - Can be empty string to clear - Multiple emails can be comma-separated See Also: GetEmail, SetPhone """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(email, "email") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(email, wsHandle) person.Email.set_String(wsHandle, mkstr) @OperationsMethod def GetPhone(self, person_or_hvo, wsHandle=None): """ Get the phone number of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Phone number (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> phone = project.Person.GetPhone(person) >>> print(phone) +1-555-123-4567 Notes: - Returns empty string if phone not set - No phone number validation or formatting is performed - Can store multiple numbers See Also: SetPhone, GetEmail """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) phone = ITsString(person.PlaceOfBirth.get_String(wsHandle)).Text return phone or "" @OperationsMethod def SetPhone(self, person_or_hvo, phone, wsHandle=None): """ Set the phone number of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO phone (str): Phone number to set wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or phone is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetPhone(person, "+1-555-123-4567") >>> # Multiple numbers >>> project.Person.SetPhone(person, ... "Mobile: +1-555-123-4567, Office: +1-555-890-1234") >>> # Clear phone >>> project.Person.SetPhone(person, "") Notes: - No phone validation or formatting is performed - Can be empty string to clear - Multiple numbers can be listed See Also: GetPhone, SetEmail """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(phone, "phone") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(phone, wsHandle) person.PlaceOfBirth.set_String(wsHandle, mkstr) @OperationsMethod def GetAddress(self, person_or_hvo, wsHandle=None): """ Get the address of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Address (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> address = project.Person.GetAddress(person) >>> print(address) 123 Main St, City, Country Notes: - Returns empty string if address not set - No address format validation - Can be multi-line using newline characters See Also: SetAddress, GetResidences """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) # Use Abbreviation field for address storage address = ITsString(person.Abbreviation.get_String(wsHandle)).Text return address or "" @OperationsMethod def SetAddress(self, person_or_hvo, address, wsHandle=None): """ Set the address of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO address (str): Address to set wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or address is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetAddress(person, ... "123 Main St\\nCity, State 12345\\nCountry") >>> # Clear address >>> project.Person.SetAddress(person, "") Notes: - Can be empty string to clear - No format validation - Use \\n for multi-line addresses See Also: GetAddress, AddResidence """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(address, "address") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(address, wsHandle) person.Abbreviation.set_String(wsHandle, mkstr) # --- Academic Information --- @OperationsMethod def GetEducation(self, person_or_hvo, wsHandle=None): """ Get the education information of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Education information (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> education = project.Person.GetEducation(person) >>> print(education) PhD Linguistics, University of Example, 2010 Notes: - Returns empty string if education not set - Can contain degrees, institutions, years, etc. - No specific format required See Also: SetEducation, GetPositions """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) education = ITsString(person.Description.get_String(wsHandle)).Text return education or "" @OperationsMethod def SetEducation(self, person_or_hvo, education, wsHandle=None): """ Set the education information of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO education (str): Education information to set wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or education is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetEducation(person, ... "PhD Linguistics, University of Example, 2010\\n" ... "MA Anthropology, State University, 2005") >>> # Clear education >>> project.Person.SetEducation(person, "") Notes: - Can be empty string to clear - Can be multi-line using \\n - No format validation See Also: GetEducation, AddPosition """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(education, "education") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) mkstr = TsStringUtils.MakeString(education, wsHandle) person.Description.set_String(wsHandle, mkstr) @OperationsMethod def GetPositions(self, person_or_hvo): """ Get the positions/roles associated with a person. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: list: List of ICmPossibility objects representing positions (empty list if none) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> positions = project.Person.GetPositions(person) >>> for pos in positions: ... name = ITsString(pos.Name.BestAnalysisAlternative).Text ... print(f"Position: {name}") Position: Consultant Position: Native Speaker Notes: - Returns empty list if no positions assigned - Positions are ICmPossibility objects from project lists - Typically from the Positions possibility list See Also: AddPosition """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return list(person.PositionsRC) @OperationsMethod def AddPosition(self, person_or_hvo, position): """ Add a position/role to a person. Args: person_or_hvo: Either an ICmPerson object or its HVO position: ICmPossibility object representing the position Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or position is None FP_ParameterError: If position is not a valid ICmPossibility Example: >>> person = project.Person.Find("John Smith") >>> # Get position from project's positions list >>> positions_list = project.lp.PositionsOA >>> if positions_list: ... consultant_pos = positions_list.PossibilitiesOS[0] ... project.Person.AddPosition(person, consultant_pos) Notes: - Position must be an ICmPossibility from project lists - Duplicate positions are allowed - Use GetPositions() to verify before adding See Also: GetPositions """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(position, "position") person = self.__ResolveObject(person_or_hvo) try: position_poss = ICmPossibility(position) person.PositionsRC.Add(position_poss) except (AttributeError, System.InvalidCastException) as e: raise FP_ParameterError("position must be a valid ICmPossibility object") @OperationsMethod def Duplicate(self, person_or_hvo, insert_after=True): """ Duplicate a person, creating a new copy with a new GUID. Args: person_or_hvo: The ICmPerson object or HVO to duplicate. insert_after (bool): If True (default), insert after the source person. If False, insert at end of people collection. deep (bool): Reserved for future use (persons have no owned objects). Currently has no effect. Returns: ICmPerson: The newly created duplicate person with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If person_or_hvo is None. Example: >>> # Duplicate a person >>> person = project.Person.Find("John Smith") >>> dup = project.Person.Duplicate(person) >>> print(f"Original: {project.Person.GetGuid(person)}") >>> print(f"Duplicate: {project.Person.GetGuid(dup)}") Original: 12345678-1234-1234-1234-123456789abc Duplicate: 87654321-4321-4321-4321-cba987654321 >>> # Verify properties copied >>> print(project.Person.GetEmail(dup)) john.smith@example.com Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original person's position - Simple properties copied: Name, Gender, Email, DateOfBirth - MultiString properties copied: Abbreviation (address), Description (education), Comment (notes), PlaceOfBirth (phone) - Reference collections copied: PositionsRC, PlacesOfResidenceRC, LanguagesRC - DateCreated and DateModified are NOT copied (set automatically) - deep parameter has no effect (persons have no owned objects) See Also: Create, Delete, GetGuid """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") # Get source person source = self.__ResolveObject(person_or_hvo) # Create new person using factory (auto-generates new GUID) with self._TransactionCM("Duplicate person"): factory = self.project.project.ServiceLocator.GetService(ICmPersonFactory) duplicate = factory.Create() # Determine insertion position and add to parent FIRST if insert_after: source_index = self.project.lp.PeopleOA.PossibilitiesOS.IndexOf(source) self.project.lp.PeopleOA.PossibilitiesOS.Insert(source_index + 1, duplicate) else: self.project.lp.PeopleOA.PossibilitiesOS.Add(duplicate) # Copy simple MultiString properties duplicate.Name.CopyAlternatives(source.Name) duplicate.Gender.CopyAlternatives(source.Gender) duplicate.Email.CopyAlternatives(source.Email) duplicate.Abbreviation.CopyAlternatives(source.Abbreviation) # Address duplicate.Description.CopyAlternatives(source.Description) # Education duplicate.Comment.CopyAlternatives(source.Comment) # Notes duplicate.PlaceOfBirth.CopyAlternatives(source.PlaceOfBirth) # Phone # Copy DateOfBirth (GenDate field) if hasattr(source, "DateOfBirth") and source.DateOfBirth: duplicate.DateOfBirth = source.DateOfBirth # Copy Reference Collection (RC) properties if hasattr(source, "PositionsRC"): for position in source.PositionsRC: duplicate.PositionsRC.Add(position) if hasattr(source, "PlacesOfResidenceRC"): for residence in source.PlacesOfResidenceRC: duplicate.PlacesOfResidenceRC.Add(residence) if hasattr(source, "LanguagesRC"): for language in source.LanguagesRC: duplicate.LanguagesRC.Add(language) # Note: deep parameter has no effect for persons (no owned objects) return duplicate # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """ Get syncable properties for cross-project synchronization. Returns all syncable properties of a person including MultiString fields and reference collections (as GUIDs). Args: item: The ICmPerson object Returns: dict: Dictionary of syncable properties Example: >>> props = project.Person.GetSyncableProperties(person) >>> print(props) {'Name': 'John Smith', 'Gender': 'Male', 'Email': 'john@...', ...} """ self._ValidateParam(item, "item") person = self.__ResolveObject(item) wsHandle = self.project.project.DefaultAnalWs props = {} # MultiString properties props["Name"] = ITsString(person.Name.get_String(wsHandle)).Text or "" props["Gender"] = ITsString(person.Gender.get_String(wsHandle)).Text or "" props["Email"] = ITsString(person.Email.get_String(wsHandle)).Text or "" props["Abbreviation"] = ITsString(person.Abbreviation.get_String(wsHandle)).Text or "" # Address props["Description"] = ITsString(person.Description.get_String(wsHandle)).Text or "" # Education props["Comment"] = ITsString(person.Comment.get_String(wsHandle)).Text or "" # Notes props["PlaceOfBirth"] = ITsString(person.PlaceOfBirth.get_String(wsHandle)).Text or "" # Phone # GenDate field if hasattr(person, "DateOfBirth") and person.DateOfBirth: props["DateOfBirth"] = str(person.DateOfBirth) else: props["DateOfBirth"] = None # Reference Collection (RC) properties - return GUIDs as strings if hasattr(person, "PositionsRC"): props["Positions"] = [str(pos.Guid) for pos in person.PositionsRC] if hasattr(person, "PlacesOfResidenceRC"): props["PlacesOfResidence"] = [str(place.Guid) for place in person.PlacesOfResidenceRC] if hasattr(person, "LanguagesRC"): props["Languages"] = [str(lang.Guid) for lang in person.LanguagesRC] return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """ Compare two persons and return detailed differences. Args: item1: First person (from source project) item2: Second person (from target project) ops1: Operations instance for item1's project (defaults to self) ops2: Operations instance for item2's project (defaults to self) Returns: tuple: (is_different, differences_dict) where differences_dict contains 'properties' dict with changed property details Example: >>> is_diff, diffs = ops1.CompareTo(person1, person2, ops1, ops2) >>> if is_diff: ... for prop, details in diffs['properties'].items(): ... print(f"{prop}: {details['source']} -> {details['target']}") """ if ops1 is None: ops1 = self if ops2 is None: ops2 = self is_different = False differences = {"properties": {}} # Get syncable properties from both items props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) # Compare each property for key in set(props1.keys()) | set(props2.keys()): val1 = props1.get(key) val2 = props2.get(key) # Handle list comparisons (for RC properties) if isinstance(val1, list) and isinstance(val2, list): if set(val1) != set(val2): is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} elif val1 != val2: is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} return is_different, differences # --- Metadata --- @OperationsMethod def GetGuid(self, person_or_hvo): """ Get the GUID (Globally Unique Identifier) of a person. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: System.Guid: The person's GUID Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> guid = project.Person.GetGuid(person) >>> print(guid) a1b2c3d4-e5f6-7890-abcd-ef1234567890 >>> # Use GUID to retrieve person later >>> person2 = project.Object(guid) >>> print(project.Person.GetName(person2)) John Smith Notes: - GUIDs are unique across all FLEx projects - GUIDs are persistent (don't change) - Useful for linking people across projects - Can be used with FLExProject.Object() to retrieve person See Also: FLExProject.Object, FLExProject.BuildGotoURL """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return person.Guid @OperationsMethod def GetDateCreated(self, person_or_hvo): """ Get the creation date of a person record. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: System.DateTime: The date and time the person record was created Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> created = project.Person.GetDateCreated(person) >>> print(f"Created: {created}") Created: 2025-01-15 14:30:22 Notes: - Returns System.DateTime object (not Python datetime) - Automatically set when person is created - Cannot be modified (read-only property) - Timezone is local to the FLEx project See Also: GetDateModified """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return person.DateCreated @OperationsMethod def GetDateModified(self, person_or_hvo): """ Get the last modification date of a person record. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: System.DateTime: The date and time the person record was last modified Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> modified = project.Person.GetDateModified(person) >>> print(f"Last modified: {modified}") Last modified: 2025-01-20 09:15:43 Notes: - Returns System.DateTime object (not Python datetime) - Automatically updated when person changes - Cannot be modified directly (read-only property) - Updates on any change to person properties See Also: GetDateCreated """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return person.DateModified # --- Relationships --- @OperationsMethod def GetResidences(self, person_or_hvo): """ Get the places of residence for a person. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: list: List of ICmLocation objects (empty list if none) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> residences = project.Person.GetResidences(person) >>> for loc in residences: ... name = ITsString(loc.Name.BestAnalysisAlternative).Text ... print(f"Residence: {name}") Residence: New York, USA Residence: London, UK Notes: - Returns empty list if no residences assigned - Residences are ICmLocation objects - Locations may need to be created first See Also: AddResidence """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return list(person.PlacesOfResidenceRC) @OperationsMethod def AddResidence(self, person_or_hvo, location): """ Add a place of residence to a person. Args: person_or_hvo: Either an ICmPerson object or its HVO location: ICmLocation object representing the residence Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or location is None FP_ParameterError: If location is not a valid ICmLocation Example: >>> person = project.Person.Find("John Smith") >>> # Assuming location was created elsewhere >>> # location = ... >>> # project.Person.AddResidence(person, location) Notes: - Location must be an ICmLocation object - Duplicate locations are allowed - Locations must be created before adding See Also: GetResidences """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(location, "location") person = self.__ResolveObject(person_or_hvo) try: location_obj = ICmLocation(location) person.PlacesOfResidenceRC.Add(location_obj) except (AttributeError, System.InvalidCastException) as e: raise FP_ParameterError("location must be a valid ICmLocation object") @OperationsMethod def GetLanguages(self, person_or_hvo): """ Get the languages known by a person. Args: person_or_hvo: Either an ICmPerson object or its HVO Returns: list: List of ICmPossibility objects representing languages (empty list if none) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> languages = project.Person.GetLanguages(person) >>> for lang in languages: ... name = ITsString(lang.Name.BestAnalysisAlternative).Text ... print(f"Language: {name}") Language: English Language: Spanish Notes: - Returns empty list if no languages assigned - Languages are ICmPossibility objects from language lists - Typically from the Languages possibility list See Also: AddLanguage """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return list(person.LanguagesRC) @OperationsMethod def AddLanguage(self, person_or_hvo, language): """ Add a language to a person's known languages. Args: person_or_hvo: Either an ICmPerson object or its HVO language: ICmPossibility object representing the language Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or language is None FP_ParameterError: If language is not a valid ICmPossibility Example: >>> person = project.Person.Find("John Smith") >>> # Get language from project's languages list >>> languages_list = project.lp.LanguagesOA >>> if languages_list: ... spanish = languages_list.PossibilitiesOS[0] ... project.Person.AddLanguage(person, spanish) Notes: - Language must be an ICmPossibility from project lists - Duplicate languages are allowed - Use GetLanguages() to verify before adding See Also: GetLanguages """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(language, "language") person = self.__ResolveObject(person_or_hvo) try: language_poss = ICmPossibility(language) person.LanguagesRC.Add(language_poss) except (AttributeError, System.InvalidCastException) as e: raise FP_ParameterError("language must be a valid ICmPossibility object") # --- Notes --- @OperationsMethod def GetNotes(self, person_or_hvo, wsHandle=None): """ Get the notes for a person. Args: person_or_hvo: Either an ICmPerson object or its HVO wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: Notes text (empty string if not set) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> notes = project.Person.GetNotes(person) >>> print(notes) Primary consultant for dialect study. Available weekdays. Notes: - Returns empty string if no notes - Can be multi-line text - Useful for storing additional information See Also: AddNote, SetEducation """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) # Use Comment field for notes notes = ITsString(person.Comment.get_String(wsHandle)).Text return notes or "" @OperationsMethod def AddNote(self, person_or_hvo, note, wsHandle=None): """ Add a note to a person (appends to existing notes). Args: person_or_hvo: Either an ICmPerson object or its HVO note (str): Note text to add wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or note is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.AddNote(person, "Primary consultant") >>> project.Person.AddNote(person, "Available weekdays") >>> print(project.Person.GetNotes(person)) Primary consultant Available weekdays Notes: - Appends to existing notes with newline separator - Use GetNotes() and SetNotes() for replacing all notes - Empty note text is ignored See Also: GetNotes """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(note, "note") if not note or not note.strip(): return # Ignore empty notes person = self.__ResolveObject(person_or_hvo) wsHandle = self.__WSHandleAnalysis(wsHandle) # Get existing notes existing = ITsString(person.Comment.get_String(wsHandle)).Text or "" # Append new note if existing: new_notes = existing + "\n" + note else: new_notes = note mkstr = TsStringUtils.MakeString(new_notes, wsHandle) person.Comment.set_String(wsHandle, mkstr) # --- Private Helper Methods --- def __ResolveObject(self, person_or_hvo): """ Resolve HVO or object to ICmPerson. Args: person_or_hvo: Either an ICmPerson object or an HVO (int) Returns: ICmPerson: The resolved person object Raises: FP_ParameterError: If HVO doesn't refer to a person """ if isinstance(person_or_hvo, int): obj = self.project.Object(person_or_hvo) if not isinstance(obj, ICmPerson): raise FP_ParameterError("HVO does not refer to a person") return obj return person_or_hvo def __WSHandle(self, wsHandle): """ Get writing system handle, defaulting to vernacular WS. Args: wsHandle: Optional writing system handle Returns: int: The writing system handle """ if wsHandle is None: return self.project.project.DefaultVernWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultVernWs) def __WSHandleAnalysis(self, wsHandle): """ Get writing system handle, defaulting to analysis WS. Args: wsHandle: Optional writing system handle Returns: int: The writing system handle """ if wsHandle is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs)