Skip to content

1. The extension module

An extension module is not a separate species. It is an ordinary module, with a manifest, models, views and permissions. Two things set it apart: it depends on the one it extends, and it never imports it.

The manifest

modules/pipeline_rivals/module.json:

{
"name": "pipeline_rivals",
"depends": ["core", "pipeline"],
"summary": "Competitor tracking on opportunities: who else was bidding, and who won.",
"version": "0.1.0",
"category": "sales"
}

depends does two things, and the second is the one that matters. It orders the loading, so pipeline_lead already exists when our module speaks. And it declares the relation: uninstalling pipeline while pipeline_rivals is installed is no longer possible by accident.

The name says what it is: <extended module>_<what you add>. A reader seeing pipeline_rivals in a module list knows who it is talking about without opening the folder.

The entry point

"""``pipeline_rivals`` -- competitor tracking, added to ``pipeline``.
An extension module. It adds two fields to ``pipeline_lead``, ships its own
model, layers one field onto an existing screen, and listens to the kernel's
events rather than calling into the module it extends.
Nothing here imports ``modules.pipeline``, and uninstalling this module leaves
``pipeline`` exactly as it was.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from kewos.orm import apply_module_acls
from kewos.views import register_views_from_dir
from . import listen, models
if TYPE_CHECKING:
from kewos.orm.registry import Registry
_MODULE_DIR = Path(__file__).parent
def register(registry: Registry) -> None:
"""Declare the extension: our model, the added fields, rights, screens."""
models.register_models(registry)
apply_module_acls(registry, _MODULE_DIR / "acl.json")
# Reads our own screens AND the `*.patch.json` files that layer two fields
# onto `pipeline`'s screens without replacing them.
register_views_from_dir(_MODULE_DIR / "views")
listen.register_listeners()

Nothing unusual. The interesting line is the one that is not there: no import modules.pipeline, and there never will be.

Our own model

Extending a module does not mean writing into its tables. The competitor is our notion, so it lives in our model:

TIERS = [
("direct", "Direct"),
("adjacent", "Adjacent"),
("inhouse", "In-house build"),
("nothing", "Doing nothing"),
]
def register_competitor(registry: Registry) -> None:
"""Declare ``rivals_competitor``."""
registry.register_model(
"rivals_competitor",
module="pipeline_rivals",
fields=[
CharField(name="name", nullable=False, max_length=120, unique=True),
# "Doing nothing" is a competitor. Leaving it out of the list is how
# a pipeline review ends up explaining every loss by price.
SelectionField(name="tier", choices=TIERS, default="direct"),
CharField(name="notes", nullable=True),
],
)

The table prefix follows the module, not the extended one: rivals_competitor, not pipeline_competitor. What is ours carries our name, which is what makes an uninstall readable.

The competitor list: name, kind

Next

The module exists and has its model. What remains is touching somebody else’s: Adding fields.