Skip to content

Gantt Views

Gantt views display tasks on a timeline with support for scheduling, dependencies, and resource management.

  • Multiple time scales: Day, week, month, and quarter views
  • Drag-and-drop: Reschedule tasks by dragging
  • Resize handles: Change task duration by resizing
  • Dependencies: Visual arrows connecting related tasks
  • Grouping: Organize tasks by project, resource, or any field
- data_type: UiView
name: task_gantt_view
identifier: task_gantt_view
type: Gantt
model: ProjectTask
arch:
- default_scale: week
content:
- type: field
name: name
display_mode: title
- type: field
name: planned_start
display_mode: start
- type: field
name: planned_end
display_mode: end

The first element in arch can include configuration properties:

PropertyTypeDefaultDescription
default_scaleString"week"Initial time scale: day, week, month, quarter
row_heightNumber36Height of each task row in pixels
bar_heightNumber24Height of task bars in pixels
reschedule_methodString—Model method that owns a drag, so dependents move with it (see Rescheduling a dependency chain)
reschedule_modesList—The choices a planner may pick from for what happens downstream (see Letting the planner choose)
- default_scale: month
row_height: 40
bar_height: 28
content: [...]

Fields in Gantt views use display_mode to define their purpose:

ModePurposeField TypeRequired
titleTask name displayed on barCharYes
startStart date for bar positioningDate/DatetimeYes
endEnd date for bar widthDate/DatetimeNo (defaults to start + 1 day)
progressProgress percentage (0-100) for fillFloat/IntegerNo
resourceRow grouping (resource/project)ManyToOneNo
dependencyTask dependencies for arrowsManyToManyNo
colorBar color overrideChar (hex) or SelectionNo
- data_type: UiView
name: project_task_gantt_view
identifier: project_task_gantt_view
type: Gantt
model: ProjectTask
arch:
- default_scale: week
content:
- type: field
name: name
display_mode: title
- type: field
name: planned_start
display_mode: start
- type: field
name: planned_end
display_mode: end
- type: field
name: progress
display_mode: progress
- type: field
name: project
display_mode: resource
- type: field
name: depends_on
display_mode: dependency
- type: field
name: color
display_mode: color

For resource allocation views (e.g., employee scheduling, room bookings):

- data_type: UiView
name: booking_gantt_view
identifier: booking_gantt_view
type: Gantt
model: ResourceBooking
arch:
- default_scale: day
content:
- type: field
name: title
display_mode: title
- type: field
name: start_datetime
display_mode: start
- type: field
name: end_datetime
display_mode: end
- type: field
name: resource
display_mode: resource
- type: field
name: booking_type
display_mode: color

Show task completion with the progress display mode:

- type: field
name: progress
display_mode: progress

The progress field should contain a value from 0-100. The task bar will show a darker fill representing the completed portion.

To show dependency arrows between tasks:

  1. Create a ManyToMany field on your model pointing to itself:
class ProjectTask(Model):
depends_on = ManyToMany(
"ProjectTask",
through="FkTaskDependency",
related_name="dependents",
description="Dependencies"
)
class FkTaskDependency(Model):
task = ManyToOne("ProjectTask", related_name="dependency_links")
depends_on = ManyToOne("ProjectTask", related_name="dependent_links")
  1. Add the field with display_mode: "dependency":
- type: field
name: depends_on
display_mode: dependency

Dependencies render as arrows from the end of the predecessor task to the start of the dependent task (finish-to-start).

Drawing an arrow does not enforce it. By default a drag writes only the record that was dragged, so a user can move a task past its dependent and leave the arrow pointing backwards — nothing raises, and the schedule simply looks right while being wrong.

Declare reschedule_method to hand the whole move to a model method instead:

- default_scale: week
reschedule_method: gantt_reschedule
content: [...]
async def gantt_reschedule(self, scheduled_start=None, scheduled_end=None):
"""Move this record and whatever the dependency graph requires."""
...
return {"type": "notify", "color": "green", "message": t("Rescheduled.")}

The method receives the dragged record and its new dates, and is responsible for the entire move. It runs on the server on purpose: the chart can only see the tasks it fetched, so a dependent outside the visible window — or filtered out of the board — would silently not move if the cascade were computed in the browser. Doing it in one call also makes the move atomic, which matters because a cascade that half-applies leaves a schedule nobody can tell is broken.

Return an action result and the board shows it; a cascade’s normal outcome is partial (“moved 4 operations, 1 already under way stayed put”), which a bare success cannot express. Raise UserError only for a move that is genuinely impossible.

