class TaskJuggler::Report

The Report class holds the fundamental description and functionality to turn the scheduled project into a user readable form. A report may contain other reports.

Attributes

content[RW]
typeSpec[RW]

Public Class Methods

new(project, id, name, parent) click to toggle source

Create a new report object.

Calls superclass method TaskJuggler::PropertyTreeNode::new
# File lib/taskjuggler/reports/Report.rb, line 48
def initialize(project, id, name, parent)
  super(project.reports, id, name, parent)
  @messageHandler = MessageHandlerInstance.instance
  checkFileName(name)
  project.addReport(self)

  # The type specifier must be set for every report. It tells whether this
  # is a task, resource, text or other report.
  @typeSpec = nil
  # Reports don't really have any scenario specific attributes. But the
  # flag handling code assumes they are. To use flags, we need them as
  # well.
  @data = Array.new(@project.scenarioCount, nil)
  @project.scenarioCount.times do |i|
    ReportScenario.new(self, i, @scenarioAttributes[i])
  end
end

Public Instance Methods

generate(requestedFormats = nil) click to toggle source

The generate function is where the action happens in this class. The report defined by all the class attributes and report elements is generated according the the requested output format(s). requestedFormats can be a list of formats that should be generated (e.

  1. :html, :csv, etc.).

# File lib/taskjuggler/reports/Report.rb, line 71
def generate(requestedFormats = nil)
  oldTimeZone = TjTime.setTimeZone(get('timezone'))

  generateIntermediateFormat

  # We either generate the requested formats or the list of formats that
  # was specified in the report definition.
  (requestedFormats || get('formats')).each do |format|
    if @name.empty?
      error('empty_report_file_name',
            "Report #{@id} has output formats requested, but the " +
            "file name is empty.", sourceFileInfo)
    end

    case format
    when :iCal
      generateICal
    when :html
      generateHTML
      copyAuxiliaryFiles
    when :csv
      generateCSV
    when :ctags
      generateCTags
    when :niku
      generateNiku
    when :tjp
      generateTJP
    when :mspxml
      generateMspXml
    else
      raise 'Unknown report output format #{format}.'
    end
  end

  TjTime.setTimeZone(oldTimeZone)
  0
end
generateIntermediateFormat() click to toggle source

Generate an output format agnostic version that can later be turned into the respective output formats.

# File lib/taskjuggler/reports/Report.rb, line 112
def generateIntermediateFormat
  if get('scenarios').empty?
    warning('all_scenarios_disabled',
            "The report #{fullId} has only disabled scenarios. The " +
            "report will possibly be empty.")
  end

  @content = nil
  case @typeSpec
  when :accountreport
    @content = AccountListRE.new(self)
  when :export
    @content = ExportRE.new(self)
  when :iCal
    @content = ICalReport.new(self)
  when :niku
    @content = NikuReport.new(self)
  when :resourcereport
    @content = ResourceListRE.new(self)
  when :tagfile
    @content = TagFile.new(self)
  when :textreport
    @content = TextReport.new(self)
  when :taskreport
    @content = TaskListRE.new(self)
  when :tracereport
    @content = TraceReport.new(self)
  when :statusSheet
    @content = StatusSheetReport.new(self)
  when :timeSheet
    @content = TimeSheetReport.new(self)
  else
    raise "Unknown report type"
  end

  # Most output format can be generated from a common intermediate
  # representation of the elements. We generate that IR first.
  @content.generateIntermediateFormat if @content
end
interactive?() click to toggle source

Return true if the report should be rendered in the interactive version, false if not. The top-level report defines the output format and the interactive setting.

# File lib/taskjuggler/reports/Report.rb, line 160
def interactive?
  @project.reportContexts.first.report.get('interactive')
end
to_html() click to toggle source

Render the content of the report as HTML (without the framing).

# File lib/taskjuggler/reports/Report.rb, line 153
def to_html
  @content ? @content.to_html : nil
