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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +13 -25
- data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +9 -26
- data/lib/jirametrics/board_config.rb +2 -2
- data/lib/jirametrics/chart_base.rb +27 -39
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_chart.rb +1 -13
- data/lib/jirametrics/dependency_chart.rb +1 -1
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
- data/lib/jirametrics/examples/standard_project.rb +1 -1
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +2 -2
- data/lib/jirametrics/file_config.rb +5 -7
- data/lib/jirametrics/file_system.rb +11 -2
- data/lib/jirametrics/groupable_issue_chart.rb +2 -4
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -3
- data/lib/jirametrics/html_report_config.rb +61 -74
- data/lib/jirametrics/issue.rb +70 -39
- data/lib/jirametrics/project_config.rb +12 -6
- data/lib/jirametrics/sprint_burndown.rb +11 -0
- data/lib/jirametrics/status_collection.rb +4 -1
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +1 -1
- metadata +5 -7
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- /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
|
-
|
22
|
-
|
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 || "-#{
|
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
|
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,
|
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
|
-
|
9
|
-
|
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
|
6
|
+
def initialize block
|
7
7
|
super()
|
8
8
|
|
9
9
|
header_text 'Hierarchy Table'
|
10
|
-
description_text
|
11
|
-
<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)
|
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
|
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
|
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.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -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
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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 =
|
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
|
|
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/
|
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.
|
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-
|
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.
|
156
|
+
rubygems_version: 3.5.11
|
159
157
|
signing_key:
|
160
158
|
specification_version: 4
|
161
159
|
summary: Extract Jira metrics
|