Fix citation filters so they no longer try to filter citations with a sources filter. Citation filter now allows specification of both source and citation data. When a citation is shown in a search or a filtered citation tree view, the corresponding source is also shown.

svn: r23144
This commit is contained in:
Tim G L Lyons 2013-09-16 21:42:28 +00:00
parent 7c90638ad3
commit 5903853640
8 changed files with 267 additions and 48 deletions

View File

@ -37,11 +37,13 @@ from ._hasnotematchingsubstringof import HasNoteMatchingSubstringOf
from ._hasnoteregexp import HasNoteRegexp from ._hasnoteregexp import HasNoteRegexp
from ._hasreferencecountof import HasReferenceCountOf from ._hasreferencecountof import HasReferenceCountOf
from ._hassource import HasSource from ._hassource import HasSource
from ._hassourceidof import HasSourceIdOf
from ._matchesfilter import MatchesFilter from ._matchesfilter import MatchesFilter
from ._matchespagesubstringof import MatchesPageSubstringOf from ._matchespagesubstringof import MatchesPageSubstringOf
from ._matchesrepositoryfilter import MatchesRepositoryFilter from ._matchesrepositoryfilter import MatchesRepositoryFilter
from ._matchessourcefilter import MatchesSourceFilter from ._matchessourcefilter import MatchesSourceFilter
from ._regexpidof import RegExpIdOf from ._regexpidof import RegExpIdOf
from ._regexpsourceidof import RegExpSourceIdOf
from ._hastag import HasTag from ._hastag import HasTag
editor_rule_list = [ editor_rule_list = [
@ -56,10 +58,12 @@ editor_rule_list = [
HasNoteRegexp, HasNoteRegexp,
HasReferenceCountOf, HasReferenceCountOf,
HasSource, HasSource,
HasSourceIdOf,
MatchesFilter, MatchesFilter,
MatchesPageSubstringOf, MatchesPageSubstringOf,
MatchesRepositoryFilter, MatchesRepositoryFilter,
MatchesSourceFilter, MatchesSourceFilter,
RegExpIdOf, RegExpIdOf,
RegExpSourceIdOf,
HasTag HasTag
] ]

View File

@ -0,0 +1,58 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2002-2006 Donald N. Allingham
# Copyright (C) 2011-2013 Tim G L Lyons
#
# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
#-------------------------------------------------------------------------
#
# Standard Python modules
#
#-------------------------------------------------------------------------
from ....const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from .._hasgrampsid import HasGrampsId
#-------------------------------------------------------------------------
#
# HasSourceIdOf
#
#-------------------------------------------------------------------------
class HasSourceIdOf(HasGrampsId):
"""Rule that checks for a citation with a source which has a specific
GRAMPS ID"""
name = _('Citation with Source <Id>')
description = _("Matches a citation with a source with a specified Gramps "
"ID")
category = _('Source filters')
def apply(self, dbase, citation):
source = dbase.get_source_from_handle(
citation.get_reference_handle())
if HasGrampsId.apply(self, dbase, source):
return True
return False

View File

@ -0,0 +1,60 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2002-2006 Donald N. Allingham
# Copyright (C) 2011 Tim G L Lyons
#
# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
#-------------------------------------------------------------------------
#
# Standard Python modules
#
#-------------------------------------------------------------------------
from ....const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from .._regexpidbase import RegExpIdBase
#-------------------------------------------------------------------------
#
# HasIdOf
#
#-------------------------------------------------------------------------
class RegExpSourceIdOf(RegExpIdBase):
"""
Rule that checks for a citation whose GRAMPS ID
matches regular expression.
"""
name = _('Citations with Source Id containing <text>')
description = _("Matches citations whose source has a Gramps ID that "
"matches the regular expression")
category = _('Source filters')
def apply(self, dbase, citation):
source = dbase.get_source_from_handle(
citation.get_reference_handle())
if RegExpIdBase.apply(self, dbase, source):
return True
return False

View File

@ -41,7 +41,7 @@ from gi.repository import Gtk
# GRAMPS modules # GRAMPS modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from ...widgets import MonitoredMenu from ...widgets import MonitoredMenu, DateEntry, BasicEntry
from gramps.gen.lib import Citation from gramps.gen.lib import Citation
from .. import build_filter_model from .. import build_filter_model
from . import SidebarFilter from . import SidebarFilter
@ -49,7 +49,8 @@ from gramps.gen.constfunc import cuni
from gramps.gen.filters import GenericFilterFactory, rules from gramps.gen.filters import GenericFilterFactory, rules
from gramps.gen.filters.rules.citation import (RegExpIdOf, HasIdOf, HasCitation, from gramps.gen.filters.rules.citation import (RegExpIdOf, HasIdOf, HasCitation,
HasNoteMatchingSubstringOf, HasNoteMatchingSubstringOf,
HasNoteRegexp, MatchesFilter, HasTag) HasNoteRegexp, MatchesFilter, HasTag,
HasSource, RegExpSourceIdOf)
from gramps.gen.utils.string import confidence from gramps.gen.utils.string import confidence
GenericCitationFilter = GenericFilterFactory('Citation') GenericCitationFilter = GenericFilterFactory('Citation')
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@ -61,6 +62,12 @@ class CitationSidebarFilter(SidebarFilter):
def __init__(self, dbstate, uistate, clicked): def __init__(self, dbstate, uistate, clicked):
self.clicked_func = clicked self.clicked_func = clicked
self.filter_src_id = BasicEntry()
self.filter_src_title = BasicEntry()
self.filter_src_author = BasicEntry()
self.filter_src_abbr = BasicEntry()
self.filter_src_pub = BasicEntry()
self.filter_src_note = BasicEntry()
self.filter_id = Gtk.Entry() self.filter_id = Gtk.Entry()
self.filter_page = Gtk.Entry() self.filter_page = Gtk.Entry()
self.filter_date = Gtk.Entry() self.filter_date = Gtk.Entry()
@ -101,16 +108,28 @@ class CitationSidebarFilter(SidebarFilter):
self.tag.pack_start(cell, True) self.tag.pack_start(cell, True)
self.tag.add_attribute(cell, 'text', 0) self.tag.add_attribute(cell, 'text', 0)
self.add_text_entry(_('ID'), self.filter_id) self.add_text_entry(_('Source: ID'), self.filter_src_id)
self.add_text_entry(_('Volume/Page'), self.filter_page) self.add_text_entry(_('Source: Title'), self.filter_src_title)
self.add_text_entry(_('Date'), self.filter_date) self.add_text_entry(_('Source: Author'), self.filter_src_author)
self.add_entry(_('Minimum Confidence|Min. Conf.'), self.filter_conf) self.add_text_entry(_('Source: Abbreviation'), self.filter_src_abbr)
self.add_text_entry(_('Note'), self.filter_note) self.add_text_entry(_('Source: Publication'), self.filter_src_pub)
self.add_text_entry(_('Source: Note'), self.filter_src_note)
self.add_text_entry(_('Citation: ID'), self.filter_id)
self.add_text_entry(_('Citation: Volume/Page'), self.filter_page)
self.add_text_entry(_('Citation: Date'), self.filter_date)
self.add_entry(_('Citation: Minimum Confidence|Min. Conf.'), self.filter_conf)
self.add_text_entry(_('Citation: Note'), self.filter_note)
self.add_entry(_('Tag'), self.tag) self.add_entry(_('Tag'), self.tag)
self.add_filter_entry(_('Custom filter'), self.generic) self.add_filter_entry(_('Custom filter'), self.generic)
self.add_entry(None, self.filter_regex) self.add_entry(None, self.filter_regex)
def clear(self, obj): def clear(self, obj):
self.filter_src_id.set_text('')
self.filter_src_title.set_text('')
self.filter_src_author.set_text('')
self.filter_src_abbr.set_text('')
self.filter_src_pub.set_text('')
self.filter_src_note.set_text('')
self.filter_id.set_text('') self.filter_id.set_text('')
self.filter_page.set_text('') self.filter_page.set_text('')
self.filter_date.set_text('') self.filter_date.set_text('')
@ -120,6 +139,15 @@ class CitationSidebarFilter(SidebarFilter):
self.generic.set_active(0) self.generic.set_active(0)
def get_filter(self): def get_filter(self):
src_id = unicode(self.filter_src_id.get_text()).strip()
src_title = unicode(self.filter_src_title.get_text()).strip()
src_author = unicode(self.filter_src_author.get_text()).strip()
src_abbr = unicode(self.filter_src_abbr.get_text()).strip()
src_pub = unicode(self.filter_src_pub.get_text()).strip()
src_note = unicode(self.filter_src_note.get_text()).strip()
gid = unicode(self.filter_id.get_text()).strip()
page = unicode(self.filter_page.get_text()).strip()
date = unicode(self.filter_date.get_text()).strip()
gid = cuni(self.filter_id.get_text()).strip() gid = cuni(self.filter_id.get_text()).strip()
page = cuni(self.filter_page.get_text()).strip() page = cuni(self.filter_page.get_text()).strip()
date = cuni(self.filter_date.get_text()).strip() date = cuni(self.filter_date.get_text()).strip()
@ -137,7 +165,9 @@ class CitationSidebarFilter(SidebarFilter):
tag = self.tag.get_active() > 0 tag = self.tag.get_active() > 0
gen = self.generic.get_active() > 0 gen = self.generic.get_active() > 0
empty = not (gid or page or date or conf or note or regex or tag or gen) empty = not (src_id or src_title or src_author or src_abbr or src_pub or
src_note or
gid or page or date or conf or note or regex or gen)
if empty: if empty:
generic_filter = None generic_filter = None
else: else:
@ -152,6 +182,14 @@ class CitationSidebarFilter(SidebarFilter):
rule = HasCitation([page, date, conf], use_regex=regex) rule = HasCitation([page, date, conf], use_regex=regex)
generic_filter.add_rule(rule) generic_filter.add_rule(rule)
if src_id:
rule = RegExpSourceIdOf([src_id], use_regex=regex)
generic_filter.add_rule(rule)
rule = HasSource([src_title, src_author, src_abbr, src_pub],
use_regex=regex)
generic_filter.add_rule(rule)
if note: if note:
if regex: if regex:
rule = HasNoteRegexp([note]) rule = HasNoteRegexp([note])

View File

@ -64,7 +64,7 @@ class SelectCitation(BaseSelector):
def get_column_titles(self): def get_column_titles(self):
return [ return [
(_('Page'), 350, BaseSelector.TEXT, 0), (_('Name'), 350, BaseSelector.TEXT, 0),
(_('ID'), 75, BaseSelector.TEXT, 1) (_('ID'), 75, BaseSelector.TEXT, 1)
] ]

View File

@ -192,7 +192,20 @@ class CitationTreeModel(CitationBaseModel, TreeBaseModel):
data The object data. data The object data.
""" """
sort_key = self.sort_func2(data) sort_key = self.sort_func2(data)
# If the source for this citation already exists (in the tree model) we
# add the citation as a child of the source. Otherwise we add the source
# first (because citations don't have any meaning without the associated
# source)
if self._get_node(data[5]): if self._get_node(data[5]):
# parent child sortkey handle
self.add_node(data[5], handle, sort_key, handle, secondary=True)
else:
# add the source node first
source_sort_key = self.sort_func(self.map(data[5]))
# parent child sortkey handle
self.add_node(None, data[5], source_sort_key, data[5])
# parent child sortkey handle
self.add_node(data[5], handle, sort_key, handle, secondary=True) self.add_node(data[5], handle, sort_key, handle, secondary=True)
def on_get_n_columns(self): def on_get_n_columns(self):

View File

@ -342,7 +342,10 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
self.__displayed = 0 self.__displayed = 0
self.set_search(search) self.set_search(search)
self.rebuild_data(self.current_filter, skip) if self.has_secondary:
self.rebuild_data(self.current_filter, self.current_filter2, skip)
else:
self.rebuild_data(self.current_filter, skip=skip)
_LOG.debug(self.__class__.__name__ + ' __init__ ' + _LOG.debug(self.__class__.__name__ + ' __init__ ' +
str(time.clock() - cput) + ' sec') str(time.clock() - cput) + ' sec')
@ -362,7 +365,9 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
self.rebuild_data = None self.rebuild_data = None
self._build_data = None self._build_data = None
self.search = None self.search = None
self.search2 = None
self.current_filter = None self.current_filter = None
self.current_filter2 = None
self.clear_cache() self.clear_cache()
self.lru_data = None self.lru_data = None
@ -447,31 +452,56 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
if search[0] == 1: # Filter if search[0] == 1: # Filter
#following is None if no data given in filter sidebar #following is None if no data given in filter sidebar
self.search = search[1] self.search = search[1]
if self.has_secondary:
self.search2 = search[1]
_LOG.debug("search2 filter %s %s" % (search[0], search[1]))
self._build_data = self._rebuild_filter self._build_data = self._rebuild_filter
elif search[0] == 0: # Search elif search[0] == 0: # Search
if search[1]: if search[1]:
# we have search[1] = (index, text_unicode, inversion) # we have search[1] = (index, text_unicode, inversion)
col, text, inv = search[1] col, text, inv = search[1]
func = lambda x: self._get_value(x, col, store_cache=False) or "" func = lambda x: self._get_value(x, col, secondary=False) or u""
if self.has_secondary:
func2 = lambda x: self._get_value(x, col, secondary=True) or u""
if search[2]: if search[2]:
self.search = ExactSearchFilter(func, text, inv) self.search = ExactSearchFilter(func, text, inv)
if self.has_secondary:
self.search2 = ExactSearchFilter(func2, text, inv)
else: else:
self.search = SearchFilter(func, text, inv) self.search = SearchFilter(func, text, inv)
if self.has_secondary:
self.search2 = SearchFilter(func2, text, inv)
else: else:
self.search = None self.search = None
if self.has_secondary:
self.search2 = None
_LOG.debug("search2 search with no data")
self._build_data = self._rebuild_search self._build_data = self._rebuild_search
else: # Fast filter else: # Fast filter
self.search = search[1] self.search = search[1]
if self.has_secondary:
self.search2 = search[2]
_LOG.debug("search2 fast filter")
self._build_data = self._rebuild_search self._build_data = self._rebuild_search
else: else:
self.search = None self.search = None
if self.has_secondary:
self.search2 = search[2]
_LOG.debug("search2 no search parameter")
self._build_data = self._rebuild_search self._build_data = self._rebuild_search
self.current_filter = self.search self.current_filter = self.search
if self.has_secondary:
self.current_filter2 = self.search2
def rebuild_data(self, data_filter=None, skip=[]): def rebuild_data(self, data_filter=None, data_filter2=None, skip=[]):
""" """
Rebuild the data map. Rebuild the data map.
When called externally (from listview), data_filter and data_filter2
should be None; set_search will already have been called to establish
the filter functions. When called internally (from __init__) both
data_filter and data_filter2 will have been set from set_search
""" """
cput = time.clock() cput = time.clock()
self.clear_cache() self.clear_cache()
@ -481,16 +511,21 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
return return
self.clear() self.clear()
self._build_data(self.current_filter, skip) if self.has_secondary:
self._build_data(self.current_filter, self.current_filter2, skip)
else:
self._build_data(self.current_filter, None, skip)
self._in_build = False self._in_build = False
self.current_filter = data_filter self.current_filter = data_filter
if self.has_secondary:
self.current_filter2 = data_filter2
_LOG.debug(self.__class__.__name__ + ' rebuild_data ' + _LOG.debug(self.__class__.__name__ + ' rebuild_data ' +
str(time.clock() - cput) + ' sec') str(time.clock() - cput) + ' sec')
def _rebuild_search(self, dfilter, skip): def _rebuild_search(self, dfilter, dfilter2, skip):
""" """
Rebuild the data map where a search condition is applied. Rebuild the data map where a search condition is applied.
""" """
@ -498,12 +533,14 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
self.__displayed = 0 self.__displayed = 0
items = self.number_items() items = self.number_items()
_LOG.debug("rebuild search primary")
self.__rebuild_search(dfilter, skip, items, self.__rebuild_search(dfilter, skip, items,
self.gen_cursor, self.add_row) self.gen_cursor, self.add_row)
if self.has_secondary: if self.has_secondary:
_LOG.debug("rebuild search secondary")
items = self.number_items2() items = self.number_items2()
self.__rebuild_search(dfilter, skip, items, self.__rebuild_search(dfilter2, skip, items,
self.gen_cursor2, self.add_row2) self.gen_cursor2, self.add_row2)
def __rebuild_search(self, dfilter, skip, items, gen_cursor, add_func): def __rebuild_search(self, dfilter, skip, items, gen_cursor, add_func):
@ -533,20 +570,27 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
if not status.was_cancelled(): if not status.was_cancelled():
status.end() status.end()
def _rebuild_filter(self, dfilter, skip): def _rebuild_filter(self, dfilter, dfilter2, skip):
""" """
Rebuild the data map where a filter is applied. Rebuild the data map where a filter is applied.
""" """
self.__total = 0 self.__total = 0
self.__displayed = 0 self.__displayed = 0
items = self.number_items() if not self.has_secondary:
self.__rebuild_filter(dfilter, skip, items, # The tree only has primary data
self.gen_cursor, self.map, self.add_row) items = self.number_items()
if self.has_secondary: _LOG.debug("rebuild filter primary")
items = self.number_items2()
self.__rebuild_filter(dfilter, skip, items, self.__rebuild_filter(dfilter, skip, items,
self.gen_cursor2, self.map2, self.add_row2) self.gen_cursor, self.map, self.add_row)
else:
# The tree has both primary and secondary data. The navigation type
# (navtype) which governs the filters that are offered, is for the
# secondary data.
items = self.number_items2()
_LOG.debug("rebuild filter secondary")
self.__rebuild_filter(dfilter2, skip, items,
self.gen_cursor2, self.map2, self.add_row2)
def __rebuild_filter(self, dfilter, skip, items, gen_cursor, data_map, def __rebuild_filter(self, dfilter, skip, items, gen_cursor, data_map,
add_func): add_func):
@ -578,6 +622,7 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
status.heartbeat() status.heartbeat()
if dfilter: if dfilter:
_LOG.debug("rebuild filter %s" % dfilter)
status_filter = progressdlg.LongOpStatus(msg=_("Applying filter"), status_filter = progressdlg.LongOpStatus(msg=_("Applying filter"),
total_steps=items, interval=items//10) total_steps=items, interval=items//10)
pmon.add_op(status_filter) pmon.add_op(status_filter)
@ -743,13 +788,14 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
if self._get_node(handle) is not None: if self._get_node(handle) is not None:
return # row already exists return # row already exists
cput = time.clock() cput = time.clock()
if not self.search or \ data = self.map(handle)
(self.search and self.search.match(handle, self.db)): if data:
#row needs to be added to the model if not self.search or \
data = self.map(handle) (self.search and self.search.match(handle, self.db)):
if data:
self.add_row(handle, data) self.add_row(handle, data)
else: else:
if not self.search2 or \
(self.search2 and self.search2.match(handle, self.db)):
self.add_row2(handle, self.map2(handle)) self.add_row2(handle, self.map2(handle))
_LOG.debug(self.__class__.__name__ + ' add_row_by_handle ' + _LOG.debug(self.__class__.__name__ + ' add_row_by_handle ' +
@ -916,6 +962,9 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
""" """
Returns the contents of a given column of a gramps object Returns the contents of a given column of a gramps object
""" """
if secondary is None:
raise NotImplementedError
if handle in self.lru_data: if handle in self.lru_data:
data = self.lru_data[handle] data = self.lru_data[handle]
else: else:
@ -923,16 +972,20 @@ class TreeBaseModel(GObject.GObject, Gtk.TreeModel):
data = self.map(handle) data = self.map(handle)
else: else:
data = self.map2(handle) data = self.map2(handle)
if not self._in_build and store_cache: if not self._in_build:
self.lru_data[handle] = data self.lru_data[handle] = data
try: if not secondary:
if not secondary: # None is used to indicate this column has no data
return self.fmap[col](data) if self.fmap[col] is None:
else: return ''
return self.fmap2[col](data) value = self.fmap[col](data)
except: else:
return '' if self.fmap2[col] is None:
return ''
value = self.fmap2[col](data)
return value
def do_get_iter(self, path): def do_get_iter(self, path):
""" """

View File

@ -175,17 +175,10 @@ class CitationTreeView(ListView):
def setup_filter(self): def setup_filter(self):
""" """
Override the setup of the default Search Bar in listview, so that only Override the setup of the default Search Bar in listview, so that only
the searchable source fields are shown. This includes renaming the the searchable source fields are shown.
'Title or Page' search to 'Title'
""" """
def name(i):
if i == 0:
return _('Title')
else:
return self.COLUMNS[i][0]
self.search_bar.setup_filter( self.search_bar.setup_filter(
[(name(pair[1]), pair[1], pair[1] in self.exact_search()) [(self.COLUMNS[(pair[1])][0], pair[1], pair[1] in self.exact_search())
for pair in self.column_order() if pair[0] and for pair in self.column_order() if pair[0] and
pair[1] in self.COLUMN_FILTERABLE]) pair[1] in self.COLUMN_FILTERABLE])
@ -593,7 +586,7 @@ class CitationTreeView(ListView):
""" """
Define the default gramplets for the sidebar and bottombar. Define the default gramplets for the sidebar and bottombar.
""" """
return (("Source Filter",), return (("Citation Filter",),
("Citation Gallery", ("Citation Gallery",
"Citation Notes", "Citation Notes",
"Citation Backlinks")) "Citation Backlinks"))