Class: Exporter

Inherits:
Object
  • Object
show all
Defined in:
lib/jirametrics/examples/standard_project.rb,
lib/jirametrics/exporter.rb,
lib/jirametrics/examples/aggregated_project.rb

Overview

This file is really intended to give you ideas about how you might configure your own reports, not as a complete setup that will work in every case.

The point of an AGGREGATED report is that we’re now looking at a higher level. We might use this in a S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a single team. For that reason, we look at slightly different things that we would on a single team board.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_system: FileSystem.new) ⇒ Exporter

Returns a new instance of Exporter.



24
25
26
27
28
29
30
31
32
# File 'lib/jirametrics/exporter.rb', line 24

def initialize file_system: FileSystem.new
  @project_configs = []
  @target_path = '.'
  @holiday_dates = []
  @downloading = false
  @file_system = file_system

  timezone_offset '+00:00'
end

Instance Attribute Details

#file_systemObject

Returns the value of attribute file_system.



7
8
9
# File 'lib/jirametrics/exporter.rb', line 7

def file_system
  @file_system
end

#project_configsObject (readonly)

Returns the value of attribute project_configs.



6
7
8
# File 'lib/jirametrics/exporter.rb', line 6

def project_configs
  @project_configs
end

Class Method Details

.configure(&block) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/jirametrics/exporter.rb', line 9

def self.configure &block
  logfile_name = 'jirametrics.log'
  logfile = File.open logfile_name, 'w'
  file_system = FileSystem.new
  file_system.logfile = logfile
  file_system.logfile_name = logfile_name

  exporter = Exporter.new file_system: file_system

  exporter.instance_eval(&block)
  @@instance = exporter
end

.instanceObject



22
# File 'lib/jirametrics/exporter.rb', line 22

def self.instance = @@instance

Instance Method Details

#aggregated_project(name:, project_names:, settings: {}) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/jirametrics/examples/aggregated_project.rb', line 11

def aggregated_project name:, project_names:, settings: {}
  project name: name do
    puts name
    file_prefix name
    self.settings.merge! settings

    aggregate do
      project_names.each do |project_name|
        include_issues_from project_name
      end
    end

    file do
      file_suffix '.html'
      issues.reject! do |issue|
        %w[Sub-task Epic].include? issue.type
      end

      html_report do
        html '<h1>Boards included in this report</h1><ul>', type: :header
        board_lines = []
        included_projects.each do |project|
          project.all_boards.each_value do |board|
            board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}"
          end
        end
        board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
        html '</ul>', type: :header

        cycletime_scatterplot do
          show_trend_lines
          # For an aggregated report we group by board rather than by type
          grouping_rules do |issue, rules|
            rules.label = issue.board.name
          end
        end
        # aging_work_in_progress_chart
        daily_wip_by_parent_chart do
          # When aggregating, the chart tends to need more vertical space
          canvas height: 400, width: 800
        end
        aging_work_table do
          # In an aggregated report, we likely only care about items that are old so exclude anything
          # under 21 days.
          age_cutoff 21
        end

        dependency_chart do
          header_text 'Dependencies across boards'
          description_text 'We are only showing dependencies across boards.'

          # By default, the issue doesn't show what board it's on and this is important for an
          # aggregated view
          chart = self
          issue_rules do |issue, rules|
            chart.default_issue_rules.call(issue, rules)
            rules.label = rules.label.split('<BR/>').insert(1, "Board: #{issue.board.name}").join('<BR/>')
          end

          link_rules do |link, rules|
            chart.default_link_rules.call(link, rules)

            # Because this is the aggregated view, let's hide any link that doesn't cross boards.
            rules.ignore if link.origin.board == link.other_issue.board
          end
        end
      end
    end
  end
end

#download(name_filter:) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/jirametrics/exporter.rb', line 41

def download name_filter:
  @downloading = true
  each_project_config(name_filter: name_filter) do |project|
    project.evaluate_next_level
    next if project.aggregated_project?

    unless project.download_config
      raise "Project #{project.name.inspect} is missing a download section in the config. " \
        'That is required in order to download'
    end

    project.download_config.run
  # load_jira_config(download_config.project_config.jira_config)
  # @ignore_ssl_errors = download_config.project_config.settings['ignore_ssl_errors']
    gateway = JiraGateway.new(
      file_system: file_system, jira_config: project.jira_config, settings: project.settings
    )
    downloader = Downloader.create(
      download_config: project.download_config,
      file_system: file_system,
      jira_gateway: gateway
    )
    downloader.run
  end
  puts "Full output from downloader in #{file_system.logfile_name}"
end

#downloading?Boolean

Returns:

  • (Boolean)


99
100
101
# File 'lib/jirametrics/exporter.rb', line 99

def downloading?
  @downloading
end

#each_project_config(name_filter:) ⇒ Object



93
94
95
96
97
# File 'lib/jirametrics/exporter.rb', line 93

def each_project_config name_filter:
  @project_configs.each do |project|
    yield project if project.name.nil? || File.fnmatch(name_filter, project.name)
  end