Prefer constraining a drop to refusing it. A drag that would break a constraint is usually better answered by moving the record as far as the constraint allows and saying so — the bar lands against whatever stopped it, which shows the rule instead of describing it. Refusing reads fine in isolation and becomes unusable wherever records are packed back-to-back, because then every drag in that direction is a violation and the user can never move anything at all.

Without the key the previous behaviour is unchanged — the drag writes just the one record.

There is no single right answer to what should happen downstream of a bar that moved. Pushing a whole job out, absorbing a delay into slack, and moving one record in order to fix the rest by hand are all real intentions, so a board can offer the choice. Declare reschedule_modes and the header grows a picker; the chosen value is passed to your method as mode:

- default_scale: week
reschedule_method: gantt_reschedule
reschedule_modes:
- Shift the Chain
- Re-plan What Follows
- Move Only This
content: [...]
async def gantt_reschedule(self, scheduled_start=None, scheduled_end=None, mode=None):
mode = resolve_reschedule_mode(mode, self.RESCHEDULE_MODES)
...

The three that ship have settled meanings, and a board that implements them should keep to those meanings — a planner who learns the picker on one board should not be surprised by it on another:

ModeWhat a drag does
Shift the ChainEvery dependent moves by the same signed delta, so gaps the planner deliberately left survive. The safe default for pushing a job out whole.
Re-plan What FollowsEverything downstream is re-derived from the links into it and re-placed against real capacity and working calendars. A delay is absorbed into whatever slack exists instead of being propagated.
Move Only ThisThe dates are written exactly as dropped and nothing else moves — no clamping either. Only offer it if you also report what it broke (below).

Declare only what your method implements. The picker is built from this list, so a mode named here but not handled offers the planner a choice that raises, and one handled but not named can never be chosen. Nothing at runtime compares the two — pin them with a test that reads the view’s declared list and compares it to the method’s own.

The first entry is the default, used when a planner has not chosen (and when an older client sends no mode at all). A choice is remembered per user, per database and per model.

These strings stay in English on the wire. They are dispatch values, not copy: the arch is translated per user before it reaches the browser, so a translatable mode would come back as a word the model has never heard of. The board renders each through the translator at display time instead, which is where a label belongs.

A mode that leaves links violated is only defensible if the damage is visible. Return the broken links as violations alongside the usual notify keys and the board draws those arrows in alarm until the next reschedule:

return {
"type": "notify",
"color": "orange",
"message": t_plural(
"{task} moved. {names} now conflicts with it.",
"{task} moved. {names} now conflict with it.",
count=len(broken), task=self.name, names=summarize_list(names),
),
"violations": [{"from": predecessor_id, "to": successor_id}, ...],
}

The model reports this rather than the chart working it out, because whether a bar starting the moment another ends is a conflict depends on what the record means: a board planning in datetimes says no, one planning in inclusive whole days says yes. Only the model knows which, and two answers to that question would drift.

Use display_mode: "resource" to organize tasks by rows:

- type: field
name: assigned_user
display_mode: resource
  • Tasks with the same group value appear together
  • Group headers are collapsible
  • Ungrouped tasks appear at the bottom

Override task bar colors with:

- type: field
name: color
display_mode: color

The field can contain:

  • Hex color codes: "#FF5733"
  • Selection values that map to colors

If no color is specified, bars use automatically generated colors based on task ID.

Include Gantt in your action’s available modes:

- data_type: WindowAction
identifier: project_tasks_action
name: Tasks
model: ProjectTask
modes: List, Kanban, Gantt, Form
default_view: project_task_list_view
views:
- - R
- - project_task_list_view
- project_task_kanban_view
- project_task_gantt_view
- project_task_form_view

Users can switch between scales using the toolbar buttons:

  • Day: Hour-by-hour view
  • Week: Day-by-day view
  • Month: Week-by-week view
  • Quarter: Month-by-month view
  • Arrow buttons: Move forward/backward by one time unit
  • Today button: Jump to current date
  • Click: Open task form
  • Drag: Move task to new dates (updates start and end)
  • Resize left edge: Change start date
  • Resize right edge: Change end date

For a model to work well with Gantt views:

class ProjectTask(Model):
_verbose_name = "Task"
# Required fields
name = Char(max_length=255, required=True)
planned_start = Date(description="Start Date")
planned_end = Date(description="End Date")
# Optional enhancements
progress = Float(default=0, description="Progress %")
project = ManyToOne("Project", related_name="tasks")
assigned_user = ManyToOne("User", related_name="tasks")
color = Char(max_length=7, description="Color")
depends_on = ManyToMany(
"ProjectTask",
through="FkTaskDependency",
related_name="dependents"
)