class TaskJuggler::TableReport

This is base class for all types of tabular reports. All tabular reports are converted to an abstract (output independent) intermediate form first, before the are turned into the requested output format.

Attributes

legend[R]

Public Class Methods

defaultColumnTitle(id) click to toggle source

Returns the default column title for the columns id.

# File lib/taskjuggler/reports/TableReport.rb, line 152
def TableReport::defaultColumnTitle(id)
  # Return an empty string for some special columns that don't have a fixed
  # title.
  specials = %w( chart hourly daily weekly monthly quarterly yearly)
  return '' if specials.include?(id)

  # Return the title for build-in hardwired columns.
  @@propertiesById.include?(id) ? @@propertiesById[id][0] : nil
end
new(report) click to toggle source

Generate a new TableReport object.

Calls superclass method TaskJuggler::ReportBase.new
# File lib/taskjuggler/reports/TableReport.rb, line 87
def initialize(report)
  super
  @report.content = self

  # Reference to the intermediate representation.
  @table = nil
  # The table is generated row after row. We need to hold some computed
  # values that are specific to certain columns. For that we use a Hash of
  # ReportTableColumn objects.
  @columns = { }

  @legend = ReportTableLegend.new

end

Public Instance Methods

alignment(colId, attributeType) click to toggle source

Return the alignment of the column based on the colId or the attributeType.

# File lib/taskjuggler/reports/TableReport.rb, line 176
def alignment(colId, attributeType)
  if @@propertiesById.has_key?(colId)
    return @@propertiesById[colId][2]
  elsif @@propertiesByType.has_key?(attributeType)
    return @@propertiesByType[attributeType][1]
  else
    :center
  end
end
calculated?(colId) click to toggle source

This function returns true if the values for the colId column need to be calculated.

# File lib/taskjuggler/reports/TableReport.rb, line 188
def calculated?(colId)
  return @@propertiesById.has_key?(colId)
end
generateIntermediateFormat() click to toggle source
# File lib/taskjuggler/reports/TableReport.rb, line 102
def generateIntermediateFormat
  super
end
indent(colId, propertyType) click to toggle source

Return if the column values should be indented based on the colId or the propertyType.

# File lib/taskjuggler/reports/TableReport.rb, line 164
def indent(colId, propertyType)
  if @@propertiesById.has_key?(colId)
    return @@propertiesById[colId][1]
  elsif @@propertiesByType.has_key?(propertyType)
    return @@propertiesByType[propertyType][0]
  else
    false
  end
end
scenarioSpecific?(colId) click to toggle source

This functions returns true if the values for the col_id column are scenario specific.

# File lib/taskjuggler/reports/TableReport.rb, line 194
def scenarioSpecific?(colId)
  if @@propertiesById.has_key?(colId)
    return @@propertiesById[colId][3]
  end
  return false
end
supportedColumns() click to toggle source
# File lib/taskjuggler/reports/TableReport.rb, line 201
def supportedColumns
  @@propertiesById.keys
end
to_csv() click to toggle source

Convert the table into an Array of Arrays. It has one Array for each line. The nested Arrays have one String for each column.

# File lib/taskjuggler/reports/TableReport.rb, line 147
def to_csv
  @table.to_csv
end
to_html() click to toggle source

Turn the TableReport into an equivalent HTML element tree.

# File lib/taskjuggler/reports/TableReport.rb, line 107
def to_html
  html = []

  html << XMLComment.new("Dynamic Report ID: " +
                         "#{@report.project.reportContexts.last.
                            dynamicReportId}")
  html << rt_to_html('header')
  html << (tableFrame = generateHtmlTableFrame)

  # Now generate the actual table with the data.
  tableFrame << generateHtmlTableRow do
    td = XMLElement.new('td')
    td << @table.to_html
    td
  end

  # Embedd the caption as RichText into the table footer.
  if a('caption')
    tableFrame << generateHtmlTableRow do
      td = XMLElement.new('td')
      td << (div = XMLElement.new('div', 'class' => 'tj_table_caption'))
      a('caption').sectionNumbers = false
      div << a('caption').to_html
      td
    end
  end

  # The legend.
  tableFrame << generateHtmlTableRow do
    td = XMLElement.new('td')
    td << @legend.to_html
    td
  end

  html << rt_to_html('footer')
  html
end

Protected Instance Methods

adjustColumnPeriod(columnDef, tasks = [], scenarios = []) click to toggle source

In case the user has not specified the report period, we try to fit all the tasks in and add an extra 5% time at both ends for some specific type of columns. scenarios is a list of scenario indexes. columnDef is a reference to the TableColumnDefinition object describing the current column.