end

#export(name_filter:) ⇒ Object



34
35
36
37
38
39
# File 'lib/jirametrics/exporter.rb', line 34

def export name_filter:
  each_project_config(name_filter: name_filter) do |project|
    project.evaluate_next_level
    project.run
  end
end

#holiday_dates(*args) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/jirametrics/exporter.rb', line 137

def holiday_dates *args
  unless args.empty?
    dates = []
    args.each do |arg|
      if arg =~ /^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/
        Date.parse($1).upto(Date.parse($2)).each { |date| dates << date }
      else
        dates << Date.parse(arg)
      end
    end
    @holiday_dates = dates
  end
  @holiday_dates
end

#info(key, name_filter:) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/jirametrics/exporter.rb', line 68

def info key, name_filter:
  selected = []
  each_project_config(name_filter: name_filter) do |project|
    project.evaluate_next_level

    project.run load_only: true
    project.issues.each do |issue|
      selected << [project, issue] if key == issue.key
    end
  rescue => e # rubocop:disable Style/RescueStandardError
    # This happens when we're attempting to load an aggregated project because it hasn't been
    # properly initialized. Since we don't care about aggregated projects, we just ignore it.
    raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
  end

  if selected.empty?
    file_system.log "No issues found to match #{key.inspect}"
  else
    selected.each do |project, issue|
      file_system.log "\nProject #{project.name}", also_write_to_stderr: true
      file_system.log issue.dump, also_write_to_stderr: true
    end
  end
end

#jira_config(filename = nil) ⇒ Object



122
123
124
125
126
127
128
129
130
# File 'lib/jirametrics/exporter.rb', line 122

def jira_config filename = nil
  if filename
    @jira_config = file_system.load_json(filename, fail_on_error: false)
    raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil?

    @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
  end
  @jira_config
end

#project(name: nil, &block) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/jirametrics/exporter.rb', line 103

def project name: nil, &block
  raise 'jira_config not set' if @jira_config.nil?

  @project_configs << ProjectConfig.new(
    exporter: self, target_path: @target_path, jira_config: @jira_config, block: block, name: name
  )
end

#standard_project(name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {}, default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}, rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],, show_experimental_charts: false) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/jirametrics/examples/standard_project.rb', line 6

def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
    default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
    rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],
    show_experimental_charts: false

  project name: name do
    puts name
    file_prefix file_prefix

    self.anonymize if anonymize
    self.settings.merge! settings

    boards.each_key do |board_id|
      block = boards[board_id]
      if block == :default
        block = lambda do |_|
          start_at first_time_in_status_category(:indeterminate)
          stop_at still_in_status_category(:done)
        end
      end
      board id: board_id do
        cycletime(&block)
      end
    end

    status_category_mappings.each do |status, category|
      status_category_mapping status: status, category: category
    end

    download do
      self.rolling_date_count(rolling_date_count) if rolling_date_count
      self.no_earlier_than(no_earlier_than) if no_earlier_than
    end

    issues.reject! do |issue|
      ignore_types.include? issue.type
    end

    discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses

    file do
      file_suffix '.html'

      issues.reject! { |issue| ignore_issues.include? issue.key } if ignore_issues

      html_report do
        board_id default_board if default_board

        html "<H1>#{name}</H1>", type: :header
        boards.each_key do |id|
          board = find_board id
          html "<div><a href='#{board.url}'>#{id} #{board.name}</a> (#{board.board_type})</div>",
               type: :header
        end

        daily_view

        cycletime_scatterplot do
          show_trend_lines
        end
        cycletime_histogram

        throughput_chart do
          description_text '<h2>Number of items completed, grouped by issue type</h2>'
        end
        throughput_chart do
          header_text nil
          description_text '<h2>Number of items completed, grouped by completion status and resolution</h2>'
          grouping_rules do |issue, rules|
            if issue.resolution
              rules.label = "#{issue.status.name}:#{issue.resolution}"
            else
              rules.label = issue.status.name
            end
          end
        end

        aging_work_in_progress_chart
        aging_work_bar_chart
        aging_work_table
        daily_wip_by_age_chart
        daily_wip_by_blocked_stalled_chart
        daily_wip_by_parent_chart
        flow_efficiency_scatterplot if show_experimental_charts
        expedited_chart
        sprint_burndown
        estimate_accuracy_chart
        dependency_chart
      end
    end
  end
end

#target_path(path = nil) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/jirametrics/exporter.rb', line 113

def target_path path = nil
  unless path.nil?
    @target_path = path
    @target_path += File::SEPARATOR unless @target_path.end_with? File::SEPARATOR
    FileUtils.mkdir_p @target_path
  end
  @target_path
end

#timezone_offset(offset = nil) ⇒ Object



132
133
134
135
# File 'lib/jirametrics/exporter.rb', line 132

def timezone_offset offset = nil
  @timezone_offset = offset unless offset.nil?
  @timezone_offset
end

#xproject(*args) ⇒ Object



111
# File 'lib/jirametrics/exporter.rb', line 111

def xproject *args; end