end

Private Instance Methods

a(attribute) click to toggle source

Convenience function to access a report attribute

# File lib/taskjuggler/reports/Report.rb, line 166
def a(attribute)
  get(attribute)
end
absoluteFileName(name) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 476
def absoluteFileName(name)
  (absoluteFileName?(name) ? '' : @project.outputDir) + name
end
absoluteFileName?(name) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 468
def absoluteFileName?(name)
  if windowsOS?
    name[0] =~ /[a-zA-Z]/ && name[1] == ?:
  else
    name[0] == ?/
  end
end
checkFileName(name) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 455
def checkFileName(name)
  if windowsOS?
    illegalChars = /[\x00\\\*\?\"<>\|]/
  else
    illegalChars = /[\\?%*:|"<>]/
  end
  if name =~ illegalChars
    error('invalid_file_name',
          'File names may not contain any of the following characters: ' +
          '\?%*:|\"<>', sourceFileInfo)
  end
end
copyAuxiliaryFiles() click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 391
def copyAuxiliaryFiles
  # Don't copy files if output is stdout, the requested by the web server
  # or the user has specified a custom aux directory.
  return if @name == '.' || a('interactive') || !a('auxdir').empty?

  copyDirectory('css')
  copyDirectory('icons')
  copyDirectory('scripts')
end
copyDirectory(dirName) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 401
def copyDirectory(dirName)
  # The directory needs to be in the same directory as the HTML report.
  auxDstDir = File.dirname(absoluteFileName(@name)) + '/'
  # Find the data directory that came with the TaskJuggler installation.
  auxSrcDir = AppConfig.dataDirs("data/#{dirName}")[0]
  # Raise an error if we haven't found the data directory
  if auxSrcDir.nil? || !File.exist?(auxSrcDir)
    dataDirError(dirName, AppConfig.dataSearchDirs("data/#{dirName}"))
  end
  # Don't copy directory if all files are up-to-date.
  return if directoryUpToDate?(auxSrcDir, auxDstDir + dirName)

  begin
    # Recursively copy the directory and all content.
    FileUtils.cp_r(auxSrcDir, auxDstDir)
  rescue IOError, SystemCallError
    error('copy_dir', "Cannot copy directory #{auxSrcDir} to " +
                      "#{auxDstDir}.\n#{$!}", sourceFileInfo)
  end
end
dataDirError(dirName, dirs) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 436
    def dataDirError(dirName, dirs)
      error('data_dir_error', <<"EOT",
Cannot find the #{dirName} directory. This is usually the result of an
improper TaskJuggler installation. If you know the directory, you can use the
TASKJUGGLER_DATA_PATH environment variable to specify the location.  The
variable should be set to the path without the /data at the end. Multiple
directories must be separated by colons. The following directories have been
tried:

#{dirs.join("\n")}
EOT
            sourceFileInfo
           )
    end
directoryUpToDate?(auxSrcDir, auxDstDir) click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 422
def directoryUpToDate?(auxSrcDir, auxDstDir)
  return false unless File.exist?(auxDstDir)

  Dir.entries(auxSrcDir).each do |file|
    next if file == '.' || file == '..'

    srcFile = (auxSrcDir + '/' + file)
    dstFile = (auxDstDir + '/' + file)
    return false if !File.exist?(dstFile) ||
                    File.mtime(srcFile) > File.mtime(dstFile)
  end
  true
end
generateCSV() click to toggle source

Generate a CSV version of the report.

# File lib/taskjuggler/reports/Report.rb, line 263
def generateCSV
  # The CSV format can only handle the first element of a report.
  return nil unless @content

  unless @content.respond_to?('to_csv')
    warning('csv_not_supported',
            "CSV format is not supported for report #{@id} of " +
            "type #{@typeSpec}.")
    return nil
  end

  return nil unless (csv = @content.to_csv)

  # Use the CSVFile class to write the Array of Arrays to a colon
  # separated file. Write to $stdout if the filename was set to '.'.
  begin
    fileName = (@name == '.' ? '.' : absoluteFileName(@name) + '.csv')
    CSVFile.new(csv, ';').write(fileName)
  rescue IOError, SystemCallError
    error('write_csv', "Cannot write to file #{fileName}.\n#{$!}",
          sourceFileInfo)
  end
end
generateCTags() click to toggle source

Generate ctags file

# File lib/taskjuggler/reports/Report.rb, line 373
def generateCTags
  unless @content.respond_to?('to_ctags')
    warning('ctags_not_supported',
            "ctags format is not supported for report #{@id} of " +
            "type #{@typeSpec}.")
    return nil
  end

  begin
    f = @name == '.' ? $stdout :
      File.new(absoluteFileName(@name), 'w')
    f.puts "#{@content.to_ctags}"
  rescue IOError, SystemCallError
    error('write_ctags', "Cannot write to file #{@name}.\n#{$!}",
          sourceFileInfo)
  end
end
generateHTML() click to toggle source

Generate an HTML version of the report.

# File lib/taskjuggler/reports/Report.rb, line 171
    def generateHTML
      return nil unless @content

      unless @content.respond_to?('to_html')
        warning('html_not_supported',
                "HTML format is not supported for report #{@id} of " +
                "type #{@typeSpec}.")
        return nil
      end

      html = HTMLDocument.new
      head = html.generateHead(@project['name'] + " - #{get('title') || @name}",
                               { 'description' => 'TaskJuggler Report',
                                 'keywords' =>
                                   'taskjuggler, project, management' },
                               a('rawHtmlHead'))
      if a('selfcontained')
        auxSrcDir = AppConfig.dataDirs('data/css')[0]
        cssFileName = (auxSrcDir ? auxSrcDir + '/tjreport.css' : '')
        # Raise an error if we haven't found the data directory
        if auxSrcDir.nil? || !File.exist?(cssFileName)
          dataDirError(cssFileName, AppConfig.dataSearchDirs('data/css'))
        end
        cssFile = IO.read(cssFileName)
        if cssFile.empty?
          error('css_file_error',
                "Cannot read '#{cssFileName}'. Make sure the file is not " +
                "empty and you have read access permission.", sourceFileInfo)
        end
        head << XMLElement.new('meta', 'http-equiv' => 'Content-Style-Type',
                               'content' => 'text/css; charset=utf-8')
        head << (style = XMLElement.new('style', 'type' => 'text/css'))
        style << XMLBlob.new("\n" + cssFile)
      else
        head << XMLElement.new('link', 'rel' => 'stylesheet',
                               'type' => 'text/css',
                               'href' => "#{a('auxdir')}css/tjreport.css")
      end
      html.html <<
        XMLComment.new("Dynamic Report ID: " +
                       "#{@project.reportContexts.last.dynamicReportId}")
      html.html << (body = XMLElement.new('body'))

      unless a('selfcontained')
        body << XMLElement.new('script', 'type' => 'text/javascript',
                               'src' => "#{a('auxdir')}scripts/wz_tooltip.js")
        body << (noscript = XMLElement.new('noscript'))
        noscript << (nsdiv = XMLElement.new('div',
                                            'style' => 'text-align:center; ' +
                                            'color:#FF0000'))
        nsdiv << XMLText.new(<<'EOT'
This page requires Javascript for full functionality. Please enable it
in your browser settings!
EOT
                            )
      end


      # Make sure we have some margins around the report.
      body << (frame = XMLElement.new('div', 'class' => 'tj_page'))

      frame << @content.to_html

      # The footer with some administrative information.
      frame << (div = XMLElement.new('div', 'class' => 'copyright'))
      div << XMLText.new(@project['copyright'] + " - ") if @project['copyright']
      div << XMLText.new("Project: #{@project['name']} " +
                        "Version: #{@project['version']} - " +
                        "Created on #{TjTime.new.to_s("%Y-%m-%d %H:%M:%S")} " +
                        "with ")
      div << XMLNamedText.new("#{AppConfig.softwareName}", 'a',
                             'href' => "#{AppConfig.contact}")
      div << XMLText.new(" v#{AppConfig.version}")

      fileName =
        if a('interactive') || @name == '.'
          # Interactive HTML reports are always sent to stdout.
          '.'
        else
          # Prepend the specified output directory unless the provided file
          # name is an absolute file name.
          absoluteFileName(@name) + '.html'
        end
      begin
        html.write(fileName)
      rescue IOError, SystemCallError
        error('write_html', "Cannot write to file #{fileName}.\n#{$!}",
              sourceFileInfo)
      end
    end
generateICal() click to toggle source

Generate the report in iCal format.

# File lib/taskjuggler/reports/Report.rb, line 354
def generateICal
  unless @content.respond_to?('to_iCal')
    warning('ical_not_supported',
            "iCalendar format is not supported for report #{@id} of " +
            "type #{@typeSpec}.")
    return nil
  end

  begin
    f = @name == '.' ? $stdout :
      File.new(absoluteFileName(@name) + '.ics', 'w')
    f.puts "#{@content.to_iCal}"
  rescue IOError, SystemCallError
    error('write_ical', "Cannot write to file #{@name}.\n#{$!}",
          sourceFileInfo)
  end
end
generateMspXml() click to toggle source

Generate the report in Microsoft Project XML format.

# File lib/taskjuggler/reports/Report.rb, line 312
def generateMspXml
  unless @content.respond_to?('to_mspxml')
    warning('mspxml_not_supported',
            "Microsoft Project XML format is not supported for " +
            "report #{@id} of type #{@typeSpec}.")
    return nil
  end

  begin
    fileName = '.'
    if @name == '.'
      $stdout.write(@content.to_mspxml)
    else
      fileName = absoluteFileName(@name) + '.xml'
      File.open(fileName, 'w') { |f| f.write(@content.to_mspxml) }
    end
  rescue IOError, SystemCallError
    error('write_mspxml', "Cannot write to file #{fileName}.\n#{$!}",
          sourceFileInfo)
  end
end
generateNiku() click to toggle source

Generate Niku report

# File lib/taskjuggler/reports/Report.rb, line 335
def generateNiku
  unless @content.respond_to?('to_niku')
    warning('niku_not_supported',
            "niku format is not supported for report #{@id} of " +
            "type #{@typeSpec}.")
    return nil
  end

  begin
    f = @name == '.' ? $stdout :
      File.new(absoluteFileName(@name) + '.xml', 'w')
    f.puts "#{@content.to_niku}"
  rescue IOError, SystemCallError
    error('write_niku', "Cannot write to file #{@name}.\n#{$!}",
          sourceFileInfo)
  end
end
generateTJP() click to toggle source

Generate the report in TJP format.

# File lib/taskjuggler/reports/Report.rb, line 288
def generateTJP
  unless @content.respond_to?('to_tjp')
    warning('tjp_not_supported',
            "TJP format is not supported for report #{@id} of " +
            "type #{@typeSpec}.")
    return nil
  end

  begin
    fileName = '.'
    if @name == '.'
      $stdout.write(@content.to_tjp)
    else
      fileName = @name
      fileName += a('definitions').include?('project') ? '.tjp' : '.tji'
      File.open(fileName, 'w') { |f| f.write(@content.to_tjp) }
    end
  rescue IOError, SystemCallError
    error('write_tjp', "Cannot write to file #{fileName}.\n#{$!}",
          sourceFileInfo)
  end
end
windowsOS?() click to toggle source
# File lib/taskjuggler/reports/Report.rb, line 451
def windowsOS?
  (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
end