# File lib/taskjuggler/reports/TableReport.rb, line 212
def adjustColumnPeriod(columnDef, tasks = [], scenarios = [])
  # If we have user specified dates for the report period or the column
  # period, we don't adjust the period. This flag is used to mark if we
  # have user-provided values.
  doNotAdjustStart = false
  doNotAdjustEnd = false

  # Determine the start date for the column.
  if columnDef.start
    # We have a user-specified, column specific start date.
    rStart = columnDef.start
    doNotAdjustStart = true
  else
    # Use the report start date.
    rStart = a('start')
    doNotAdjustStart = true if rStart != @project['start']
  end

  if columnDef.end
    rEnd = columnDef.end
    doNotAdjustEnd = true
  else
    rEnd = a('end')
    doNotAdjustEnd = true if rEnd != @project['end']
  end
  origStart = rStart
  origEnd = rEnd

  # Save the unadjusted dates to the columns Hash.
  @columns[columnDef] = TableReportColumn.new(rStart, rEnd)

  # If the task list is empty or the user has provided a custom start or
  # end date, we don't touch the report period.
  return if tasks.empty? || scenarios.empty? ||
            (doNotAdjustStart && doNotAdjustEnd)

  # Find the start date of the earliest tasks included in the report and
  # the end date of the last included tasks.
  rStart = rEnd = nil
  scenarios.each do |scenarioIdx|
    tasks.each do |task|
      date = task['start', scenarioIdx] || @project['start']
      rStart = date if rStart.nil? || date < rStart
      date = task['end', scenarioIdx] || @project['end']
      rEnd = date if rEnd.nil? || date > rEnd
    end
  end

  # We want to add at least 5% on both ends.
  margin = 0
  minWidth = rEnd - rStart + 1
  case columnDef.id
  when 'chart'
    # In case we have a 'chart' column, we enforce certain minimum width
    # The following table contains an entry for each scale. The entry
    # consists of the triple 'seconds per unit', 'minimum width units'
    # and 'margin units'. The minimum with does not include the margins
    # since they are always added.
    mwMap = {
      'hour' =>    [ 60 * 60,            18, 2 ],
      'day' =>     [ 60 * 60 * 24,       18, 2 ],
      'week' =>    [ 60 * 60 * 24 * 7,    6, 1 ],
      'month' =>   [ 60 * 60 * 24 * 31,  10, 1 ],
      'quarter' => [ 60 * 60 * 24 * 90,   6, 1 ],
      'year' =>    [ 60 * 60 * 24 * 365,  4, 1 ]
    }
    entry = mwMap[columnDef.scale]
    raise "Unknown scale #{columnDef.scale}" unless entry
    margin = entry[0] * entry[2]
    # If the with determined by start and end dates of the task is below
    # the minimum width, we increase the width to the value provided by
    # the table.
    minWidth = entry[0] * entry[1] if minWidth < entry[0] * entry[1]
  when 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'
    # For the calendar columns we use a similar approach as we use for
    # the 'chart' column.
    mwMap = {
      'hourly' =>    [ 60 * 60,            18, 2 ],
      'daily' =>     [ 60 * 60 * 24,       18, 2 ],
      'weekly' =>    [ 60 * 60 * 24 * 7,    6, 1 ],
      'monthly' =>   [ 60 * 60 * 24 * 31,  10, 1 ],
      'quarterly' => [ 60 * 60 * 24 * 90,   6, 1 ],
      'yearly' =>    [ 60 * 60 * 24 * 365,  4, 1 ]
    }
    entry = mwMap[columnDef.id]
    raise "Unknown scale #{columnDef.id}" unless entry
    margin = entry[0] * entry[2]
    minWidth = entry[0] * entry[1] if minWidth < entry[0] * entry[1]
  else
    doNotAdjustStart = doNotAdjustEnd = true
  end

  unless doNotAdjustStart && doNotAdjustEnd
    if minWidth > (rEnd - rStart + 1)
      margin = (minWidth - (rEnd - rStart + 1)) / 2
    end

    rStart -= margin
    rEnd += margin

    # This could cause rStart to be larger than rEnd.
    rStart = origStart if doNotAdjustStart
    rEnd = origEnd if doNotAdjustEnd

    # Ensure that we have a valid interval. If not, go back to the
    # original interval dates.
    if rStart >= rEnd
      rStart = origStart
      rEnd = origEnd
    end

    # Save the adjusted dates to the columns Hash.
    @columns[columnDef] = TableReportColumn.new(rStart, rEnd)
  end
end
generateAccountList(accountList, lineOffset, mode) click to toggle source

Generate a ReportTableLine for each of the accounts in accountList. If scopeLine is defined, the generated account lines will be within the scope this resource line.

# File lib/taskjuggler/reports/TableReport.rb, line 388
def generateAccountList(accountList, lineOffset, mode)
  # Get the current Query from the report context and create a copy. We
  # are going to modify it.
  accountList.query = query = @project.reportContexts.last.query.dup
  accountList.sort!

  # The primary line counter. Is not used for enclosed lines.
  no = lineOffset
  # The scope line counter. It's reset for each new scope.
  lineNo = lineOffset
  # Init the variable to get a larger scope
  line = nil
  accountList.each do |account|
    query.property = account

    no += 1
    Log.activity if lineNo % 10 == 0
    lineNo += 1
    a('scenarios').each do |scenarioIdx|
      query.scenarioIdx = scenarioIdx
      # Generate line for each account.
      line = ReportTableLine.new(@table, account, nil)

      line.no = no
      line.lineNo = lineNo
      line.subLineNo = @table.lines
      setIndent(line, a('accountroot'), accountList.treeMode?)

      # Generate a cell for each column in this line.
      a('columns').each do |columnDef|
        query.attributeId = columnDef.id
        next unless generateTableCell(line, columnDef, query)
      end
    end
  end
  lineNo
