2. Adding fields
pipeline_lead belongs to pipeline. We are going to add two fields to it without opening a
single file of that module.
The gesture
"""Adding two fields to ``pipeline_lead``, without forking it.
``extend_model`` contributes fields to a model another module declared. Thetarget does not need to exist yet: the check is deferred to freeze time, somodule load order stays free.
What this is NOT: an import of ``modules.pipeline``. This module names themodel, never the code behind it. `pipeline` can change its internals, and thiskeeps working."""
def extend_lead(registry: Registry) -> None: """Put "who else is bidding" and "who won" on the opportunity.""" registry.extend_model( "pipeline_lead", module="pipeline_rivals", fields=[ # Who we are up against while the deal is open. Link( name="rivals_competitor_id", target_model="rivals_competitor", nullable=True, on_delete="set_null", ), # Who actually won it. Separate from the field above on purpose: the # competitor we feared is often not the one that took the deal, and # collapsing the two loses exactly the fact worth reviewing. Link( name="rivals_won_by_id", target_model="rivals_competitor", nullable=True, on_delete="set_null", ), ], )Four decisions, not one accidental line
The prefix. The fields are called rivals_*, not competitor_id. They live in a table
that is not ours, beside fields other extensions may add tomorrow. A generic name is a
collision waiting to happen, and it will happen at a customer’s, not at yours.
nullable=True. Not negotiable on an existing model. A required column added to a table
that already has rows cannot be created: the migration fails, or worse, succeeds after
inventing a default value for deals closed two years ago.
on_delete="set_null". Deleting a competitor must not delete a deal. The link goes, the
deal stays. It is the only defensible answer: the valuable data here is the history of deals,
not the competitor record.
Two fields, not one. “Who we were up against” and “who won” are not the same information, and this is not modelling fussiness: in a review, the gap between the two is exactly what you are looking for. Merging them erases the question.
What lands in the database
Nothing to write. kewos reconcile sees two more columns on an already-provisioned model and
adds them:
main [active]: reconciled (0 migration(s), 2 column(s))done: 1 tenant(s), 0 failedTwo columns added, none touched. That is what schema reconciliation can do safely; anything destructive goes through a hand-written, confirmed migration.
Load order is not your problem
extend_model accepts a model that does not exist yet: the check is deferred to the registry
freeze. So you do not need to know whether pipeline was read before you. If the model still
does not exist at freeze time, the error is blunt and names the missing model, which is the
symptom of a dependency forgotten in the manifest.
Permissions follow
Our model has its own acl.json, and it reuses pipeline’s groups:
{ "rivals_competitor": { "read": ["pipeline_user", "pipeline_manager", "kws_admin"], "create": ["pipeline_manager", "kws_admin"], "write": ["pipeline_manager", "kws_admin"], "unlink": ["kws_admin"] }}Naming pipeline_manager is not an import: it is a code, a stable string, exactly like a
model name. Nobody has to be granted a new group to see competitors; whoever runs the pipeline
runs them.
Next
The fields exist and the API already returns them. They are on no screen: Completing a screen.