jirametrics 2.2.1 → 2.4pre1

Sign up to get free protection for your applications and to get access to all the features.
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