end
generateHeaderCell(columnDef) click to toggle source

Generates cells for the table header. columnDef is the TableColumnDefinition object that describes the column. Based on the id of the column different actions need to be taken to generate the header text.

# File lib/taskjuggler/reports/TableReport.rb, line 331
def generateHeaderCell(columnDef)
  rStart = @columns[columnDef].start
  rEnd = @columns[columnDef].end

  case columnDef.id
  when 'chart'
    # For the 'chart' column we generate a GanttChart object. The sizes are
    # set so that the lines of the Gantt chart line up with the lines of the
    # table.
    gantt = GanttChart.new(a('now'),
                           a('weekStartsMonday'), columnDef, self)

    gantt.generateByScale(rStart, rEnd, columnDef.scale)
    # The header consists of 2 lines separated by a 1 pixel boundary.
    gantt.header.height = @table.headerLineHeight * 2 + 1
    # The maximum width of the chart. In case it needs more space, a
    # scrollbar is shown or the chart gets truncated depending on the output
    # format.
    gantt.viewWidth = columnDef.width ? columnDef.width : 450
    column = ReportTableColumn.new(@table, columnDef, '')
    column.cell1.special = gantt
    column.cell2.hidden = true
    column.scrollbar = gantt.hasScrollbar?
    @table.equiLines = true
  when 'hourly'
    genCalChartHeader(columnDef, rStart.midnight, rEnd, :sameTimeNextHour,
                      '%A %Y-%m-%d', '%H')
  when 'daily'
    genCalChartHeader(columnDef, rStart.midnight, rEnd, :sameTimeNextDay,
                      '%b %Y', '%d')
  when 'weekly'
    genCalChartHeader(columnDef,
                      rStart.beginOfWeek(a('weekStartsMonday')), rEnd,
                      :sameTimeNextWeek, '%b %Y', '%d')
  when 'monthly'
    genCalChartHeader(columnDef, rStart.beginOfMonth, rEnd,
                      :sameTimeNextMonth, '%Y', '%b')
  when 'quarterly'
    genCalChartHeader(columnDef, rStart.beginOfQuarter, rEnd,
                      :sameTimeNextQuarter, '%Y', 'Q%Q')
  when 'yearly'
    genCalChartHeader(columnDef, rStart.beginOfYear, rEnd, :sameTimeNextYear,
                      nil, '%Y')
  else
    # This is the most common case. It does not need any special treatment.
    # We just set the pre-defined or user-defined column title in the first
    # row of the header. The 2nd row is not visible.
    column = ReportTableColumn.new(@table, columnDef, columnDef.title)
    column.cell1.rows = 2
    column.cell2.hidden = true
    column.cell1.width = columnDef.width if columnDef.width
  end
end
generateResourceList(resourceList, taskList, scopeLine) click to toggle source

Generate a ReportTableLine for each of the resources in resourceList. In case taskList is not nil, it also generates the nested task lines for each task that the resource is assigned to. If scopeLine is defined, the generated resource lines will be within the scope this task line.

# File lib/taskjuggler/reports/TableReport.rb, line 488
def generateResourceList(resourceList, taskList, scopeLine)
  # Get the current Query from the report context and create a copy. We
  # are going to modify it.
  resourceList.query = query = @project.reportContexts.last.query.dup
  query.scopeProperty = scopeLine ? scopeLine.property : nil
  resourceList.sort!

  # The primary line counter. Is not used for enclosed lines.
  no = 0
  # The scope line counter. It's reset for each new scope.
  lineNo = scopeLine ? scopeLine.lineNo : 0
  # Init the variable to get a larger scope
  line = nil
  resourceList.each do |resource|
    # Get the current Query from the report context and create a copy. We
    # are going to modify it.
    query.property = resource
    query.scopeProperty = scopeLine ? scopeLine.property : nil

    no += 1
    Log.activity if lineNo % 10 == 0
    lineNo += 1
    a('scenarios').each do |scenarioIdx|
      query.scenarioIdx = scenarioIdx
      # Generate line for each resource.
      line = ReportTableLine.new(@table, resource, scopeLine)

      line.no = no unless scopeLine
      line.lineNo = lineNo
      line.subLineNo = @table.lines
      setIndent(line, a('resourceroot'), resourceList.treeMode?)

      # Generate a cell for each column in this line.
      a('columns').each do |column|
        query.attributeId = column.id
        next unless generateTableCell(line, column, query)
      end
    end

    if taskList
      # If we have a taskList we generate nested lines for each of the
      # tasks that the resource is assigned to and pass the user-defined
      # filter.
      taskList.setSorting(a('sortTasks'))
      assignedTaskList = filterTaskList(taskList, resource,
                                        a('hideTask'), a('rollupTask'),
                                        a('openNodes'))
      assignedTaskList.sort!
      lineNo = generateTaskList(assignedTaskList, nil, line)
    end
  end
  lineNo
end
generateTaskList(taskList, resourceList, scopeLine) click to toggle source

