Other View Types
Calendar Views
Section titled “Calendar Views”Display date-based records on a calendar.
Basic Calendar
Section titled “Basic Calendar”- data_type: UiView identifier: meeting_calendar_view type: Calendar model: Meeting arch: - content: - type: field name: name display_mode: title - type: field name: start_time display_mode: start - type: field name: end_time display_mode: endDisplay Modes
Section titled “Display Modes”| Mode | Description |
|---|---|
title | Event title |
start | Start datetime |
end | End datetime |
The title field may be a scalar (Char/Text/Selection) or a relation (ManyToOne): a relation is rendered as its display name, so {"name": "contact", "display_mode": "title"} shows the customer’s name on the event.
With Quick Create
Section titled “With Quick Create”- content: - {type: field, name: name, display_mode: title} - {type: field, name: start_time, display_mode: start} - {type: field, name: end_time, display_mode: end} - type: QuickCreate ctx: {form_identifier: meeting_quick_create_form}Dragging across empty calendar space opens that form with the picked range prefilled —
by day in the Month grid, by hour in Week and Day. Without a QuickCreate the calendar
is read-only for creation: there is no form to open.
Rescheduling by dragging
Section titled “Rescheduling by dragging”An event can be dragged to a new slot, and its edges dragged to change its duration.
This needs no declaration: a drag writes the same start and end fields the view
already names, so it says exactly what opening the record and editing those fields would.
(A Kanban drop is opt-in by contrast, because it writes a workflow field.)
| Grid | Dragging the event | Dragging an edge |
|---|---|---|
| Month | Moves it by whole days — the time of day is kept, so a 9am meeting dropped on Friday is still at 9am | Left/right edge moves the start/end day |
| Week / Day | Moves it to the day and time under the cursor, snapped to 15 minutes, anchored on where inside the event it was grabbed | Top/bottom edge moves the start/end time |
An edge stops rather than crossing the opposite one, so an event can never be turned
inside out. Escape mid-drag abandons the move.
What can be dragged
Section titled “What can be dragged”The gesture is only offered where it would succeed, so a user isn’t invited to make an edit that is then taken back:
- The view — not opted out with
draggable: false, and declaring astartfield. - The model — the user has update access, and the action’s
ctxdoesn’t setedit: false. - The record — the model’s write record rules, evaluated per row by the server and returned with the events. A row those rules exclude is not draggable.
- The fields — the model’s
_controlled_editsfreeze rules, evaluated against the row. A move writes both ends, so it needs both writable; each edge grip needs its own. - The device and the moment — off on touch (press-and-drag is the scroll gesture there) and while a dialog covers the calendar.
None of this is enforcement. A drag writes through PUT /api/update, the same endpoint
the record form saves through, so record rules, freeze rules, approval locks and readonly
fields are all applied to the write itself exactly as they would be from the form. The
checks above decide what to offer; if a write is refused anyway, the event returns to
where it was and the error is shown.
To turn it off for a calendar whose dates are computed or driven by a workflow rather
than chosen, declare draggable: false on the arch root:
arch: - draggable: false content: - {type: field, name: name, display_mode: title} - {type: field, name: scheduled_at, display_mode: start}Search Views
Section titled “Search Views”Define filters and groupings for list/kanban views.
Basic Search
Section titled “Basic Search”- data_type: UiView identifier: contact_search_view type: Search model: Contact arch: - type: filter identifier: customers description: Customers filter: "(Q(is_customer=True))" - type: filter identifier: suppliers description: Suppliers filter: "(Q(is_supplier=True))" - type: group identifier: group_country name: country description: CountryFilters
Section titled “Filters”Predefined filter conditions:
- type: filter identifier: active_only description: Active filter: "(Q(active=True))"Filter Syntax
Section titled “Filter Syntax”"(Q(field='value'))" # String equality (shorthand)"(Q(field__eq='value'))" # String equality (explicit)"(Q(field=True))" # Boolean check"(Q(field__isnull=True))" # ManyToOne null check (use field name, not field_id)"(Q(stage__is_won=True))" # Related field lookup (traverse relation)"(Q(active=True) & Q(status='open'))" # AND condition"(Q(user__id__eq=uid))" # Current user - ManyToOne requires field__id__eq=uid"(Q(company__id__eq=cid))" # Current company - ManyToOne requires field__id__eq=cidImportant for ManyToOne fields with uid/cid:
- Use
field__id__eq=uidsyntax (NOTfield_id=uidorfield=uid) - This traverses the relationship to compare the related record’s ID
Available Variables
Section titled “Available Variables”| Variable | Description |
|---|---|
uid | Current logged-in user’s ID |
cid | Current active company ID (user’s selected company for the session) |
D() | Dynamic date helper (see below) |
Dynamic Date Helper D()
Section titled “Dynamic Date Helper D()”The D() helper generates dynamic dates for use in filters. It returns a date or datetime object that is evaluated at query time.
Basic usage:
D('today') # today's dateD('now') # current datetimeRelative days/weeks/months/years:
D('today', days=-30) # 30 days agoD('today', days=7) # 7 days from nowD('today', weeks=-2) # 2 weeks agoD('today', months=-3) # 3 months agoD('today', years=-1) # 1 year agoPeriod boundaries (offset: 0=current, -1=previous, 1=next):
| Expression | Description |
|---|---|
D('start_of_week', 0) | Monday of this week |
D('end_of_week', 0) | Sunday of this week |
D('start_of_month', 0) | 1st of this month |
D('end_of_month', 0) | Last day of this month |
D('start_of_month', -1) | Start of last month |
D('end_of_month', -1) | End of last month |
D('start_of_quarter', 0) | Start of this quarter |
D('end_of_quarter', 1) | End of next quarter |
D('start_of_year', 0) | Jan 1st this year |
D('end_of_year', -1) | Dec 31st last year |
Example filters using D():
- type: filter identifier: overdue description: Overdue filter: "(Q(state='Posted') & Q(due_date__lt=D('today')))"- type: filter identifier: due_this_week description: Due This Week filter: "(Q(due_date__gte=D('start_of_week', 0)) & Q(due_date__lte=D('end_of_week', 0)))"- type: filter identifier: last_30_days description: Last 30 Days filter: "(Q(create_date__gte=D('today', days=-30)))"- type: filter identifier: this_quarter description: This Quarter filter: "(Q(date__gte=D('start_of_quarter', 0)) & Q(date__lte=D('end_of_quarter', 0)))"Supported Operators
Section titled “Supported Operators”| Operator | Example | Description |
|---|---|---|
= or __eq | Q(state='Pending') | Exact match |
__neq | Q(state__neq='Draft') | Not equal |
__isnull | Q(user__isnull=True) | Null check (use field name, not field_id) |
__gt | Q(amount__gt=100) | Greater than |
__gte | Q(amount__gte=100) | Greater than or equal |
__lt | Q(amount__lt=100) | Less than |
__lte | Q(amount__lte=100) | Less than or equal |
__in | Q(state__in=['Draft','Pending']) | Value in list |
__nin | Q(state__nin=['Done']) | Value not in list |
__contains | Q(name__contains='test') | Contains (case-sensitive) |
__icontains | Q(name__icontains='test') | Contains (case-insensitive) |
__iexact | Q(name__iexact='test') | Case-insensitive exact |
Related Field Lookups
Section titled “Related Field Lookups”For ManyToOne fields, you can filter by related record’s fields using __fieldname:
- type: filter identifier: won_filter description: Won filter: "(Q(stage__is_won=True))"Note: When filtering by user-definable data (like stages), use semantic boolean flags (e.g., is_won, is_lost) rather than names or identifiers which users can change.
Groups
Section titled “Groups”Grouping options for the view — the Organize By list in the browse panel:
- type: group identifier: group_stage name: stage description: StageGrouping by a date
Section titled “Grouping by a date”Name a date field and the group covers every period. The user picks Day, Week, Month, Quarter or Year from a chip on the group’s own row, so one node replaces the five you would otherwise declare:
- type: group identifier: sales_order_date name: order_date # bare: the user picks the period description: Order Date # label the FIELD, not a period interval: month # optional — which period it opens on (default: month)description names the field, because the period is no longer part of the group’s
identity — the chip shows it, and it changes as the user picks. Writing
description: Month leaves a row captioned “Month” whose chip reads “Quarter”.
To pin one period instead, put it in the name as field:interval. The group then
covers that period only and shows no chip:
- type: group identifier: sales_order_month name: order_date:month # pinned — no period chip description: Order MonthPin a period only when the view genuinely means one bucket. A set of pinned groups covering the same field at different periods is the long way of writing the bare form, and it costs the user a row each.
The periods are day, week, month, quarter and year — the same set a
Pivot dimension accepts.
Browse Panel
Section titled “Browse Panel”The browse panel provides a sidebar for faceted filtering. It displays field values with record counts, allowing users to quickly filter by clicking on values.
Basic Browse Panel
Section titled “Basic Browse Panel”Add browsepanel elements to your search view:
- data_type: UiView identifier: contact_search_view type: Search model: Contact arch: - type: filter identifier: customers description: Customers filter: "(Q(is_customer=True))" - type: browsepanel name: company_type description: Type expanded: true - type: browsepanel name: categories description: Categories - type: browsepanel name: country description: CountryBrowse Panel Properties
Section titled “Browse Panel Properties”| Property | Type | Default | Description |
|---|---|---|---|
name | string | required | Field name to browse by |
description | string | field label | Display label in the panel |
expanded | boolean | false | Whether section is expanded by default |
limit | number | 8 | Maximum values to show before “Show more” |
default | list | none | Values ticked when the view opens (Selection/Boolean only) |
Supported Field Types
Section titled “Supported Field Types”The browse panel supports the following field types:
| Field Type | Behavior |
|---|---|
ManyToOne | Shows related record names with counts |
ManyToMany | Shows related record names with counts |
OneToOne | Shows related record names with counts |
Selection | Shows selection choices with counts |
Boolean | Shows both values with counts (Active/Archived for active, else Yes/No) |
Default Selections
Section titled “Default Selections”default pre-ticks facet values, so the view opens already filtered. Entries are the field’s
own values — the stored text for a Selection, true/false for a Boolean:
- type: browsepanel name: active description: Records default: [true, false]The user can untick them like any other selection; nothing is locked.
Only Selection and Boolean facets may declare a default, and a relation facet is
rejected: it selects record ids, which differ from one database to the next, so a shipped
default would tick whichever record happens to hold that id on the customer’s install. To
open a view narrowed to particular related records, use a filter node keyed on a stable
value (Q(stage__is_won=True)) — the same rule that applies to filters generally.
Archivable Models: the Records Facet
Section titled “Archivable Models: the Records Facet”Every archivable model (one with an active field) gets a Records facet — Active /
Archived — added automatically, with no view configuration. Reads hide archived records by
default, so this facet is how a user reaches them.
Declare the facet yourself only to change its defaults, as above. Ticking both values is
the supported way to show archived rows alongside active ones — do not hand-write a
filter chip for it. This matters on models where active means not switched on yet
rather than retired (currencies, languages): the list is opened precisely to find the row
the default read hides, so it should open showing both.
How It Works
Section titled “How It Works”- Multi-select: Users can select multiple values within a field (OR logic)
- Cross-field AND: Selections across different fields use AND logic
- Dynamic counts: Record counts update based on other active filters
- Self-exclusion: A facet is never narrowed by its own selection — selecting a value keeps that facet fully browsable while still cross-narrowing the others, so the counts stay meaningful
- Toggle visibility: Users can show/hide the panel via the toolbar button
Hierarchical (Drill-Down) Browse
Section titled “Hierarchical (Drill-Down) Browse”When a browsepanel field is a ManyToOne/ManyToMany whose target model is a
tree (it opts into the hierarchy primitive
via _parent_field), the panel automatically renders that facet as an
expandable, multi-level tree instead of a flat list. No extra view configuration
is needed.
- Nodes nest under their parents to any depth, with per-node expand/collapse.
- Counts are rolled up the tree, so a parent shows the total for itself plus
all descendants. A roll-up is always a count of records, never of links: on a
ManyToMany, a record carrying two sibling nodes counts once under their shared parent, so a parent’s count never exceeds the number of records that selecting it would show. - Selecting a node filters the record list to that node and its entire
subtree (matched via the materialised
parent_path), so picking a top-level category captures everything beneath it. - Only branches that lead to matching records appear; an ancestor with no direct records is still shown so users can drill through it.
For example, a category browse field on Product becomes a drill-down tree as
soon as ProductCategory declares _parent_field = "parent" — the search view
entry stays exactly the same {"type": "browsepanel", "name": "category"}.
Example with Multiple Fields
Section titled “Example with Multiple Fields”- data_type: UiView identifier: product_search_view type: Search model: Product arch: - type: browsepanel name: category description: Category expanded: true - type: browsepanel name: brand description: Brand - type: browsepanel name: product_type description: Type limit: 5 - type: filter identifier: in_stock description: In Stock filter: "(Q(quantity__gt=0))"Complete Search Example
Section titled “Complete Search Example”- data_type: UiView identifier: lead_search_view type: Search model: CrmLead arch: - type: filter identifier: my_opportunities description: My Opportunities filter: "(Q(user__id__eq=uid))" - type: filter identifier: unassigned description: Unassigned filter: "(Q(user__isnull=True))" - type: filter identifier: won_filter description: Won filter: "(Q(stage__is_won=True))" - type: filter identifier: lost_filter description: Lost filter: "(Q(stage__is_lost=True))" - type: group identifier: group_stage name: stage description: Stage - type: group identifier: group_user name: user description: Salesperson - type: group identifier: group_source name: source description: SourceWizard Views
Section titled “Wizard Views”Wizard views define the UI for transient models (wizards). Wizards are temporary forms used for user input before executing an action.
Basic Wizard
Section titled “Basic Wizard”- data_type: UiView identifier: record_payment_wizard_view type: Wizard model: RecordPaymentWizard arch: - type: row content: - type: column span: 6 content: - {type: field, name: amount, properties: {widget: NumberInput}} - type: column span: 6 content: - {type: field, name: date, properties: {widget: DatePickerInput}} - type: footer buttons: - label: Confirm method: action_confirm variant: primaryDisplay Options
Section titled “Display Options”Control how the wizard opens with view-level properties:
| Property | Values | Default | Description |
|---|---|---|---|
size | xs, sm, md, lg, xl, fullscreen | Auto | Modal/drawer size |
displayMode | drawer, modal | Auto | How the wizard opens |
Default behavior (when not specified):
- Multi-step wizards (has
stepper): Fullscreen modal (xl) - Single-step wizards: Drawer (
md)
Custom Size Example
Section titled “Custom Size Example”- data_type: UiView identifier: large_wizard_view type: Wizard model: MyWizard size: lg displayMode: drawer arch: [...]How a wizard is presented
Section titled “How a wizard is presented”You don’t choose between a drawer and a modal — the framework decides, so wizards stay consistent across the app:
| Situation | Presentation |
|---|---|
| Single-step | Drawer, at the shared drawer-panel width |
Multi-step (has a stepper) | Fullscreen modal |
| Opened while another overlay is already open | Modal, whatever it would otherwise have been |
That last rule matters and is deliberately not yours to make: you cannot know that someone will one day open your wizard from inside a drawer, and a drawer sliding over a drawer is never the intent. It is resolved at the moment the wizard opens.
Set display_mode only when the result genuinely reads wrong. It chooses how the wizard
presents when it opens free-standing — the stacking rule still applies on top of it, so
Drawer on a wizard opened from inside another overlay still yields a modal. That is
deliberate: a drawer over a drawer is a defect regardless of who asked for it.
- data_type: UiView identifier: my_wizard_view type: Wizard model: MyWizard display_mode: Modal # or Drawer; leave it out to let the rules above decideMulti-Step Wizard
Section titled “Multi-Step Wizard”Use a stepper element for multi-step wizards:
- data_type: UiView identifier: onboarding_wizard_view type: Wizard model: OnboardingWizard arch: - type: stepper steps: - {label: Company Info, key: company} - {label: Users, key: users} - {label: Settings, key: settings} - type: step key: company content: - {type: field, name: company_name} - type: step key: users content: - {type: field, name: admin_email} - type: step key: settings content: - {type: field, name: timezone} - type: footer buttons: - {label: Complete, method: action_confirm, variant: primary}Footer Buttons
Section titled “Footer Buttons”The footer element defines action buttons under buttons:. Unlike the rest of a view, a
footer button’s keys sit at the node level — there is no properties: wrapper:
- type: footer buttons: - label: Confirm method: action_confirm variant: primary - label: Save Draft method: action_save_draft variant: outlineA Cancel button is automatically added to all wizards.
Footer buttons take visible and disabled as true/false/Q-expressions, resolved
against the wizard’s live inputs — the values the user is typing, not a saved record:
- type: footer buttons: - label: Add to Order method: action_add variant: filled disabled: Q(is_complete=False)This is where disabled earns its keep most obviously. A wizard is a short, fixed sequence,
so its footer must not change shape as the form fills in: the confirm button belongs there
from the first render, greyed until the inputs justify it. Hiding it instead leaves the user
looking for the way forward. Keep the method’s own validation regardless — the gate is an
affordance, not a check (see Hiding vs
disabling).
Window Actions
Section titled “Window Actions”Connect views to the application:
- data_type: WindowAction identifier: leads_action name: Leads model: CrmLead modes: Kanban,List,Form default_view: lead_kanban_view views: - - R - - lead_kanban_view - lead_list_view - lead_form_view search_view: lead_search_view action_ctx: view: {filters: [my_leads]}Action Properties
Section titled “Action Properties”| Property | Description |
|---|---|
name | Action display name |
model | Model to display |
modes | Available view modes |
default_view | Default view identifier |
views | List of view identifiers |
search_view | Search view identifier |
action_ctx | Default context |
limit | Records per page |
global_filter | Always-applied filter |
Context: field prefills vs. view directives
Section titled “Context: field prefills vs. view directives”The action action_ctx carries two distinct kinds of entry, kept in separate
namespaces so they can never be confused:
default_<field>— pre-fills<field>on a new record created from the action (e.g.default_company: 3,default_type: "Opportunity", or a relational command-form default likedefault_lines: [["create", {...}]]). The create path maps everydefault_<field>straight onto the model field of that name.view: { filters: [...], groups: [...] }— list/search view directives: which search-viewfilter/groupidentifiers start active. These describe the view, not a record, and are read only by the list/search layer.
Keeping view directives under view (rather than default_filters /
default_groups) is deliberate: their names reference data dimensions and would
otherwise collide with a same-named model field. For example User owns a filters
relation, so a default_filters key would be mis-read as a field prefill and inject
bogus relational command data on user creation. The view namespace makes that
collision structurally impossible — no reserved-key list to maintain.
Context: record-toolbar flags
Section titled “Context: record-toolbar flags”Alongside those, action_ctx takes a small set of booleans that withhold a standard
toolbar affordance for this action. Each defaults to true; only false is
meaningful. They shape the UI on top of model permissions — a flag can take an
affordance away, never grant one the user’s ACL withholds.
| Key | Withholds |
|---|---|
create | The New button — and with it Import, which is bulk creation |
edit | Form editing (the record opens read-only) |
delete | Delete in the record’s three-dot menu |
clone | Duplicate in the record’s three-dot menu |
Use them where records exist only because something else produced them — a work order raised by a confirmed manufacturing order, an entry posted by a workflow:
- data_type: WindowAction identifier: work_order_action model: WorkOrder action_ctx: create: false # no New, and no Import clone: false # duplicating would invent an operation nothing asked forNext Steps
Section titled “Next Steps”- Widgets - All available widgets
- View Inheritance - Extending views