Skip to content

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. The
target does not need to exist yet: the check is deferred to freeze time, so
module load order stays free.
What this is NOT: an import of ``modules.pipeline``. This module names the
model, never the code behind it. `pipeline` can change its internals, and this
keeps 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 failed

Two 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.