Navigating the app¶
Every screen in a Victor app is described by metadata, not hand-built pages. Three pieces work together:
- Menus form the navigation — the entries a user clicks to move around the app.
- Actions tie a model to the set of view kinds a user can open for it.
- Views (
list,form,kanban,card,search) declare exactly what the user sees on screen.
This page walks through the app the way a user experiences it — from the top-level navigation down to a single record — and shows the view XML that produces each screen. If you are coming from Odoo, this will feel familiar: you define a model and a few views, and the framework renders the tables, forms, and boards for you. You never write bespoke screens.
The running example is the built-in contact app. Its complete view file declares a list, a form, a kanban board, a search bar, an action, and a menu — everything below is drawn from it.
Where views come from
You declare views in your module's XML. See Views for the full view DSL and Models & fields for the models the views render.
Menus: the app's navigation¶
Menus are the entry points. Each <menu> is one navigation entry, ordered by sequence.
- A top-level menu (one with an
icon) is an app entry point — the things a user picks from the app navigation. - A menu that has child menus renders as a tabbed page: each child becomes a tab, and the active tab embeds that child's record collection.
- A leaf menu (no children) opens its action's record collection directly.
<action id="action_contacts" name="Contacts" model="contact" views="list,kanban,form" path="contacts"/>
<menu id="menu" parent="base.menu_root" label="Contacts" action="contact.action_contacts" icon="Users" sequence="10"/>
The action defines the screen; the menu makes it reachable. Here the Contacts menu is a top-level app (it has an icon and sits under the root menu), and clicking it opens the contact collection.
<menu> attributes¶
| Attribute | Purpose |
|---|---|
id |
Identifier for this menu within the module. |
parent |
Dotted reference of the parent menu, e.g. base.menu_root. |
label |
Text shown in the navigation. |
action |
Dotted reference of the action to open (leaf menus). |
icon |
Icon name, e.g. Users. A menu with an icon reads as a top-level app entry. |
sequence |
Sort order (integer); lower numbers appear first. |
Grouped tabs
To group several screens under one tabbed page (for example a Settings area), give a parent menu no action and hang child menus off it. Each child renders as a tab. See Adding a Settings tab.
Actions: binding a model to its views¶
An <action> connects a model to the view kinds a user can open for it, and gives the collection a URL slug. The views attribute is a comma-separated list — it declares which kinds the user can switch between, and, through the position of its first non-form kind, which view the collection opens on.
- The toolbar switcher offers exactly the kinds you list. Its buttons appear in a fixed built-in order, not the order you write them in — e.g.
views="kanban,list,form"still shows the List button before the Kanban button. - The first non-form kind is the default landing view. In
views="list,kanban,form"the user lands on the list; the form opens when they click into a record.
<action> attributes¶
| Attribute | Purpose |
|---|---|
id |
Identifier for this action within the module. |
name |
Title shown in the collection header. |
model |
The model whose records this action shows. |
views |
Comma-separated view kinds the user can switch between; the first non-form kind is the landing view. E.g. list,kanban,form. |
path |
URL slug for the collection, e.g. contacts. |
The list view¶
The default landing for most collections is the list view — a paginated table. Each <field> child is one column, in order.
<view id="view_list" model="contact" type="list">
<field name="display_name"/>
<field name="email"/>
<field name="is_company"/>
<field name="stage"/>
<field name="tag_ids" widget="tags"/>
</view>
The list toolbar gives the user everything they need to work through the table:
- Sorting by the table's columns.
- A pager to page through large collections.
- A search box and the filter bar declared by the model's search view (below).
- Multi-select with delete — tick rows (or select all) and remove them in one action.
You can also add action buttons as columns — see Record action buttons.
Searching, filtering, and grouping¶
The filter chips and group-by options that appear above a collection come from a search view. It is attached to the model by kind and is not listed in the action's views attribute — if a model has no search view, the collection simply shows no filter bar.
<view id="view_search" model="contact" type="search">
<search>
<filter name="companies" string="Companies" domain='[["is_company", "=", true]]' group="kind"/>
<filter name="people" string="People" domain='[["is_company", "=", false]]' group="kind"/>
<filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>
</search>
</view>
Each <filter> is a toggle chip:
- A filter with a
domainnarrows the list when the chip is active. Separate (ungrouped) domain filters are AND-ed together — the list must match all of them. - A filter with
context="{'group_by': 'field'}"groups the list by that field. group="…"tags chips as part of one facet. When two or more active chips in a group each test the same field with=— asCompaniesandPeopleboth testis_company— they collapse into a single "is one of" (OR) match, so activating both shows companies and people together instead of matching nothing. Chips that test different fields, or use an operator other than=, still combine with AND, just like ungrouped filters.
<filter> attributes¶
| Attribute | Purpose |
|---|---|
name |
Stable key for the chip. |
string |
The chip's label. |
domain |
JSON array of [field, operator, value] triples, applied when the chip is active. |
group |
Optional; tags chips as one facet. Two or more active same-field = chips collapse into a single "is one of" (OR) match; otherwise grouped chips combine with AND, like ungrouped ones. |
context |
Optional; {'group_by': 'field'} to group records by a field. |
Domain operators¶
A domain is a list of [field, operator, value] triples. The available operators:
| Operator | Meaning |
|---|---|
= |
Equal to. |
!= |
Not equal to. |
< |
Less than. |
<= |
Less than or equal to. |
> |
Greater than. |
>= |
Greater than or equal to. |
ilike |
Case-insensitive substring match. |
in |
Value is one of an array of values. |
Opening a record: the form view¶
Clicking a row in a list, or a card in a kanban or card view, opens that record in its form view — the single-record screen for viewing and editing.
Fields are laid out inside titled <group> blocks (a two-column label + value layout). An optional <notebook> adds tabbed pages below or alongside the groups; the first page is active by default.
<view id="view_form" model="contact" type="form">
<group string="Details">
<field name="display_name"/>
<field name="email" widget="email"/>
<field name="is_company"/>
<field name="stage"/>
<field name="parent_id"/>
<field name="tag_ids" widget="tags"/>
</group>
<notebook>
<page string="Notes">
<field name="email"/>
</page>
</notebook>
</view>
The form toolbar lets the user save, discard, and delete the record, and hosts any record action buttons for the model.
<group> and <notebook> / <page>¶
| Tag | Purpose |
|---|---|
<group string="…"> |
A titled two-column label + value block wrapping related fields. |
<notebook> |
Tab container placed on the form. |
<page string="…"> |
One tab inside the notebook; the first page is active by default. |
The kanban board¶
A kanban view shows records as cards arranged into columns. The <kanban> element's attributes drive the board, and its <field> children are the fields shown on each card.
<view id="view_kanban" model="contact" type="kanban">
<kanban default_group_by="is_company" records_draggable="1">
<field name="display_name"/>
<field name="email"/>
</kanban>
</view>
default_group_by— the field used to form the initial columns.records_draggable="1"— lets the user drag a card between columns to move it.
Clicking a card opens the record's form, just like a list row.
The card view¶
A card view renders records as a responsive grid of cards. The first <field> is the card heading; the remaining fields become label: value rows. Any <button> becomes a card action.
<view id="view_card" model="contact" type="card">
<field name="display_name"/>
<field name="email"/>
<button action="archive" string="Archive" confirm="Archive?" variant="ghost"/>
</view>
Card, list, and kanban are switchable from the toolbar when the action offers more than one — for example views="card,list" lets the user flip between the card grid and the table.
Creating, editing, and deleting records¶
The framework wires up the full record lifecycle from your views — you do not build any of it:
- Create — the New button opens a blank form for the model.
- Edit — open a record, change its fields, and Save from the form toolbar. Discard abandons unsaved changes.
- Delete — remove a single record from its form toolbar, or select multiple rows in the list (including select all) and delete them together.
Record action buttons¶
Beyond create/edit/delete, a model can expose custom actions as buttons. A <button> runs a named server action on the record and can appear in a list row, the form toolbar, or on a card.
A destructive list-row button with a confirmation prompt:
<button action="install" string="Install" confirm="Uninstall? This drops data." variant="destructive"/>
Form-toolbar buttons that appear only in the right record state:
<button action="confirm" string="Confirm" variant="default" invisible="state == 'done'"/>
<button action="archive" string="Archive" confirm="Archive this record?" variant="ghost"/>
On a form, invisible is evaluated against the saved record, so a button can show only when the record is in a matching state (for example, hide Confirm once state == 'done').
<button> attributes¶
| Attribute | Purpose |
|---|---|
action |
The registered server action to run on the record. |
string |
The button label. |
confirm |
Optional confirmation text shown before the action fires. |
preview |
Optional read-only action that describes the consequences before the real action runs. |
variant |
Button style: default, soft, ghost, or destructive. |
invisible |
Optional expression (e.g. state == 'done'); hides the button when true. Evaluated on the saved record. |
show_on_new |
1 to also render on an unsaved record — clicking it creates the record, then runs the action. |
run_on_save |
1 to run the action automatically after every save, with no click. |
Two automatic modes
Use show_on_new="1" when an action should be available on a brand-new record (it creates, then acts). Use run_on_save="1" for a check that must always run when the record is saved — it fires on both create and update, and the result surfaces as a toast. See View actions & buttons for how to register the server action a button calls.
Field widgets¶
A <field> renders as one column (in list, kanban, and card views) or one input/value (in a form). Its display picks a widget automatically: an explicit widget= wins, otherwise the field's type default is used, falling back to a plain string.
<field> attributes¶
| Attribute | Purpose |
|---|---|
name |
The field to show. |
widget |
Optional named widget, e.g. email or tags. |
readonly |
1 to show the read-only widget even in edit mode. |
invisible |
1 to not render the field. |
required |
required="0" suppresses the required asterisk on a field that is required in the model. The asterisk itself comes from the model's required=True, not from this attribute. |
Field types and their default widgets¶
The field type chosen on the model determines the default display and edit widget when you don't set widget=:
| Type | Notes |
|---|---|
String |
Single-line text. |
Text |
Multi-line text. |
Integer |
Whole number. |
Float |
Decimal number. |
Boolean |
Checkbox / toggle. |
Date |
Date picker. |
Datetime |
Date and time picker. |
Selection(options=[(value, label), …]) |
Choice from a fixed set of options. |
Reference(target=…) |
Link to a single record of another model. |
Many2many(target=…) |
Set of related records; pairs well with widget="tags". |
The fields the views above reference come from the model itself:
from victor import fields, models
class Contact(models.Model):
name = "contact"
label = "Contact"
display_name = fields.String(label="Name", required=True)
email = fields.String(label="Email")
is_company = fields.Boolean(label="Is company", default=False)
stage = fields.Selection(
options=[("lead", "Lead"), ("customer", "Customer"), ("vendor", "Vendor")],
label="Stage", default="lead",
)
parent_id = fields.Reference(target="contact", label="Parent company")
tag_ids = fields.Many2many(target="tag", label="Tags")
Putting it together¶
Reading a screen back to its definition is straightforward once you know the pieces:
- A menu put the app in the navigation.
- Its action named the model and the view kinds — the first non-form kind is where the user landed.
- A view (
list,kanban, orcard) laid out the collection; the search view supplied its filter bar. - Clicking a record opened the form view, with its groups, notebook tabs, and toolbar buttons.
Every one of those is metadata you declare. To go deeper, see Models & fields, Views, and View actions & buttons.