Skip to content

Extending other modules

You rarely build a module in isolation. Often you want to add a field to a model that another module already owns, or slot one of your fields into a form that another module already renders. Victor lets you do both without touching the other module's source — you declare the change from your own module, and the framework merges it in.

There are two kinds of extension:

  • Model extension — add fields to a model another module defines.
  • View extension — insert, replace, or remove nodes in a view another module defines.

Both follow the same rule: you may only extend something that belongs to a module you depend on. That dependency is declared once, in module.yaml.

Prerequisites

This page assumes you already know how to define your own models & fields and views. Extension reuses exactly those building blocks — you are just pointing them at something that already exists.

Declare the dependency

You can only extend a model or view that lives in a module listed in your requires. Add the owning module's technical name there:

name: acme_crm
label: Acme CRM
version: 0.1.0
requires: [contact]
models: [models]
data: [views/lead.xml, views/inherit_contact.xml]

requires does two things: it guarantees the contact module is installed and loaded before yours, and it grants you permission to reach into its models and views. Without it, your extension has nothing valid to target.

models: registers your Python model file(s); data: loads your view XML — the same keys you use for any module. Extensions are just ordinary model and view files that happen to target existing names.

Add fields to another module's model

To extend a model, declare a models.Model whose name equals the target model's technical name, and add your new fields on it. Setting name to a name that already exists tells Victor "extend this model" rather than "create a new one".

from victor import fields, models


class ContactExt(models.Model):
    name = "contact"  # match the existing model's technical name to extend it
    lead_count = fields.Integer(label="Leads")

Here contact is a model owned by the built-in contact app. Because your module.yaml requires contact, this class merges a new lead_count field into it. Records of contact now carry that field everywhere they are used.

The class name doesn't matter

Only the name attribute is significant. ContactExt, MyContactFields — call the Python class whatever reads well. Victor keys the extension off name = "contact", not off the class name.

The distinction is entirely in name:

name value Meaning
A technical name no module has defined yet Defines a new model.
A technical name an existing model already uses Extends that model — your fields are added to it.

You add fields exactly as you would on a model of your own — any fields.* type is fair game. Extension is additive: the fields you declare are merged into the base model, so new names simply appear alongside the ones already there. You cannot remove a base field or change the model's identity (its technical name) from an extension.

A same-name field overrides the base field

Additive is the common case, but there is one nuance. If a field you declare uses the same name as one the base model already defines, your definition replaces that field rather than adding a second copy — the last module to declare the name wins. Reach for this deliberately when you want to retune an existing field (say, relabel it); pick a fresh name whenever you mean to add something new.

Extend an existing view

To change a view another module renders, use the <extend> element at the root of your view file (inside <victor>). Instead of describing a whole new view, <extend> describes a set of edits applied to an existing one.

<victor>
  <extend id="contact_form_lead" view="contact.view_form">
    <after field="email"><field name="lead_count"/></after>
  </extend>
</victor>

This locates the email field in the contact module's view_form and inserts your lead_count field directly after it.

The <extend> element

Attribute Required Description
id yes A name for your extension. Unique within your module.
view yes Qualified reference to the base view you are editing: <module>.<view_id>, e.g. contact.view_form.
priority no Orders this extension relative to other extensions of the same view. Defaults to 16; lower runs earlier. Only relevant when several modules extend the same view.

The view reference is always qualified with the owning module — that is how Victor knows which base view to load, and it must be a view in a module you require.

Directive ops

Each child of <extend> is a directive op: it locates a node in the base view and edits it. The available ops:

Op Effect
<after> Insert the op's children immediately after the target node.
<before> Insert the op's children immediately before the target node.
<inside> Append the op's children into the target node (as its last children).
<replace> Swap the target node for the op's children.
<remove> Delete the target node.
<set> Write the op's other attributes onto the target node.

after, before, inside, and replace carry a fragment (the nodes to insert or substitute) as their children. remove takes no children. set takes no children — it carries attributes that are copied onto the target.

Locating the target node

Every op names exactly one node to act on, using one of three locator attributes:

Locator Targets
field="<name>" The <field name="<name>"> element — the common case.
id="<id>" The element carrying that id.
select="<path>" A path selector: steps like tag[@attr='val'] separated by /, with .. to step up to the parent.

Use field= whenever you are targeting a field (nearly always). Reach for id= when a node carries an explicit id, and select= for structural nodes that a single field/id can't pinpoint.

Putting the ops together

The following shows every op and locator. Only <after> with field= appears in the shipped example above; the rest follow the same shape.

<extend id="my_ext" view="contact.view_form">
  <!-- locate a node by field=, id=, or select= -->
  <after field="email"><field name="lead_count"/></after>
  <before field="stage"><field name="note"/></before>
  <inside id="details"><field name="extra"/></inside>
  <replace field="stage"><field name="stage" widget="..."/></replace>
  <remove field="stage"/>
  <set field="email" widget="email"/>
</extend>

Read each op as "find X, then do Y": <after field="email">…</after> finds the email field and places its children after it; <set field="email" widget="email"> finds the email field and sets widget="email" on it; <remove field="stage"/> deletes the stage field entirely.

The target must exist

An op fails if its locator matches no node in the base view. If the base module renames or removes a field you targeted, your extension breaks — keep locators aligned with the version of the module you require.

A model-only extension

Not every extension touches a view. If all you need is an extra field, a model extension on its own is enough — no view file, no data: entry.

# models.py
from victor import fields, models


class WidgetExt(models.Model):
    name = "widget"  # extends the existing widget model
    note = fields.String(label="Note")
# module.yaml
name: extmod
label: Widget Extension
version: 0.1.0
requires: [widget]
models: [models]

That is a complete, valid module. It requires widget, adds a note field to the existing widget model, and does nothing else. Add a view extension later if and when you want that field to appear on screen.

Checklist

To extend another module, make sure you have:

  1. Listed the owning module in requires: in your module.yaml.
  2. For a model extension — a models.Model whose name equals the target model's technical name, registered under models:.
  3. For a view extension — an <extend view="<module>.<view_id>" id="…"> block with one or more directive ops, in a file registered under data:.

From here, see Models & fields for the field types you can add and Views for the full view DSL your inserted fragments are written in.