Source code for flexicon.code.Notebook.DataNotebookOperations

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

import logging

logger = logging.getLogger(__name__)

# Import FLEx LCM types
from SIL.LCModel import (
    IRnGenericRec,
    IRnGenericRecFactory,
    IRnResearchNbkRepository,
    ICmPossibility,
    ICmPossibilityRepository,
    ICmPerson,
    IText,
    ICmFile,
    ICmFileFactory,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
from System import Guid, DateTime
import System

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

# Import string utilities
from ..Shared.string_utils import normalize_text, normalize_match_key


[docs] class DataNotebookOperations(BaseOperations): """ This class provides operations for managing research notebook records in a FieldWorks project. Research notebook records (IRnGenericRec) are used to document field research, observations, interviews, and other data collection activities. They support hierarchical organization, linking to texts and participants, media attachments, categorization by record type, and status tracking. Notebook records are essential for documenting the research process and maintaining a structured record of linguistic fieldwork activities. This class should be accessed via FLExProject.DataNotebook property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Create a new notebook record record = project.DataNotebook.Create( "Interview with Speaker A", "Detailed notes from interview about kinship terminology" ) # Set record type record_types = project.DataNotebook.GetAllRecordTypes() if record_types: project.DataNotebook.SetRecordType(record, record_types[0]) # Set date of event project.DataNotebook.SetDateOfEvent(record, "2024-01-15") # Add researchers researcher = project.Person.Find("John Doe") if researcher: project.DataNotebook.AddResearcher(record, researcher) # Link to text text = project.Texts.Find("Interview 1") if text: project.DataNotebook.LinkToText(record, text) # Add media file media = project.Media.Find("interview_audio.wav") if media: project.DataNotebook.AddMediaFile(record, media) # Set status project.DataNotebook.SetStatus(record, "Reviewed") # Create hierarchical structure subrecord = project.DataNotebook.CreateSubRecord( record, "Analysis Notes", "Follow-up analysis of terminology patterns" ) # Query records recent = project.DataNotebook.FindByDate("2024-01-01", "2024-12-31") for rec in recent: title = project.DataNotebook.GetTitle(rec) date = project.DataNotebook.GetDateOfEvent(rec) print(f"{title} - {date}") project.CloseProject() """ def __init__(self, project): """ Initialize DataNotebookOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) def _GetSequence(self, parent): """Specify which sequence to reorder for notebook records.""" return parent.RecordsOS def __WSHandle(self, wsHandle): """ Internal helper for writing system handles. Args: wsHandle: Writing system handle or None for default analysis WS. Returns: int: The writing system handle to use. """ if wsHandle is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs) def __ValidatedRecordHvo(self, record_or_hvo): """ Internal function to validate and convert record_or_hvo to HVO. Args: record_or_hvo: Either an IRnGenericRec object or its HVO (integer). Returns: int: The HVO of the notebook record. Raises: FP_NullParameterError: If record_or_hvo is None. """ self._ValidateParam(record_or_hvo, "record_or_hvo") try: hvo = record_or_hvo.Hvo except AttributeError: hvo = record_or_hvo return hvo def __GetRecordObject(self, record_or_hvo): """ Internal function to get IRnGenericRec object from record_or_hvo. Args: record_or_hvo: Either an IRnGenericRec object or its HVO (integer). Returns: IRnGenericRec: The notebook record object. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the object doesn't exist or isn't valid. """ hvo = self.__ValidatedRecordHvo(record_or_hvo) try: obj = self.project.project.GetObject(hvo) return IRnGenericRec(obj) except ( TypeError, System.InvalidCastException, AttributeError, KeyError, System.Collections.Generic.KeyNotFoundException, ) as e: raise FP_ParameterError(f"Invalid notebook record object or HVO: {record_or_hvo} - {e}") # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all research notebook records in the project. Returns all top-level notebook records. Use GetSubRecords() to navigate the hierarchical structure of records and their sub-records. Yields: IRnGenericRec: Each notebook record object. Example: >>> for record in project.DataNotebook.GetAll(): ... title = project.DataNotebook.GetTitle(record) ... date = project.DataNotebook.GetDateCreated(record) ... status = project.DataNotebook.GetStatus(record) ... print(f"{title} - {date} - {status}") Interview with Speaker A - 2024-01-15 - Reviewed Field Observation Notes - 2024-01-20 - Draft Elicitation Session 3 - 2024-02-01 - Approved Notes: - Returns an iterator for memory efficiency - Returns only top-level records (not sub-records) - Records are returned in database order - Use GetSubRecords() to access hierarchical sub-records - Each record can have title, content, type, dates, and links See Also: Create, Find, GetSubRecords, FindByDate """ repos = self.project.project.ServiceLocator.GetService(IRnResearchNbkRepository) for record in repos.AllInstances(): # Only yield top-level records (those without an owner that's also a record) try: owner = record.Owner if not isinstance(owner, IRnGenericRec): yield record except (AttributeError, System.NullReferenceException) as e: yield record @OperationsMethod def Create(self, title, content=None, wsHandle=None): """ Create a new research notebook record. Creates a top-level notebook record with the specified title and optional content. Use CreateSubRecord() to create hierarchical sub-records. Args: title (str): The title/heading of the notebook record. content (str, optional): The main content/body text of the record. Defaults to None (empty content). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: IRnGenericRec: The newly created notebook record object. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If title is None. FP_ParameterError: If title is empty. Example: >>> # Create a basic record >>> record = project.DataNotebook.Create( ... "Interview with Speaker A", ... "Notes from interview about kinship terms" ... ) >>> print(project.DataNotebook.GetTitle(record)) Interview with Speaker A >>> # Create with specific writing system >>> record = project.DataNotebook.Create( ... "Entrevista con Hablante A", ... content="Notas sobre terminología de parentesco", ... wsHandle=project.WSHandle('es') ... ) >>> # Create and configure >>> record = project.DataNotebook.Create("Field Observation") >>> project.DataNotebook.SetDateOfEvent(record, "2024-01-15") >>> project.DataNotebook.SetStatus(record, "Draft") Notes: - Title is required, content is optional - Created records have no type, researchers, or links by default - Use Set* methods to configure additional properties - DateCreated and DateModified are set automatically - GUID is assigned automatically - Default status depends on project configuration See Also: CreateSubRecord, SetTitle, SetContent, SetRecordType """ self._EnsureWriteEnabled() self._ValidateParam(title, "title") if not title.strip(): raise FP_ParameterError("Title cannot be empty") wsHandle = self.__WSHandle(wsHandle) # Get the research notebook repository repos = self.project.project.ServiceLocator.GetService(IRnResearchNbkRepository) factory = self.project.project.ServiceLocator.GetService(IRnGenericRecFactory) # Create the record in the RecordsOC collection with self._TransactionCM(f"Create notebook record '{title}'"): record = factory.Create() repos.RecordsOC.Add(record) # Set title mkstr = TsStringUtils.MakeString(title, wsHandle) record.Title.set_String(wsHandle, mkstr) # Set content if provided if content: mkstr = TsStringUtils.MakeString(content, wsHandle) record.Text.set_String(wsHandle, mkstr) return record @OperationsMethod def Delete(self, record_or_hvo): """ Delete a notebook record from the project. Deletes the specified notebook record and all its sub-records. This operation also removes all links to texts, researchers, participants, and media files. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> # Delete by object >>> record = project.DataNotebook.Find("Old Interview Notes") >>> if record: ... project.DataNotebook.Delete(record) ... print("Record deleted") >>> # Delete by HVO >>> project.DataNotebook.Delete(12345) >>> # Delete with confirmation >>> record = project.DataNotebook.Find("Interview 1") >>> if record: ... title = project.DataNotebook.GetTitle(record) ... # Confirm deletion ... if confirm_delete(title): ... project.DataNotebook.Delete(record) Notes: - Deletes the record and all sub-records recursively - Removes all links but doesn't delete linked objects - Cannot be undone through API (uses FieldWorks undo system) - Linked texts, researchers, and media files are not deleted - Custom field data is also deleted See Also: Create, GetSubRecords """ self._EnsureWriteEnabled() record = self.__GetRecordObject(record_or_hvo) # Get the repository and remove the record repos = self.project.project.ServiceLocator.GetService(IRnResearchNbkRepository) with self._TransactionCM("Delete notebook record"): # SubRecordsOS lives on IRnGenericRec (the parent record's concrete # interface), not on the base ICmObject that pythonnet returns from # .Owner. The prior hasattr(owner, "SubRecordsOS") was therefore # always False on a real sub-record -- execution fell through to # repos.RecordsOC.Remove(record), which removes from the top-level # records collection (where the sub-record never lived). Sub-record # deletes either silently no-op'd or removed the wrong row. # Route through _GetTypedOwner to recover IRnGenericRec before # checking the slot. (issue #133, same class as #98/#116) parent = self._GetTypedOwner(record) if parent is not None and hasattr(parent, "SubRecordsOS"): parent.SubRecordsOS.Remove(record) else: repos.RecordsOC.Remove(record) @OperationsMethod def Exists(self, title, wsHandle=None): """ Check if a notebook record with the given title exists. Args: title (str): The title to search for (case-sensitive). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: bool: True if a record with this title exists, False otherwise. Raises: FP_NullParameterError: If title is None. Example: >>> # Check before creating >>> if not project.DataNotebook.Exists("Interview 1"): ... record = project.DataNotebook.Create("Interview 1") ... else: ... print("Record already exists") Record already exists >>> # Check multiple writing systems >>> exists_en = project.DataNotebook.Exists("Interview", "en") >>> exists_es = project.DataNotebook.Exists("Entrevista", "es") Notes: - Search is case-sensitive - Searches in the specified writing system only - Returns True if at least one record matches - Does not search content, only titles See Also: Find, Create """ self._ValidateParam(title, "title") return self.Find(title, wsHandle) is not None @OperationsMethod def Find(self, title, wsHandle=None): """ Find a notebook record by its title. Searches for the first record that exactly matches the given title in the specified writing system. Args: title (str): The title to search for (case-sensitive). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: IRnGenericRec: The matching notebook record object, or None if not found. Raises: FP_NullParameterError: If title is None. Example: >>> # Find and display record >>> record = project.DataNotebook.Find("Interview 1") >>> if record: ... content = project.DataNotebook.GetContent(record) ... date = project.DataNotebook.GetDateOfEvent(record) ... print(f"{content} - {date}") ... else: ... print("Record not found") >>> # Find in specific writing system >>> record = project.DataNotebook.Find( ... "Entrevista 1", ... wsHandle=project.WSHandle('es') ... ) >>> # Find and update >>> record = project.DataNotebook.Find("Old Title") >>> if record: ... project.DataNotebook.SetTitle(record, "New Title") Notes: - Search is case-sensitive and exact match only - Returns first match if multiple records have same title - Returns None if no match found - Searches only in specified writing system - Use FindBy* methods for more complex queries See Also: Exists, FindByDate, FindByResearcher, FindByType """ self._ValidateParam(title, "title") wsHandle = self.__WSHandle(wsHandle) target = normalize_match_key(title, casefold=False) for record in self.GetAll(): record_title = ITsString(record.Title.get_String(wsHandle)).Text if normalize_match_key(record_title, casefold=False) == target: return record return None # --- Property Operations: Title --- @OperationsMethod def GetTitle(self, record_or_hvo, wsHandle=None): """ Get the title of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The title text, or empty string if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> title = project.DataNotebook.GetTitle(record) >>> print(title) Interview 1 >>> # Get title in multiple languages >>> title_en = project.DataNotebook.GetTitle(record, "en") >>> title_es = project.DataNotebook.GetTitle(record, "es") >>> print(f"EN: {title_en}, ES: {title_es}") EN: Interview 1, ES: Entrevista 1 Notes: - Returns empty string if title not set in specified WS - Titles are multi-lingual (can be set in multiple writing systems) - Use SetTitle() to modify See Also: SetTitle, GetContent """ record = self.__GetRecordObject(record_or_hvo) wsHandle = self.__WSHandle(wsHandle) try: text = ITsString(record.Title.get_String(wsHandle)).Text return normalize_text(text) or "" except (AttributeError, TypeError) as e: logger.debug(f"Could not get record title: {e}") return "" @OperationsMethod def SetTitle(self, record_or_hvo, title, wsHandle=None): """ Set the title of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. title (str): The new title text. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or title is None. FP_ParameterError: If the record doesn't exist or title is empty. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> project.DataNotebook.SetTitle(record, "Interview with Speaker A") >>> print(project.DataNotebook.GetTitle(record)) Interview with Speaker A >>> # Set in multiple languages >>> project.DataNotebook.SetTitle(record, "Interview 1", "en") >>> project.DataNotebook.SetTitle(record, "Entrevista 1", "es") Notes: - Title must not be empty - Titles support multiple writing systems - Previous title value is overwritten See Also: GetTitle, SetContent """ self._EnsureWriteEnabled() self._ValidateParam(title, "title") if not title.strip(): raise FP_ParameterError("Title cannot be empty") record = self.__GetRecordObject(record_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(title, wsHandle) record.Title.set_String(wsHandle, mkstr) # --- Property Operations: Content --- @OperationsMethod def GetContent(self, record_or_hvo, wsHandle=None): """ Get the content/body text of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The content text, or empty string if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> content = project.DataNotebook.GetContent(record) >>> print(content) Detailed notes from interview about kinship terminology. Speaker provided examples of terms for siblings and cousins. >>> # Get content with specific length limit >>> content = project.DataNotebook.GetContent(record) >>> summary = content[:100] + "..." if len(content) > 100 else content >>> print(summary) Notes: - Returns empty string if content not set - Content can be lengthy (multiple paragraphs) - Content is multi-lingual - May contain structured text markup See Also: SetContent, GetTitle """ record = self.__GetRecordObject(record_or_hvo) wsHandle = self.__WSHandle(wsHandle) try: text = ITsString(record.Text.get_String(wsHandle)).Text return normalize_text(text) or "" except (AttributeError, TypeError) as e: logger.debug(f"Could not get record content: {e}") return "" @OperationsMethod def SetContent(self, record_or_hvo, content, wsHandle=None): """ Set the content/body text of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. content (str): The new content text. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or content is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> project.DataNotebook.SetContent( ... record, ... "Detailed notes from interview about kinship terminology. " ... "Speaker provided examples of terms for siblings and cousins." ... ) >>> # Update content incrementally >>> existing = project.DataNotebook.GetContent(record) >>> new_content = existing + "\\n\\nAdditional observations..." >>> project.DataNotebook.SetContent(record, new_content) >>> # Set in multiple languages >>> project.DataNotebook.SetContent(record, "English notes", "en") >>> project.DataNotebook.SetContent(record, "Notas en español", "es") Notes: - Content can be empty string - Previous content is completely replaced - Supports multiple writing systems - No length limit on content See Also: GetContent, SetTitle """ self._EnsureWriteEnabled() self._ValidateParam(content, "content") record = self.__GetRecordObject(record_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(content, wsHandle) record.Text.set_String(wsHandle, mkstr) # --- Record Type Operations --- @OperationsMethod def GetRecordType(self, record_or_hvo): """ Get the type/category of a notebook record. Record types categorize notebook records (e.g., "Interview", "Observation", "Elicitation Session", "Literature Notes"). Types are defined as possibilities in the project's notebook record type list. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: ICmPossibility: The record type object, or None if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> rec_type = project.DataNotebook.GetRecordType(record) >>> if rec_type: ... type_name = ITsString( ... rec_type.Name.get_String(project.project.DefaultAnalWs) ... ).Text ... print(f"Type: {type_name}") ... else: ... print("No type set") Type: Interview Notes: - Returns None if no type has been set - Types are ICmPossibility objects - Use GetAllRecordTypes() to see available types - Types can be hierarchical (with subtypes) See Also: SetRecordType, GetAllRecordTypes """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Type") and record.Type: return record.Type return None @OperationsMethod def SetRecordType(self, record_or_hvo, record_type): """ Set the type/category of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. record_type: The type to set (ICmPossibility object from record types list). Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or record_type is None. FP_ParameterError: If the record or type doesn't exist. Example: >>> # Set record type >>> record = project.DataNotebook.Find("Field Notes 1") >>> types = project.DataNotebook.GetAllRecordTypes() >>> interview_type = next( ... (t for t in types if "Interview" in str(t.Name)), None ... ) >>> if interview_type: ... project.DataNotebook.SetRecordType(record, interview_type) >>> # Find type by name and set >>> record = project.DataNotebook.Create("New Observation") >>> obs_type = project.DataNotebook.FindRecordTypeByName("Observation") >>> if obs_type: ... project.DataNotebook.SetRecordType(record, obs_type) Notes: - record_type must be from the project's record types list - Previous type is replaced - Can be set to None to remove type See Also: GetRecordType, GetAllRecordTypes, FindRecordTypeByName """ self._EnsureWriteEnabled() self._ValidateParam(record_type, "record_type") record = self.__GetRecordObject(record_or_hvo) record.Type = record_type @OperationsMethod def GetAllRecordTypes(self): """ Get all available notebook record types in the project. Returns all record type possibilities that can be assigned to notebook records. These types are used to categorize different kinds of research activities and observations. Returns: list: List of ICmPossibility objects representing record types. Example: >>> # List all record types >>> for rec_type in project.DataNotebook.GetAllRecordTypes(): ... name = ITsString( ... rec_type.Name.get_String(project.project.DefaultAnalWs) ... ).Text ... print(f"Type: {name}") Type: Interview Type: Observation Type: Elicitation Session Type: Literature Notes Type: Methodology >>> # Find specific type >>> types = project.DataNotebook.GetAllRecordTypes() >>> interview = next((t for t in types if "Interview" in str(t.Name)), None) Notes: - Returns empty list if no types are defined - Types are ICmPossibility objects - Types can be hierarchical - Use SetRecordType() to assign a type to a record See Also: GetRecordType, SetRecordType, FindRecordTypeByName """ # Get the record types list from the project if hasattr(self.project.lp, "RecTypesOA"): rec_types_list = self.project.lp.RecTypesOA if rec_types_list is not None: return list(rec_types_list.PossibilitiesOS) return [] @OperationsMethod def FindRecordTypeByName(self, type_name, wsHandle=None): """ Find a record type by its name. Args: type_name (str): The name of the record type to find. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ICmPossibility: The matching record type, or None if not found. Raises: FP_NullParameterError: If type_name is None. Example: >>> # Find and set record type >>> record = project.DataNotebook.Create("Interview Notes") >>> interview_type = project.DataNotebook.FindRecordTypeByName("Interview") >>> if interview_type: ... project.DataNotebook.SetRecordType(record, interview_type) ... else: ... print("Type not found") Notes: - Search is case-sensitive - Returns first match only - Returns None if not found See Also: GetAllRecordTypes, SetRecordType """ self._ValidateParam(type_name, "type_name") wsHandle = self.__WSHandle(wsHandle) for rec_type in self.GetAllRecordTypes(): name = ITsString(rec_type.Name.get_String(wsHandle)).Text if name == type_name: return rec_type return None # --- Date/Time Operations --- @OperationsMethod def GetDateCreated(self, record_or_hvo): """ Get the creation date of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: DateTime: The creation date, or None if not available. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> created = project.DataNotebook.GetDateCreated(record) >>> if created: ... print(f"Created: {created.ToString('yyyy-MM-dd HH:mm:ss')}") Created: 2024-01-15 14:30:00 Notes: - Set automatically when record is created - Cannot be modified - Returns .NET DateTime object See Also: GetDateModified, GetDateOfEvent """ record = self.__GetRecordObject(record_or_hvo) try: if hasattr(record, "DateCreated"): return record.DateCreated except (AttributeError, System.NullReferenceException) as e: pass return None @OperationsMethod def GetDateModified(self, record_or_hvo): """ Get the last modification date of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: DateTime: The last modification date, or None if not available. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> modified = project.DataNotebook.GetDateModified(record) >>> if modified: ... print(f"Modified: {modified.ToString('yyyy-MM-dd HH:mm:ss')}") Modified: 2024-01-20 09:15:00 Notes: - Updated automatically when record is modified - Cannot be set manually - Returns .NET DateTime object See Also: GetDateCreated, GetDateOfEvent """ record = self.__GetRecordObject(record_or_hvo) try: if hasattr(record, "DateModified"): return record.DateModified except (AttributeError, System.NullReferenceException) as e: pass return None @OperationsMethod def GetDateOfEvent(self, record_or_hvo): """ Get the event date of a notebook record. The event date represents when the documented event/observation occurred, which may differ from the creation or modification dates of the record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: DateTime: The event date, or None if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> event_date = project.DataNotebook.GetDateOfEvent(record) >>> if event_date: ... print(f"Event: {event_date.ToString('yyyy-MM-dd')}") ... else: ... print("Event date not set") Event: 2024-01-15 Notes: - Can be different from creation/modification dates - Useful for documenting when fieldwork occurred - Returns None if not set See Also: SetDateOfEvent, GetDateCreated """ record = self.__GetRecordObject(record_or_hvo) try: if hasattr(record, "DateOfEvent") and record.DateOfEvent: return record.DateOfEvent except (AttributeError, System.NullReferenceException) as e: pass return None @OperationsMethod def SetDateOfEvent(self, record_or_hvo, date): """ Set the event date of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. date: The date to set (DateTime object or string in format "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS"). Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or date is None. FP_ParameterError: If the record doesn't exist or date format is invalid. Example: >>> # Set with DateTime object >>> from System import DateTime >>> record = project.DataNotebook.Find("Interview 1") >>> project.DataNotebook.SetDateOfEvent(record, DateTime.Now) >>> # Set with string >>> project.DataNotebook.SetDateOfEvent(record, "2024-01-15") >>> project.DataNotebook.SetDateOfEvent(record, "2024-01-15 14:30:00") >>> # Set date from user input >>> date_str = "2024-01-15" >>> project.DataNotebook.SetDateOfEvent(record, date_str) Notes: - Accepts DateTime object or string format - String format: "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS" - Represents when the documented event occurred - Independent of creation/modification dates See Also: GetDateOfEvent, GetDateCreated """ self._EnsureWriteEnabled() self._ValidateParam(date, "date") record = self.__GetRecordObject(record_or_hvo) # Convert string to DateTime if needed if isinstance(date, str): try: date = DateTime.Parse(date) except (System.FormatException, ValueError, TypeError) as e: raise FP_ParameterError(f"Invalid date format: {date}. Use 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' - {e}") record.DateOfEvent = date # --- Hierarchy Operations --- @OperationsMethod def GetSubRecords(self, record_or_hvo): """ Get all direct sub-records of a notebook record. Notebook records support hierarchical organization where records can contain sub-records. This method returns only direct children, not all descendants. Args: record_or_hvo: The parent record object (IRnGenericRec) or its HVO. Returns: list: List of IRnGenericRec objects that are sub-records. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> # Get and display sub-records >>> record = project.DataNotebook.Find("Interview 1") >>> subs = project.DataNotebook.GetSubRecords(record) >>> print(f"Found {len(subs)} sub-records:") >>> for sub in subs: ... title = project.DataNotebook.GetTitle(sub) ... print(f" - {title}") Found 3 sub-records: - Kinship Terms Section - Color Terms Section - Grammar Notes >>> # Recursive display of hierarchy >>> def show_hierarchy(rec, indent=0): ... title = project.DataNotebook.GetTitle(rec) ... print(" " * indent + title) ... for sub in project.DataNotebook.GetSubRecords(rec): ... show_hierarchy(sub, indent + 1) >>> show_hierarchy(record) Notes: - Returns empty list if no sub-records exist - Returns only direct children (not grandchildren) - Use recursion to traverse entire hierarchy - Sub-records can have their own sub-records See Also: CreateSubRecord, GetParentRecord """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "SubRecordsOS"): return list(record.SubRecordsOS) return [] @OperationsMethod def CreateSubRecord(self, parent_record_or_hvo, title, content=None, wsHandle=None): """ Create a new sub-record under a parent notebook record. Creates a child record in the hierarchical structure. Sub-records inherit the context of their parent but can have their own properties, links, and media. Args: parent_record_or_hvo: The parent record (IRnGenericRec) or its HVO. title (str): The title of the new sub-record. content (str, optional): The content of the sub-record. Defaults to None. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: IRnGenericRec: The newly created sub-record object. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If parent_record_or_hvo or title is None. FP_ParameterError: If parent doesn't exist or title is empty. Example: >>> # Create hierarchical structure >>> parent = project.DataNotebook.Create("Interview with Speaker A") >>> sub1 = project.DataNotebook.CreateSubRecord( ... parent, ... "Kinship Terminology", ... "Discussion of family relationship terms" ... ) >>> sub2 = project.DataNotebook.CreateSubRecord( ... parent, ... "Color Terms", ... "Basic color terminology elicitation" ... ) >>> # Create nested sub-records >>> subsub = project.DataNotebook.CreateSubRecord( ... sub1, ... "Parent Terms", ... "Terms for mother and father" ... ) Notes: - Sub-records can have their own sub-records (unlimited nesting) - Sub-records are independent objects with their own properties - Deleting parent deletes all sub-records recursively - Sub-records can link to different texts/researchers than parent See Also: Create, GetSubRecords, GetParentRecord """ self._EnsureWriteEnabled() self._ValidateParam(title, "title") if not title.strip(): raise FP_ParameterError("Title cannot be empty") parent = self.__GetRecordObject(parent_record_or_hvo) wsHandle = self.__WSHandle(wsHandle) # Create the sub-record factory = self.project.project.ServiceLocator.GetService(IRnGenericRecFactory) with self._TransactionCM(f"Create sub-record '{title}'"): subrecord = factory.Create() parent.SubRecordsOS.Add(subrecord) # Set title mkstr = TsStringUtils.MakeString(title, wsHandle) subrecord.Title.set_String(wsHandle, mkstr) # Set content if provided if content: mkstr = TsStringUtils.MakeString(content, wsHandle) subrecord.Text.set_String(wsHandle, mkstr) return subrecord @OperationsMethod def GetParentRecord(self, record_or_hvo): """ Get the parent record of a sub-record. Args: record_or_hvo: The sub-record object (IRnGenericRec) or its HVO. Returns: IRnGenericRec: The parent record object, or None if this is a top-level record. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> # Navigate up the hierarchy >>> subrecord = project.DataNotebook.Find("Kinship Terms Section") >>> parent = project.DataNotebook.GetParentRecord(subrecord) >>> if parent: ... parent_title = project.DataNotebook.GetTitle(parent) ... print(f"Parent: {parent_title}") ... else: ... print("This is a top-level record") Parent: Interview 1 >>> # Get full path to root >>> def get_path(rec): ... path = [] ... current = rec ... while current: ... path.insert(0, project.DataNotebook.GetTitle(current)) ... current = project.DataNotebook.GetParentRecord(current) ... return " > ".join(path) >>> print(get_path(subrecord)) Interview 1 > Kinship Terms Section Notes: - Returns None for top-level records - Parent is always an IRnGenericRec - Use GetSubRecords() to navigate down the hierarchy See Also: GetSubRecords, CreateSubRecord """ record = self.__GetRecordObject(record_or_hvo) try: owner = record.Owner if isinstance(owner, IRnGenericRec): return IRnGenericRec(owner) except (AttributeError, TypeError, System.InvalidCastException) as e: pass return None # --- Linking Operations: Researchers --- @OperationsMethod def GetResearchers(self, record_or_hvo): """ Get all researchers associated with a notebook record. Researchers are people (ICmPerson objects) who conducted the fieldwork or created the observations documented in the record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of ICmPerson objects representing researchers. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> researchers = project.DataNotebook.GetResearchers(record) >>> print(f"Researchers ({len(researchers)}):") >>> for person in researchers: ... name = project.Person.GetName(person) ... print(f" - {name}") Researchers (2): - John Doe - Jane Smith Notes: - Returns empty list if no researchers assigned - Researchers are ICmPerson objects - Multiple researchers can be assigned to one record See Also: AddResearcher, RemoveResearcher, GetParticipants """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Researchers") and record.Researchers: return list(record.Researchers) return [] @OperationsMethod def AddResearcher(self, record_or_hvo, person): """ Add a researcher to a notebook record. Associates a person (researcher/fieldworker) with the notebook record. The same researcher can be added to multiple records. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. person: The person object (ICmPerson) to add as a researcher. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or person is None. FP_ParameterError: If the record or person doesn't exist. Example: >>> # Add single researcher >>> record = project.DataNotebook.Find("Interview 1") >>> researcher = project.Person.Find("John Doe") >>> if researcher: ... project.DataNotebook.AddResearcher(record, researcher) >>> # Add multiple researchers >>> researchers = ["John Doe", "Jane Smith", "Maria Garcia"] >>> for name in researchers: ... person = project.Person.Find(name) ... if person: ... project.DataNotebook.AddResearcher(record, person) >>> # Create and add new researcher >>> new_researcher = project.Person.Create("Alice Johnson") >>> project.DataNotebook.AddResearcher(record, new_researcher) Notes: - Adding same researcher twice has no effect (no duplicates) - Researchers are not deleted when removed from records - Use Person operations to manage person objects See Also: GetResearchers, RemoveResearcher, AddParticipant """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Researchers"): if person not in record.Researchers: record.Researchers.Add(person) @OperationsMethod def RemoveResearcher(self, record_or_hvo, person): """ Remove a researcher from a notebook record. Removes the association between a person and a notebook record. The person object itself is not deleted. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. person: The person object (ICmPerson) to remove. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or person is None. FP_ParameterError: If the record or person doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> researcher = project.Person.Find("John Doe") >>> project.DataNotebook.RemoveResearcher(record, researcher) Notes: - Only removes the link, doesn't delete the person - No error if person wasn't linked to record See Also: AddResearcher, GetResearchers """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Researchers"): if person in record.Researchers: record.Researchers.Remove(person) # --- Linking Operations: Participants --- @OperationsMethod def GetParticipants(self, record_or_hvo): """ Get all participants associated with a notebook record. Participants are people (ICmPerson objects) who participated in the recorded event (e.g., consultants, speakers, informants). Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of ICmPerson objects representing participants. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> participants = project.DataNotebook.GetParticipants(record) >>> print(f"Participants ({len(participants)}):") >>> for person in participants: ... name = project.Person.GetName(person) ... print(f" - {name}") Participants (3): - Speaker A - Speaker B - Interpreter C Notes: - Returns empty list if no participants assigned - Participants differ from researchers (subjects vs. investigators) - Multiple participants can be assigned to one record See Also: AddParticipant, RemoveParticipant, GetResearchers """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Participants") and record.Participants: return list(record.Participants) return [] @OperationsMethod def AddParticipant(self, record_or_hvo, person): """ Add a participant to a notebook record. Associates a person (consultant/speaker/informant) with the notebook record as a participant in the documented event. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. person: The person object (ICmPerson) to add as a participant. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or person is None. FP_ParameterError: If the record or person doesn't exist. Example: >>> # Add single participant >>> record = project.DataNotebook.Find("Interview 1") >>> speaker = project.Person.Find("Speaker A") >>> if speaker: ... project.DataNotebook.AddParticipant(record, speaker) >>> # Add multiple participants >>> participants = ["Speaker A", "Speaker B", "Interpreter C"] >>> for name in participants: ... person = project.Person.Find(name) ... if person: ... project.DataNotebook.AddParticipant(record, person) Notes: - Adding same participant twice has no effect - Participants represent subjects/consultants - Use AddResearcher() for investigators/fieldworkers See Also: GetParticipants, RemoveParticipant, AddResearcher """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Participants"): if person not in record.Participants: record.Participants.Add(person) @OperationsMethod def RemoveParticipant(self, record_or_hvo, person): """ Remove a participant from a notebook record. Removes the association between a person and a notebook record. The person object itself is not deleted. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. person: The person object (ICmPerson) to remove. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or person is None. FP_ParameterError: If the record or person doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> speaker = project.Person.Find("Speaker A") >>> project.DataNotebook.RemoveParticipant(record, speaker) Notes: - Only removes the link, doesn't delete the person - No error if person wasn't linked to record See Also: AddParticipant, GetParticipants """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Participants"): if person in record.Participants: record.Participants.Remove(person) # --- Linking Operations: Locations --- @OperationsMethod def GetLocations(self, record_or_hvo): """ Get all locations associated with a notebook record. Locations are places (ICmLocation objects) where the documented event or data collection occurred. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of ICmLocation objects representing locations. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> locations = project.DataNotebook.GetLocations(record) >>> print(f"Locations ({len(locations)}):") >>> for location in locations: ... name = project.Location.GetName(location) ... coords = project.Location.GetCoordinates(location) ... print(f" - {name}: {coords}") Locations (2): - Barasana Village: (-1.2345, -70.6789) - Papurí River Area: (1.1234, -70.5678) Notes: - Returns empty list if no locations assigned - Locations are ICmLocation objects - Multiple locations can be assigned to one record - Useful for tracking where fieldwork occurred See Also: AddLocation, RemoveLocation, GetResearchers """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "LocationsRC") and record.LocationsRC: return list(record.LocationsRC) return [] @OperationsMethod def AddLocation(self, record_or_hvo, location): """ Add a location to a notebook record. Associates a geographic location with the notebook record, indicating where the documented event or data collection took place. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. location: The location object (ICmLocation) to add. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or location is None. FP_ParameterError: If the record or location doesn't exist. Example: >>> # Add single location >>> record = project.DataNotebook.Find("Interview 1") >>> location = project.Location.Find("Barasana Village") >>> if location: ... project.DataNotebook.AddLocation(record, location) >>> # Add multiple locations >>> locations = ["Village A", "Village B", "Recording Site"] >>> for name in locations: ... loc = project.Location.Find(name) ... if loc: ... project.DataNotebook.AddLocation(record, loc) >>> # Create and add new location >>> new_location = project.Location.Create("New Site", "en") >>> project.Location.SetCoordinates(new_location, -1.23, -70.45) >>> project.DataNotebook.AddLocation(record, new_location) Notes: - Adding same location twice has no effect (no duplicates) - Locations are not deleted when removed from records - Use Location operations to manage location objects - Useful for documenting fieldwork sites See Also: GetLocations, RemoveLocation, AddResearcher """ self._EnsureWriteEnabled() self._ValidateParam(location, "location") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "LocationsRC"): if location not in record.LocationsRC: record.LocationsRC.Add(location) @OperationsMethod def RemoveLocation(self, record_or_hvo, location): """ Remove a location from a notebook record. Removes the association between a location and a notebook record. The location object itself is not deleted. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. location: The location object (ICmLocation) to remove. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or location is None. FP_ParameterError: If the record or location doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> location = project.Location.Find("Old Site") >>> project.DataNotebook.RemoveLocation(record, location) Notes: - Only removes the link, doesn't delete the location - No error if location wasn't linked to record See Also: AddLocation, GetLocations """ self._EnsureWriteEnabled() self._ValidateParam(location, "location") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "LocationsRC"): if location in record.LocationsRC: record.LocationsRC.Remove(location) # --- Linking Operations: Sources --- @OperationsMethod def GetSources(self, record_or_hvo): """ Get all bibliographic sources associated with a notebook record. Sources are references or publications (typically ICmPossibility objects from the project's bibliography) that are cited or related to the notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of source/reference objects. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> sources = project.DataNotebook.GetSources(record) >>> print(f"Sources ({len(sources)}):") >>> for source in sources: ... # Display source information ... print(f" - {source}") Sources (2): - Smith 2020 - Johnson & Garcia 2018 Notes: - Returns empty list if no sources assigned - Sources can be bibliographic references or other citation objects - Multiple sources can be assigned to one record - Useful for documenting which publications relate to the data See Also: AddSource, RemoveSource, GetResearchers """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "SourcesRC") and record.SourcesRC: return list(record.SourcesRC) return [] @OperationsMethod def AddSource(self, record_or_hvo, source): """ Add a bibliographic source/reference to a notebook record. Associates a source or bibliographic reference with the notebook record, indicating which publications or materials are relevant to the documented data. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. source: The source/reference object to add. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or source is None. FP_ParameterError: If the record or source doesn't exist. Example: >>> # Add single source >>> record = project.DataNotebook.Find("Interview 1") >>> source = project.Bibliography.Find("Smith 2020") >>> if source: ... project.DataNotebook.AddSource(record, source) >>> # Add multiple sources >>> sources = ["Smith 2020", "Johnson & Garcia 2018"] >>> for name in sources: ... src = project.Bibliography.Find(name) ... if src: ... project.DataNotebook.AddSource(record, src) >>> # Create and add new source >>> new_source = project.Bibliography.Create("Doe 2024") >>> project.DataNotebook.AddSource(record, new_source) Notes: - Adding same source twice has no effect (no duplicates) - Sources are not deleted when removed from records - Use Bibliography operations to manage source objects - Useful for tracking citations and references See Also: GetSources, RemoveSource, AddResearcher """ self._EnsureWriteEnabled() self._ValidateParam(source, "source") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "SourcesRC"): if source not in record.SourcesRC: record.SourcesRC.Add(source) @OperationsMethod def RemoveSource(self, record_or_hvo, source): """ Remove a bibliographic source/reference from a notebook record. Removes the association between a source and a notebook record. The source object itself is not deleted. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. source: The source/reference object to remove. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or source is None. FP_ParameterError: If the record or source doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> source = project.Bibliography.Find("Old Reference") >>> project.DataNotebook.RemoveSource(record, source) Notes: - Only removes the link, doesn't delete the source - No error if source wasn't linked to record See Also: AddSource, GetSources """ self._EnsureWriteEnabled() self._ValidateParam(source, "source") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "SourcesRC"): if source in record.SourcesRC: record.SourcesRC.Remove(source) # --- Text Linking Operations --- @OperationsMethod def GetTexts(self, record_or_hvo): """ Get all texts linked to a notebook record. Retrieves all IText objects that are linked to this notebook record. Texts might be recordings, transcriptions, or other textual data that the notebook record references or documents. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of IText objects linked to the record. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> texts = project.DataNotebook.GetTexts(record) >>> print(f"Linked texts ({len(texts)}):") >>> for text in texts: ... title = project.Texts.GetTitle(text) ... print(f" - {title}") Linked texts (2): - Interview Recording 1 - Interview Transcription 1 Notes: - Returns empty list if no texts are linked - Links are bidirectional (can find records from texts) - Multiple texts can be linked to one record See Also: LinkToText, UnlinkFromText """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "TextsRC") and record.TextsRC: return list(record.TextsRC) return [] @OperationsMethod def LinkToText(self, record_or_hvo, text): """ Link a notebook record to a text. Creates a connection between a notebook record and a text object, indicating that the record documents or relates to that text. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. text: The text object (IText) to link to. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or text is None. FP_ParameterError: If the record or text doesn't exist. Example: >>> # Link record to text >>> record = project.DataNotebook.Find("Interview Notes") >>> text = project.Texts.Find("Interview Recording 1") >>> if text: ... project.DataNotebook.LinkToText(record, text) >>> # Link to multiple texts >>> text_titles = ["Recording 1", "Recording 2", "Transcription 1"] >>> for title in text_titles: ... text = project.Texts.Find(title) ... if text: ... project.DataNotebook.LinkToText(record, text) Notes: - Linking same text twice has no effect - Links are bidirectional - Useful for connecting field notes to recordings See Also: UnlinkFromText, GetTexts """ self._EnsureWriteEnabled() self._ValidateParam(text, "text") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "TextsRC"): if text not in record.TextsRC: record.TextsRC.Add(text) @OperationsMethod def UnlinkFromText(self, record_or_hvo, text): """ Unlink a notebook record from a text. Removes the connection between a notebook record and a text object. The text itself is not deleted. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. text: The text object (IText) to unlink from. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or text is None. FP_ParameterError: If the record or text doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview Notes") >>> text = project.Texts.Find("Recording 1") >>> if text: ... project.DataNotebook.UnlinkFromText(record, text) Notes: - Only removes the link, doesn't delete the text - No error if text wasn't linked to record See Also: LinkToText, GetTexts """ self._EnsureWriteEnabled() self._ValidateParam(text, "text") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "TextsRC"): if text in record.TextsRC: record.TextsRC.Remove(text) # --- Media File Operations --- @OperationsMethod def GetMediaFiles(self, record_or_hvo): """ Get all media files attached to a notebook record. Retrieves all ICmFile objects (audio, video, images) that are attached to this notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: list: List of ICmFile objects representing media files. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> media_files = project.DataNotebook.GetMediaFiles(record) >>> print(f"Media files ({len(media_files)}):") >>> for media in media_files: ... path = project.Media.GetInternalPath(media) ... print(f" - {path}") Media files (2): - audio/interview1.wav - images/notes_photo.jpg Notes: - Returns empty list if no media files attached - Media can be audio, video, or images - Files are ICmFile references, not file paths See Also: AddMediaFile, RemoveMediaFile """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "MediaFilesOS") and record.MediaFilesOS: return list(record.MediaFilesOS) return [] @OperationsMethod def AddMediaFile(self, record_or_hvo, media_file): """ Add a media file to a notebook record. Attaches a media file (audio, video, or image) to the notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. media_file: The media file object (ICmFile) to attach. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or media_file is None. FP_ParameterError: If the record or media file doesn't exist. Example: >>> # Add existing media file >>> record = project.DataNotebook.Find("Interview 1") >>> media = project.Media.Find("interview1.wav") >>> if media: ... project.DataNotebook.AddMediaFile(record, media) >>> # Create and add new media file >>> new_media = project.Media.Create("/path/to/recording.wav") >>> project.DataNotebook.AddMediaFile(record, new_media) >>> # Add multiple media files >>> media_files = ["audio.wav", "photo1.jpg", "photo2.jpg"] >>> for filename in media_files: ... media = project.Media.Find(filename) ... if media: ... project.DataNotebook.AddMediaFile(record, media) Notes: - Media file must exist in project before linking - Adding same file twice has no effect - Supports audio, video, and image files See Also: RemoveMediaFile, GetMediaFiles """ self._EnsureWriteEnabled() self._ValidateParam(media_file, "media_file") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "MediaFilesOS"): if media_file not in record.MediaFilesOS: record.MediaFilesOS.Add(media_file) @OperationsMethod def RemoveMediaFile(self, record_or_hvo, media_file): """ Remove a media file from a notebook record. Detaches a media file from the notebook record. The media file itself is not deleted from the project. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. media_file: The media file object (ICmFile) to remove. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or media_file is None. FP_ParameterError: If the record or media file doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> media = project.Media.Find("old_recording.wav") >>> if media: ... project.DataNotebook.RemoveMediaFile(record, media) Notes: - Only removes the link, doesn't delete the media file - No error if file wasn't attached to record See Also: AddMediaFile, GetMediaFiles """ self._EnsureWriteEnabled() self._ValidateParam(media_file, "media_file") record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "MediaFilesOS"): if media_file in record.MediaFilesOS: record.MediaFilesOS.Remove(media_file) # --- Status Operations --- @OperationsMethod def GetStatus(self, record_or_hvo): """ Get the status of a notebook record. Status indicates the review state or workflow stage of the record (e.g., "Draft", "Reviewed", "Approved", "Published"). Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: ICmPossibility: The status object, or None if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> status = project.DataNotebook.GetStatus(record) >>> if status: ... status_name = ITsString( ... status.Name.get_String(project.project.DefaultAnalWs) ... ).Text ... print(f"Status: {status_name}") ... else: ... print("No status set") Status: Reviewed Notes: - Returns None if no status has been set - Status values are ICmPossibility objects - Status list is project-specific See Also: SetStatus, GetAllStatuses """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Status") and record.Status: return record.Status return None @OperationsMethod def SetStatus(self, record_or_hvo, status): """ Set the status of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. status: The status to set (ICmPossibility object or status name string). Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or status is None. FP_ParameterError: If the record or status doesn't exist. Example: >>> # Set status by name >>> record = project.DataNotebook.Find("Interview 1") >>> project.DataNotebook.SetStatus(record, "Reviewed") >>> # Set status by object >>> statuses = project.DataNotebook.GetAllStatuses() >>> approved = next((s for s in statuses if "Approved" in str(s.Name)), None) >>> if approved: ... project.DataNotebook.SetStatus(record, approved) >>> # Workflow example >>> record = project.DataNotebook.Create("New Interview") >>> project.DataNotebook.SetStatus(record, "Draft") >>> # ... after review ... >>> project.DataNotebook.SetStatus(record, "Reviewed") >>> # ... after approval ... >>> project.DataNotebook.SetStatus(record, "Approved") Notes: - Status must be from project's status list - Can pass string (finds by name) or ICmPossibility object - Previous status is replaced See Also: GetStatus, GetAllStatuses """ self._EnsureWriteEnabled() self._ValidateParam(status, "status") record = self.__GetRecordObject(record_or_hvo) # If status is a string, find the matching possibility if isinstance(status, str): status_obj = self.FindStatusByName(status) if status_obj is None: raise FP_ParameterError(f"Status not found: {status}") status = status_obj record.Status = status @OperationsMethod def GetAllStatuses(self): """ Get all available status values for notebook records. Returns all status possibilities that can be assigned to notebook records for tracking workflow and review stages. Returns: list: List of ICmPossibility objects representing status values. Example: >>> # List all statuses >>> for status in project.DataNotebook.GetAllStatuses(): ... name = ITsString( ... status.Name.get_String(project.project.DefaultAnalWs) ... ).Text ... print(f"Status: {name}") Status: Draft Status: In Review Status: Reviewed Status: Approved Status: Published Notes: - Returns empty list if no statuses defined - Status list is project-specific - Typically includes workflow stages See Also: GetStatus, SetStatus, FindStatusByName """ # Get the status list from the project if hasattr(self.project.lp, "StatusOA"): status_list = self.project.lp.StatusOA if status_list: return list(status_list.PossibilitiesOS) return [] @OperationsMethod def FindStatusByName(self, status_name, wsHandle=None): """ Find a status by its name. Args: status_name (str): The name of the status to find. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ICmPossibility: The matching status object, or None if not found. Raises: FP_NullParameterError: If status_name is None. Example: >>> # Find and set status >>> record = project.DataNotebook.Create("New Record") >>> draft_status = project.DataNotebook.FindStatusByName("Draft") >>> if draft_status: ... project.DataNotebook.SetStatus(record, draft_status) Notes: - Search is case-sensitive - Returns first match only - Returns None if not found See Also: GetAllStatuses, SetStatus """ self._ValidateParam(status_name, "status_name") wsHandle = self.__WSHandle(wsHandle) for status in self.GetAllStatuses(): name = ITsString(status.Name.get_String(wsHandle)).Text if name == status_name: return status return None # --- Query Operations --- @OperationsMethod def FindByDate(self, start_date=None, end_date=None): """ Find notebook records by date range. Searches for records whose DateOfEvent falls within the specified range. Args: start_date: Start date (DateTime or string "YYYY-MM-DD"). None means no start limit. end_date: End date (DateTime or string "YYYY-MM-DD"). None means no end limit. Returns: list: List of IRnGenericRec objects matching the date criteria. Example: >>> # Find records from specific date range >>> records = project.DataNotebook.FindByDate("2024-01-01", "2024-12-31") >>> print(f"Found {len(records)} records in 2024") >>> for record in records: ... title = project.DataNotebook.GetTitle(record) ... date = project.DataNotebook.GetDateOfEvent(record) ... print(f"{title} - {date}") >>> # Find records after a certain date >>> recent = project.DataNotebook.FindByDate(start_date="2024-06-01") >>> # Find records before a certain date >>> old = project.DataNotebook.FindByDate(end_date="2023-12-31") Notes: - Searches DateOfEvent, not DateCreated/Modified - Records without DateOfEvent are excluded - Both dates are inclusive - Accepts DateTime objects or date strings See Also: FindByResearcher, FindByType, GetDateOfEvent """ # Convert string dates to DateTime if needed if start_date and isinstance(start_date, str): start_date = DateTime.Parse(start_date) if end_date and isinstance(end_date, str): end_date = DateTime.Parse(end_date) results = [] for record in self.GetAll(): event_date = self.GetDateOfEvent(record) if not event_date: continue # Check if date falls within range if start_date and event_date < start_date: continue if end_date and event_date > end_date: continue results.append(record) return results @OperationsMethod def FindByResearcher(self, person): """ Find all notebook records associated with a specific researcher. Args: person: The person object (ICmPerson) or person name (str) to search for. Returns: list: List of IRnGenericRec objects that have this researcher. Raises: FP_NullParameterError: If person is None. Example: >>> # Find by person object >>> researcher = project.Person.Find("John Doe") >>> if researcher: ... records = project.DataNotebook.FindByResearcher(researcher) ... print(f"John Doe worked on {len(records)} records") ... for record in records: ... title = project.DataNotebook.GetTitle(record) ... print(f" - {title}") >>> # Find by name >>> records = project.DataNotebook.FindByResearcher("Jane Smith") Notes: - Searches in Researchers collection - Can pass person object or name string - Returns empty list if no matches See Also: FindByDate, FindByType, GetResearchers """ self._ValidateParam(person, "person") # If string provided, find the person if isinstance(person, str): person_obj = self.project.Person.Find(person) if person_obj is None: return [] person = person_obj results = [] for record in self.GetAll(): researchers = self.GetResearchers(record) if person in researchers: results.append(record) return results @OperationsMethod def FindByType(self, record_type): """ Find all notebook records of a specific type. Args: record_type: The record type (ICmPossibility object or type name string). Returns: list: List of IRnGenericRec objects with this type. Raises: FP_NullParameterError: If record_type is None. Example: >>> # Find by type name >>> interviews = project.DataNotebook.FindByType("Interview") >>> print(f"Found {len(interviews)} interview records") >>> for record in interviews: ... title = project.DataNotebook.GetTitle(record) ... date = project.DataNotebook.GetDateOfEvent(record) ... print(f"{title} - {date}") >>> # Find by type object >>> types = project.DataNotebook.GetAllRecordTypes() >>> obs_type = next((t for t in types if "Observation" in str(t.Name)), None) >>> if obs_type: ... observations = project.DataNotebook.FindByType(obs_type) Notes: - Can pass type object or name string - Returns empty list if no matches - Does not search sub-records by default See Also: FindByDate, FindByResearcher, GetRecordType """ self._ValidateParam(record_type, "record_type") # If string provided, find the type if isinstance(record_type, str): type_obj = self.FindRecordTypeByName(record_type) if type_obj is None: return [] record_type = type_obj results = [] for record in self.GetAll(): rec_type = self.GetRecordType(record) if rec_type and rec_type == record_type: results.append(record) return results @OperationsMethod def Duplicate(self, record_or_hvo, insert_after=False, deep=True): """ Duplicate a notebook record, creating a new copy with a new GUID. Args: record_or_hvo: The IRnGenericRec object or HVO to duplicate. insert_after (bool): For sub-records (SubRecordsOS is an ordered sequence), controls whether the duplicate is inserted immediately after the source. Ignored for top-level records: RecordsOC is an unordered ILcmOwningCollection with no Insert() method, so top-level duplicates are always appended via Add(). deep (bool): If True (default), also duplicate owned objects (sub-records). If False, only copy simple properties and references. Returns: IRnGenericRec: The newly created duplicate record with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If record_or_hvo is None. Example: >>> # Shallow duplicate (no sub-records) >>> record = project.DataNotebook.Find("Interview 1") >>> dup = project.DataNotebook.Duplicate(record) >>> print(f"Original: {project.DataNotebook.GetGuid(record)}") >>> print(f"Duplicate: {project.DataNotebook.GetGuid(dup)}") Original: 12345678-1234-1234-1234-123456789abc Duplicate: 87654321-4321-4321-4321-cba987654321 >>> # Deep duplicate (includes all sub-records) >>> deep_dup = project.DataNotebook.Duplicate(record, deep=True) >>> print(f"Sub-records: {len(project.DataNotebook.GetSubRecords(deep_dup))}") Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original record's position - Simple properties copied: Title, Text (content) - Reference properties copied: Type, Status, Confidence - DateTime properties copied: DateOfEvent - Owned objects (deep=True): SubRecordsOS - Collections NOT duplicated: Researchers, Participants, TextsRC, MediaFilesOS - DateCreated and DateModified are NOT copied (set automatically) See Also: Create, Delete, GetGuid, GetSubRecords """ self._EnsureWriteEnabled() self._ValidateParam(record_or_hvo, "record_or_hvo") # Get source record source = self.__GetRecordObject(record_or_hvo) # Determine parent (owner) owner = source.Owner # Create new record using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(IRnGenericRecFactory) duplicate = factory.Create() # Determine insertion position and add to parent FIRST if isinstance(owner, IRnGenericRec): # Parent is another notebook record (sub-record) parent_record = IRnGenericRec(owner) if insert_after: source_index = parent_record.SubRecordsOS.IndexOf(source) parent_record.SubRecordsOS.Insert(source_index + 1, duplicate) else: parent_record.SubRecordsOS.Add(duplicate) else: # Parent is the top-level repository. RecordsOC is an unordered # ILcmOwningCollection; insert_after has no semantic meaning here # and is ignored -- the duplicate is always appended via Add(). repos = self.project.project.ServiceLocator.GetService(IRnResearchNbkRepository) repos.RecordsOC.Add(duplicate) # Copy simple MultiString properties duplicate.Title.CopyAlternatives(source.Title) duplicate.Text.CopyAlternatives(source.Text) # Copy Reference Atomic (RA) properties if hasattr(source, "Type") and source.Type: duplicate.Type = source.Type if hasattr(source, "Status") and source.Status: duplicate.Status = source.Status if hasattr(source, "Confidence") and source.Confidence: duplicate.Confidence = source.Confidence # Copy DateTime properties if hasattr(source, "DateOfEvent") and source.DateOfEvent: duplicate.DateOfEvent = source.DateOfEvent # Handle owned objects if deep=True if deep: # Duplicate sub-records into the NEW duplicate (not the original's parent) if hasattr(source, "SubRecordsOS"): for subrecord in source.SubRecordsOS: self._DuplicateSubRecordInto(subrecord, duplicate, deep=True) return duplicate def _DuplicateSubRecordInto(self, source_rec, parent_dup, deep=True): """Duplicate a sub-record into the specified parent's SubRecordsOS.""" factory = self.project.project.ServiceLocator.GetService(IRnGenericRecFactory) dup_rec = factory.Create() parent_dup.SubRecordsOS.Add(dup_rec) # Copy properties dup_rec.Title.CopyAlternatives(source_rec.Title) dup_rec.Text.CopyAlternatives(source_rec.Text) if hasattr(source_rec, "Type") and source_rec.Type: dup_rec.Type = source_rec.Type if hasattr(source_rec, "Status") and source_rec.Status: dup_rec.Status = source_rec.Status if hasattr(source_rec, "Confidence") and source_rec.Confidence: dup_rec.Confidence = source_rec.Confidence if hasattr(source_rec, "DateOfEvent") and source_rec.DateOfEvent: dup_rec.DateOfEvent = source_rec.DateOfEvent # Recurse into nested sub-records if deep and hasattr(source_rec, "SubRecordsOS"): for nested_rec in source_rec.SubRecordsOS: self._DuplicateSubRecordInto(nested_rec, dup_rec, deep=True) # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """Get syncable properties for cross-project synchronization.""" self._ValidateParam(item, "item") record = self.__ResolveObject(item) wsHandle = self.project.project.DefaultAnalWs props = {} props["Title"] = ITsString(record.Title.get_String(wsHandle)).Text or "" props["Text"] = ITsString(record.Text.get_String(wsHandle)).Text or "" if hasattr(record, "Type") and record.Type: props["Type"] = str(record.Type.Guid) else: props["Type"] = None if hasattr(record, "Status") and record.Status: props["Status"] = str(record.Status.Guid) else: props["Status"] = None if hasattr(record, "Confidence") and record.Confidence: props["Confidence"] = str(record.Confidence.Guid) else: props["Confidence"] = None if hasattr(record, "DateOfEvent") and record.DateOfEvent: props["DateOfEvent"] = str(record.DateOfEvent) else: props["DateOfEvent"] = None return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """Compare two notebook records and return detailed differences.""" if ops1 is None: ops1 = self if ops2 is None: ops2 = self is_different = False differences = {"properties": {}} props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) for key in set(props1.keys()) | set(props2.keys()): val1 = props1.get(key) val2 = props2.get(key) if val1 != val2: is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} return is_different, differences # --- Metadata Operations --- @OperationsMethod def GetGuid(self, record_or_hvo): """ Get the GUID (globally unique identifier) of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: Guid: The GUID of the record. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> guid = project.DataNotebook.GetGuid(record) >>> print(f"GUID: {guid}") GUID: 12345678-1234-1234-1234-123456789abc Notes: - GUID is assigned automatically when record is created - GUID is immutable and unique across all projects - Useful for external references and synchronization See Also: GetConfidence """ record = self.__GetRecordObject(record_or_hvo) return record.Guid @OperationsMethod def GetConfidence(self, record_or_hvo): """ Get the confidence level of a notebook record. Confidence indicates how certain or reliable the information in the record is considered to be. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. Returns: ICmPossibility: The confidence level object, or None if not set. Raises: FP_NullParameterError: If record_or_hvo is None. FP_ParameterError: If the record doesn't exist. Example: >>> record = project.DataNotebook.Find("Interview 1") >>> confidence = project.DataNotebook.GetConfidence(record) >>> if confidence: ... conf_name = ITsString( ... confidence.Name.get_String(project.project.DefaultAnalWs) ... ).Text ... print(f"Confidence: {conf_name}") ... else: ... print("No confidence level set") Confidence: High Notes: - Returns None if not set - Confidence levels are project-specific - Common values: High, Medium, Low, Uncertain See Also: SetConfidence, GetGuid """ record = self.__GetRecordObject(record_or_hvo) if hasattr(record, "Confidence") and record.Confidence: return record.Confidence return None @OperationsMethod def SetConfidence(self, record_or_hvo, confidence): """ Set the confidence level of a notebook record. Args: record_or_hvo: The notebook record object (IRnGenericRec) or its HVO. confidence: The confidence level (ICmPossibility object or name string). Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If record_or_hvo or confidence is None. FP_ParameterError: If the record or confidence level doesn't exist. Example: >>> # Set confidence by name >>> record = project.DataNotebook.Find("Interview 1") >>> project.DataNotebook.SetConfidence(record, "High") >>> # Set confidence by object >>> conf_levels = project.Confidence.GetAll() >>> high = next((c for c in conf_levels if "High" in str(c.Name)), None) >>> if high: ... project.DataNotebook.SetConfidence(record, high) Notes: - Can pass confidence object or name string - Must be from project's confidence levels list - Previous value is replaced See Also: GetConfidence """ self._EnsureWriteEnabled() self._ValidateParam(confidence, "confidence") record = self.__GetRecordObject(record_or_hvo) # If confidence is a string, find the matching possibility if isinstance(confidence, str): conf_obj = self.project.Confidence.Find(confidence) if conf_obj is None: raise FP_ParameterError(f"Confidence level not found: {confidence}") confidence = conf_obj with self._TransactionCM('Set Confidence'): record.Confidence = confidence