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