jirametrics 2.2.1 → 2.3

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 (32) 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/board_config.rb +2 -2
  7. data/lib/jirametrics/chart_base.rb +27 -39
  8. data/lib/jirametrics/cycletime_histogram.rb +1 -1
  9. data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
  10. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  11. data/lib/jirametrics/daily_wip_chart.rb +1 -13
  12. data/lib/jirametrics/dependency_chart.rb +1 -1
  13. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
  14. data/lib/jirametrics/examples/standard_project.rb +1 -1
  15. data/lib/jirametrics/expedited_chart.rb +3 -1
  16. data/lib/jirametrics/exporter.rb +2 -2
  17. data/lib/jirametrics/file_config.rb +5 -7
  18. data/lib/jirametrics/file_system.rb +11 -2
  19. data/lib/jirametrics/groupable_issue_chart.rb +2 -4
  20. data/lib/jirametrics/hierarchy_table.rb +4 -4
  21. data/lib/jirametrics/html/aging_work_table.erb +3 -3
  22. data/lib/jirametrics/html_report_config.rb +61 -74
  23. data/lib/jirametrics/issue.rb +70 -39
  24. data/lib/jirametrics/project_config.rb +12 -6
  25. data/lib/jirametrics/sprint_burndown.rb +11 -0
  26. data/lib/jirametrics/status_collection.rb +4 -1
  27. data/lib/jirametrics/throughput_chart.rb +1 -1
  28. data/lib/jirametrics.rb +1 -1
  29. metadata +5 -7
  30. data/lib/jirametrics/experimental/generator.rb +0 -210
  31. data/lib/jirametrics/experimental/info.rb +0 -77
  32. /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
@@ -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
 
@@ -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>
@@ -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
@@ -35,16 +35,6 @@ class Issue
35
35
  raise "Unable to initialize #{raw['key']}"
36
36
  end
37
37
 
38
- def sort_changes!
39
- @changes.sort! do |a, b|
40
- # It's common that a resolved will happen at the same time as a status change.
41
- # Put them in a defined order so tests can be deterministic.
42
- compare = a.time <=> b.time
43
- compare = 1 if compare.zero? && a.resolution?
44
- compare
45
- end
46
- end
47
-
48
38
  def key = @raw['key']
49
39
 
50
40
  def type = @raw['fields']['issuetype']['name']
@@ -53,9 +43,7 @@ class Issue
53
43
 
54
44
  def summary = @raw['fields']['summary']
55
45
 
56
- def status
57
- Status.new raw: @raw['fields']['status']
58
- end
46
+ def status = Status.new(raw: @raw['fields']['status'])
59
47
 
60
48
  def labels = @raw['fields']['labels'] || []
61
49
 
@@ -69,37 +57,13 @@ class Issue
69
57
  end
70
58
 
71
59
  def key_as_i
72
- $1.to_i if key =~ /-(\d+)$/
60
+ key =~ /-(\d+)$/ ? $1.to_i : 0
73
61
  end
74
62
 
75
63
  def component_names
76
64
  @raw['fields']['components']&.collect { |component| component['name'] } || []
77
65
  end
78
66
 
79
- def fabricate_change field_name:
80
- first_status = nil
81
- first_status_id = nil
82
-
83
- created_time = parse_time @raw['fields']['created']
84
- first_change = @changes.find { |change| change.field == field_name }
85
- if first_change.nil?
86
- # There have been no changes of this type yet so we have to look at the current one
87
- return nil unless @raw['fields'][field_name]
88
-
89
- first_status = @raw['fields'][field_name]['name']
90
- first_status_id = @raw['fields'][field_name]['id'].to_i
91
- else
92
- # Otherwise, we look at what the first one had changed away from.
93
- first_status = first_change.old_value
94
- first_status_id = first_change.old_value_id
95
- end
96
- ChangeItem.new time: created_time, artificial: true, author: author, raw: {
97
- 'field' => field_name,
98
- 'to' => first_status_id,
99
- 'toString' => first_status
100
- }
101
- end
102
-
103
67
  def first_time_in_status *status_names
104
68
  @changes.find { |change| change.current_status_matches(*status_names) }&.time
105
69
  end
@@ -195,7 +159,7 @@ class Issue
195
159
  end
196
160
 
197
161
  def created
198
- # This shouldn't be necessary and yet we've seen one case where it was.
162
+ # This nil check shouldn't be necessary and yet we've seen one case where it was.
199
163
  parse_time @raw['fields']['created'] if @raw['fields']['created']
200
164
  end
201
165
 
@@ -478,6 +442,31 @@ class Issue
478
442
  comparison
479
443
  end
480
444
 