Generate a ReportTableLine for each of the tasks in taskList. In case resourceList is not nil, it also generates the nested resource lines for each resource that is assigned to the particular task. If scopeLine is defined, the generated task lines will be within the scope this resource line.

# File lib/taskjuggler/reports/TableReport.rb, line 431
def generateTaskList(taskList, resourceList, scopeLine)
  # Get the current Query from the report context and create a copy. We
  # are going to modify it.
  taskList.query = query = @project.reportContexts.last.query.dup
  query.scopeProperty = scopeLine ? scopeLine.property : nil
  taskList.sort!

  # The primary line counter. Is not used for enclosed lines.
  no = 0
  # The scope line counter. It's reset for each new scope.
  lineNo = scopeLine ? scopeLine.lineNo : 0
  # Init the variable to get a larger scope
  line = nil
  taskList.each do |task|
    # Get the current Query from the report context and create a copy. We
    # are going to modify it.
    query.property = task
    query.scopeProperty = scopeLine ? scopeLine.property : nil

    no += 1
    Log.activity if lineNo % 10 == 0
    lineNo += 1
    a('scenarios').each do |scenarioIdx|
      query.scenarioIdx = scenarioIdx
      # Generate line for each task.
      line = ReportTableLine.new(@table, task, scopeLine)

      line.no = no unless scopeLine
      line.lineNo = lineNo
      line.subLineNo = @table.lines
      setIndent(line, a('taskroot'), taskList.treeMode?)

      # Generate a cell for each column in this line.
      a('columns').each do |columnDef|
        query.attributeId = columnDef.id
        next unless generateTableCell(line, columnDef, query)
      end
    end

    if resourceList
      # If we have a resourceList we generate nested lines for each of the
      # resources that are assigned to this task and pass the user-defined
      # filter.
      resourceList.setSorting(a('sortResources'))
      assignedResourceList = filterResourceList(resourceList, task,
          a('hideResource'), a('rollupResource'), a('openNodes'))
      assignedResourceList.sort!
      lineNo = generateResourceList(assignedResourceList, nil, line)
    end
  end
  lineNo
end

Private Instance Methods

checkCellText(cell) click to toggle source

Make sure we have a valid cell text. If not, this is the result of an error. This could happen after scheduling errors.

# File lib/taskjuggler/reports/TableReport.rb, line 1181
def checkCellText(cell)
  unless cell.text
    cell.text = '<Error>'
    cell.fontColor = '#FF0000'
  end
end
genCalChartAccountCell(query, line, columnDef, t, sameTimeNextFunc) click to toggle source

Generate the cells for the account lines of a calendar column. These lines do not directly belong to the @table object but to an embedded ColumnTable object. Therefor a single @table column usually has many cells on each single line. scenarioIdx is the index of the scenario that is reported in this line. line is the @table line. t is the start date for the calendar. sameTimeNextFunc is the function that will move the date to the next cell.

# File lib/taskjuggler/reports/TableReport.rb, line 822
def genCalChartAccountCell(query, line, columnDef, t, sameTimeNextFunc)
  # We modify the start and end dates to match the cell boundaries. So
  # we need to make sure we don't modify the original Query but our own
  # copies.
  query = query.dup

  firstCell = nil
  endDate = query.end
  while t < endDate
    # call TjTime::sameTimeNext... function
    nextT = t.send(sameTimeNextFunc)
    query.attributeId = 'balance'
    query.start = t
    query.end = nextT
    query.process

    # Create a new cell
    cell = newCell(query, line)

    cell.text = query.to_s

    cdText = columnDef.cellText.getPattern(query)
    cell.text = cdText if cdText
    cell.showTooltipHint = false

    setAccountCellBgColor(query, line, cell)

    setCustomCellAttributes(cell, columnDef, query)

    tryCellMerging(cell, line, firstCell)

    t = nextT
    firstCell = cell unless firstCell
  end
end
genCalChartHeader(columnDef, t, rEnd, sameTimeNextFunc, timeformat1, timeformat2) click to toggle source

Generate the header data for calendar tables. They consists of columns for each hour, day, week, etc. columnDef is the definition of the columns. t is the start time for the calendar. sameTimeNextFunc is a function that is called to advance t to the next table column interval. timeformat1 and timeformat2 are strftime format Strings that are used to generate the upper and lower title of the particular column.

