Class: Exporter

Inherits:
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.



36
37
38
39
40
41
42
43
44
# File 'lib/jirametrics/exporter.rb', line 36

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.



19
20
21
# File 'lib/jirametrics/exporter.rb', line 19

def file_system
  @file_system
end

#project_configsObject (readonly)

Returns the value of attribute project_configs.



18
19
20
# File 'lib/jirametrics/exporter.rb', line 18

def project_configs
  @project_configs
end

Class Method Details

.configure(&block) ⇒ Object



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

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



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

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
81
# File 'lib/jirametrics/examples/aggregated_project.rb', line 11

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

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

    file_prefix name

    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.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



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/jirametrics/exporter.rb', line 53

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
    downloader = Downloader.new(
      download_config: project.download_config,
      file_system: file_system,
      jira_gateway: JiraGateway.new(file_system: file_system)
    )
    downloader.run
  end
  puts "Full output from downloader in #{file_system.logfile_name}"
end

#downloading?Boolean

Returns:

  • (Boolean)


106
107
108
# File 'lib/jirametrics/exporter.rb', line 106

def downloading?
  @downloading
end

#each_project_config(name_filter:) ⇒ Object



100
101
102
103
104
# File 'lib/jirametrics/exporter.rb', line 100

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



46
47
48
49
50
51
# File 'lib/jirametrics/exporter.rb', line 46

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



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/jirametrics/exporter.rb', line 142

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(keys, name_filter:) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/jirametrics/exporter.rb', line 75

def info keys, name_filter:
  selected = []
  each_project_config(name_filter: name_filter) do |project|
    project.evaluate_next_level
    # next if project.aggregated_project?

    project.run load_only: true
    project.board_configs.each do |board_config|
      board_config.run
    end
    project.issues.each do |issue|
      selected << [project, issue] if keys.include? 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

  selected.each do |project, issue|
    puts "\nProject #{project.name}"
    puts issue.dump
  end
end

#jira_config(filename = nil) ⇒ Object



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

def jira_config filename = nil
  if filename
    @jira_config = file_system.load_json(filename)
    @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
  end
  @jira_config
end

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



110
111
112
113
114
115
116
# File 'lib/jirametrics/exporter.rb', line 110

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
# 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
    self.anonymize if anonymize

    self.settings.merge! settings

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

    file_prefix file_prefix
    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

    boards.each_key do |board_id|
      block = boards[board_id]
      if block == :default
        block = lambda do |_|
          start_at first_time_in_status_category('In Progress')
          stop_at still_in_status_category('Done')
        end
      end
      board id: board_id do
        cycletime(&block)
      end
    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

        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



120
121
122
123
124
125
126
127
# File 'lib/jirametrics/exporter.rb', line 120

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



137
138
139
140
# File 'lib/jirametrics/exporter.rb', line 137

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

#xproject(*args) ⇒ Object



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

def xproject *args; end