Worked example: build a Helpdesk¶
This tutorial builds a complete Helpdesk app from an empty folder. When you finish you'll have a
ticket model, a list / form / kanban / search interface, a menu, and some demo data — all without
writing any frontend code.
It assumes you've skimmed How modules work. Every construct used here is explained in the Developer Guide; this page ties them together.
What you'll build¶
A Helpdesk app with a Ticket model: a subject, a description, a priority, a stage, and a link
to the customer (a contact). Users get a table, a card board grouped by stage, a form, and filters.
Final layout:
helpdesk/
├── module.yaml
├── __init__.py
├── models/
│ ├── __init__.py
│ └── ticket.py
├── views/
│ └── helpdesk.xml
└── data/
└── demo.xml
Step 1 — the manifest¶
Create helpdesk/module.yaml. It declares the module and lists which files to load. The Helpdesk
depends on base (core menus) and contact (the contact model a ticket links to).
name: helpdesk
label: Helpdesk
version: 1.0.0
author: Your Company
requires: [base, contact]
models: [models]
data:
- views/helpdesk.xml
- data/demo.xml
Add an empty helpdesk/__init__.py so the folder is a Python package.
data order matters within a file's references
Files load top to bottom. Load your views before demo data that relies on those models.
Step 2 — the model¶
A model is a Python class that subclasses models.Model. Create helpdesk/models/ticket.py:
from victor import fields, models
class Ticket(models.Model):
name = "ticket" # technical name (snake_case, singular)
label = "Ticket" # human-readable name
_description = "A customer support request." # required on every model
display_name = fields.String(label="Subject", required=True)
description = fields.Text(label="Description")
priority = fields.Selection(
options=[("low", "Low"), ("normal", "Normal"), ("high", "High"), ("urgent", "Urgent")],
label="Priority", default="normal",
)
stage = fields.Selection(
options=[("new", "New"), ("in_progress", "In progress"), ("done", "Done")],
label="Stage", default="new",
)
customer_id = fields.Reference(target="contact", label="Customer")
resolved = fields.Boolean(label="Resolved", default=False)
Then register it from helpdesk/models/__init__.py:
Why display_name?
The framework uses a record's display_name as its title — it's what a Reference picker shows
when someone links a ticket elsewhere. Don't call a field name; that attribute is the model's
technical name. See Models & fields.
Step 3 — the views¶
Create helpdesk/views/helpdesk.xml. One file declares every screen, plus the action and menu that
make the app reachable.
<victor>
<!-- Table -->
<view id="view_list" model="ticket" type="list">
<field name="display_name"/>
<field name="priority"/>
<field name="stage"/>
<field name="customer_id"/>
<field name="resolved"/>
</view>
<!-- Form -->
<view id="view_form" model="ticket" type="form">
<group string="Ticket">
<field name="display_name"/>
<field name="priority"/>
<field name="stage"/>
<field name="customer_id"/>
<field name="resolved"/>
</group>
<notebook>
<page string="Description">
<field name="description" widget="textarea"/>
</page>
</notebook>
</view>
<!-- Card board grouped by stage -->
<view id="view_kanban" model="ticket" type="kanban">
<kanban default_group_by="stage" records_draggable="1">
<field name="display_name"/>
<field name="priority"/>
<field name="customer_id"/>
</kanban>
</view>
<!-- Filter bar -->
<view id="view_search" model="ticket" type="search">
<search>
<filter name="open" string="Open" domain='[["resolved", "=", false]]'/>
<filter name="high" string="High priority" domain='[["priority", "in", ["high", "urgent"]]]'/>
<filter name="by_stage" string="Stage" context="{'group_by': 'stage'}"/>
</search>
</view>
<!-- Bind the model to its screens, and add a menu -->
<action id="action_tickets" name="Tickets" model="ticket"
views="list,kanban,form" path="tickets"/>
<menu id="menu_helpdesk" parent="base.menu_root" label="Helpdesk"
action="helpdesk.action_tickets" icon="LifeBuoy" sequence="20"/>
</victor>
What this gives you:
views="list,kanban,form"makes list the landing screen, with a switcher to the kanban board; the form opens when a user clicks a ticket.- Dragging a card between kanban columns moves the ticket to that stage.
- The search view adds Open and High priority chips and a group by stage option.
See Views and Actions & menus for the full DSL.
Step 4 — demo data¶
Create helpdesk/data/demo.xml. Seeds are created when the module installs. Here we create a customer
and two tickets that link to it.
<victor>
<seed model="contact" id="acme">
<display_name>Acme Corp</display_name>
<is_company>true</is_company>
</seed>
<seed model="ticket" id="t_login">
<display_name>Cannot log in</display_name>
<priority>high</priority>
<stage>new</stage>
<customer_id>acme</customer_id>
</seed>
<seed model="ticket" id="t_invoice">
<display_name>Wrong amount on invoice</display_name>
<priority>normal</priority>
<stage>in_progress</stage>
<customer_id>acme</customer_id>
</seed>
</victor>
References resolve within the same file
<customer_id>acme</customer_id> works because the acme contact is a seed declared above in
this same file. A reference to a record in another file or module is not resolved. See
Seed & demo data.
Step 5 — install it¶
- Put the
helpdesk/folder in your project's git repository. - Push. Victor Cloud deploys your module to the instance.
- In the app, open the top-level Modules section, find Helpdesk, and click Install.
Open the app menu — Helpdesk now appears. You'll land on the ticket list, with the two demo tickets and the kanban board grouped by stage. Full details in Publishing & installing.
Iterating
After changing your module, push again and click Upgrade on Helpdesk in the Modules section to apply the new version.
Step 6 — extend a neighbouring app (optional)¶
Modules compose. Suppose you want an Open tickets count on the contact form. You don't edit the
contact app — you extend it from Helpdesk.
Add a field to the existing contact model by declaring a class with the same name
(helpdesk/models/contact_ext.py, then import it from models/__init__.py):
from victor import fields, models
class ContactExt(models.Model):
name = "contact" # extend the existing model
open_tickets = fields.Integer(label="Open tickets")
Then inject it into the contact form with <extend> (add this to views/helpdesk.xml):
<extend id="contact_form_helpdesk" view="contact.view_form">
<after field="email">
<field name="open_tickets"/>
</after>
</extend>
An Integer renders as a number by default — no widget needed. See
Extending other modules.
Where to go next¶
Your Helpdesk is a normal Victor app now — add intelligence and automation to it:
- Building agents — a support agent that triages and answers tickets
- Skills — teach that agent your escalation procedure
- Tools & server actions — let it call your own functions
- Workflows · Automations · Triggers — e.g. a nightly job that chases stale tickets
Keep the Cheat sheet handy for the exact field types, widgets, and keys.