# File lib/taskjuggler/reports/TableReport.rb, line 550
def genCalChartHeader(columnDef, t, rEnd, sameTimeNextFunc,
                      timeformat1, timeformat2)
  tableColumn = ReportTableColumn.new(@table, columnDef, '')
  # Overwrite the built-in time formats if the user specified a different
  # one.
  timeformat1 = columnDef.timeformat1 if columnDef.timeformat1
  timeformat2 = columnDef.timeformat2 if columnDef.timeformat2

  # Calendar chars only work when all lines have same height.
  @table.equiLines = true

  # Embedded tables have unpredictable width. So we always need to make room
  # for a potential scrollbar.
  tableColumn.scrollbar = true

  # Create the table that is embedded in this column.
  tableColumn.cell1.special = table = ColumnTable.new
  table.equiLines = true
  table.selfcontained = a('selfcontained')
  tableColumn.cell2.hidden = true
  table.viewWidth = columnDef.width ? columnDef.width : 450

  # Iterate over the report interval until we hit the end date. The
  # iteration is done with 2 nested loops. The outer loops generates the
  # intervals for the upper (larger) scale. The inner loop generates the
  # lower (smaller) scale.
  while t < rEnd
    cellsInInterval = 0
    # Label for upper scale. The yearly calendar only has a lower scale.
    currentInterval = t.to_s(timeformat1) if timeformat1
    firstColumn = nil
    # The innter loops terminates when the label for the upper scale has
    # changed to the next scale cell.
    while t < rEnd && (timeformat1.nil? ||
                       t.to_s(timeformat1) == currentInterval)
      # call TjTime::sameTimeNext... function to get the end of the column.
      nextT = t.send(sameTimeNextFunc)
      iv = TimeInterval.new(t, nextT)
      # Create the new column object.
      column = ReportTableColumn.new(table, nil, '')
      # Store the date of the column in the original form.
      column.cell1.data = t.to_s(a('timeFormat'))
      # The upper scale cells will be merged into one large cell that spans
      # all lower scale cells that belong to this upper cell.
      if firstColumn.nil?
        firstColumn = column
        column.cell1.text = currentInterval
      else
        column.cell1.hidden = true
      end
      column.cell2.text = t.to_s(timeformat2)
      # We assume an average of 7 pixel per character
      width = 8 + 7 * column.cell2.text.length
      # Ensure a minimum with of 28 to have good looking tables even with
      # small column headers (like day of months numbers).
      column.cell2.width = width <= 28 ? 28 : width
      # Off-duty cells will have a different color than working time cells.
      unless @project.hasWorkingTime(iv)
        column.cell2.category = 'tabhead_offduty'
      end
      cellsInInterval += 1

      t = nextT
    end
    # The the first upper scale cell how many trailing hidden cells are
    # following.
    firstColumn.cell1.columns = cellsInInterval
  end
end
genCalChartResourceCell(query, line, columnDef, t, sameTimeNextFunc) click to toggle source

Generate the cells for the resource lines of a calendar column. These lines do not directly belong to the @table object but to an embedded ColumnTable object. Therefor a single @table column usually has many cells on each single line. scenarioIdx is the index of the scenario that is reported in this line. line is the @table line. t is the start date for the calendar. sameTimeNextFunc is the function that will move the date to the next cell.

# File lib/taskjuggler/reports/TableReport.rb, line 948
def genCalChartResourceCell(query, line, columnDef, t,
                            sameTimeNextFunc)
  # Find out if we have an enclosing task scope.
  if line.scopeLine && line.scopeLine.property.is_a?(Task)
    task = line.scopeLine.property
    # Get the interval of the task. In case a date is invalid due to a
    # scheduling problem, we use the full project interval.
    taskStart = task['start', query.scenarioIdx]
    taskEnd = task['end', query.scenarioIdx]
    taskIv = TimeInterval.new(taskStart.nil? ? @project['start'] :
                                               taskStart,
                              taskEnd.nil? ?  @project['end'] : taskEnd)
  else
    task = nil
  end

  # We modify the start and end dates to match the cell boundaries. So
  # we need to make sure we don't modify the original Query but our own
  # copies.
  query = query.dup

  firstCell = nil
  endDate = query.end
  while t < endDate
    # Create a new cell
    cell = newCell(query, line)

    # call TjTime::sameTimeNext... function
    nextT = t.send(sameTimeNextFunc)
    cellIv = TimeInterval.new(t, nextT)
    # Get work load for all tasks.
    query.scopeProperty = nil
    query.attributeId = 'effort'
    query.startIdx = @project.dateToIdx(t)
    query.endIdx = @project.dateToIdx(nextT)
    query.process
    workLoad = query.to_num
    scaledWorkLoad = query.to_s

    if task
      # Get work load for the particular task.
      query.scopeProperty = task
      query.process
      workLoadTask = query.to_num
      scaledWorkLoad = query.to_s
    else
      workLoadTask = 0.0
    end
    # Get unassigned work load.
    query.attributeId = 'freework'
    query.process
    freeLoad = query.to_num
    case columnDef.content
    when 'empty'
      # We only generate cells will different background colors.
    when 'load'
      # Report the workload of the resource in this time interval.
      # To increase readability, we don't show 0.0 values.
      wLoad = task ? workLoadTask : workLoad
      if wLoad > 0.0
        cell.text = scaledWorkLoad
      end
    else
      raise "Unknown column content #{column.content}"
    end

    cdText = columnDef.cellText.getPattern(query)
    cell.text = cdText if cdText

    # Set the tooltip for the cell. We might delete it again.
    cell.tooltip = columnDef.tooltip.getPattern(query) || nil
    cell.showTooltipHint = false

    # Determine cell category (mostly the background color)
    cell.category = if task
                      if cellIv.overlaps?(taskIv)
                        if workLoadTask > 0.0 && freeLoad == 0.0
                          'busy'
                        elsif workLoad == 0.0 && freeLoad == 0.0
                          cell.tooltip = nil
                          'offduty'
                        else
                          'loaded'
                        end
                      else
                        if freeLoad > 0.0
                          'free'
                        elsif workLoad == 0.0 && freeLoad == 0.0
                          cell.tooltip = nil
                          'offduty'
                        else
                          cell.tooltip = nil
                          'resourcecell'
                        end
                      end
                    else
                      if workLoad > 0.0 && freeLoad == 0.0
                        'busy'
                      elsif workLoad > 0.0 && freeLoad > 0.0
                        'loaded'
                      elsif workLoad == 0.0 && freeLoad > 0.0
                        'free'
                      else
                        cell.tooltip = nil
                        'offduty'
                      end
                    end
    cell.category += line.property.get('index') % 2 == 1 ? '1' : '2'

    setCustomCellAttributes(cell, columnDef, query)

    tryCellMerging(cell, line, firstCell)

    t = nextT
    firstCell = cell unless firstCell
  end

  legend.addCalendarItem('Resource is fully loaded', 'busy1')
  legend.addCalendarItem('Resource is partially loaded', 'loaded1')
  legend.addCalendarItem('Resource is available', 'free')
  legend.addCalendarItem('Off duty time', 'offduty')
