diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index fc600d0..5d42bbd 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -251,6 +251,14 @@ class BalPlugin(BasePlugin): # (handled by BalWindow.get_wallet_password). Default ON. self.AUTO_SIGN = BalConfig(config, "bal_auto_sign", True) + # REBUILD_ON_CLOSE: when enabled (default), closing the wallet or + # quitting Electrum runs the "Build your will" wizard + # (BalBuildWillDialog) to rebuild and re-validate the will. When + # disabled, on_close() only persists the current in-memory willitems to + # the wallet DB: no rebuild dialog, no auto-sign/broadcast, no + # invalidation prompts at close. Default ON. + self.REBUILD_ON_CLOSE = BalConfig(config, "bal_rebuild_on_close", True) + # EDITABLE_DATES (Group C / C2): when enabled, the delivery-time and # check-alive date fields are editable everywhere (toolbar / Heirs tab), # not only inside the "Build your will" wizard. Default OFF, so the dates diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index 1b233e8..3d8d57b 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -456,6 +456,14 @@ class Plugin(BalPlugin): # be saved on a USB stick and a copy given to the heirs). heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR) + # "Rebuild will on wallet close" checkbox. Bound to the persisted + # REBUILD_ON_CLOSE config (default ON). When ticked, closing the wallet + # / quitting Electrum runs the "Build your will" wizard to rebuild and + # re-validate the will. When unticked, the will is only rebuilt when + # the user presses Check/Prepare. Visible to all users (BASIC and + # ADVANCED). + heir_rebuild_on_close = BalCheckBox(self.REBUILD_ON_CLOSE) + # USER TYPE selector (SIMPLE / ADVANCED, global). A two-choice combo # (not a free-text field) bound to the USER_TYPE config: # index 0 -> "BASIC" -> stored value "basic" (DEFAULT) @@ -810,6 +818,24 @@ class Plugin(BalPlugin): 2, ) + # "Rebuild will on wallet close" row (always visible, BASIC + ADVANCED). + # Placed below the rebroadcast button so the existing rows keep their + # numbers. + lbl_rebuild_on_close = QLabel(_("Rebuild will on wallet close")) + help_rebuild_on_close = HelpButton( + "Run the 'Build your will' wizard every time the wallet is closed " + "or Electrum is quit, so the will is rebuilt and re-validated.\n" + "When disabled, the will is only rebuilt when you press Check or " + "Prepare. The last built state is still saved to the wallet." + ) + grid.addWidget(lbl_rebuild_on_close, 15, 0) + grid.addWidget(heir_rebuild_on_close, 15, 1) + grid.addWidget(help_rebuild_on_close, 15, 2) + reset_btn_rebuild_on_close = _make_reset_btn( + self.REBUILD_ON_CLOSE, heir_rebuild_on_close, "check" + ) + grid.addWidget(reset_btn_rebuild_on_close, 15, 3) + # ----------------------------------------------------------------- # # Group C / C4b: "Reset" button that restores the dialog settings to # # their factory defaults. It only resets the settings exposed by THIS # @@ -842,6 +868,7 @@ class Plugin(BalPlugin): (self.CALENDAR_APP, edit_calendar_app, "line"), (self.SAVE_HISTORY, heir_save_history, "check"), (self.HISTORY_LABEL, edit_history_label, "line"), + (self.REBUILD_ON_CLOSE, heir_rebuild_on_close, "check"), ] for cfg, widget, kind in resets: # Persist the default value back into the Electrum config. diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py index fc26b65..34587d6 100644 --- a/bal/gui/qt/window.py +++ b/bal/gui/qt/window.py @@ -1044,9 +1044,12 @@ class BalWindow: return # 1) Business logic: build/save the will on close (unchanged behaviour). + # REBUILD_ON_CLOSE gates the "Build your will" wizard only: the will is + # still persisted so a manual Build/Check from the session is not lost. try: - close_window = BalBuildWillDialog(self) - close_window.build_will_task() + if self.bal_plugin.REBUILD_ON_CLOSE.get(): + close_window = BalBuildWillDialog(self) + close_window.build_will_task() self.save_willitems() except Exception as e: _logger.error(f"on_close: build/save will failed: {e}") diff --git a/tests/test_rebuild_on_close_setting.py b/tests/test_rebuild_on_close_setting.py new file mode 100644 index 0000000..6a7f581 --- /dev/null +++ b/tests/test_rebuild_on_close_setting.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Tests for the "Rebuild will on wallet close" (REBUILD_ON_CLOSE) setting. + +Covers: + + * the persisted ``bal_rebuild_on_close`` configuration key exists and + defaults to ON (True), and can be turned off and read back; + * ``BalWindow.on_close()`` runs the "Build your will" wizard + (``BalBuildWillDialog.build_will_task()``) when the setting is ON; + * ``BalWindow.on_close()`` SKIPS the wizard when the setting is OFF, but + still calls ``save_willitems()`` so the last built state is persisted. + +The on_close tests drive the real ``BalWindow.on_close`` method with a +light-weight fake controller and a recording stub for ``BalBuildWillDialog``, +so no full wallet/GUI machinery is needed. + +Run: + source /home/steal/devel/bal/electrum/env/bin/activate + QT_QPA_PLATFORM=offscreen python3 tests/test_rebuild_on_close_setting.py +""" + +import os +import sys +import types + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import bal.gui.qt.window as window_mod # noqa: E402 +from bal.core.plugin_base import BalConfig # noqa: E402 + +CONFIG_KEY = "bal_rebuild_on_close" + + +# --------------------------------------------------------------------------- # +# Mocks +# --------------------------------------------------------------------------- # + +class FakeConfig: + """Minimal mock for Electrum's config object (key/value store).""" + + def __init__(self): + self._store = {} + + def get(self, key, default=None): + return self._store.get(key, default) + + def set_key(self, key, value, save=True): + self._store[key] = value + + +class FakeBuildWillDialog: + """Recording stub for BalBuildWillDialog, patched into window.py.""" + + instances = [] + + def __init__(self, bal_window): + self.bal_window = bal_window + FakeBuildWillDialog.instances.append(self) + + def build_will_task(self): + self.bal_window._wizard_ran = True + + +class _Tabs: + def update(self): + pass + + +class _NoOp: + willexecutors_action = None + tabs = _Tabs() + + def close(self): + pass + + def toggle_tab(self, tab): + pass + + def update(self): + pass + + def removeAction(self, action): + pass + + +def _make_fake_window(rebuild_on_close): + """Return a fake controller with the attributes on_close() touches.""" + fake = types.SimpleNamespace() + fake.disable_plugin = False + fake.bal_plugin = types.SimpleNamespace( + REBUILD_ON_CLOSE=BalConfig(FakeConfig(), CONFIG_KEY, rebuild_on_close) + ) + fake.willitems = {} + fake.will = {} + fake.saved = [] + fake.save_willitems = lambda: fake.saved.append("save") + fake.heirs_tab = _NoOp() + fake.will_tab = _NoOp() + fake.tools_menu = _NoOp() + fake.window = _NoOp() + fake._menubar_initialized = True + return fake + + +def _call_on_close(fake): + original = window_mod.BalBuildWillDialog + FakeBuildWillDialog.instances = [] + try: + window_mod.BalBuildWillDialog = FakeBuildWillDialog + window_mod.BalWindow.on_close(fake) + finally: + window_mod.BalBuildWillDialog = original + + +# --------------------------------------------------------------------------- # +# Config key +# --------------------------------------------------------------------------- # + +def test_rebuild_on_close_config_defaults_on(): + """bal_rebuild_on_close must default to ON (True) when not yet stored.""" + cfg = FakeConfig() + rebuild = BalConfig(cfg, CONFIG_KEY, True) + assert rebuild.get() is True + + +def test_rebuild_on_close_config_can_be_disabled(): + """Once turned off and persisted, bal_rebuild_on_close reads back False.""" + cfg = FakeConfig() + rebuild = BalConfig(cfg, CONFIG_KEY, True) + rebuild.set(False) + assert rebuild.get() is False + # A fresh wrapper over the same config still sees the stored value. + assert BalConfig(cfg, CONFIG_KEY, True).get() is False + + +# --------------------------------------------------------------------------- # +# on_close() behaviour +# --------------------------------------------------------------------------- # + +def test_on_close_runs_wizard_when_enabled(): + """With REBUILD_ON_CLOSE ON, on_close() builds the will and saves it.""" + fake = _make_fake_window(True) + _call_on_close(fake) + assert len(FakeBuildWillDialog.instances) == 1, "wizard must be opened" + assert fake._wizard_ran is True, "wizard build_will_task must run" + assert fake.saved == ["save"], "save_willitems must run" + + +def test_on_close_skips_wizard_when_disabled(): + """With REBUILD_ON_CLOSE OFF, on_close() skips the wizard but saves.""" + fake = _make_fake_window(False) + _call_on_close(fake) + assert len(FakeBuildWillDialog.instances) == 0, "wizard must NOT be opened" + assert not hasattr(fake, "_wizard_ran"), "wizard must not run" + assert fake.saved == ["save"], "save_willitems must still run" + + +if __name__ == "__main__": + test_rebuild_on_close_config_defaults_on() + test_rebuild_on_close_config_can_be_disabled() + test_on_close_runs_wizard_when_enabled() + test_on_close_skips_wizard_when_disabled() + print("OK: all tests passed")