diff --git a/Form/UK1841.py b/Form/UK1841.py new file mode 100644 index 000000000..537e148ec --- /dev/null +++ b/Form/UK1841.py @@ -0,0 +1,226 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2019-2020 Steve Youngs +# +# 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. +# + +# ------------------------------------------------------------------------ +# +# Gramps modules +# +# ------------------------------------------------------------------------ +from gramps.gen.datehandler import displayer as date_displayer +from gramps.gen.db import DbTxn +from gramps.gen.display.name import displayer as name_displayer +from gramps.gen.display.place import displayer as place_displayer +from gramps.gen.lib import (Date, Event, EventType, EventRef, EventRoleType, + Name, Person) +from gramps.gen.utils.db import get_participant_from_event + +# ------------------------------------------------------------------------ +# +# Gramplet modules +# +# ------------------------------------------------------------------------ +import actionutils + +# ------------------------------------------------------------------------ +# +# Internationalisation +# +# ------------------------------------------------------------------------ +from gramps.gen.const import GRAMPS_LOCALE as glocale +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation + +_ = _trans.gettext + + +def get_actions(dbstate, citation, form_event): + """ + return a list of all actions that this module can provide for the given citation and form + each list entry is a string, describing the action category, and a list of actions that can be performed. + """ + actions = [] + actions.append(PrimaryNameCitation.get_actions( + dbstate, citation, form_event)) + actions.append(AlternateName.get_actions(dbstate, citation, form_event)) + actions.append(BirthEvent.get_actions(dbstate, citation, form_event)) + actions.append(OccupationEvent.get_actions(dbstate, citation, form_event)) + actions.append(ResidenceEvent.get_actions(dbstate, citation, form_event)) + return actions + + +class PrimaryNameCitation: + @staticmethod + def get_actions(dbstate, citation, form_event): + db = dbstate.db + actions = [] + for (person, attr) in actionutils.get_form_person_attr(db, form_event.get_handle(), 'Name'): + actions.append((name_displayer.display(person), attr.get_value(), actionutils.CAN_EDIT_DETAIL, + # lambda dbstate, uistate, track, edit_detail, callback, citation_handle=citation.handle, person_handle=person.handle: PrimaryNameCitation.command(dbstate, uistate, track, edit_detail, callback, citation_handle, person_handle))) + # action command callback + lambda dbstate, uistate, track, edit_detail, callback, person=person: + actionutils.edit_name(actionutils.update_name(name=person.get_primary_name(), citation_handle=citation.handle), + dbstate, uistate, track, edit_detail, + # edit_name callback + lambda name, person=person, dbstate=dbstate, uistate=uistate, track=track, edit_detail=edit_detail: + actionutils.commit_person(actionutils.update_person(person=person, primary_name=name), + dbstate, uistate, track, False, # nothing to edit, so force edit_detail=False + # commit_person callback + # call the top level callback + lambda person, callback=callback: callback())))) + + return (_("Add Primary Name citation"), actions) + + +class AlternateName: + @staticmethod + def get_actions(dbstate, citation, form_event): + db = dbstate.db + actions = [] + for (person, attr) in actionutils.get_form_person_attr(db, form_event.get_handle(), 'Name'): + detail = _('Given Name: {name}').format(name=attr.get_value()) + actions.append((name_displayer.display(person), detail, + # the user should split the 'Name' attribute into the consituent parts of a Name object, so force MUST_EDIT_DETAIL + actionutils.MUST_EDIT_DETAIL, + # action command callback + lambda dbstate, uistate, track, edit_detail, callback, person=person, name=attr.get_value(): + actionutils.edit_name(actionutils.make_name(first_name=name, citation_handle=citation.handle), dbstate, uistate, track, edit_detail, + # edit_name callback + lambda name, dbstate=dbstate, uistate=uistate, track=track, edit_detail=edit_detail: + actionutils.add_alternate_name_to_person(name, person.handle, + dbstate, uistate, track, False, # nothing to edit, so force edit_detail=False + # add_alternate_name_to_person callback + # call the top level callback + lambda person, callback=callback: callback())))) + return (_("Add alternate name"), actions) + + +class BirthEvent: + @staticmethod + def get_actions(dbstate, citation, form_event): + db = dbstate.db + actions = [] + # if there is no date on the form, no actions can be performed + if form_event.get_date_object(): + for (person, attr) in actionutils.get_form_person_attr(db, form_event.get_handle(), 'Age'): + age_string = attr.get_value() + if age_string: + birth_date = None + if actionutils.represents_int(age_string): + age = int(age_string) + if age: + birth_date = form_event.get_date_object() - age + birth_date.make_vague() + # Age was rounded down to the nearest five years for those aged 15 or over + # In practice this rule was not always followed by enumerators + if age < 15: + # no adjustment required + birth_date.set_modifier(Date.MOD_ABOUT) + elif not birth_date.is_compound(): + # in theory, birth_date will never be compound since 1841 census date was 1841-06-06. Let's handle it anyway. + # create a compound range spanning the possible birth years + birth_range = (birth_date - 5).get_dmy() + \ + (False,) + birth_date.get_dmy() + (False,) + birth_date.set(Date.QUAL_NONE, Date.MOD_RANGE, birth_date.get_calendar( + ), birth_range, newyear=birth_date.get_new_year()) + birth_date.set_quality(Date.QUAL_CALCULATED) + detail = _('Age: {age}\nDate: {date}').format( + age=age_string, date=date_displayer.display(birth_date)) + else: + detail = _('Age: {age}').format(age=age_string) + + actions.append((name_displayer.display(person), detail, + actionutils.CAN_EDIT_DETAIL if birth_date else actionutils.MUST_EDIT_DETAIL, + # action command callback + lambda dbstate, uistate, track, edit_detail, callback, person=person, birth_date=birth_date: + # add a birth event + actionutils.add_event(actionutils.make_event(type=EventType.BIRTH, date_object=birth_date, citation_handle=citation.handle), + dbstate, uistate, track, edit_detail, + # add_event callback + lambda event, dbstate=dbstate, uistate=uistate, track=track, edit_detail=edit_detail, callback=callback, person_handle=person.handle: + # and then add a reference to the event to person + actionutils.add_event_ref_to_person(actionutils.make_event_ref(event_handle=event.get_handle(), role=EventRoleType.PRIMARY), person_handle, dbstate, uistate, track, edit_detail, + # add_event_ref_to_person callback + # call the top level callback + lambda person, callback=callback: callback())))) + return (_("Add Birth event"), actions) + + +class OccupationEvent: + @staticmethod + def get_actions(dbstate, citation, form_event): + db = dbstate.db + actions = [] + for (person, attr) in actionutils.get_form_person_attr(db, form_event.get_handle(), 'Occupation'): + occupation = attr.get_value() + if (occupation): + detail = _('Description: {occupation}').format( + occupation=occupation) + actions.append((name_displayer.display(person), detail, + actionutils.CAN_EDIT_DETAIL, + # action command callback + lambda dbstate, uistate, track, edit_detail, callback, person=person, occupation=occupation: + # add a occupation event + actionutils.add_event(actionutils.make_event(type=EventType.OCCUPATION, description=occupation, date_object=form_event.get_date_object(), citation_handle=citation.handle), + dbstate, uistate, track, edit_detail, + # add_event callback + lambda event, dbstate=dbstate, uistate=uistate, track=track, edit_detail=edit_detail, callback=callback, person_handle=person.handle: + # and then add a reference to the event to person + actionutils.add_event_ref_to_person(actionutils.make_event_ref(event_handle=event.get_handle(), role=EventRoleType.PRIMARY), person_handle, dbstate, uistate, track, edit_detail, + # add_event_ref_to_person callback + # call the top level callback + lambda person, callback=callback: callback())))) + return (_("Add Occupation event"), actions) + + +class ResidenceEvent: + @staticmethod + def get_actions(dbstate, citation, form_event): + db = dbstate.db + # build a list of all the people referenced in the form. For 1841, all people have a PRIMARY event role + event_ref_details = [] + for item in db.find_backlink_handles(form_event.get_handle(), include_classes=['Person']): + handle = item[1] + person = db.get_person_from_handle(handle) + for event_ref in person.get_event_ref_list(): + if event_ref.ref == form_event.get_handle(): + event_ref_details.append( + (person.get_handle(), EventRoleType.PRIMARY)) + actions = [] + if event_ref_details: + detail = None + if form_event.get_place_handle(): + place = place_displayer.display( + db, db.get_place_from_handle(form_event.get_place_handle())) + detail = _('Place: {place}').format(place=place) + + actions.append((get_participant_from_event(db, form_event.get_handle()), detail, actionutils.MUST_EDIT_DETAIL, + # action command callback + lambda dbstate, uistate, track, edit_detail, callback: + # add a residence event + actionutils.add_event(actionutils.make_event(type=EventType.RESIDENCE, place_handle=form_event.get_place_handle(), date_object=form_event.get_date_object(), citation_handle=citation.handle), + dbstate, uistate, track, edit_detail, + # add_event callback + lambda event, dbstate=dbstate, uistate=uistate, track=track, edit_detail=edit_detail, callback=callback: + # call the top level callback with a dummy people argument that is the list of people to who we added an event_ref to + callback(people=[actionutils.do_add_event_ref_to_person(actionutils.make_event_ref(event_handle=event.get_handle(), role=event_ref_detail[1]), event_ref_detail[0], dbstate) for event_ref_detail in event_ref_details])))) + + return (_("Add Residence event"), actions) diff --git a/Form/actionutils.py b/Form/actionutils.py new file mode 100644 index 000000000..6005a7e16 --- /dev/null +++ b/Form/actionutils.py @@ -0,0 +1,201 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2019-2020 Steve Youngs +# +# 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. +# + +# ------------------------------------------------------------------------ +# +# Gramps modules +# +# ------------------------------------------------------------------------ +from gramps.gen.db import DbTxn +from gramps.gen.display.name import displayer as name_displayer +from gramps.gen.lib import (Event, EventType, EventRef, EventRoleType, + Name, Person) +from gramps.gui.editors import EditEvent, EditName, EditPerson + +# ------------------------------------------------------------------------ +# +# Internationalisation +# +# ------------------------------------------------------------------------ +from gramps.gen.const import GRAMPS_LOCALE as glocale +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation + +_ = _trans.gettext + +# Constants to define the options for editing the details of an action +CANNOT_EDIT_DETAIL = 0 +CAN_EDIT_DETAIL = 1 +MUST_EDIT_DETAIL = 2 + + +def __init__(): + pass + + +def make_event(type=EventType(), description=None, date_object=None, citation_handle=None, place_handle=None): + """ + make an event initialised with the supplied values. + return the event created + """ + event = Event() + event.set_type(type) + event.set_description(description) + event.set_date_object(date_object) + event.add_citation(citation_handle) + event.set_place_handle(place_handle) + return event + + +def make_event_ref(event_handle=None, role=EventRoleType()): + """ + make an event_ref initialised with the supplied values. + return the event_ref created + """ + event_ref = EventRef() + event_ref.set_reference_handle(event_handle) + event_ref.set_role(role) + return event_ref + + +def make_name(first_name=None, citation_handle=None): + name = Name() + if first_name: + name.set_first_name(first_name) + if citation_handle: + name.add_citation(citation_handle) + return name + + +def update_name(name, first_name=None, citation_handle=None): + if first_name: + name.set_first_name(first_name) + if citation_handle: + name.add_citation(citation_handle) + return name + + +def update_person(person, primary_name=None): + person.set_primary_name(primary_name) + return person + + +def commit_person(person, dbstate, uistate, track, edit_detail, callback): + """ + commit person to the database, optionally showing the editor window first. + callback(person) is called after successful commit. + Note: If the editor window is cancelled, the callback is not called. + """ + if edit_detail: + EditPerson(dbstate, uistate, track, person, callback) + else: + db = dbstate.db + with DbTxn(_("Update Person ({name})").format(name=name_displayer.display(person)), db) as trans: + db.commit_person(person, trans) + if callback: + callback(person) + + +def add_event(event, dbstate, uistate, track, edit_detail, callback): + """ + Add a new event to the database, calling callback(event) on successful completion. + If edit_detail is true, and the user cancels the editor window, the callback is not called. + """ + db = dbstate.db + if edit_detail: + EditEvent(dbstate, uistate, track, event, callback) + else: + # add the event to the database + with DbTxn(_("Add Event ({0})").format(event.get_gramps_id()), db) as trans: + db.add_event(event, trans) + if callback: + callback(event) + + +def do_add_event_ref_to_person(event_ref, person_handle, dbstate): + """ + Add event_ref to person_handle + return: person_handle + """ + # Add new event reference to the person person_handle + db = dbstate.db + person = db.get_person_from_handle(person_handle) + person.add_event_ref(event_ref) + with DbTxn(_("Add Event ({name})").format(name=name_displayer.display(person)), db) as trans: + db.commit_person(person, trans) + return person_handle + + +def add_alternate_name_to_person(name, person_handle, dbstate, uistate, track, edit_detail, callback): + # Add new altername name to the person person_handle + db = dbstate.db + person = db.get_person_from_handle(person_handle) + person.add_alternate_name(name) + commit_person(person, dbstate, uistate, track, edit_detail, callback) + + +def add_event_ref_to_person(event_ref, person_handle, dbstate, uistate, track, edit_detail, callback): + """ + Add event_ref to person_handle, calling callback(person) on successful completion. + If edit_detail is true, and the user cancels the editor window, the callback is not called. + return: the person to whom the evert_ref was added. + """ + # Add new event reference to the person person_handle + db = dbstate.db + person = db.get_person_from_handle(person_handle) + person.add_event_ref(event_ref) + commit_person(person, dbstate, uistate, track, edit_detail, callback) + + +def edit_name(name, dbstate, uistate, track, edit_detail, callback): + if edit_detail: + EditName(dbstate, uistate, track, name, callback) + else: + callback(name) + + +def get_form_person_attr(db, form_event_handle, attr_type): + """ + Find all persons referencing the form_event and which have an attribute of type attr_type. + returns a list of matching (person, attribute) tuples + """ + result = [] + for item in db.find_backlink_handles(form_event_handle, include_classes=['Person']): + handle = item[1] + person = db.get_person_from_handle(handle) + for event_ref in person.get_event_ref_list(): + if event_ref.ref == form_event_handle: + for attr in event_ref.get_attribute_list(): + if (attr.get_type() == attr_type): + result.append((person, attr)) + return result + + +def represents_int(s): + """ + return True iff s is convertable to an int, False otherwise + """ + try: + int(s) + return True + except ValueError: + return False diff --git a/Form/form.py b/Form/form.py index 179d870c7..9469f9df2 100644 --- a/Form/form.py +++ b/Form/form.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2009-2015 Nick Hall +# Copyright (C) 2019 Steve Youngs # # 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 @@ -82,6 +83,11 @@ CONFIG.register('interface.form-horiz-position', -1) CONFIG.register('interface.form-vert-position', -1) +CONFIG.register('interface.form-actions-width', 600) +CONFIG.register('interface.form-actions-height', 400) +CONFIG.register('interface.form-actions-horiz-position', -1) +CONFIG.register('interface.form-actions-vert-position', -1) + CONFIG.init() #------------------------------------------------------------------------ diff --git a/Form/formactions.py b/Form/formactions.py new file mode 100644 index 000000000..3acdd10a3 --- /dev/null +++ b/Form/formactions.py @@ -0,0 +1,345 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2019-2020 Steve Youngs +# +# 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. +# + +""" +Form action chooser +""" +# ------------------------------------------------------------------------- +# +# Standard Python modules +# +# ------------------------------------------------------------------------- +import logging +import importlib.util +import inspect +import os +import sys + +# ------------------------------------------------------------------------ +# +# GTK modules +# +# ------------------------------------------------------------------------ +from gi.repository import Gtk, GObject + +# ------------------------------------------------------------------------ +# +# Gramps modules +# +# ------------------------------------------------------------------------ +from gramps.gen.config import config +from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gen.datehandler import get_date +from gramps.gen.db import DbTxn +from gramps.gui.managedwindow import ManagedWindow +import gramps.gui.dialog +import gramps.gui.display + +# ------------------------------------------------------------------------ +# +# Gramplet modules +# +# ------------------------------------------------------------------------ +import actionutils +from editform import find_form_event +from form import (get_form_id, get_form_type) + +# ------------------------------------------------------------------------ +# +# Logging +# +# ------------------------------------------------------------------------ +LOG = logging.getLogger('.form') + +# ------------------------------------------------------------------------ +# +# Internationalisation +# +# ------------------------------------------------------------------------ +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation +_ = _trans.gettext + +# ------------------------------------------------------------------------ +# +# FormActions class +# +# ------------------------------------------------------------------------ + + +class FormActions(ManagedWindow): + """ + Form Action selector. + """ + RUN_ACTION_COL = 0 + RUN_INCONSISTENT_COL = 1 + ACTION_COL = 2 + DETAIL_COL = 3 + CAN_EDIT_DETAIL_COL = 4 # actionutils.CANNOT_EDIT_DETAIL, actionutils.CAN_EDIT_DETAIL or actionutils.MUST_EDIT_DETAIL + ACTION_COMMAND_COL = 5 + EDIT_DETAIL_COL = 6 + + def __init__(self, dbstate, uistate, track, citation): + self.dbstate = dbstate + self.uistate = uistate + self.track = track + self.db = dbstate.db + self.citation = citation + source_handle = self.citation.get_reference_handle() + self.source = self.db.get_source_from_handle(source_handle) + self.form_id = get_form_id(self.source) + + self.close_after_run = False # if True, close this window after running action command(s) + + ManagedWindow.__init__(self, uistate, track, citation) + + self.actions_module = None + # for security reasons provide the full path to the actions_module .py file + full_path = os.path.join(os.path.dirname( + __file__), '{form_id}.py'.format(form_id=self.form_id)) + if os.path.exists(full_path): + # temporarily modify sys.path so that any import statements in the module get processed correctly + sys.path.insert(0, os.path.dirname(__file__)) + try: + spec = importlib.util.spec_from_file_location( + 'form.actions.{form_id}'.format(form_id=self.form_id), full_path) + self.actions_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.actions_module) + except (ValueError, ImportError, SyntaxError) as err: + self.actions_module = None + LOG.warning(_("Form plugin error (from '{path}'): {error}").format( + path=full_path, error=err)) + finally: + # must make sure we restore sys.path + sys.path.pop(0) + + self.event = find_form_event(self.db, self.citation) + top = self.__create_gui() + self.set_window(top, None, self.get_title()) + self._local_init() + + self._populate_model() + self.tree.expand_all() + + self.show() + + def _local_init(self): + self.setup_configs('interface.form-actions', 750, 550) + + def __create_gui(self): + """ + Create and display the GUI components of the action selector. + """ + root = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) + root.set_transient_for(self.uistate.window) + # Initial position for first run + root.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + + self.model = Gtk.TreeStore( + bool, bool, str, str, int, GObject.TYPE_PYOBJECT, bool) + self.tree = Gtk.TreeView(model=self.model) + renderer_text = Gtk.CellRendererText() + column1 = Gtk.TreeViewColumn(_("Action")) + renderer_action_toggle = Gtk.CellRendererToggle() + renderer_action_toggle.connect('toggled', self.on_action_toggled) + column1.pack_start(renderer_action_toggle, False) + column1.add_attribute(renderer_action_toggle, + 'active', self.RUN_ACTION_COL) + column1.add_attribute(renderer_action_toggle, + 'inconsistent', self.RUN_INCONSISTENT_COL) + column1.pack_start(renderer_text, True) + column1.add_attribute(renderer_text, 'text', self.ACTION_COL) + + render_edit_detail_toggle = Gtk.CellRendererToggle() + render_edit_detail_toggle.connect( + "toggled", self.on_edit_detail_toggled) + column2 = Gtk.TreeViewColumn( + _("Edit"), render_edit_detail_toggle, active=self.EDIT_DETAIL_COL) + column2.set_cell_data_func( + render_edit_detail_toggle, FormActions.detail_data_func) + + column3 = Gtk.TreeViewColumn( + _("Detail"), renderer_text, text=self.DETAIL_COL) + + self.tree.append_column(column1) + self.tree.append_column(column2) + self.tree.append_column(column3) + + self.tree.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + + slist = Gtk.ScrolledWindow() + slist.add(self.tree) + slist.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + button_box = Gtk.ButtonBox() + button_box.set_layout(Gtk.ButtonBoxStyle.END) + + help_btn = Gtk.Button(label=_('_Help'), use_underline=True) + help_btn.connect('clicked', self.display_help) + button_box.add(help_btn) + button_box.set_child_secondary(help_btn, True) + + close_btn = Gtk.Button(label=_('_Close'), use_underline=True) + close_btn.connect('clicked', self.close) + button_box.add(close_btn) + + run_btn = Gtk.Button(label=_('_Run'), use_underline=True) + run_btn.connect('clicked', self.run_actions) + button_box.add(run_btn) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_margin_left(2) + vbox.set_margin_right(2) + vbox.set_margin_top(2) + vbox.set_margin_bottom(2) + + vbox.pack_start(slist, expand=True, fill=True, padding=0) + vbox.pack_end(button_box, expand=False, fill=True, padding=0) + + root.add(vbox) + + return root + + def on_action_toggled(self, widget, path): + row_iter = self.model.get_iter(path) + parent = self.model.iter_parent(row_iter) + if not parent: + # user clicked an action category row. toggle all children + new_state = not self.model[row_iter][self.RUN_ACTION_COL] + child = self.model.iter_children(row_iter) + while child: + self.model[child][self.RUN_ACTION_COL] = new_state + child = self.model.iter_next(child) + # all children are now consistent + self.model[row_iter][self.RUN_INCONSISTENT_COL] = False + # toggle RUN_ACTION_COL for the row that was clicked + self.model[row_iter][self.RUN_ACTION_COL] = not self.model[row_iter][self.RUN_ACTION_COL] + if parent: + # update the status of the parent + (consistent, value) = FormActions.all_children_consistent( + self.model, parent, FormActions.RUN_ACTION_COL) + self.model[parent][self.RUN_INCONSISTENT_COL] = not consistent + self.model[parent][self.RUN_ACTION_COL] = consistent and value + + @staticmethod + def detail_data_func(col, cell, model, iter, user_data): + can_edit_detail = model.get_value( + iter, FormActions.CAN_EDIT_DETAIL_COL) + cell.set_property("visible", can_edit_detail != + actionutils.CANNOT_EDIT_DETAIL) + cell.set_property("activatable", can_edit_detail != actionutils.MUST_EDIT_DETAIL) + + def on_edit_detail_toggled(self, widget, path): + edit_detail = not self.model[path][self.EDIT_DETAIL_COL] + self.model[path][self.EDIT_DETAIL_COL] = edit_detail + if edit_detail: + # as a convenience, if the user turns EDIT_DETAIL_COL on, automatically turn on RUN_ACTION_COL + self.model[path][self.RUN_ACTION_COL] = True + + @staticmethod + def all_children_consistent(model, parent, col): + consistent = True + value = False + child = model.iter_children(parent) + if child: # handle case of no children + # start with value of first child + value = model.get_value(child, col) + # advance to second child (if there is one) + child = model.iter_next(child) + # loop over all remaining children until we find an inconsistent value or reach the end + while consistent and child: + consistent = model.get_value(child, col) == value + child = model.iter_next(child) + return (consistent, value) + + def _populate_model(self): + if self.actions_module: + # get the all actions that the actions module can provide for the form + # because the module is dynamically loaded, use getattr to retrieve the actual function to call + all_actions = getattr(self.actions_module, 'get_actions')( + self.dbstate, self.citation, self.event) + for (title, actions) in all_actions: + if actions: + # add the action category + parent = self.model.append( + None, (False, False, title, None, actionutils.CANNOT_EDIT_DETAIL, None, False)) + for action_detail in actions: + # add available actions within this category + self.model.append( + parent, (False, False) + action_detail + (action_detail[2] == actionutils.MUST_EDIT_DETAIL,)) + + def run_actions(self, widget): + # run the selected actions + self.uistate.progress.show() + self.uistate.pulse_progressbar(0) + # get the list of actions to be run + # this helps give meaningful progress information (because we know how many actions in total will be run) + actions = [] + for action_type_row in self.model: + for action_row in action_type_row.iterchildren(): + if action_row.model.get_value(action_row.iter, self.RUN_ACTION_COL): + actions.append((action_row.model.get_value(action_row.iter, self.ACTION_COMMAND_COL), + action_row.model.get_value(action_row.iter, self.EDIT_DETAIL_COL))) + self.count_actions = len(actions) + # run the actions, sequentially + self.do_next_action(actions, self.dbstate, self.uistate, self.track) + + def do_next_action(self, actions, dbstate, uistate, track): + # update the progressbar based on the number of actions completed so far + actions_completed = self.count_actions - len(actions) + self.uistate.pulse_progressbar(actions_completed / self.count_actions * 100) + if actions: + # actions remaining + # take the top action + (action, edit_detail) = actions[0] + # and run it passing, a callback to ourselves, but with actions=actions[1:] + # effectively indirect recursion via the callback + action(dbstate, uistate, track, edit_detail, + # kwargs is added for convenince of the action command authors. + # it is not used, but should not be removed. + lambda self=self, actions=actions[1:], dbstate=dbstate, uistate=uistate, track=track, **kwargs: + self.do_next_action(actions=actions, dbstate=dbstate, uistate=uistate, track=track)) + else: + # no more actions. Stop showing progress + uistate.progress.hide() + # and, optionally, close our window now that the actions have all run + if self.close_after_run: + self.close() + else: + gramps.gui.dialog.OkDialog(_("All actions run successfully."), parent=self.window) + + def close(self, *obj): + ManagedWindow.close(self) + + def display_help(self, obj): + """ + Display the relevant portion of Gramps manual + """ + gramps.gui.display.display_help(webpage='Form_Addons') + + def get_title(self): + if self.source and self.citation: + title = _('Form: {source_title}: {event_reference}').format( + source_title=self.source.get_title(), event_reference=self.citation.get_page()) + else: + title = None + return title diff --git a/Form/formgramplet.py b/Form/formgramplet.py index 2a2a4fc0e..b415e4b33 100644 --- a/Form/formgramplet.py +++ b/Form/formgramplet.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2009-2015 Nick Hall +# Copyright (C) 2019-2020 Steve Youngs # # 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 @@ -44,6 +45,7 @@ from gramps.gen.errors import WindowActiveError from gramps.gen.lib import Citation from editform import EditForm +from formactions import FormActions from selectform import SelectForm from form import get_form_citation @@ -130,6 +132,10 @@ def __create_gui(self): edit.connect("clicked", self.__edit_form, view.get_selection()) button_box.add(edit) + actions = Gtk.Button(label=_('_Actions'), use_underline=True) + actions.connect("clicked", self.__form_actions, view.get_selection()) + button_box.add(actions) + vbox.pack_start(view, expand=True, fill=True, padding=0) vbox.pack_end(button_box, expand=False, fill=True, padding=4) @@ -170,6 +176,18 @@ def __edit_form(self, widget, selection): except WindowActiveError: pass + def __form_actions(self, widget, selection): + """ + Display actions for the selected form. + """ + model, iter_ = selection.get_selected() + if iter_: + citation = model.get_value(iter_, 0) + try: + FormActions(self.gui.dbstate, self.gui.uistate, [], citation) + except WindowActiveError: + pass + def main(self): """ Called to update the display.