end
genCalChartTaskCell(query, line, columnDef, t, sameTimeNextFunc) click to toggle source

Generate the cells for the task lines of a calendar column. These lines do not directly belong to the @table object but to an embedded ColumnTable object. Therefor a single @table column usually has many cells on each single line. scenarioIdx is the index of the scenario that is reported in this line. line is the @table line. t is the start date for the calendar. sameTimeNextFunc is the function that will move the date to the next cell.

# File lib/taskjuggler/reports/TableReport.rb, line 865
def genCalChartTaskCell(query, line, columnDef, t, sameTimeNextFunc)
  task = line.property
  # Find out if we have an enclosing resource scope.
  if line.scopeLine && line.scopeLine.property.is_a?(Resource)
    resource = line.scopeLine.property
  else
    resource = nil
  end

  # Get the interval of the task. In case a date is invalid due to a
  # scheduling problem, we use the full project interval.
  taskStart = task['start', query.scenarioIdx]
  taskEnd = task['end', query.scenarioIdx]
  taskIv = TimeInterval.new(taskStart.nil? ?  @project['start'] : taskStart,
                            taskEnd.nil? ?  @project['end'] : taskEnd)

  # We modify the start and end dates to match the cell boundaries. So
  # we need to make sure we don't modify the original Query but our own
  # copies.
  query = query.dup

  firstCell = nil
  endDate = query.end
  while t < endDate
    # call TjTime::sameTimeNext... function
    nextT = t.send(sameTimeNextFunc)
    cellIv = TimeInterval.new(t, nextT)
    case columnDef.content
    when 'empty'
      # Create a new cell
      cell = newCell(query, line)
      # We only generate cells will different background colors.
    when 'load'
      query.attributeId = 'effort'
      query.start = t
      query.end = nextT
      query.process

      # Create a new cell
      cell = newCell(query, line)

      # To increase readability show empty cells instead of 0.0 values.
      cell.text = query.to_s if query.to_num != 0.0
    else
      raise "Unknown column content #{column.content}"
    end

    cdText = columnDef.cellText.getPattern(query)
    cell.text = cdText if cdText
    cell.showTooltipHint = false

    # Determine cell category (mostly the background color)
    if cellIv.overlaps?(taskIv)
      # The cell is either a container or leaf task
      cell.category = task.container? ? 'calconttask' : 'caltask'
    elsif !@project.isWorkingTime(cellIv)
      # The cell is a vacation cell.
      cell.category = 'offduty'
    else
      # The cell is just filled with the background color.
      cell.category = 'taskcell'
    end
    cell.category += line.property.get('index') % 2  == 1 ? '1' : '2'

    setCustomCellAttributes(cell, columnDef, query)
    tryCellMerging(cell, line, firstCell)

    t = nextT
    firstCell = cell unless firstCell
  end

  legend.addCalendarItem('Container Task', 'calconttask1')
  legend.addCalendarItem('Task', 'caltask1')
  legend.addCalendarItem('Off duty time', 'offduty')
end
genCalculatedCell(query, line, columnDef) click to toggle source

Generate a ReportTableCell filled with a calculted value from the property or other sources of information. It returns true if the cell exists, false for a hidden cell. query is the Query to get the cell value. line is the ReportTableLine of the cell. columnDef is the TableColumnDefinition of the column.

