def match_date_range(date: Date, start_date: Date, end_date: Date) -> bool: """Check if date falls in given range.""" if start_date: if start_date.match(date, comparison=">"): return False if end_date: if end_date.match(date, comparison="<"): return False return True
class Timeline: """Timeline class.""" def __init__( self, db_handle: DbReadBase, dates: Optional[str] = None, events: Optional[List[str]] = None, ratings: bool = False, relatives: Optional[List[str]] = None, relative_events: Optional[List[str]] = None, discard_empty: bool = True, omit_anchor: bool = True, precision: int = 1, locale: GrampsLocale = glocale, ): """Initialize timeline.""" self.db_handle = db_handle self.timeline: List[Tuple[Event, Person, str, str]] = [] self.dates = dates self.start_date = None self.end_date = None self.ratings = ratings self.discard_empty = discard_empty self.precision = precision self.locale = locale self.anchor_person = None self.omit_anchor = omit_anchor self.depth = 1 self.eligible_events: Set[str] = set() self.event_filters: List[str] = events or [] self.eligible_relative_events: Set[str] = set() self.relative_event_filters: List[str] = relative_events or [] self.relative_filters: List[str] = relatives or [] self.set_event_filters(self.event_filters) self.set_relative_event_filters(self.relative_event_filters) self.birth_dates: Dict[str, Date] = {} if dates and "-" in dates: start, end = dates.split("-") if "/" in start: year, month, day = start.split("/") self.start_date = Date((int(year), int(month), int(day))) else: self.start_date = None if "/" in end: year, month, day = end.split("/") self.end_date = Date((int(year), int(month), int(day))) else: self.end_date = None def set_start_date(self, date: Union[Date, str]): """Set optional timeline start date.""" if isinstance(date, str): year, month, day = date.split("/") self.start_date = Date((int(year), int(month), int(day))) else: self.start_date = date def set_end_date(self, date: Union[Date, str]): """Set optional timeline end date.""" if isinstance(date, str): year, month, day = date.split("/") self.end_date = Date((int(year), int(month), int(day))) else: self.end_date = date def set_discard_empty(self, discard_empty: bool): """Set discard empty identifier.""" self.discard_empty = discard_empty def set_precision(self, precision: int): """Set optional precision for span.""" self.precision = precision def set_locale(self, locale: str): """Set optional locale for span.""" self.locale = get_locale_for_language(locale, default=True) def set_event_filters(self, filters: Optional[List[str]] = None): """Prepare the event filter table.""" self.event_filters = filters or [] self.eligible_events = self._prepare_eligible_events( self.event_filters) def set_relative_event_filters(self, filters: Optional[List[str]] = None): """Prepare the relative event filter table.""" self.relative_event_filters = filters or [] self.eligible_relative_events = self._prepare_eligible_events( self.relative_event_filters) def _prepare_eligible_events(self, event_filters: List[str]): """Prepare an event filter list.""" eligible_events = {"Birth", "Death"} event_type = EventType() default_event_types = event_type.get_standard_xml() default_event_map = event_type.get_map() custom_event_types = self.db_handle.get_event_types() for key in event_filters: if key in default_event_types: eligible_events.add(key) continue if key in custom_event_types: eligible_events.add(key) continue if key not in EVENT_CATEGORIES: raise ValueError( f"{key} is not a valid event or event category") for entry in event_type.get_menu_standard_xml(): event_key = entry[0].lower().replace("life events", "vital") if event_key in event_filters: for event_id in entry[1]: if event_id in default_event_map: eligible_events.add(default_event_map[event_id]) break if "custom" in event_filters: for event_name in custom_event_types: eligible_events.add(event_name) return eligible_events def get_age(self, start_date: str, date: str): """Return calculated age or empty string otherwise.""" age = "" if start_date: span = Span(start_date, date) if span.is_valid(): age = str( span.format(precision=self.precision, dlocale=self.locale).strip("()")) return age def is_death_indicator(self, event: Event) -> bool: """Check if an event indicates death timeframe.""" if event.type in DEATH_INDICATORS: return True for event_name in [ "Funeral", "Interment", "Reinterment", "Inurnment", "Memorial", "Visitation", "Wake", "Shiva", ]: if self.locale.translation.sgettext(event_name) == str(event.type): return True return False def is_eligible(self, event: Event, relative: bool): """Check if an event is eligible for the timeline.""" if relative: if self.relative_event_filters == []: return True return str(event.get_type()) in self.eligible_relative_events if self.event_filters == []: return True return str(event.get_type()) in self.eligible_events def add_event(self, event: Tuple[Event, Person, str, str], relative: bool = False): """Add event to timeline if needed.""" if self.discard_empty: if event[0].date.sortval == 0: return if self.end_date: if self.end_date.match(event[0].date, comparison="<"): return if self.start_date: if self.start_date.match(event[0].date, comparison=">"): return for item in self.timeline: if item[0].handle == event[0].handle: return if self.is_eligible(event[0], relative): if self.ratings: count, confidence = get_rating(self.db_handle, event[0]) event[0].citations = count event[0].confidence = confidence self.timeline.append(event) def add_person( self, handle: Handle, anchor: bool = False, start: bool = True, end: bool = True, ancestors: int = 1, offspring: int = 1, ): """Add events for a person to the timeline.""" if self.anchor_person and handle == self.anchor_person.handle: return person = self.db_handle.get_person_from_handle(handle) if person.handle not in self.birth_dates: event = get_birth_or_fallback(self.db_handle, person) if event: self.birth_dates.update({person.handle: event.date}) for event_ref in person.event_ref_list: event = self.db_handle.get_event_from_handle(event_ref.ref) role = event_ref.get_role().xml_str() self.add_event((event, person, "self", role)) if anchor and not self.anchor_person: self.anchor_person = person self.depth = max(ancestors, offspring) + 1 if self.start_date is None and self.end_date is None: if len(self.timeline) > 0: if start or end: self.timeline.sort(key=lambda x: x[0].get_date_object( ).get_sort_value()) if start: self.start_date = self.timeline[0][0].date if end: if self.is_death_indicator(self.timeline[-1][0]): self.end_date = self.timeline[-1][0].date else: data = probably_alive_range( person, self.db_handle) self.end_date = data[1] for family in person.parent_family_list: self.add_family(family, ancestors=ancestors) for family in person.family_list: self.add_family(family, anchor=person, ancestors=ancestors, offspring=offspring) else: for family in person.family_list: self.add_family(family, anchor=person, events_only=True) def add_relative(self, handle: Handle, ancestors: int = 1, offspring: int = 1): """Add events for a relative of the anchor person.""" person = self.db_handle.get_person_from_handle(handle) calculator = get_relationship_calculator(reinit=True, clocale=self.locale) calculator.set_depth(self.depth) relationship = calculator.get_one_relationship(self.db_handle, self.anchor_person, person) if self.relative_filters: found = False for relative in self.relative_filters: if relative in relationship: found = True break if not found: return if self.relative_event_filters: for event_ref in person.event_ref_list: event = self.db_handle.get_event_from_handle(event_ref.ref) role = event_ref.get_role().xml_str() self.add_event((event, person, relationship, role), relative=True) event = get_birth_or_fallback(self.db_handle, person) if event: self.add_event((event, person, relationship, "Primary"), relative=True) if person.handle not in self.birth_dates: self.birth_dates.update({person.handle: event.date}) event = get_death_or_fallback(self.db_handle, person) if event: self.add_event((event, person, relationship, "Primary"), relative=True) for family_handle in person.family_list: family = self.db_handle.get_family_from_handle(family_handle) event = get_marriage_or_fallback(self.db_handle, family) if event: self.add_event((event, person, relationship, "Family"), relative=True) event = get_divorce_or_fallback(self.db_handle, family) if event: self.add_event((event, person, relationship, "Family"), relative=True) if offspring > 1: for child_ref in family.child_ref_list: self.add_relative(child_ref.ref, offspring=offspring - 1) if ancestors > 1: if "father" in relationship or "mother" in relationship: for family_handle in person.parent_family_list: self.add_family(family_handle, include_children=False, ancestors=ancestors - 1) def add_family( self, handle: Handle, anchor: Optional[Person] = None, include_children: bool = True, ancestors: int = 1, offspring: int = 1, events_only: bool = False, ): """Add events for all family members to the timeline.""" family = self.db_handle.get_family_from_handle(handle) if anchor: for event_ref in family.event_ref_list: event = self.db_handle.get_event_from_handle(event_ref.ref) role = event_ref.get_role().xml_str() self.add_event((event, anchor, "self", role)) if events_only: return if self.anchor_person: if (family.father_handle and family.father_handle != self.anchor_person.handle): self.add_relative(family.father_handle, ancestors=ancestors) if (family.mother_handle and family.mother_handle != self.anchor_person.handle): self.add_relative(family.mother_handle, ancestors=ancestors) if include_children: for child in family.child_ref_list: if child.ref != self.anchor_person.handle: self.add_relative(child.ref, offspring=offspring) else: if family.father_handle: self.add_person(family.father_handle) if family.mother_handle: self.add_person(family.mother_handle) for child in family.child_ref_list: self.add_person(child.ref) def profile(self, page=0, pagesize=20): """Return a profile for the timeline.""" profiles = [] self.timeline.sort( key=lambda x: x[0].get_date_object().get_sort_value()) events = self.timeline if page > 0: offset = (page - 1) * pagesize events = events[offset:offset + pagesize] for (event, person_object, relationship, role) in events: label = self.locale.translation.sgettext(str(event.type)) if (person_object and self.anchor_person and self.anchor_person.handle != person_object.handle and relationship not in ["self", "", None]): label = f"{label} ({relationship.title()})" try: obj = self.db_handle.get_place_from_handle(event.place) place = get_place_profile_for_object(self.db_handle, obj, locale=self.locale) place["display_name"] = pd.display_event(self.db_handle, event) place["handle"] = event.place except HandleError: place = {} age = "" person = {} if person_object is not None: person_age = "" get_person = True if self.anchor_person: if self.anchor_person.handle in self.birth_dates: age = self.get_age( self.birth_dates[self.anchor_person.handle], event.date) if self.anchor_person.handle == person_object.handle: person_age = age if self.omit_anchor: get_person = False if get_person: person = get_person_profile_for_object(self.db_handle, person_object, {}, locale=self.locale) if not person_age and person_object.handle in self.birth_dates: person_age = self.get_age( self.birth_dates[person_object.handle], event.date) if not age: age = person_age person["age"] = person_age profile = { "date": self.locale.date_displayer.display(event.date), "description": event.description, "gramps_id": event.gramps_id, "handle": event.handle, "label": self.locale.translation.sgettext(label), "media": [x.ref for x in event.media_list], "person": person, "place": place, "age": age, "type": event.type, "role": self.locale.translation.gettext(role), } profile["person"]["relationship"] = str(relationship) if self.ratings: profile["citations"] = event.citations profile["confidence"] = event.confidence profiles.append(profile) return profiles