445
+ def dump
446
+ result = +''
447
+ result << "#{key} (#{type}): #{compact_text summary, 200}\n"
448
+
449
+ assignee = raw['fields']['assignee']
450
+ result << " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n" unless assignee.nil?
451
+
452
+ raw['fields']['issuelinks'].each do |link|
453
+ result << " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}\n" if link['outwardIssue']
454
+ result << " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}\n" if link['inwardIssue']
455
+ end
456
+ changes.each do |change|
457
+ value = change.value
458
+ old_value = change.old_value
459
+
460
+ message = " [change] #{change.time} [#{change.field}] "
461
+ message << "#{compact_text(old_value).inspect} -> " unless old_value.nil? || old_value.empty?
462
+ message << compact_text(value).inspect
463
+ message << " (#{change.author})"
464
+ message << ' <<artificial entry>>' if change.artificial?
465
+ result << message << "\n"
466
+ end
467
+ result
468
+ end
469
+
481
470
  private
482
471
 
483
472
  def assemble_author raw
@@ -508,4 +497,46 @@ class Issue
508
497
  @changes << ChangeItem.new(raw: raw, time: created, author: author, artificial: true)
509
498
  end
510
499
  end
500
+
501
+ def compact_text text, max = 60
502
+ return nil if text.nil?
503
+
504
+ text = text.gsub(/\s+/, ' ').strip
505
+ text = "#{text[0..max]}..." if text.length > max
506
+ text
507
+ end
508
+
509
+ def sort_changes!
510
+ @changes.sort! do |a, b|
511
+ # It's common that a resolved will happen at the same time as a status change.
512
+ # Put them in a defined order so tests can be deterministic.
513
+ compare = a.time <=> b.time
514
+ compare = 1 if compare.zero? && a.resolution?
515
+ compare
516
+ end
517
+ end
518
+
519
+ def fabricate_change field_name:
520
+ first_status = nil
521
+ first_status_id = nil
522
+
523
+ created_time = parse_time @raw['fields']['created']
524
+ first_change = @changes.find { |change| change.field == field_name }
525
+ if first_change.nil?
526
+ # There have been no changes of this type yet so we have to look at the current one
527
+ return nil unless @raw['fields'][field_name]
528
+
529
+ first_status = @raw['fields'][field_name]['name']
530
+ first_status_id = @raw['fields'][field_name]['id'].to_i
531
+ else
532
+ # Otherwise, we look at what the first one had changed away from.
533
+ first_status = first_change.old_value
534
+ first_status_id = first_change.old_value_id
535
+ end
536
+ ChangeItem.new time: created_time, artificial: true, author: author, raw: {
537
+ 'field' => field_name,
538
+ 'to' => first_status_id,
539
+ 'toString' => first_status
540
+ }
541
+ end
511
542
  end
@@ -49,7 +49,7 @@ class ProjectConfig
49
49
  end
50
50
 
51
51
  def load_settings
52
- JSON.parse(File.read(File.join(__dir__, 'settings.json')))
52
+ JSON.parse(file_system.load(File.join(__dir__, 'settings.json')))
53
53
  end
54
54
 
55
55
  def guess_project_id
@@ -89,6 +89,8 @@ class ProjectConfig
89
89
  raise 'Not allowed to have both an aggregate and a download section. Pick only one.' if @download_config
90
90
 
91
91
  @aggregate_config = AggregateConfig.new project_config: self, block: block
92
+
93
+ # Processing of aggregates should only happen during the export
92
94
  return if @exporter.downloading?
93
95
 
94
96
  @aggregate_config.evaluate_next_level
@@ -120,7 +122,7 @@ class ProjectConfig
120
122
 
121
123
  def load_board board_id:, filename:
122
124
  board = Board.new(
123
- raw: JSON.parse(File.read(filename)), possible_statuses: @possible_statuses
125
+ raw: JSON.parse(file_system.load(filename)), possible_statuses: @possible_statuses
124
126
  )
125
127
  board.project_config = self
126
128
  @all_boards[board_id] = board
@@ -162,7 +164,7 @@ class ProjectConfig
162
164
  # We may not always have this file. Load it if we can.
163
165
  return unless File.exist? filename
164
166
 
165
- statuses = JSON.parse(File.read(filename))
167
+ statuses = JSON.parse(file_system.load(filename))
166
168
  .map { |snippet| Status.new(raw: snippet) }
167
169
  statuses
168
170
  .find_all { |status| status.global? }
@@ -178,7 +180,7 @@ class ProjectConfig
178
180
 
179
181
  board_id = $1.to_i
180
182
  timezone_offset = exporter.timezone_offset
181
- JSON.parse(File.read("#{target_path}#{file}"))['values'].each do |json|
183
+ JSON.parse(file_system.load("#{target_path}#{file}"))['values'].each do |json|
182
184
  @all_boards[board_id].sprints << Sprint.new(raw: json, timezone_offset: timezone_offset)
183
185
  end
184
186
  end
@@ -231,7 +233,7 @@ class ProjectConfig
231
233
 
232
234
  def load_project_metadata
233
235
  filename = "#{@target_path}/#{file_prefix}_meta.json"
234
- json = JSON.parse(File.read(filename))
236
+ json = JSON.parse(file_system.load(filename))
235
237
 
236
238
  @data_version = json['version'] || 1
237
239
 
@@ -360,7 +362,7 @@ class ProjectConfig
360
362
  default_board = nil
361
363
 
362
364
  group_filenames_and_board_ids(path: path).each do |filename, board_ids|