# File lib/taskjuggler/reports/TableReport.rb, line 751
def genCalculatedCell(query, line, columnDef)
  # Create a new cell
  cell = newCell(query, line)

  unless setScenarioSettings(cell, query.scenarioIdx,
                             scenarioSpecific?(columnDef.id))
    return false
  end

  setStandardCellAttributes(query, cell, columnDef, nil, line)

  if query.process
    cell.text = (rti = query.to_rti) ? rti : query.to_s
  end

  # Some columns need some extra care.
  case columnDef.id
  when 'alert'
    id = @project['alertLevels'][query.to_sort].id
    cell.icon = "flag-#{id}"
    cell.fontColor = @project['alertLevels'][query.to_sort].color
  when 'alerttrend'
    icons = %w( up flat down )
    cell.icon = "trend-#{icons[query.to_sort]}"
  when 'line'
    cell.text = line.lineNo.to_s
  when 'name'
    property = query.property
    cell.icon =
      if property.is_a?(Task)
        if property.container?
          'taskgroup'
        else
          'task'
        end
      elsif property.is_a?(Resource)
        if property.container?
          'resourcegroup'
        else
          'resource'
        end
      else
        nil
      end
      cell.iconTooltip = RichText.new("'''ID:''' #{property.fullId}").
      generateIntermediateFormat
  when 'no'
    cell.text = line.no.to_s
  when 'bsi'
    cell.indent = 2 if line.scopeLine
  when 'scenario'
    cell.text = @project.scenario(query.scenarioIdx).name
  end

  # Replace the cell text if the user has requested a custom cell text.
  cdText = columnDef.cellText.getPattern(query)
  cell.text = cdText if cdText

  setCustomCellAttributes(cell, columnDef, query)
  checkCellText(cell)

  true
end
genStandardCell(query, line, columnDef) click to toggle source

Generate a ReportTableCell filled the value of an attribute of the property that line is for. It returns true if the cell exists, false for a hidden cell.

# File lib/taskjuggler/reports/TableReport.rb, line 708
def genStandardCell(query, line, columnDef)
  # Find out, what type of PropertyTreeNode we are dealing with.
  property = line.property
  if property.is_a?(Task)
    propertyList = @project.tasks
  elsif property.is_a?(Resource)
    propertyList = @project.resources
  elsif property.is_a?(Account)
    propertyList = @project.accounts
  else
    raise "Unknown property type #{property.class}"
  end

  # Create a new cell
  cell = newCell(query, line)

  unless setScenarioSettings(cell, query.scenarioIdx,
                             propertyList.scenarioSpecific?(columnDef.id))
    return false
  end

  setStandardCellAttributes(query, cell, columnDef,
                            propertyList.attributeType(columnDef.id), line)

  # If the user has requested a custom cell text, this will be used
  # instead of the queried one.
  if (cdText = columnDef.cellText.getPattern(query))
    cell.text = cdText
  elsif query.process
    cell.text = (rti = query.to_rti) ? rti : query.to_s
  end

  setCustomCellAttributes(cell, columnDef, query)
  checkCellText(cell)

  true
end
generateTableCell(line, columnDef, query) click to toggle source

Generate a cell of the table. line is the ReportTableLine that this cell should belong to. property is the PropertyTreeNode that is reported in this line. columnDef is the TableColumnDefinition of the column this cell should belong to. scenarioIdx is the index of the scenario that is reported in this line.

There are 4 kinds of cells. The most simple one is the standard cell. It literally reports the value of a property attribute. Calculated cells are more flexible. They contain computed values. The values are computed at cell generation time. The calendar columns consist of multiple sub columns. In such a case many cells are generated with a single call of this method. The last kind of cell is actually not a cell. It just generates the chart objects that belong to the property in this line.

# File lib/taskjuggler/reports/TableReport.rb, line 633
def generateTableCell(line, columnDef, query)
  # Adjust the Query to use column specific settings. We create a copy of
  # the Query to avoid spoiling the original query with column specific
  # settings.
  query = query.dup
  query.start = @columns[columnDef].start
  query.end = @columns[columnDef].end
  query.listType = columnDef.listType
  query.listItem = columnDef.listItem

  case columnDef.id
  when 'chart'
    # Generate a hidden cell. The real meat is in the actual chart object,
    # not in this cell.
    cell = ReportTableCell.new(line, query, '')
    cell.hidden = true
    cell.text = nil
    # The GanttChart can be reached via the special variable of the column
    # header.
    chart = columnDef.column.cell1.special
    GanttLine.new(chart, query, (line.subLineNo - 1) * (line.height + 1),
                  line.height, a('selfcontained') ? nil : columnDef.tooltip)
    return true
  # The calendar cells can be all generated by the same function. But we
  # need to use different parameters.
  when 'hourly'
    start = query.start.midnight
    sameTimeNextFunc = :sameTimeNextHour
  when 'daily'
    start = query.start.midnight
    sameTimeNextFunc = :sameTimeNextDay
  when 'weekly'
    start = query.start.beginOfWeek(a('weekStartsMonday'))
    sameTimeNextFunc = :sameTimeNextWeek
  when 'monthly'
    start = query.start.beginOfMonth
    sameTimeNextFunc = :sameTimeNextMonth
  when 'quarterly'
    start = query.start.beginOfQuarter
    sameTimeNextFunc = :sameTimeNextQuarter
  when 'yearly'
    start = query.start.beginOfYear
    sameTimeNextFunc = :sameTimeNextYear
  else
    if calculated?(columnDef.id)
      return genCalculatedCell(query, line, columnDef)
    else
      return genStandardCell(query, line, columnDef)
    end
  end

  # The calendar cells don't live in this ReportTable but in an embedded
  # ReportTable that can be reached via the column header special variable.
  # For embedded column tables we need to create a new line.
  tcLine = ReportTableLine.new(columnDef.column.cell1.special,
                               line.property, line.scopeLine)

  PlaceHolderCell.new(line, tcLine)
  # Depending on the property type we use different generator functions.
  if query.property.is_a?(Task)
    genCalChartTaskCell(query, tcLine, columnDef, start, sameTimeNextFunc)
  elsif query.property.is_a?(Resource)
    genCalChartResourceCell(query, tcLine, columnDef, start,
                            sameTimeNextFunc)
  elsif query.property.is_a?(Account)
    genCalChartAccountCell(query, tcLine, columnDef, start, sameTimeNextFunc)
  else
    raise "Unknown property type #{query.property.class}"
  end
  true
