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
from ..Shared.gendate_utils import gendate_from_input


[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 flexicon 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 (Gender is an int code; ICmPerson has no # email/phone fields, issue #352) project.Person.SetDateOfBirth(person, "1985-03-15") project.Person.SetGender(person, 1) # Add contact information project.Person.SetAddress(person, "123 Main St, City, Country") 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 EnumerableWrapper (subscriptable, len()-able, lazily materialized) over all ICmPerson objects in the project database, allowing iteration over all registered people. Returns: EnumerableWrapper[ICmPerson]: Each person object in the project Example: >>> for person in project.Person.GetAll(): ... name = project.Person.GetName(person) ... print(name) John Smith Maria Garcia Ahmed Hassan Notes: - Returns an EnumerableWrapper (subscriptable, len()-able) for memory efficiency; the underlying LCM enumerator is only materialized into a list on first len()/index/iteration access - 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.SetGender(consultant, 1) 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 with self._TransactionCM("Delete person"): 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: ... print(f"Found: {project.Person.GetName(person)}") Found: John Smith >>> # 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) with self._TransactionCM(f"Set person name '{name}'"): 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: Accepted for compatibility but ignored (gender is not a writing-system field). Returns: int: The LCM gender code (0 for new persons) Raises: FP_NullParameterError: If person_or_hvo is None Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetGender(person, 1) >>> print(project.Person.GetGender(person)) 1 Notes: - Gender is an Int32 field on ICmPerson, not a string (live-proven, issue #352); code meanings are defined by FieldWorks See Also: SetGender """ self._ValidateParam(person_or_hvo, "person_or_hvo") person = self.__ResolveObject(person_or_hvo) return person.Gender if person.Gender is not None else 0 @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 (int): The LCM gender code to set wsHandle: Accepted for compatibility but ignored (gender is not a writing-system field). Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or gender is None FP_ParameterError: If gender is not an int Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetGender(person, 1) >>> print(project.Person.GetGender(person)) 1 Notes: - Gender is an Int32 field on ICmPerson, not a string (live-proven, issue #352); code meanings are defined by FieldWorks See Also: GetGender """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(gender, "gender") if isinstance(gender, bool) or not isinstance(gender, int): raise FP_ParameterError("gender must be an int (ICmPerson.Gender is Int32)") person = self.__ResolveObject(person_or_hvo) with self._TransactionCM("Set person gender"): person.Gender = gender # --- 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"); a System.DateTime is also accepted Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If person_or_hvo or date_str is None FP_ParameterError: If date_str does not parse as a date Example: >>> person = project.Person.Find("John Smith") >>> project.Person.SetDateOfBirth(person, "1985-03-15") >>> # Clear date >>> project.Person.SetDateOfBirth(person, "") Notes: - Accepts any format System.DateTime.Parse does - ISO format (YYYY-MM-DD) recommended - Can be empty string to clear - Stored as an exact AD GenDate; ICmPerson.DateOfBirth is not a string field, and assigning a str raised TypeError (issue #330) 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) # Parse outside the bracket so a malformed date raises before any # undo task opens. gen_date = gendate_from_input(date_str, allow_empty=True) with self._TransactionCM("Set person date of birth"): person.DateOfBirth = gen_date # --- 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: - ICmPerson has no Email field (live-proven: hasattr is False, issue #352), so this method always raises. It is kept only to fail with an actionable message instead of a bare AttributeError. See Also: SetEmail, GetPhone """ raise FP_ParameterError( "ICmPerson has no Email field (issue #352); " "GetEmail cannot return a value" ) @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: - ICmPerson has no Email field (live-proven: hasattr is False, issue #352), so this method always raises. It is kept only to fail with an actionable message instead of a bare AttributeError. See Also: GetEmail, SetPhone """ raise FP_ParameterError( "ICmPerson has no Email field (issue #352); " "SetEmail cannot store a value" ) @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: - ICmPerson has no phone field: the old code read a multistring off PlaceOfBirth, which does not exist either (only PlaceOfBirthRA, an ICmLocation reference; live-proven, issue #352). This method always raises and is kept only to fail with an actionable message instead of a bare AttributeError. See Also: SetPhone, GetEmail """ raise FP_ParameterError( "ICmPerson has no phone field (issue #352); " "GetPhone cannot return a value" ) @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: - ICmPerson has no phone field: the old code wrote a multistring to PlaceOfBirth, which does not exist either (only PlaceOfBirthRA, an ICmLocation reference; live-proven, issue #352). This method always raises and is kept only to fail with an actionable message instead of a bare AttributeError. See Also: GetPhone, SetEmail """ raise FP_ParameterError( "ICmPerson has no phone field (issue #352); " "SetPhone cannot store a value" ) @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) with self._TransactionCM("Set person address"): 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) with self._TransactionCM("Set person education"): 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: # Cast outside the transaction: an invalid position must raise # without opening an empty named undo entry. position_poss = ICmPossibility(position) with self._TransactionCM("Add position to person"): 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, deep=False): """ 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): Accepted for API uniformity across Operations classes. Person has no owned objects, so this parameter is ignored. 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.GetGender(dup)) 1 Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original person's position - Copied: Name, Gender (int), DateOfBirth, Abbreviation (address), Description (education), PlaceOfBirthRA (reference) - Email/phone have no backing field and are not copied (issue #352) - Reference collections copied: PositionsRC, PlacesOfResidenceRC - 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 MultiString/MultiUnicode properties. Gender is Int32 # (direct assignment, issue #352); Email/PlaceOfBirth/Comment # do not exist on ICmPerson (live-proven) and are not copied. duplicate.Name.CopyAlternatives(source.Name) duplicate.Gender = source.Gender duplicate.Abbreviation.CopyAlternatives(source.Abbreviation) # Address duplicate.Description.CopyAlternatives(source.Description) # Education if source.PlaceOfBirthRA is not None: duplicate.PlaceOfBirthRA = source.PlaceOfBirthRA # 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) # Email, phone, notes and LanguagesRC have no backing field # on ICmPerson (live-proven, issue #352) and are not copied. # 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': 1, ...} """ self._ValidateParam(item, "item") person = self.__ResolveObject(item) wsHandle = self.project.project.DefaultAnalWs props = {} # MultiString properties. Gender is Int32 (issue #352); Email, # phone and Comment have no backing field and are not synced. props["Name"] = ITsString(person.Name.get_String(wsHandle)).Text or "" props["Gender"] = person.Gender if person.Gender is not None else 0 props["Abbreviation"] = ITsString(person.Abbreviation.get_String(wsHandle)).Text or "" # Address props["Description"] = ITsString(person.Description.get_String(wsHandle)).Text or "" # Education if person.PlaceOfBirthRA is not None: props["PlaceOfBirthRA"] = str(person.PlaceOfBirthRA.Guid) else: props["PlaceOfBirthRA"] = None # 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] 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: # Cast outside the transaction: an invalid location must raise # without opening an empty named undo entry. location_obj = ICmLocation(location) with self._TransactionCM("Add residence to person"): 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 (LanguagesOA >>> # lives on LexDb, not LangProject) >>> languages_list = project.lexDB.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: # Cast outside the transaction: an invalid language must raise # without opening an empty named undo entry. language_poss = ICmPossibility(language) with self._TransactionCM("Add language to person"): 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: - ICmPerson has no notes field: the old code used Comment, which does not exist (live-proven: hasattr is False, issue #352). NotesOA belongs to ICmAgent, and ICmPerson implements ICmPossibility, not ICmAgent, so the StText is not reachable either. This method always raises and is kept only to fail with an actionable message instead of a bare AttributeError. See Also: AddNote, SetEducation """ raise FP_ParameterError( "ICmPerson has no notes field (issue #352); " "GetNotes cannot return a value" ) @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 as a new NotesOA paragraph - Empty note text is ignored See Also: GetNotes """ self._EnsureWriteEnabled() self._ValidateParam(person_or_hvo, "person_or_hvo") self._ValidateParam(note, "note") # No backing field (see GetNotes): ICmPerson has no Comment # member and NotesOA is ICmAgent-only (issue #352). raise FP_ParameterError( "ICmPerson has no notes field (issue #352); " "AddNote cannot store a value" ) # --- 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 getattr(obj, "ClassName", None) == "CmPerson": try: return ICmPerson(obj) except Exception: pass if isinstance(obj, ICmPerson): return obj raise FP_ParameterError("HVO does not refer to a person") if getattr(person_or_hvo, "ClassName", None) == "CmPerson": try: return ICmPerson(person_or_hvo) except Exception: pass 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)