From 7f41373f0742e88aff03aebe347c25599070c874 Mon Sep 17 00:00:00 2001 From: Paul Franklin Date: Mon, 11 Apr 2016 19:22:37 -0700 Subject: [PATCH] 8649: Familygroup report: Add filter option --- gramps/gen/db/base.py | 4 +- gramps/gen/filters/__init__.py | 3 +- gramps/gen/filters/_genericfilter.py | 24 +++++ gramps/gen/plug/report/utils.py | 76 ++++++++++++- gramps/gen/proxy/filter.py | 5 +- gramps/gen/proxy/proxybase.py | 8 +- gramps/plugins/database/bsddb_support/read.py | 39 ++++++- gramps/plugins/database/dictionarydb.py | 3 +- gramps/plugins/textreport/familygroup.py | 101 +++++++++++++----- 9 files changed, 227 insertions(+), 36 deletions(-) diff --git a/gramps/gen/db/base.py b/gramps/gen/db/base.py index 4daac2252..ebfb1967b 100644 --- a/gramps/gen/db/base.py +++ b/gramps/gen/db/base.py @@ -344,10 +344,12 @@ class DbReadBase(object): """ raise NotImplementedError - def get_family_handles(self): + def get_family_handles(self, sort_handles=False): """ Return a list of database handles, one handle for each Family in the database. + + If sort_handles is True, the list is sorted by surnames. """ raise NotImplementedError diff --git a/gramps/gen/filters/__init__.py b/gramps/gen/filters/__init__.py index 745dcac7b..d4bee52cb 100644 --- a/gramps/gen/filters/__init__.py +++ b/gramps/gen/filters/__init__.py @@ -27,7 +27,8 @@ CustomFilters = None from ..const import CUSTOM_FILTERS from ._filterlist import FilterList -from ._genericfilter import GenericFilter, GenericFilterFactory, DeferredFilter +from ._genericfilter import (GenericFilter, GenericFilterFactory, + DeferredFilter, DeferredFamilyFilter) from ._paramfilter import ParamFilter from ._searchfilter import SearchFilter, ExactSearchFilter diff --git a/gramps/gen/filters/_genericfilter.py b/gramps/gen/filters/_genericfilter.py index cb5990019..3cd8da982 100644 --- a/gramps/gen/filters/_genericfilter.py +++ b/gramps/gen/filters/_genericfilter.py @@ -406,3 +406,27 @@ class DeferredFilter(GenericFilter): if self.name_pair[1]: return self._(self.name_pair[0]) % self.name_pair[1] return self._(self.name_pair[0]) + +class DeferredFamilyFilter(GenericFamilyFilter): + """ + Filter class allowing for deferred translation of the filter name + """ + + def __init__(self, filter_name, family_name): + GenericFamilyFilter.__init__(self, None) + self.name_pair = [filter_name, family_name] + + def get_name(self, ulocale=glocale): + """ + return the filter name, possibly translated + + If ulocale is passed in (a :class:`.GrampsLocale`) then + the translated value will be returned instead. + + :param ulocale: allow deferred translation of strings + :type ulocale: a :class:`.GrampsLocale` instance + """ + self._ = ulocale.translation.gettext + if self.name_pair[1]: + return self._(self.name_pair[0]) % self.name_pair[1] + return self._(self.name_pair[0]) diff --git a/gramps/gen/plug/report/utils.py b/gramps/gen/plug/report/utils.py index 3a14a4668..35e39c27b 100644 --- a/gramps/gen/plug/report/utils.py +++ b/gramps/gen/plug/report/utils.py @@ -5,7 +5,7 @@ # Copyright (C) 2007-2009 Brian G. Matherly # Copyright (C) 2008 James Friedmann # Copyright (C) 2010 Jakim Friant -# Copyright (C) 2015 Paul Franklin +# Copyright (C) 2015-2016 Paul Franklin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -309,3 +309,77 @@ def get_person_filters(person, include_single=True, name_format=None): the_filters = [all, des, df, ans, com] the_filters.extend(CustomFilters.get_filters('Person')) return the_filters + +#------------------------------------------------------------------------- +# +# Family Filters +# +#------------------------------------------------------------------------- +def get_family_filters(database, family, + include_single=True, name_format=None): + """ + Return a list of filters that are relevant for the given family + + :param database: The database that the family is in. + :type database: DbBase + :param family: the family the filters should apply to. + :type family: :class:`~.family.Family` + :param include_single: include a filter to include the single family + :type include_single: boolean + :param name_format: optional format to control display of person's name + :type name_format: None or int + """ + from ...filters import (GenericFilterFactory, rules, CustomFilters, + DeferredFamilyFilter) + from ...display.name import displayer as name_displayer + + if family: + real_format = name_displayer.get_default_format() + if name_format is not None: + name_displayer.set_default_format(name_format) + fhandle = family.get_father_handle() + if fhandle: + father = database.get_person_from_handle(fhandle) + father_name = name_displayer.display(father) + else: + father_name = _("unknown father") + mhandle = family.get_mother_handle() + if mhandle: + mother = database.get_person_from_handle(mhandle) + mother_name = name_displayer.display(mother) + else: + mother_name = _("unknown mother") + gramps_id = family.get_gramps_id() + name = _("%(father_name)s and %(mother_name)s (%(family_id)s)") % { + 'father_name': father_name, + 'mother_name': mother_name, + 'family_id': gramps_id} + name_displayer.set_default_format(real_format) + else: + # Do this in case of command line options query (show=filter) + name = _("FAMILY") + gramps_id = '' + + if include_single: + FilterClass = GenericFilterFactory('Family') + filt_id = FilterClass() + filt_id.set_name(name) + filt_id.add_rule(rules.family.HasIdOf([gramps_id])) + + all = DeferredFamilyFilter(_T_("Every family"), None) + all.add_rule(rules.family.AllFamilies([])) + + # feature request 2356: avoid genitive form + df = DeferredFamilyFilter(_T_("Descendant Families of %s"), name) + df.add_rule(rules.family.IsDescendantOf([gramps_id, 1])) + + # feature request 2356: avoid genitive form + ans = DeferredFamilyFilter(_T_("Ancestor Families of %s"), name) + ans.add_rule(rules.family.IsAncestorOf([gramps_id, 1])) + + if include_single: + the_filters = [filt_id, all, df, ans] + else: + the_filters = [all, df, ans] + the_filters.extend(CustomFilters.get_filters('Family')) + return the_filters diff --git a/gramps/gen/proxy/filter.py b/gramps/gen/proxy/filter.py index 81c6bb9ea..ea2306dc9 100644 --- a/gramps/gen/proxy/filter.py +++ b/gramps/gen/proxy/filter.py @@ -548,11 +548,12 @@ class FilterProxyDb(ProxyDbBase): else: return map(self.get_event_from_handle, self.elist) - def get_family_handles(self): + def get_family_handles(self, sort_handles=False): """ Return a list of database handles, one handle for each Family in - the database. + the database. If sort_handles is True, the list is sorted by surnames """ + # FIXME: flist is not a sorted list of handles return list(self.flist) def iter_family_handles(self): diff --git a/gramps/gen/proxy/proxybase.py b/gramps/gen/proxy/proxybase.py index 958fb9631..5aa31af82 100644 --- a/gramps/gen/proxy/proxybase.py +++ b/gramps/gen/proxy/proxybase.py @@ -319,18 +319,20 @@ class ProxyDbBase(DbReadBase): def get_person_handles(self, sort_handles=False): """ Return a list of database handles, one handle for each Person in - the database. + the database. If sort_handles is True, the list is sorted by surnames """ + # FIXME: this is not a sorted list of handles if self.db.is_open: return list(self.iter_person_handles()) else: return [] - def get_family_handles(self, sort_handles=True): + def get_family_handles(self, sort_handles=False): """ Return a list of database handles, one handle for each Family in - the database. + the database. If sort_handles is True, the list is sorted by surnames """ + # FIXME: this is not a sorted list of handles if self.db.is_open: return list(self.iter_family_handles()) else: diff --git a/gramps/plugins/database/bsddb_support/read.py b/gramps/plugins/database/bsddb_support/read.py index 6870415a1..327f2ac64 100644 --- a/gramps/plugins/database/bsddb_support/read.py +++ b/gramps/plugins/database/bsddb_support/read.py @@ -110,6 +110,16 @@ def find_byte_surname(key, data): return surn.encode('utf-8') return surn +def find_fullname(key, data): + """ + Creating a fullname from raw data of a person, to use for sort and index + returns a byte string + """ + fullname_data = [(data[3][5][0][0] + ' ' + data[3][4], # surname givenname + data[3][5][0][1], data[3][5][0][2], + data[3][5][0][3], data[3][5][0][4])] + return __index_surname(fullname_data) + def find_surname(key, data): """ Creating a surname from raw data of a person, to use for sort and index @@ -254,7 +264,7 @@ class DbBsddbRead(DbReadBase, Callback): .. method:: get__handles() returns a list of handles for the object type, optionally sorted - (for Person, Place, Source and Media objects) + (for Citation, Family, Media, Person, Place, Source, and Tag objects) .. method:: iter__handles() @@ -1143,16 +1153,21 @@ class DbBsddbRead(DbReadBase, Callback): return self.all_handles(self.event_map) return [] - def get_family_handles(self): + def get_family_handles(self, sort_handles=False): """ Return a list of database handles, one handle for each Family in the database. + If sort_handles is True, the list is sorted by surnames. + .. warning:: For speed the keys are directly returned, so on python3 bytestrings are returned! """ if self.db_is_open: - return self.all_handles(self.family_map) + handle_list = self.all_handles(self.family_map) + if sort_handles: + handle_list.sort(key=self.__sortbyfamily_key) + return handle_list return [] def get_repository_handles(self): @@ -1804,6 +1819,24 @@ class DbBsddbRead(DbReadBase, Callback): return glocale.sort_key(find_surname(handle, self.person_map.get(handle))) + def __sortbyfamily_key(self, handle): + if isinstance(handle, str): + handle = handle.encode('utf-8') + data = self.family_map.get(handle) + data2 = data[2] + if isinstance(data2, str): + data2 = data2.encode('utf-8') + data3 = data[3] + if isinstance(data3, str): + data3 = data3.encode('utf-8') + if data2: # father handle + return glocale.sort_key(find_fullname(data2, + self.person_map.get(data2))) + elif data3: # mother handle + return glocale.sort_key(find_fullname(data3, + self.person_map.get(data3))) + return '' + def __sortbyplace(self, first, second): if isinstance(first, str): first = first.encode('utf-8') diff --git a/gramps/plugins/database/dictionarydb.py b/gramps/plugins/database/dictionarydb.py index 54b5abe24..7a17faf50 100644 --- a/gramps/plugins/database/dictionarydb.py +++ b/gramps/plugins/database/dictionarydb.py @@ -158,7 +158,8 @@ class DictionaryDb(DbGeneric): ## Fixme: implement sort return [bytes(key, "utf-8") for key in self._person_dict.keys()] - def get_family_handles(self): + def get_family_handles(self, sort_handles=False): + ## Fixme: implement sort return [bytes(key, "utf-8") for key in self._family_dict.keys()] def get_event_handles(self): diff --git a/gramps/plugins/textreport/familygroup.py b/gramps/plugins/textreport/familygroup.py index 9431dbcc0..bba2b074f 100644 --- a/gramps/plugins/textreport/familygroup.py +++ b/gramps/plugins/textreport/familygroup.py @@ -4,8 +4,8 @@ # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2007-2008 Brian G. Matherly # Copyright (C) 2010 Jakim Friant -# Copyright (C) 2013-2016 Paul Franklin # Copyright (C) 2015 Gerald Kunzmann +# Copyright (C) 2013-2016 Paul Franklin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -39,7 +39,7 @@ from functools import partial from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext from gramps.gen.lib import EventRoleType, EventType, NoteType, Person -from gramps.gen.plug.menu import BooleanOption, FamilyOption +from gramps.gen.plug.menu import BooleanOption, FamilyOption, FilterOption from gramps.gen.plug.report import Report from gramps.gen.plug.report import utils as ReportUtils from gramps.gen.plug.report import MenuReportOptions @@ -70,7 +70,9 @@ class FamilyGroup(Report): This report needs the following parameters (class variables) that come in the options class. - family_handle - Handle of the family to write report on. + filter - Filter to be applied to the families of the database. + The option class carries its number, and the function + returning the list of filters. includeAttrs - Whether to include attributes name_format - Preferred format to display names incl_private - Whether to include private data @@ -78,20 +80,14 @@ class FamilyGroup(Report): years_past_death - Consider as living this many years after death """ Report.__init__(self, database, options, user) + self._user = user menu = options.menu stdoptions.run_private_data_option(self, menu) stdoptions.run_living_people_option(self, menu) self.db = self.database - self.family_handle = None - - family_id = menu.get_option_by_name('family_id').get_value() - family = self.db.get_family_from_gramps_id(family_id) - if family: - self.family_handle = family.get_handle() - else: - self.family_handle = None + self.filter = menu.get_option_by_name('filter').get_filter() get_option_by_name = menu.get_option_by_name get_value = lambda name:get_option_by_name(name).get_value() @@ -109,8 +105,8 @@ class FamilyGroup(Report): self.incChiMar = get_value('incChiMar') self.includeAttrs = get_value('incattrs') - rlocale = self.set_locale(get_value('trans')) - self._ = rlocale.translation.sgettext # needed for English + self._locale = self.set_locale(get_value('trans')) + self._ = self._locale.translation.sgettext # needed for English stdoptions.run_name_format_option(self, menu) @@ -654,8 +650,22 @@ class FamilyGroup(Report): self.dump_family(child_family_handle, (generation+1)) def write_report(self): - if self.family_handle: - self.dump_family(self.family_handle, 1) + flist = self.db.get_family_handles(sort_handles=True) + if not self.filter: + fam_list = flist + else: + with self._user.progress(_('Family Group Report'), + _('Applying filter...'), + self.db.get_number_of_families()) as step: + fam_list = self.filter.apply(self.db, flist, step) + if fam_list: + with self._user.progress(_('Family Group Report'), + _('Writing families'), + len(fam_list)) as step: + for family_handle in fam_list: + self.dump_family(family_handle, 1) + self.doc.page_break() + step() else: self.doc.start_paragraph('FGR-Title') self.doc.write_text(self._("Family Group Report")) @@ -663,7 +673,7 @@ class FamilyGroup(Report): #------------------------------------------------------------------------ # -# MenuReportOptions +# FamilyGroupOptions # #------------------------------------------------------------------------ class FamilyGroupOptions(MenuReportOptions): @@ -673,6 +683,10 @@ class FamilyGroupOptions(MenuReportOptions): """ def __init__(self, name, dbase): + self.__db = dbase + self.__fid = None + self.__filter = None + self.__recursive = None MenuReportOptions.__init__(self, name, dbase) def add_menu_options(self, menu): @@ -682,20 +696,30 @@ class FamilyGroupOptions(MenuReportOptions): add_option = partial(menu.add_option, category_name) ########################## - family_id = FamilyOption(_("Center Family")) - family_id.set_help(_("The center family for the report")) - add_option("family_id", family_id) + self.__filter = FilterOption(_("Filter"), 0) + self.__filter.set_help( + _("Select the filter to be applied to the report.")) + add_option("filter", self.__filter) + self.__filter.connect('value-changed', self.__filter_changed) - stdoptions.add_name_format_option(menu, category_name) + self.__fid = FamilyOption(_("Center Family")) + self.__fid.set_help(_("The center family for the filter")) + add_option("family_id", self.__fid) + self.__fid.connect('value-changed', self.__update_filters) + + self._nf = stdoptions.add_name_format_option(menu, category_name) + self._nf.connect('value-changed', self.__update_filters) + + self.__update_filters() stdoptions.add_private_data_option(menu, category_name) stdoptions.add_living_people_option(menu, category_name) - recursive = BooleanOption(_('Recursive'), False) - recursive.set_help(_("Create reports for all descendants " - "of this family.")) - add_option("recursive", recursive) + self.__recursive = BooleanOption(_('Recursive (down)'), False) + self.__recursive.set_help(_("Create reports for all descendants " + "of this family.")) + add_option("recursive", self.__recursive) stdoptions.add_localization_option(menu, category_name) @@ -763,6 +787,35 @@ class FamilyGroupOptions(MenuReportOptions): "information.")) add_option("missinginfo", missinginfo) + def __update_filters(self): + """ + Update the filter list based on the selected family + """ + fid = self.__fid.get_value() + family = self.__db.get_family_from_gramps_id(fid) + nfv = self._nf.get_value() + filter_list = ReportUtils.get_family_filters(self.__db, family, + include_single=True, + name_format=nfv) + self.__filter.set_filters(filter_list) + + def __filter_changed(self): + """ + Handle filter change. + If the filter is not family-specific, disable the family option + """ + filter_value = self.__filter.get_value() + if filter_value in [0, 2, 3]: # filters that rely on the center family + self.__fid.set_available(True) + else: # filters that don't + self.__fid.set_available(False) + # only allow recursion if the center family is the only family + if self.__recursive and filter_value == 0: + self.__recursive.set_available(True) + elif self.__recursive: + self.__recursive.set_value(False) + self.__recursive.set_available(False) + def make_default_style(self, default_style): """Make default output style for the Family Group Report.""" para = ParagraphStyle()