1. Le module d'extension
Un module d’extension n’est pas un genre à part. C’est un module normal, avec un manifeste, des modèles, des vues et des droits. Deux choses seulement le distinguent : il dépend de celui qu’il étend, et il ne l’importe jamais.
Le manifeste
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 fait deux choses, et la seconde est celle qui compte. Elle ordonne le
chargement, donc pipeline_lead existe déjà quand notre module parle. Et elle déclare la
relation : désinstaller pipeline sans désinstaller pipeline_rivals n’est plus possible
par accident.
Le nom, lui, dit ce qu’il est : <module étendu>_<ce qu'on ajoute>. Un lecteur qui voit
pipeline_rivals dans une liste de modules sait de qui il parle sans ouvrir le dossier.
Le point d’entrée
"""``pipeline_rivals`` -- competitor tracking, added to ``pipeline``.
An extension module. It adds two fields to ``pipeline_lead``, ships its ownmodel, layers one field onto an existing screen, and listens to the kernel'sevents 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 Pathfrom typing import TYPE_CHECKING
from kewos.orm import apply_module_aclsfrom 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()Rien d’inhabituel. La ligne intéressante est celle qui n’y est pas : il n’y a pas
d’import modules.pipeline, et il n’y en aura pas.
Notre propre modèle
Étendre un module ne veut pas dire écrire dans ses tables. Le concurrent est notre notion, il vit dans notre modèle :
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), ], )Le préfixe des tables suit le module, pas le module étendu : rivals_competitor, pas
pipeline_competitor. Ce qui est à nous porte notre nom, ce qui rend une désinstallation
lisible.

Et ensuite
Le module existe et il a son modèle. Reste à toucher celui de quelqu’un d’autre : Ajouter des champs.