lib/reports/TableReport.rb in taskjuggler-0.0.3 vs lib/reports/TableReport.rb in taskjuggler-0.0.4

- old
+ new

@@ -1,11 +1,11 @@ #!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableReport.rb -- The TaskJuggler III Project Management Software # -# Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger <cs@kde.org> +# Copyright (c) 2006, 2007, 2008, 2009, 2010 by Chris Schlaeger <cs@kde.org> # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # @@ -24,24 +24,34 @@ class TableReport < ReportBase attr_reader :legend @@propertiesById = { - # ID Header Indent Align Calced. Scen Spec. - 'complete' => [ 'Completion', false, :right, true, true ], - 'cost' => [ 'Cost', true, :right, true, true ], - 'duration' => [ 'Duration', true, :right, true, true ], - 'effort' => [ 'Effort', true, :right, true, true ], - 'id' => [ 'Id', false, :left, true, false ], - 'line' => [ 'Line No.', false, :right, true, false ], - 'name' => [ 'Name', true, :left, false, false ], - 'no' => [ 'No.', false, :right, true, false ], - 'rate' => [ 'Rate', true, :right, true, true ], - 'resources' => [ 'Resources', false, :left, true, true ], - 'revenue' => [ 'Revenue', true, :right, true, true ], - 'scenario' => [ 'Scenario', false, :left, true, true ], - 'wbs' => [ 'WBS', false, :left, true, false ] + # ID Header Indent Align Scen Spec. + 'alert' => [ 'Alert', true, :left, false ], + 'alertmessage' => [ 'Alert Message', false, :left, false ], + 'alertsummary' => [ 'Alert Summary', false, :left, false ], + 'alerttrend' => [ 'Alert Trend', false, :left, false ], + 'complete' => [ 'Completion', false, :right, true ], + 'cost' => [ 'Cost', true, :right, true ], + 'duration' => [ 'Duration', true, :right, true ], + 'effort' => [ 'Effort', true, :right, true ], + 'effortdone' => [ 'Effort Done', true, :right, true ], + 'effortleft' => [ 'Effort Left', true, :right, true ], + 'freetime' => [ 'Free Time', true, :right, true ], + 'id' => [ 'Id', false, :left, false ], + 'line' => [ 'Line No.', false, :right, false ], + 'name' => [ 'Name', true, :left, false ], + 'no' => [ 'No.', false, :right, false ], + 'rate' => [ 'Rate', true, :right, true ], + 'resources' => [ 'Resources', false, :left, true ], + 'responsible' => [ 'Responsible', false, :left, true ], + 'revenue' => [ 'Revenue', true, :right, true ], + 'scenario' => [ 'Scenario', false, :left, true ], + 'status' => [ 'Status', false, :left, true ], + 'targets' => [ 'Targets', false, :left, true ], + 'wbs' => [ 'WBS', false, :left, false ] } @@propertiesByType = { # Type Indent Align DateAttribute => [ false, :left ], FixnumAttribute => [ false, :right ], @@ -60,20 +70,25 @@ @legend = ReportTableLegend.new end + def generateIntermediateFormat + super + end + + # Turn the TableReport into an equivalent HTML element tree. def to_html html = [] html << rt_to_html('header') html << (table = XMLElement.new('table', 'summary' => 'Report Table', 'cellspacing' => '2', 'border' => '0', 'cellpadding' => '0', 'align' => 'center', - 'class' => 'tabback')) + 'class' => 'tjtable')) # The headline is put in a sub-table to appear bigger. if a('headline') table << (thead = XMLElement.new('thead')) thead << (tr = XMLElement.new('tr')) @@ -83,13 +98,14 @@ 'cellpadding' => '5', 'align' => 'center', 'width' => '100%')) table1 << (tr1 = XMLElement.new('tr')) tr1 << (td1 = XMLElement.new('td', 'align' => 'center', 'style' => 'font-size:16px; ' + - 'font-weight:bold', + 'font-weight:bold; ' + + 'padding:5px', 'class' => 'tabfront')) - td1 << XMLNamedText.new(a('headline'), 'p') + td1 << a('headline').to_html end # Now generate the actual table with the data. table << (tbody = XMLElement.new('tbody')) tbody << (tr = XMLElement.new('tr')) @@ -110,77 +126,31 @@ tbody << (tr = XMLElement.new('tr', 'style' => 'font-size:10px;')) tr << (td = XMLElement.new('td', 'style' => 'padding-left:1px; padding-right:1px;')) td << @legend.to_html - # The footer with some administrative information. - tbody << (tr = XMLElement.new('tr', 'style' => 'font-size:9px')) - tr << (td = XMLElement.new('td', 'class' => 'tabfooter')) - td << XMLText.new(@project['copyright'] + " - ") if @project['copyright'] - td << XMLText.new("Project: #{@project['name']} " + - "Version: #{@project['version']} - " + - "Created on #{TjTime.now.to_s("%Y-%m-%d %H:%M:%S")} " + - "with ") - td << XMLNamedText.new("#{AppConfig.packageName}", 'a', - 'href' => "#{AppConfig.contact}") - td << XMLText.new(" v#{AppConfig.version}") - html << rt_to_html('footer') html end # Convert the table into an Array of Arrays. It has one Array for each # line. The nested Arrays have one String for each column. def to_csv @table.to_csv end - # This is the default attribute value to text converter. It is used - # whenever we need no special treatment. - def cellText(value, type) - begin - if value.nil? - if type == DateAttribute - nil - else - '' - end - else - # Certain attribute types need special treatment. - if type == DateAttribute - value.value.to_s(a('timeFormat')) - elsif type == RichTextAttribute - value.value - elsif type == ReferenceAttribute - value.label - else - value.to_s - end - end - rescue TjException - '' - end - end + # Returns the default column title for the columns _id_. + 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) - # This function returns true if the values for the _colId_ column need to be - # calculated. - def calculated?(colId) - if @@propertiesById.has_key?(colId) - return @@propertiesById[colId][3] - end - return false + # Return the title for build-in hardwired columns. + @@propertiesById.include?(id) ? @@propertiesById[id][0] : nil end - # This functions returns true if the values for the _col_id_ column are - # scenario specific. - def scenarioSpecific?(colId) - if @@propertiesById.has_key?(colId) - return @@propertiesById[colId][4] - end - return false - end - # Return if the column values should be indented based on the _colId_ or the # _propertyType_. def indent(colId, propertyType) if @@propertiesById.has_key?(colId) return @@propertiesById[colId][1] @@ -201,19 +171,23 @@ else :center end end - # Returns the default column title for the columns _id_. - 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) + # This function returns true if the values for the _colId_ column need to be + # calculated. + def calculated?(colId) + return @@propertiesById.has_key?(colId) + end - # Return the title for build-in hardwired columns. - @@propertiesById.include?(id) ? @@propertiesById[id][0] : nil + # This functions returns true if the values for the _col_id_ column are + # scenario specific. + def scenarioSpecific?(colId) + if @@propertiesById.has_key?(colId) + return @@propertiesById[colId][3] + end + return false end def supportedColumns @@propertiesById.keys end @@ -248,11 +222,10 @@ minWidth = @end - @start + 1 columns.each do |column| case column.id when 'chart' # In case we have a 'chart' column, we enforce certain minimum width - # of the chart. The width depends on the selected scale. # 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 = { @@ -312,11 +285,11 @@ # 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 + 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 @@ -353,13 +326,15 @@ # _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. def generateTaskList(taskList, resourceList, scopeLine) - queryAttrs = { 'scopeProperty' => scopeLine ? scopeLine.property : nil, + queryAttrs = { 'project' => @project, + 'scopeProperty' => scopeLine ? scopeLine.property : nil, 'loadUnit' => a('loadUnit'), 'numberFormat' => a('numberFormat'), + 'timeFormat' => a('timeFormat'), 'currencyFormat' => a('currencyFormat'), 'start' => @start, 'end' => @end, 'costAccount' => a('costAccount'), 'revenueAccount' => a('revenueAccount') } taskList.query = Query.new(queryAttrs) @@ -370,25 +345,33 @@ # 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 = @project.reportContext.query.dup + 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| - next unless generateTableCell(line, task, columnDef, scenarioIdx) + query.attributeId = columnDef.id + next unless generateTableCell(line, task, columnDef, query) end end if resourceList # If we have a resourceList we generate nested lines for each of the @@ -407,13 +390,15 @@ # 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. def generateResourceList(resourceList, taskList, scopeLine) - queryAttrs = { 'scopeProperty' => scopeLine ? scopeLine.property : nil, + queryAttrs = { 'project' => @project, + 'scopeProperty' => scopeLine ? scopeLine.property : nil, 'loadUnit' => a('loadUnit'), 'numberFormat' => a('numberFormat'), + 'timeFormat' => a('timeFormat'), 'currencyFormat' => a('currencyFormat'), 'start' => @start, 'end' => @end, 'costAccount' => a('costAccount'), 'revenueAccount' => a('revenueAccount') } resourceList.query = Query.new(queryAttrs) @@ -424,25 +409,33 @@ # 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 = @project.reportContext.query.dup + 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| - next unless generateTableCell(line, resource, column, scenarioIdx) + query.attributeId = column.id + next unless generateTableCell(line, resource, column, query) end end if taskList # If we have a taskList we generate nested lines for each of the @@ -479,11 +472,11 @@ # Create the table that is embedded in this column. tableColumn.cell1.special = table = ColumnTable.new table.equiLines = true tableColumn.cell2.hidden = true - table.maxWidth = columnDef.width + 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. @@ -539,24 +532,32 @@ # 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. - def generateTableCell(line, property, columnDef, scenarioIdx) + def generateTableCell(line, property, columnDef, query) + if columnDef.start || columnDef.end + # If the user has specified a new start or end time for this column, + # we have to duplicate the query before we modify it. + query = query.dup + query.start = columnDef.start if columnDef.start + query.end = columnDef.end if columnDef.end + end + 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) + 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, property, - line.scopeLine ? line.scopeLine.property : nil, - scenarioIdx, (line.subLineNo - 1) * (line.height + 1), + GanttLine.new(chart, property, line.scopeProperty, + query.scenarioIdx, + (line.subLineNo - 1) * (line.height + 1), line.height) return true # The calendar cells can be all generated by the same function. But we # need to use different parameters. when 'hourly' @@ -577,14 +578,13 @@ when 'yearly' start = @start.beginOfYear sameTimeNextFunc = :sameTimeNextYear else if calculated?(columnDef.id) - genCalculatedCell(scenarioIdx, line, columnDef, property) - return true + return genCalculatedCell(query, line, columnDef, property) else - return genStandardCell(scenarioIdx, line, columnDef) + 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. @@ -592,184 +592,178 @@ tcLine = ReportTableLine.new(columnDef.column.cell1.special, line.property, line.scopeLine) # Depending on the property type we use different generator functions. if property.is_a?(Task) - genCalChartTaskCell(scenarioIdx, tcLine, columnDef, start, - sameTimeNextFunc) + genCalChartTaskCell(query, tcLine, columnDef, start, sameTimeNextFunc) elsif property.is_a?(Resource) - genCalChartResourceCell(scenarioIdx, tcLine, columnDef, start, + genCalChartResourceCell(query, tcLine, columnDef, start, sameTimeNextFunc) else raise "Unknown property type #{property.class}" end true end # 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. - def genStandardCell(scenarioIdx, line, columnDef) - property = line.property - + 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 else raise "Unknown property type #{property.class}" end - colId = columnDef.id - # Get the value no matter if it's scenario specific or not. - if propertyList.scenarioSpecific?(colId) - value = property.getAttr(colId, scenarioIdx) - else - value = property.getAttr(colId) - end - # Get the type of the property - type = propertyList.attributeType(colId) # Create a new cell - cell = newCell(line, cellText(value, type)) - if type == ReferenceAttribute - cell.url = value.url - end + cell = newCell(query, line) - # Check if we are dealing with multiple scenarios. - if a('scenarios').length > 1 - # Check if the attribute is not scenario specific - unless propertyList.scenarioSpecific?(columnDef.id) - 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 + unless setScenarioSettings(cell, query.scenarioIdx, + propertyList.scenarioSpecific?(columnDef.id)) + return false end setStandardCellAttributes(cell, columnDef, propertyList.attributeType(columnDef.id), line) - scopeProperty = line.scopeLine ? line.scopeLine.property : nil - query = Query.new('property' => property, - 'scopeProperty' => scopeProperty, - 'attributeId' => columnDef.id, - 'scenarioIdx' => scenarioIdx, - 'loadUnit' => a('loadUnit'), - 'numberFormat' => a('numberFormat'), - 'currencyFormat' => a('currencyFormat'), - 'start' => @start, 'end' => @end, - 'costAccount' => a('costAccount'), - 'revenueAccount' => a('revenueAccount')) - if cell.text - if columnDef.cellText - cell.text = expandMacros(columnDef.cellText, cell.text, query) - end - else - cell.text = '<Error>' - cell.fontColor = 0xFF0000 + # 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 - setCellURL(cell, columnDef, query) + # 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 cell background color if the user has requested a custom + # color. + cellColor = columnDef.cellColor.getPattern(query) + cell.cellColor = cellColor if cellColor + + # If the user has requested a custom tooltip, add it to the cell. + cell.tooltip = columnDef.tooltip.getPattern(query) || nil + + checkCellText(cell) + true end # 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. _scenarioIdx_ is the index of the reported scenario. - # _line_ is the ReportTableLine of the cell. _columnDef_ is the + # 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. _property_ is the PropertyTreeNode # that is reported in this cell. - def genCalculatedCell(scenarioIdx, line, columnDef, property) + def genCalculatedCell(query, line, columnDef, property) # Create a new cell - cell = newCell(line) + cell = newCell(query, line) - unless scenarioSpecific?(columnDef.id) - if scenarioIdx != a('scenarios').first - cell.hidden = true - return false - end - cell.rows = a('scenarios').length + unless setScenarioSettings(cell, query.scenarioIdx, + scenarioSpecific?(columnDef.id)) + return false end setStandardCellAttributes(cell, columnDef, nil, line) - scopeProperty = line.scopeLine ? line.scopeLine.property : nil - return if columnDef.hideCellText && - columnDef.hideCellText.eval(property, scopeProperty) + if query.process + cell.text = (rti = query.to_rti) ? rti : query.to_s + end - query = Query.new('property' => property, - 'scopeProperty' => scopeProperty, - 'attributeId' => columnDef.id, - 'scenarioIdx' => scenarioIdx, - 'loadUnit' => a('loadUnit'), - 'numberFormat' => a('numberFormat'), - 'currencyFormat' => a('currencyFormat'), - 'start' => @start, 'end' => @end, - 'costAccount' => a('costAccount'), - 'revenueAccount' => a('revenueAccount')) - query.process - cell.text = query.result - # Some columns need some extra care. case columnDef.id + when 'alert' + id = @project.alertLevelId(query.to_num) + cell.icon = "flag-#{id}" + cell.fontColor = @project.alertLevelColor(query.to_sort) + when 'alerttrend' + icons = %w( up flat down ) + cell.icon = "trend-#{icons[query.to_sort]}" when 'line' cell.text = line.lineNo.to_s + when 'name' + 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 when 'no' cell.text = line.no.to_s when 'wbs' cell.indent = 2 if line.scopeLine when 'scenario' - cell.text = @project.scenario(scenarioIdx).name + cell.text = @project.scenario(query.scenarioIdx).name end - if columnDef.cellText - cell.text = expandMacros(columnDef.cellText, cell.text, query) - end - setCellURL(cell, columnDef, query) + # Replace the cell text if the user has requested a custom cell text. + cdText = columnDef.cellText.getPattern(query) + cell.text = cdText if cdText + + # 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 + + # Register the custom tooltip if the user has requested one. + cdTooltip = columnDef.tooltip.getPattern(query) + cell.tooltip = cdTooltip if cdTooltip + + checkCellText(cell) + + true end # 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. - def genCalChartTaskCell(scenarioIdx, line, columnDef, t, sameTimeNextFunc) + 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. - taskIv = Interval.new(task['start', scenarioIdx].nil? ? - @project['start'] : task['start', scenarioIdx], - task['end', scenarioIdx].nil? ? - @project['end'] : task['end', scenarioIdx]) + taskIv = Interval.new(task['start', query.scenarioIdx].nil? ? + @project['start'] : task['start', query.scenarioIdx], + task['end', query.scenarioIdx].nil? ? + @project['end'] : task['end', query.scenarioIdx]) - query = Query.new('property' => task, 'scopeProperty' => resource, - 'scenarioIdx' => scenarioIdx, - 'loadUnit' => a('loadUnit'), - 'numberFormat' => a('numberFormat'), - 'currencyFormat' => a('currencyFormat'), - 'costAccount' => a('costAccount'), - 'revenueAccount' => a('revenueAccount')) - firstCell = nil while t < @end # Create a new cell - cell = newCell(line) + cell = newCell(query, line) # call TjTime::sameTimeNext... function nextT = t.send(sameTimeNextFunc) cellIv = Interval.new(t, nextT) case columnDef.content @@ -779,11 +773,11 @@ query.attributeId = 'effort' query.startIdx = t query.endIdx = nextT query.process # To increase readability, we don't show 0.0 values. - cell.text = query.result if query.numericalResult > 0.0 + cell.text = query.to_s if query.to_num != 0.0 else raise "Unknown column content #{column.content}" end # Determine cell category (mostly the background color) @@ -812,63 +806,56 @@ # 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. - def genCalChartResourceCell(scenarioIdx, line, columnDef, t, + def genCalChartResourceCell(query, line, columnDef, t, sameTimeNextFunc) resource = line.property # 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. - taskIv = Interval.new(task['start', scenarioIdx].nil? ? - @project['start'] : task['start', scenarioIdx], - task['end', scenarioIdx].nil? ? - @project['end'] : task['end', scenarioIdx]) + taskIv = Interval.new(task['start', query.scenarioIdx].nil? ? + @project['start'] : + task['start', query.scenarioIdx], + task['end', query.scenarioIdx].nil? ? + @project['end'] : task['end', query.scenarioIdx]) else task = nil end - query = Query.new('property' => resource, - 'scenarioIdx' => scenarioIdx, - 'loadUnit' => a('loadUnit'), - 'numberFormat' => a('numberFormat'), - 'currencyFormat' => a('currencyFormat'), - 'costAccount' => a('costAccount'), - 'revenueAccount' => a('revenueAccount')) - firstCell = nil while t < @end # Create a new cell - cell = newCell(line) + cell = newCell(query, line) # call TjTime::sameTimeNext... function nextT = t.send(sameTimeNextFunc) cellIv = Interval.new(t, nextT) # Get work load for all tasks. query.scopeProperty = nil query.attributeId = 'effort' query.startIdx = @project.dateToIdx(t, true) query.endIdx = @project.dateToIdx(nextT, true) - 1 query.process - workLoad = query.numericalResult - scaledWorkLoad = query.result + workLoad = query.to_num + scaledWorkLoad = query.to_s if task # Get work load for the particular task. query.scopeProperty = task query.process - workLoadTask = query.numericalResult - scaledWorkLoad = query.result + workLoadTask = query.to_num + scaledWorkLoad = query.to_s else workLoadTask = 0.0 end # Get unassigned work load. query.attributeId = 'freework' query.process - freeLoad = query.numericalResult + 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. @@ -923,10 +910,12 @@ legend.addCalendarItem('Resource is partially loaded', 'loaded1') legend.addCalendarItem('Resource is available', 'free') legend.addCalendarItem('Off duty time', 'offduty') end + # This method takes care of often used cell attributes like indentation, + # alignment and background color. def setStandardCellAttributes(cell, columnDef, attributeType, line) # Determine whether it should be indented if indent(columnDef.id, attributeType) cell.indent = line.indentation end @@ -940,20 +929,42 @@ 'taskcell1' : 'taskcell2' else cell.category = line.property.get('index') % 2 == 1 ? 'resourcecell1' : 'resourcecell2' end + + # Set column width + cell.width = columnDef.width if columnDef.width end + 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 + # Create a new ReportTableCell object and initialize some common values. - def newCell(line, text = '') + def newCell(query, line) property = line.property - cell = ReportTableCell.new(line, text) + cell = ReportTableCell.new(line, query) # Cells for containers should be using bold font face. - cell.bold = true if property.container? - + cell.bold = true if property.container? && line.bold + cell.selfcontained = a('selfcontained') cell end # Determine the indentation for this line. def setIndent(line, propertyRoot, treeMode) @@ -961,92 +972,31 @@ 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. - line.indentation += level if treeMode + if treeMode + line.indentation += level + line.bold = true + end end - # Set the URL associated with the cell text. _cell_ is the ReportTableCell. - # _columnDef_ is the user specified definition for the cell content and - # look. _query_ is the query used to resolve dynamic macros in the cellURL. - def setCellURL(cell, columnDef, query) - return unless columnDef.cellURL - - if columnDef.hideCellURL && - columnDef.hideCellURL.eval(query.property, query.scopeProperty) - url = nil - else - url = expandMacros(columnDef.cellURL, cell.text, query) + # Make sure we have a valid cell text. If not, this is the result of an + # error. This could happen after scheduling errors. + def checkCellText(cell) + unless cell.text + cell.text = '<Error>' + cell.fontColor = '#FF0000' end - cell.url = url unless url.nil? || url.empty? end # Try to merge equal cells without text to multi-column cells. def tryCellMerging(cell, line, firstCell) if cell.text == '' && firstCell && (c = line.last(1)) && c == cell cell.hidden = true c.columns += 1 end end - # Expand the run-time macros in _pattern_. ${0} is a special case and will - # be replaced with the _originalText_. For all other macros the _query_ will - # be used with the macro name used for the attributeId of the query. The - # method returns the expanded pattern. - def expandMacros(pattern, originalText, query) - return pattern unless pattern.include?('${') - - out = '' - # Scan the pattern for macros ${...} - i = 0 - while i < pattern.length - c = pattern[i] - if c == ?$ - # This could be a macro - if pattern[i + 1] != ?{ - # It's not. Just append the '$' - out << c - else - # It is a macro. - i += 2 - macro = '' - # Scan for the end '}' and get the macro name. - while i < pattern.length && pattern[i] != ?} - macro << pattern[i] - i += 1 - end - if macro == '0' - # This turns RichText into plain ASCII! - out += originalText - else - # resolve by query - # If the macro is prefixed by a '?' it may be undefined. - if macro[0] == ?? - macro = macro[1..-1] - ignoreErrors = true - else - ignoreErrors = false - end - query.attributeId = macro - query.process - unless query.ok || ignoreErrors - raise TjException.new, query.errorMessage - end - # This turns RichText into plain ASCII! - out += query.result - end - end - else - # Just append the character to the output. - out << c - end - i += 1 - end - - out - end - end end -