jirametrics 2.2.1 → 2.4pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +13 -25
  3. data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
  5. data/lib/jirametrics/aging_work_table.rb +9 -26
  6. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  7. data/lib/jirametrics/board_config.rb +2 -2
  8. data/lib/jirametrics/change_item.rb +13 -5
  9. data/lib/jirametrics/chart_base.rb +27 -39
  10. data/lib/jirametrics/columns_config.rb +4 -0
  11. data/lib/jirametrics/cycletime_histogram.rb +1 -1
  12. data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
  13. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  14. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +3 -16
  15. data/lib/jirametrics/daily_wip_chart.rb +1 -13
  16. data/lib/jirametrics/data_quality_report.rb +4 -1
  17. data/lib/jirametrics/dependency_chart.rb +1 -1
  18. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
  19. data/lib/jirametrics/examples/standard_project.rb +1 -1
  20. data/lib/jirametrics/expedited_chart.rb +3 -1
  21. data/lib/jirametrics/exporter.rb +3 -3
  22. data/lib/jirametrics/file_config.rb +12 -8
  23. data/lib/jirametrics/file_system.rb +11 -2
  24. data/lib/jirametrics/groupable_issue_chart.rb +2 -4
  25. data/lib/jirametrics/hierarchy_table.rb +4 -4
  26. data/lib/jirametrics/html/aging_work_table.erb +3 -3
  27. data/lib/jirametrics/html/index.erb +1 -0
  28. data/lib/jirametrics/html_report_config.rb +61 -74
  29. data/lib/jirametrics/issue.rb +129 -57
  30. data/lib/jirametrics/project_config.rb +13 -7
  31. data/lib/jirametrics/sprint_burndown.rb +11 -0
  32. data/lib/jirametrics/status_collection.rb +4 -1
  33. data/lib/jirametrics/throughput_chart.rb +1 -1
  34. data/lib/jirametrics.rb +1 -1
  35. metadata +5 -7
  36. data/lib/jirametrics/experimental/generator.rb +0 -210
  37. data/lib/jirametrics/experimental/info.rb +0 -77
  38. /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class StoryPointAccuracyChart < ChartBase
4
- def initialize configuration_block = nil
3
+ class EstimateAccuracyChart < ChartBase
4
+ def initialize configuration_block
5
5
  super()
6
6
 
7
7
  header_text 'Estimate Accuracy'
@@ -12,7 +12,7 @@ class StoryPointAccuracyChart < ChartBase
12
12
  </div>
13
13
  <div class="p">
14
14
  The #{color_block '--estimate-accuracy-chart-completed-fill-color'} completed dots indicate
15
- cycletimes.
15
+ cycletimes.
16
16
  <% if @has_aging_data %>
17
17
  The #{color_block '--estimate-accuracy-chart-active-fill-color'} aging dots
18
18
  (click on the legend to turn them on) show the current
@@ -27,7 +27,7 @@ class StoryPointAccuracyChart < ChartBase
27
27
  @y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
28
28
  @y_axis_sort_order = nil
29
29
 
30
- instance_eval(&configuration_block) if configuration_block
30
+ instance_eval(&configuration_block)
31
31
  end
32
32
 
33
33
  def run
@@ -39,26 +39,7 @@ class StoryPointAccuracyChart < ChartBase
39
39
  end
40
40
 
41
41
  def scan_issues
42
- aging_hash = {}
43
- completed_hash = {}
44
-
45
- issues.each do |issue|
46
- cycletime = issue.board.cycletime
47
- start_time = cycletime.started_time(issue)
48
- stop_time = cycletime.stopped_time(issue)
49
-
50
- next unless start_time
51
-
52
- hash = stop_time ? completed_hash : aging_hash
53
-
54
- estimate = @y_axis_block.call issue, start_time
55
- cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
56
-
57
- next if estimate.nil?
58
-
59
- key = [estimate, cycle_time]
60
- (hash[key] ||= []) << issue
61
- end
42
+ completed_hash, aging_hash = split_into_completed_and_aging issues: issues
62
43
 
