Example #1
0
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
Example #2
0
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