Aller au contenu

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 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()

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.

La liste des concurrents : nom, catégorie

Et ensuite

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