jirametrics 2.2.1 → 2.3

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