363
- content = File.read(File.join(path, filename))
365
+ content = file_system.load(File.join(path, filename))
364
366
  if board_ids == :unknown
365
367
  boards = [(default_board ||= find_default_board)]
366
368
  else
@@ -435,4 +437,8 @@ class ProjectConfig
435
437
  end
436
438
  exporter.file_system.log "Discarded data from #{issues_cutoff_times.count} issues out of a total #{issues.size}"
437
439
  end
440
+
441
+ def file_system
442
+ @exporter.file_system
443
+ end
438
444
  end
@@ -108,6 +108,17 @@ class SprintBurndown < ChartBase
108
108
  result
109
109
  end
110
110
 
111
+ def sprints_in_time_range board
112
+ board.sprints.select do |sprint|
113
+ sprint_end_time = sprint.completed_time || sprint.end_time
114
+ sprint_start_time = sprint.start_time
115
+ next false if sprint_start_time.nil?
116
+
117
+ time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) ||
118
+ (sprint_start_time < time_range.begin && sprint_end_time > time_range.end)
119
+ end || []
120
+ end
121
+
111
122
  # select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
112
123
  def changes_for_one_issue issue:, sprint:
113
124
  story_points = 0.0
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ class StatusNotFoundError < StandardError
4
+ end
5
+
3
6
  class StatusCollection
4
7
  def initialize
5
8
  @list = []
@@ -32,7 +35,7 @@ class StatusCollection
32
35
  next
33
36
  else
34
37
  all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
35
- raise "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
38
+ raise StatusNotFoundError, "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
36
39
  end
37
40
  end
38
41
 
@@ -5,7 +5,7 @@ class ThroughputChart < ChartBase
5
5
 
6
6
  attr_accessor :possible_statuses
7
7
 
8
- def initialize block = nil
8
+ def initialize block
9
9
  super()
10
10
 
11
11
  header_text 'Throughput Chart'
data/lib/jirametrics.rb CHANGED
@@ -61,7 +61,7 @@ class JiraMetrics < Thor
61
61
  require 'jirametrics/trend_line_calculator'
62
62
  require 'jirametrics/status'
63
63
  require 'jirametrics/issue_link'
64
- require 'jirametrics/story_point_accuracy_chart'
64
+ require 'jirametrics/estimate_accuracy_chart'
65
65
  require 'jirametrics/status_collection'
66
66
  require 'jirametrics/sprint'
67
67
  require 'jirametrics/issue'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: '2.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-16 00:00:00.000000000 Z
11
+ date: 2024-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: random-word
@@ -87,11 +87,10 @@ files:
87
87
  - lib/jirametrics/discard_changes_before.rb
88
88
  - lib/jirametrics/download_config.rb
89
89
  - lib/jirametrics/downloader.rb
90
+ - lib/jirametrics/estimate_accuracy_chart.rb
90
91
  - lib/jirametrics/examples/aggregated_project.rb
91
92
  - lib/jirametrics/examples/standard_project.rb
92
93
  - lib/jirametrics/expedited_chart.rb
93
- - lib/jirametrics/experimental/generator.rb
94
- - lib/jirametrics/experimental/info.rb
95
94
  - lib/jirametrics/exporter.rb
96
95
  - lib/jirametrics/file_config.rb
97
96
  - lib/jirametrics/file_system.rb
@@ -107,12 +106,12 @@ files:
107
106
  - lib/jirametrics/html/cycletime_scatterplot.erb
108
107
  - lib/jirametrics/html/daily_wip_chart.erb
109
108
  - lib/jirametrics/html/data_quality_report.erb
109
+ - lib/jirametrics/html/estimate_accuracy_chart.erb
110
110
  - lib/jirametrics/html/expedited_chart.erb
111
111
  - lib/jirametrics/html/hierarchy_table.erb
112
112
  - lib/jirametrics/html/index.css
113
113
  - lib/jirametrics/html/index.erb
114
114
  - lib/jirametrics/html/sprint_burndown.erb
115
- - lib/jirametrics/html/story_point_accuracy_chart.erb
116
115
  - lib/jirametrics/html/throughput_chart.erb
117
116
  - lib/jirametrics/html_report_config.rb
118
117
  - lib/jirametrics/issue.rb
@@ -127,7 +126,6 @@ files:
127
126
  - lib/jirametrics/sprint_issue_change_data.rb
128
127
  - lib/jirametrics/status.rb
129
128
  - lib/jirametrics/status_collection.rb
130
- - lib/jirametrics/story_point_accuracy_chart.rb
131
129
  - lib/jirametrics/throughput_chart.rb
132
130
  - lib/jirametrics/tree_organizer.rb
133
131
  - lib/jirametrics/trend_line_calculator.rb
@@ -155,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
153
  - !ruby/object:Gem::Version
156
154
  version: '0'
157
155
  requirements: []
158
- rubygems_version: 3.5.10
156
+ rubygems_version: 3.5.11
159
157
  signing_key:
160
158
  specification_version: 4
161
159
  summary: Extract Jira metrics