end
newCell(query, line) click to toggle source

Create a new ReportTableCell object and initialize some common values.

# File lib/taskjuggler/reports/TableReport.rb, line 1139
def newCell(query, line)
  property = line.property
  cell = ReportTableCell.new(line, query)

  # Cells for containers should be using bold font face.
  cell.bold = true if property.container? && line.bold
  cell
end
setAccountCellBgColor(query, line, cell) click to toggle source
# File lib/taskjuggler/reports/TableReport.rb, line 1162
def setAccountCellBgColor(query, line, cell)
  if query.costAccount &&
     (query.property.isChildOf?(query.costAccount) ||
      query.costAccount == query.property)
    prefix = 'cost'
  elsif query.revenueAccount &&
        (query.property.isChildOf?(query.revenueAccount) ||
         query.revenueAccount == query.property)
    prefix = 'revenue'
  else
    prefix = ''
  end

  cell.category = prefix + 'accountcell' +
                  (line.property.get('index') % 2 == 1 ? '1' : '2')
end
setCustomCellAttributes(cell, columnDef, query) click to toggle source
# File lib/taskjuggler/reports/TableReport.rb, line 1097
def setCustomCellAttributes(cell, columnDef, query)
  # Replace the cell background color if the user has requested a custom
  # color.
  cellColor = columnDef.cellColor.getPattern(query)
  cell.cellColor = cellColor if cellColor

  # Replace the font color setting if the user has requested a custom
  # color.
  fontColor = columnDef.fontColor.getPattern(query)
  cell.fontColor = fontColor if fontColor

  # Replace the default cell alignment if the user has requested a custom
  # alignment.
  hAlign = columnDef.hAlign.getPattern(query)
  cell.alignment = hAlign if hAlign

  # Register the custom tooltip if the user has requested one.
  cdTooltip = columnDef.tooltip.getPattern(query)
  cell.tooltip = cdTooltip if cdTooltip
end
setIndent(line, propertyRoot, treeMode) click to toggle source

Determine the indentation for this line.

# File lib/taskjuggler/reports/TableReport.rb, line 1149
def setIndent(line, propertyRoot, treeMode)
  property = line.property
  scopeLine = line.scopeLine
  level = property.level - (propertyRoot ? propertyRoot.level : 0)
  # We indent at least as much as the scopeline + 1, if we have a scope.
  line.indentation = scopeLine.indentation + 1 if scopeLine
  # In tree mode we indent according to the level.
  if treeMode
    line.indentation += level
    line.bold = true
  end
end
setScenarioSettings(cell, scenarioIdx, scenarioSpecific) click to toggle source
# File lib/taskjuggler/reports/TableReport.rb, line 1119
def setScenarioSettings(cell, scenarioIdx, scenarioSpecific)
  # Check if we are dealing with multiple scenarios.
  if a('scenarios').length > 1
    # Check if the attribute is not scenario specific
    unless scenarioSpecific
      if scenarioIdx == a('scenarios').first
        #  Use a somewhat bigger font.
        cell.fontSize = 15
      else
        # And hide the cells for all but the first scenario.
        cell.hidden = true
        return false
      end
      cell.rows = a('scenarios').length
    end
  end
  true
end
setStandardCellAttributes(query, cell, columnDef, attributeType, line) click to toggle source

This method takes care of often used cell attributes like indentation, alignment and background color.

# File lib/taskjuggler/reports/TableReport.rb, line 1073
def setStandardCellAttributes(query, cell, columnDef, attributeType, line)
  # Determine whether it should be indented
  if indent(columnDef.id, attributeType)
    cell.indent = line.indentation
  end

  # Determine the cell alignment
  cell.alignment = alignment(columnDef.id, attributeType)

  # Set background color
  if line.property.is_a?(Task)
    cell.category = 'taskcell'
    cell.category += line.property.get('index') % 2 == 1 ? '1' : '2'
  elsif line.property.is_a?(Resource)
    cell.category = 'resourcecell'
    cell.category += line.property.get('index') % 2 == 1 ? '1' : '2'
  elsif line.property.is_a?(Account)
    setAccountCellBgColor(query, line, cell)
  end

  # Set column width
  cell.width = columnDef.width if columnDef.width
end
tryCellMerging(cell, line, firstCell) click to toggle source

Try to merge equal cells without text to multi-column cells.

# File lib/taskjuggler/reports/TableReport.rb, line 1189
def tryCellMerging(cell, line, firstCell)
  if cell.text == '' && firstCell && (c = line.last(1)) && c == cell
    cell.hidden = true
    c.columns += 1
  end
end