63
44
  @has_aging_data = !aging_hash.empty?
64
45
 
@@ -93,7 +74,32 @@ class StoryPointAccuracyChart < ChartBase
93
74
  'borderColor' => border_color,
94
75
  'hidden' => starts_hidden
95
76
  }
96
- end.compact
77
+ end
78
+ end
79
+
80
+ def split_into_completed_and_aging issues:
81
+ aging_hash = {}
82
+ completed_hash = {}
83
+
84
+ issues.each do |issue|
85
+ cycletime = issue.board.cycletime
86
+ start_time = cycletime.started_time(issue)
87
+ stop_time = cycletime.stopped_time(issue)
88
+
89
+ next unless start_time
90
+
91
+ hash = stop_time ? completed_hash : aging_hash
92
+
93
+ estimate = @y_axis_block.call issue, start_time
94
+ cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
95
+
96
+ next if estimate.nil?
97
+
98
+ key = [estimate, cycle_time]
99
+ (hash[key] ||= []) << issue
100
+ end
101
+
102
+ [completed_hash, aging_hash]
97
103
  end
98
104
 
99
105
  def hash_sorter
@@ -86,7 +86,7 @@ class Exporter
86
86
  daily_wip_by_parent_chart
87
87
  expedited_chart
88
88
  sprint_burndown
89
- story_point_accuracy_chart
89
+ estimate_accuracy_chart
90
90
 
91
91
  dependency_chart do
92
92
  link_rules do |link, rules|
@@ -20,7 +20,7 @@ class ExpeditedChart < ChartBase
20
20
  attr_accessor :issues, :cycletime, :possible_statuses, :date_range
21
21
  attr_reader :expedited_label
22
22
 
23
- def initialize
23
+ def initialize block
24
24
  super()
25
25
 
26
26
  header_text 'Expedited work'
@@ -38,6 +38,8 @@ class ExpeditedChart < ChartBase
38
38
  </div>
39
39
  #{describe_non_working_days}
40
40
  HTML
41
+
42
+ instance_eval(&block)
41
43
  end
42
44
 
43
45
  def run
@@ -5,7 +5,7 @@ require 'fileutils'
5
5
  class Object
6
6
  def deprecated message:, date:
7
7
  text = +''
8
- text << "Deprecated(#{date}):"
8
+ text << "Deprecated(#{date}): "
9
9
  text << message
10
10
  text << "\n-> Called from #{caller(1..1).first}"
11
11
  warn text
@@ -32,11 +32,12 @@ class Exporter
32
32
 
33
33
  def initialize file_system: FileSystem.new
34
34
  @project_configs = []
35
- @timezone_offset = '+00:00'
36
35
  @target_path = '.'
37
36
  @holiday_dates = []
38
37
  @downloading = false
39
38
  @file_system = file_system
39
+
40
+ timezone_offset '+00:00'
40
41
  end
41
42
 
42
43
  def export name_filter:
@@ -79,7 +80,6 @@ class Exporter
79
80
  end
80
81
 
81
82
  def project name: nil, &block
82
- raise 'target_path was never set!' if @target_path.nil?
83
83
  raise 'jira_config not set' if @jira_config.nil?
84
84
 
