4. Reacting, and leaving
Our extension needs to know that a deal has just been lost, so it can check that somebody
named the winner. It is not going to ask pipeline. It listens.
The listener
"""Reacting to ``pipeline`` without importing it.
The extension needs to know when an opportunity is lost. It does NOT call into``pipeline`` to find out: it listens.
That is the whole difference between extending a module and forking it. Theevent is a public fact; the code behind it is not."""
from kewos.events import subscribe
#: Emitted by the kernel on every write. We filter on the model rather than ask#: `pipeline` for a dedicated event: a module we extend owes us nothing._WRITE_EVENT = "record.updated"
def note_the_winner(event: Event) -> None: """When an opportunity is marked lost, check that a winner was named.""" payload = event.payload or {} if payload.get("model") != "pipeline_lead": return record = payload.get("record") if record is None or getattr(record, "state", None) != "lost": return if getattr(record, "rivals_won_by_id", None) is None: _log.info("opportunity %s was lost with no competitor named", record.id)
def register_listeners() -> None: """Subscribe on module load.""" subscribe(_WRITE_EVENT, note_the_winner)That is the whole contract. The kernel emits record.updated on every write; we filter on
the model. We do not ask pipeline for a dedicated event, because a module you extend owes
you nothing: what it publishes, it publishes for everyone.
It fixes nothing, it says so
The handler does not write. It logs the omission so a review can see it. Filling the field in place of the human would be guessing, and it would poison exactly the report this module exists to produce: an invented winner counts as a winner.
The same need, with no code
What this file does can also be placed in the visual engine and shipped with the module, the way the first walkthrough does in its “Automating” step. The question to settle is always the same: is this rule yours, or the customer’s? What follows from your design judgement goes in code; what the customer will want to change without you goes in a flow.
Uninstalling without a trace
This is the test that tells you whether you extended or forked. Uninstall pipeline_rivals:
rivals_competitorgoes: it was our table;pipeline_leadloses two contributed columns, and nothing else;- the view patch goes, and
pipeline’s view is what it was, because it was never replaced; - the subscription goes with the process.
None of this needed uninstall code. It follows directly from the three choices in the previous steps: our model carries our prefix, so do our fields, and our screen is a patch.
Now run the opposite in your head. If you had copied pipeline_lead.form.json to slip two
fields in, uninstalling would leave you the copy, frozen at the version you took it from.
The finished extension, to download
- pipeline_rivals-0.1.0.kws (7 KB) — this walkthrough’s module, with its two view patches, its listener and its demo data.
74af7d7e9382e16b26815412a4f29d6e608a961180f416feabf0882c14f2619f pipeline_rivals-0.1.0.kwssha256sum pipeline_rivals-0.1.0.kws
kewos module install pipeline_rivals-0.1.0.kws \ --database-url "$KEWOS_DATABASE_URL" \ --modules-root "$KEWOS_MODULES_ROOT" \ --allow-unsignedkewos reconcile --confirmpipeline has to be installed and activated first, or the registry freeze stops on the
missing dependency, naming it. The full procedure, and why the package is unsigned, are in the
last step of the first walkthrough, Shipping.
The checklist
Before shipping an extension, these five answers must be “yes”:
grep -r "import modules\." my_module/returns nothing.- Everything I created carries my module’s prefix.
- My added fields are
nullable=True. - My screens on other people’s models are
*.patch.json, not full views. - My manifest declares
dependson what I extend.
Next
You can build a module and extend one. What remains is the same work as anywhere else: packaging and installing it, described in the last step of the first walkthrough, Shipping.