Difference between revisions of "Develop an Add-on Rule"

From Gramps
Jump to: navigation, search
m (Sample Gramps Plug-in Registration file)
m (Converting a query to an add-on Filter Rule)
Line 29: Line 29:
 
== Converting a query to an add-on Filter Rule ==
 
== Converting a query to an add-on Filter Rule ==
 
=== Sample Gramps Plug-in Registration file ===
 
=== Sample Gramps Plug-in Registration file ===
[https://raw.githubusercontent.com/gramps-project/addons-source/master/FilterRules/infamilyrule.gpr.py <code>infamilyrule.gpr.py</code>]
+
[https://github.com/gramps-project/addons-source/blob/master/FilterRules/hasrolerule.gpr.py <code>hasrolerule.gpr.py</code>]
  
 
<pre>#
 
<pre>#
Line 35: Line 35:
 
#    for Gramps - a GTK+/GNOME based genealogy program
 
#    for Gramps - a GTK+/GNOME based genealogy program
 
#
 
#
# Copyright (C) 2020 Paul Culley
+
# Copyright (C) 2018 Paul Culley
# Copyright (C) 2020  Matthias Kemmer
 
 
#
 
#
 
# This program is free software; you can redistribute it and/or modify
 
# This program is free software; you can redistribute it and/or modify
Line 52: Line 51:
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
#
 
#
"""Filter rule that matches people who are matched by a family filter."""
+
"""
 +
Filter rule to match an Event Role with a particular value.
 +
"""
  
 
register(RULE,
 
register(RULE,
         id='PersonsInFamilyFilterMatch',
+
         id='HasPersonEventRole',
         name=_('People who are part of families matching <filter>'),
+
         name=_("People with events with a selected role"),
         description=_('People who are part of families matching <filter>'),
+
         description=_("Matches people with an event with a selected role"),
         version='1.0.9',
+
         version='0.0.14',
         authors=["Matthias Kemmer", "Paul Culley"],
+
         authors=["Paul Culley"],
         authors_email=["[email protected]", "[email protected]"],
+
         authors_email=["[email protected]"],
 
         gramps_target_version='5.1',
 
         gramps_target_version='5.1',
 
         status=STABLE,
 
         status=STABLE,
         fname="infamilyrule.py",
+
         fname="hasrolerule.py",
         ruleclass='PersonsInFamilyFilterMatch',  # must match the rule class name
+
         ruleclass='HasPersonEventRole',  # must be rule class name
 
         namespace='Person',  # one of the primary object classes
 
         namespace='Person',  # one of the primary object classes
 +
        )
 +
 +
register(RULE,
 +
        id='HasFamilyEventRole',
 +
        name=_("Families with events with a selected role"),
 +
        description=_("Matches families with an event with a selected role"),
 +
        version='0.0.14',
 +
        authors=["Paul Culley"],
 +
        authors_email=["[email protected]"],
 +
        gramps_target_version='5.1',
 +
        status=STABLE,
 +
        fname="hasrolerule.py",
 +
        ruleclass='HasFamilyEventRole',  # must be rule class name
 +
        namespace='Family',  # one of the primary object classes
 
         )
 
         )
 
</pre>
 
</pre>
  
 
=== Sample Gramps Plug-in source file ===
 
=== Sample Gramps Plug-in source file ===
[https://github.com/gramps-project/addons-source/blob/master/FilterRules/infamilyrule.py <code>infamilyrule.py</code>]
+
[https://github.com/gramps-project/addons-source/blob/master/FilterRules/hasrolerule.py <code>hasrolerule.py</code>]
  
 
''Filenames'' or <code>Filenames</code>
 
''Filenames'' or <code>Filenames</code>
  
<pre>#
+
<pre>#  
#
+
#
 
# plug-in Python module
 
# plug-in Python module
 
#    for Gramps - a GTK+/GNOME based genealogy program
 
#    for Gramps - a GTK+/GNOME based genealogy program
#
+
#  
# Copyright (C) 2020 Paul Culley
+
# Copyright (C) 2018 Paul Culley
# Copyright (C) 2020  Matthias Kemmer
+
#  
#
 
 
# This program is free software; you can redistribute it and/or modify
 
# 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
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation; either version 2 of the License, or
 
# the Free Software Foundation; either version 2 of the License, or
 
# (at your option) any later version.
 
# (at your option) any later version.
#
+
#  
 
# This program is distributed in the hope that it will be useful,
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
# GNU General Public License for more details.
#
+
#  
 
# You should have received a copy of the GNU General Public License
 
# You should have received a copy of the GNU General Public License
 
# along with this program; if not, write to the Free Software
 
# along with this program; if not, write to the Free Software
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
+
#  
  
"""Filter rule to match persons in a matching family."""
+
"""
 +
Filter rule to match persons with a particular event.
 +
"""
 +
# -------------------------------------------------------------------------
 +
#
 +
# Standard Python modules
 +
#
 +
# -------------------------------------------------------------------------
  
 
# -------------------------------------------------------------------------
 
# -------------------------------------------------------------------------
#
+
#  
 
# Gramps modules
 
# Gramps modules
#
+
#  
 
# -------------------------------------------------------------------------
 
# -------------------------------------------------------------------------
from gramps.gui.editors.filtereditor import MyBoolean, MyFilters
+
from gramps.gen.lib.eventroletype import EventRoleType
from gramps.gen.filters.rules._matchesfilterbase import MatchesFilterBase
+
from gramps.gui.editors.filtereditor import MySelect, MyBoolean
 +
from gramps.gen.filters.rules import Rule
 
from gramps.gen.const import GRAMPS_LOCALE as glocale
 
from gramps.gen.const import GRAMPS_LOCALE as glocale
 
try:
 
try:
Line 113: Line 135:
 
_ = _trans.gettext
 
_ = _trans.gettext
  
# These globals are the only easy way I could think of to communicate between
 
# checkboxes to make sure at least one was selected.
 
child_state = True  # used to indicate state of checkbox
 
parent_state = True
 
 
 
class IncChildren(MyBoolean):
 
    """Include children."""
 
  
 +
class Roletype(MySelect):
 +
    """ Provide a Role type selector """
 
     def __init__(self, db):
 
     def __init__(self, db):
         MyBoolean.__init__(self, _("Include Children"))
+
         MySelect.__init__(self, EventRoleType, db.get_event_roles())
        self.set_tooltip_text(_("Include the children in the matching"
 
                                " families."))
 
        self.connect("toggled", self.toggled)
 
        self.set_active(True)
 
 
 
    def toggled(self, widget):
 
        """Make sure user doesn't get to turn off both children and parents."""
 
        if not parent_state:
 
            if not widget.get_active():
 
                widget.set_active(True)
 
        global child_state
 
        child_state = widget.get_active()
 
        # print("child:", child_state)
 
 
 
    def set_text(self, val):
 
        """Set the selector state to display the passed value."""
 
        is_active = bool(int(val))
 
        self.set_active(is_active)
 
        global child_state
 
        child_state = is_active
 
 
 
  
class IncParents(MyBoolean):
 
    """Provide a negation switch."""
 
  
 +
class NoMatch(MyBoolean):
 +
    """ Provide a negation switch """
 
     def __init__(self, db):
 
     def __init__(self, db):
         MyBoolean.__init__(self, _("Include Parents"))
+
         MyBoolean.__init__(self, _("Does NOT match with selected Role"))
         self.set_tooltip_text(_("Include the parents in the matching"
+
         self.set_tooltip_text(_("Finds the items that don't have event Roles "
                                 " families."))
+
                                 "of the selected type."))
        self.connect("toggled", self.toggled)
+
# -------------------------------------------------------------------------
        self.set_active(True)
+
#
 +
# HasEvent
 +
#
 +
# -------------------------------------------------------------------------
  
    def toggled(self, widget):
 
        """Make sure user doesn't get to turn off both children and parents."""
 
        if not child_state:
 
            if not widget.get_active():
 
                widget.set_active(True)
 
        global parent_state
 
        parent_state = widget.get_active()
 
        # print("parent:", parent_state)
 
  
    def set_text(self, val):
+
class HasPersonEventRole(Rule):
        """Set the selector state to display the passed value."""
+
    """Rule that checks for a person with a selected event role"""
        is_active = bool(int(val))
 
        self.set_active(is_active)
 
        global parent_state
 
        parent_state = is_active
 
  
 +
    labels = [(_('Role'), Roletype),
 +
              (_('Inverse'), NoMatch)]
 +
    name = _('People with events with the <role>')
 +
    description = _("Matches people with an event with a selected role")
 +
    category = _('Event filters')
  
class FamFilt(MyFilters):
+
    def apply(self, dbase, person):
    """Add custom family filter selector."""
+
        if not self.list[0]:
 
+
            return False
    # This is a horrible hack that is needed because the filtereditor doesn't
+
        for event_ref in person.get_event_ref_list():
    # have support for a 'Family Filter name' selector. So we have to make our
+
            if not event_ref:
    # own. Furthermore, we don't have the needed reference to the 'filterdb',
+
                continue
    # the list of custom filters.
+
            if self.list[1] == '1':
    def __init__(self, db):
+
                if event_ref.role.xml_str() != self.list[0]:
        import inspect
+
                    return True
        stack = inspect.stack()  # our stack frame
+
            else:
        caller_locals = stack[1][0].f_locals  # locals from caller
+
                if event_ref.role.xml_str() == self.list[0]:
        # the caller has an attribute 'filterdb' which has what we need
+
                    return True
        MyFilters.__init__(self,
+
        return False
                          caller_locals["filterdb"].get_filters('Family'))
 
 
 
 
 
# -------------------------------------------------------------------------
 
#
 
# Person part of matching family
 
#
 
# -------------------------------------------------------------------------
 
class PersonsInFamilyFilterMatch(MatchesFilterBase):
 
    """Rule that checks for a person with a selected event role."""
 
  
    labels = [(_('Family Filter name:'), FamFilt),
 
              (_('Include Children'), IncChildren),
 
              (_('Include Parents'), IncParents)]
 
    name = _('People who are part of families matching <filter>')
 
    description = _("People who are part of families matching <filter>")
 
    category = _('General filters')
 
    # we want to have this filter show family filters
 
    namespace = 'Family'
 
  
    def prepare(self, db, user):
+
class HasFamilyEventRole(Rule):
        """Prepare a reference list for the filter."""
+
    """Rule that checks for a family with a selected event role"""
        self.persons = set()
 
        MatchesFilterBase.prepare(self, db, user)
 
        self.MFF_filt = self.find_filter()
 
        if self.MFF_filt:
 
            for family_handle in db.iter_family_handles():
 
                if self.MFF_filt.check(db, family_handle):
 
                    family = db.get_family_from_handle(family_handle)
 
                    if bool(int(self.list[2])):
 
                        father = family.get_father_handle()
 
                        mother = family.get_mother_handle()
 
                        self.persons.add(father)
 
                        self.persons.add(mother)
 
                    if bool(int(self.list[1])):
 
                        for child_ref in family.get_child_ref_list():
 
                            self.persons.add(child_ref.ref)
 
  
     def apply(self, _db, obj):
+
     labels = [(_('Role'), Roletype),
        """
+
              (_('Inverse'), NoMatch)]
        Return True if a person applies to the filter rule.
+
    name = _('Families with events with the <role>')
 +
    description = _("Matches families with an event with a selected role")
 +
    category = _('Event filters')
  
         :returns: True or False
+
    def apply(self, dbase, family):
         """
+
         if not self.list[0]:
        if obj.get_handle() in self.persons:
+
            return False
             return True
+
         for event_ref in family.get_event_ref_list():
 +
            if not event_ref:
 +
                continue
 +
            if self.list[1] == '1':
 +
                if event_ref.role.xml_str() != self.list[0]:
 +
                    return True
 +
             else:
 +
                if event_ref.role.xml_str() == self.list[0]:
 +
                    return True
 
         return False
 
         return False
 
 
</pre>
 
</pre>
  

Revision as of 17:53, 23 September 2022

Gnome-important.png
🚧 Work In Progress

This wikipage is a cloned outline from a template. It is being roughed in.

Since you are reading this paragraph, then the WikiContributor has not progressed to the point of trimming out the excess template material. That means the wikipage is probably not ready for collaborative editing yet. Multiple people editing now might unintentionally overwrite their work. Please post your suggestion on the Discussion page instead of directly editing the content.

Custom Filters are built upon Query Rules. Sometimes a filter rule has not been provided to search the part of the Tree desired. Or the existing Rules can not be combined to isolate the desired part of the Tree. If those particular must be repeatedly isolated, building a query rule might be the next logical step.

coding style guide (placeholder)

In various rules that allow a Person ID to be selected as a parameter value, both built-in rules & add-on rules use inconsistent placeholders:

  • <Id>
  • <id>
  • <person>

It would be better if they were harmonized.

I suspect any variant of <ID> would ambiguous because sometimes these rules (while needing a Person Gramps ID) are in different category custom rules.

What should the standard nomenclature be for the rule names, descriptions & documentation?

Using add-ons to design and test flight Filter Rule

Designing and optimizing a quality query can be challenging. Even a slow rough-cut query is acceptable for a single-use. But clean and fast code is vital to a frequently used add-on Filter Rule.

There are some power tool add-ons to help when designing queries, testing them and optimize their runtimes.

Particularly, using SuperTool design a rough query, the Query Gramplet to experiment with optimizations, the Generic custom rules to commit the query to a something usable with Custom Filters, and FilterParams to tune is one way to use add-ons in concert.

Converting a query to an add-on Filter Rule

Sample Gramps Plug-in Registration file

hasrolerule.gpr.py

#
# GPR (Gramps plug-in Registration) of a Python module
#    for Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2018  Paul Culley
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""
Filter rule to match an Event Role with a particular value.
"""

register(RULE,
         id='HasPersonEventRole',
         name=_("People with events with a selected role"),
         description=_("Matches people with an event with a selected role"),
         version='0.0.14',
         authors=["Paul Culley"],
         authors_email=["[email protected]"],
         gramps_target_version='5.1',
         status=STABLE,
         fname="hasrolerule.py",
         ruleclass='HasPersonEventRole',  # must be rule class name
         namespace='Person',  # one of the primary object classes
         )

register(RULE,
         id='HasFamilyEventRole',
         name=_("Families with events with a selected role"),
         description=_("Matches families with an event with a selected role"),
         version='0.0.14',
         authors=["Paul Culley"],
         authors_email=["[email protected]"],
         gramps_target_version='5.1',
         status=STABLE,
         fname="hasrolerule.py",
         ruleclass='HasFamilyEventRole',  # must be rule class name
         namespace='Family',  # one of the primary object classes
         )

Sample Gramps Plug-in source file

hasrolerule.py

Filenames or Filenames

# 
#  
# plug-in Python module
#    for Gramps - a GTK+/GNOME based genealogy program
# 
# Copyright (C) 2018  Paul Culley
# 
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# 

"""
Filter rule to match persons with a particular event.
"""
# -------------------------------------------------------------------------
# 
# Standard Python modules
# 
# -------------------------------------------------------------------------

# -------------------------------------------------------------------------
# 
# Gramps modules
# 
# -------------------------------------------------------------------------
from gramps.gen.lib.eventroletype import EventRoleType
from gramps.gui.editors.filtereditor import MySelect, MyBoolean
from gramps.gen.filters.rules import Rule
from gramps.gen.const import GRAMPS_LOCALE as glocale
try:
    _trans = glocale.get_addon_translator(__file__)
except ValueError:
    _trans = glocale.translation
_ = _trans.gettext


class Roletype(MySelect):
    """ Provide a Role type selector """
    def __init__(self, db):
        MySelect.__init__(self, EventRoleType, db.get_event_roles())


class NoMatch(MyBoolean):
    """ Provide a negation switch """
    def __init__(self, db):
        MyBoolean.__init__(self, _("Does NOT match with selected Role"))
        self.set_tooltip_text(_("Finds the items that don't have event Roles "
                                "of the selected type."))
# -------------------------------------------------------------------------
# 
# HasEvent
# 
# -------------------------------------------------------------------------


class HasPersonEventRole(Rule):
    """Rule that checks for a person with a selected event role"""

    labels = [(_('Role'), Roletype),
              (_('Inverse'), NoMatch)]
    name = _('People with events with the <role>')
    description = _("Matches people with an event with a selected role")
    category = _('Event filters')

    def apply(self, dbase, person):
        if not self.list[0]:
            return False
        for event_ref in person.get_event_ref_list():
            if not event_ref:
                continue
            if self.list[1] == '1':
                if event_ref.role.xml_str() != self.list[0]:
                    return True
            else:
                if event_ref.role.xml_str() == self.list[0]:
                    return True
        return False


class HasFamilyEventRole(Rule):
    """Rule that checks for a family with a selected event role"""

    labels = [(_('Role'), Roletype),
              (_('Inverse'), NoMatch)]
    name = _('Families with events with the <role>')
    description = _("Matches families with an event with a selected role")
    category = _('Event filters')

    def apply(self, dbase, family):
        if not self.list[0]:
            return False
        for event_ref in family.get_event_ref_list():
            if not event_ref:
                continue
            if self.list[1] == '1':
                if event_ref.role.xml_str() != self.list[0]:
                    return True
            else:
                if event_ref.role.xml_str() == self.list[0]:
                    return True
        return False

Publishing a new Filter Rule

Update the Rule Expansions wiki page

See also

  • In in MantisBT bug reporter for Gramps
    • Feature Requests: search for add-on custom filter Rules. (Filter for "filter, rule")
    • 0011689: [GrampsAIO-5.1.2-new_libs_win64] Active and Default Person filter rules