85
85
  @project_configs << ProjectConfig.new(
@@ -5,10 +5,11 @@ require 'csv'
5
5
  class FileConfig
6
6
  attr_reader :project_config, :issues
7
7
 
8
- def initialize project_config:, block:
8
+ def initialize project_config:, block:, today: Date.today
9
9
  @project_config = project_config
10
10
  @block = block
11
11
  @columns = nil
12
+ @today = today
12
13
  end
13
14
 
14
15
  def run
@@ -18,11 +19,8 @@ class FileConfig
18
19
  if @columns
19
20
  all_lines = prepare_grid
20
21
 
21
- File.open(output_filename, 'w') do |file|
22
- all_lines.each do |output_line|
23
- file.puts CSV.generate_line(output_line)
24
- end
25
- end
22
+ content = all_lines.collect { |line| CSV.generate_line line }.join
23
+ project_config.exporter.file_system.save_file content: content, filename: output_filename
26
24
  elsif @html_report
27
25
  @html_report.run
28
26
  else
@@ -59,7 +57,7 @@ class FileConfig
59
57
  segments = []
60
58
  segments << project_config.target_path
61
59
  segments << project_config.file_prefix
62
- segments << (@file_suffix || "-#{Date.today}.csv")
60
+ segments << (@file_suffix || "-#{@today}.csv")
63
61
  segments.join
64
62
  end
65
63
 
@@ -68,7 +66,9 @@ class FileConfig
68
66
  # is that all empty values in the first column should be at the bottom.
69
67
  def sort_output all_lines
70
68
  all_lines.sort do |a, b|
71
- if a[0].nil?
69
+ if a[0] == b[0]
70
+ a[1..] <=> b[1..]
71
+ elsif a[0].nil?
72
72
  1
73
73
  elsif b[0].nil?
74
74
  -1
@@ -112,6 +112,10 @@ class FileConfig
112
112
  object.to_s
113
113
  end
114
114
 
115
+ def to_integer object
116
+ object.to_i
117
+ end
118
+
115
119
  def file_suffix suffix = nil
116
120
  @file_suffix = suffix unless suffix.nil?
117
121
  @file_suffix
@@ -5,17 +5,26 @@ require 'json'
5
5
  class FileSystem
6
6
  attr_accessor :logfile, :logfile_name
7
7
 
8
+ # Effectively the same as File.read except it forces the encoding to UTF-8
9
+ def load filename
10
+ File.read filename, encoding: 'UTF-8'
11
+ end
12
+
8
13
  def load_json filename, fail_on_error: true
9
14
  return nil if fail_on_error == false && File.exist?(filename) == false
10
15
 
11
- JSON.parse File.read(filename)
16
+ JSON.parse load(filename)
12
17
  end
13
18
 
14
19
  def save_json json:, filename:
20
+ save_file content: JSON.pretty_generate(compress json), filename: filename
21
+ end
22
+
23
+ def save_file content:, filename:
15
24
  file_path = File.dirname(filename)
16
25
  FileUtils.mkdir_p file_path unless File.exist?(file_path)
17
26
 
18
- File.write(filename, JSON.pretty_generate(compress json))
27
+ File.write(filename, content)
19
28
  end
20
29
 
21
30
  def log message
@@ -5,10 +5,8 @@ require 'jirametrics/grouping_rules'
5
5
 
6
6
  module GroupableIssueChart
7
7
  def init_configuration_block user_provided_block, &default_block
8
- if user_provided_block
9
- instance_eval(&user_provided_block)
10
- return if @group_by_block
11
- end
8
+ instance_eval(&user_provided_block)
9
+ return if @group_by_block
12
10
 
13
11
  instance_eval(&default_block)
14
12
  end
@@ -3,15 +3,15 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class HierarchyTable < ChartBase
6
- def initialize block = nil
6
+ def initialize block
7
7
  super()
8
8
 
9
9
  header_text 'Hierarchy Table'
10
- description_text <<-HTML
11
- <p>content goes here</p>
10
+ description_text <<~HTML
11
+ <p>Shows all issues through this time period and the full hierarchy of their parents.</p>
12
12
  HTML
13
13
 
14
- instance_eval(&block) if block
14
+ instance_eval(&block)
15
15
  end
16
16
 
17
17
  def run
@@ -7,7 +7,7 @@
7
7
  <th>Issue</th>
8
8
  <th>Status</th>
9
9
  <th>Fix versions</th>
10
- <% if any_scrum_boards? %>
10
+ <% if any_scrum_boards %>
11
11
  <th>Sprints</th>
12
12
  <% end %>
13
13
  <th><%= aggregated_project? ? 'Board' : 'Who' %></th>
@@ -40,9 +40,9 @@
40
40
  </div>
41
41
  <% end %>
42
42
  </td>
43
- <td><%= format_status issue.status.name, board: issue.board %><%= unmapped_status_text(issue) unless current_status_visible? issue %></td>
43
+ <td><%= format_status issue.status.name, board: issue.board %></td>
44
44
  <td><%= fix_versions_text(issue) %></td>
45
- <% if any_scrum_boards? %>
45
+ <% if any_scrum_boards %>
46
46
  <td><%= sprints_text(issue) %></td>
47
47
  <% end %>
48
48
  <td><%= aggregated_project? ? issue.board.name : issue.assigned_to %></td>
@@ -1,6 +1,7 @@
1
1
  <html>
2
2
  <head>
3
3
  <meta charset="UTF-8">
4
+ <link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
4
5
  <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
5
6
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
7
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
@@ -9,18 +9,45 @@ class HtmlReportConfig
9
9
 
10
10
  attr_reader :file_config, :sections
11
11
 
12
+ def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
13
+ lines = []
14
+ lines << "def #{name} &block"
15
+ lines << ' block = ->(_) {} unless block'
16
+ if deprecated_warning
17
+ lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
18
+ end
19
+ lines << " execute_chart #{classname}.new(block)"
20
+ lines << 'end'
21
+ module_eval lines.join("\n"), __FILE__, __LINE__
22
+ end
23
+
24
+ define_chart name: 'aging_work_bar_chart', classname: 'AgingWorkBarChart'
25
+ define_chart name: 'aging_work_table', classname: 'AgingWorkTable'
26
+ define_chart name: 'cycletime_scatterplot', classname: 'CycletimeScatterplot'
27
+ define_chart name: 'daily_wip_chart', classname: 'DailyWipChart'
28
+ define_chart name: 'daily_wip_by_age_chart', classname: 'DailyWipByAgeChart'
29
+ define_chart name: 'daily_wip_by_blocked_stalled_chart', classname: 'DailyWipByBlockedStalledChart'
30
+ define_chart name: 'daily_wip_by_parent_chart', classname: 'DailyWipByParentChart'
31
+ define_chart name: 'throughput_chart', classname: 'ThroughputChart'
32
+ define_chart name: 'expedited_chart', classname: 'ExpeditedChart'
33
+ define_chart name: 'cycletime_histogram', classname: 'CycletimeHistogram'
34
+ define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
35
+ define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
36
+
37
+ define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
38
+ deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
39
+ define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
40
+ deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
41
+
12
42
  def initialize file_config:, block:
13
43
  @file_config = file_config
14
44
  @block = block
15
- # @cycletimes = []
16
45
  @sections = []
17
46
  end
18
47
 
19
48
  def cycletime label = nil, &block
20
- # TODO: This is about to become deprecated
21
-
22
49
  @file_config.project_config.all_boards.each_value do |board|
23
- raise 'Multiple cycletimes not supported yet' if board.cycletime
50
+ raise 'Multiple cycletimes not supported' if board.cycletime
24
51
 
25
52
  board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
26
53
  end
@@ -39,21 +66,36 @@ class HtmlReportConfig
39
66
  execute_chart DataQualityReport.new(@original_issue_times || {})
40
67
  @sections.rotate!(-1)
41
68
 
42
- File.open @file_config.output_filename, 'w' do |file|
43
- html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
44
- css = load_css html_directory: html_directory
45
- erb = ERB.new File.read(File.join(html_directory, 'index.erb'))
46
- file.puts erb.result(binding)
47
- end
69
+ html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
70
+ css = load_css html_directory: html_directory
71
+ erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
72
+ file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
73
+ end
74
+
75
+ def file_system
76
+ @file_config.project_config.exporter.file_system
77
+ end
78
+
79
+ def log message
80
+ file_system.log message
48
81
  end
49
82
 
50
83
  def load_css html_directory:
51
- base_css = File.read(File.join(html_directory, 'index.css'))
84
+ base_css_filename = File.join(html_directory, 'index.css')
85
+ base_css = file_system.load(base_css_filename)
86
+ log("Loaded CSS: #{base_css_filename}")
87
+
52
88
  extra_css_filename = settings['include_css']
53
- return base_css unless extra_css_filename && File.exist?(extra_css_filename)
89
+ if extra_css_filename
90
+ if File.exist?(extra_css_filename)
91
+ base_css << "\n\n" << file_system.load(extra_css_filename)
92
+ log("Loaded CSS: #{extra_css_filename}")
93
+ else
94
+ log("Unable to find specified CSS file: #{extra_css_filename}")
95
+ end
96
+ end
54
97
 
55
- @file_config.project_config.exporter.file_system.log("including css from file: #{extra_css_filename}")
56
- base_css << "\n\n" << File.read(extra_css_filename)
98
+ base_css
57
99
  end
58
100
 
59
101
  def board_id id = nil
@@ -66,6 +108,8 @@ class HtmlReportConfig
66
108
  end
67
109
 
68
110
  def aging_work_in_progress_chart board_id: nil, &block
111
+ block ||= ->(_) {}
112
+
69
113
  if board_id.nil?
70
114
  ids = issues.collect { |i| i.board.id }.uniq.sort
71
115
  else
@@ -79,50 +123,6 @@ class HtmlReportConfig
79
123
  end
80
124
  end
81
125
 
82
- def aging_work_bar_chart &block
83
- execute_chart AgingWorkBarChart.new(block)
84
- end
85
-
86
- def aging_work_table &block
87
- execute_chart AgingWorkTable.new(block)
88
- end
89
-
90
- def cycletime_scatterplot &block
91
- execute_chart CycletimeScatterplot.new block
92
- end
93
-
94
- def daily_wip_chart &block
95
- execute_chart DailyWipChart.new(block)
96
- end
97
-
98
- def daily_wip_by_age_chart &block
99
- execute_chart DailyWipByAgeChart.new block
100
- end
101
-
102
- def daily_wip_by_type &block
103
- execute_chart DailyWipChart.new block
104
- end
105
-
106
- def daily_wip_by_blocked_stalled_chart
107
- execute_chart DailyWipByBlockedStalledChart.new
108
- end
109
-
110
- def daily_wip_by_parent_chart &block
111
- execute_chart DailyWipByParentChart.new block
112
- end
113
-
114
- def throughput_chart &block
115
- execute_chart ThroughputChart.new(block)
116
- end
117
-
118
- def expedited_chart
119
- execute_chart ExpeditedChart.new
120
- end
121
-
122
- def cycletime_histogram &block
123
- execute_chart CycletimeHistogram.new block
124
- end
125
-
126
126
  def random_color
127
127
  "##{Random.bytes(3).unpack1('H*')}"
128
128
  end
@@ -140,14 +140,6 @@ class HtmlReportConfig
140
140
  end
141
141
  end
142
142
 
143
- def story_point_accuracy_chart &block
144
- execute_chart StoryPointAccuracyChart.new block
145
- end
146
-
147
- def hierarchy_table &block
148
- execute_chart HierarchyTable.new block
149
- end
150
-
151
143
  def discard_changes_before_hook issues_cutoff_times
152
144
  # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
153
145
 
@@ -173,6 +165,7 @@ class HtmlReportConfig
173
165
  def execute_chart chart, &after_init_block
174
166
  project_config = @file_config.project_config
175
167
 
168
+ chart.file_system = file_system
176
169
  chart.issues = issues
177
170
  chart.time_range = project_config.time_range
178
171
  chart.timezone_offset = timezone_offset
@@ -199,19 +192,13 @@ class HtmlReportConfig
199
192
  @file_config.issues
200
193
  end
201
194
 
195
+ # For use by the user config
202
196
  def find_board id
203
197
  @file_config.project_config.all_boards[id]
204
198
  end
205
199
 
206
- def project_name
207
- @file_config.project_config.name
208
- end
209
-
200
+ # For use by the user config
210
201
  def boards
211
202
  @file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
212
203
  end
213
-
214
- def find_project_by_name name
215
- @file_config.project_config.exporter.project_configs.find { |p| p.name == name }
216
- end
217
204
  end