jirametrics 2.6 → 2.12pre9
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 +9 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
- data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
- data/lib/jirametrics/aging_work_table.rb +54 -7
- data/lib/jirametrics/blocked_stalled_change.rb +1 -1
- data/lib/jirametrics/board.rb +47 -13
- data/lib/jirametrics/board_config.rb +7 -2
- data/lib/jirametrics/board_movement_calculator.rb +155 -0
- data/lib/jirametrics/change_item.rb +19 -8
- data/lib/jirametrics/chart_base.rb +63 -27
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +68 -3
- data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
- data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
- data/lib/jirametrics/daily_wip_chart.rb +7 -9
- data/lib/jirametrics/data_quality_report.rb +219 -41
- data/lib/jirametrics/dependency_chart.rb +3 -4
- data/lib/jirametrics/download_config.rb +2 -2
- data/lib/jirametrics/downloader.rb +57 -37
- data/lib/jirametrics/estimate_accuracy_chart.rb +35 -12
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/aggregated_project.rb +3 -6
- data/lib/jirametrics/examples/standard_project.rb +14 -13
- data/lib/jirametrics/expedited_chart.rb +7 -8
- data/lib/jirametrics/exporter.rb +28 -13
- data/lib/jirametrics/file_config.rb +23 -6
- data/lib/jirametrics/file_system.rb +41 -4
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
- data/lib/jirametrics/html/aging_work_table.erb +6 -4
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +28 -5
- data/lib/jirametrics/html/index.erb +8 -4
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +33 -23
- data/lib/jirametrics/issue.rb +170 -54
- data/lib/jirametrics/jira_gateway.rb +16 -3
- data/lib/jirametrics/project_config.rb +251 -135
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/sprint_burndown.rb +38 -36
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status.rb +81 -26
- data/lib/jirametrics/status_collection.rb +77 -39
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +23 -6
- metadata +11 -13
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
|
@@ -18,7 +18,7 @@ class SprintBurndown < ChartBase
|
|
|
18
18
|
attr_accessor :board_id
|
|
19
19
|
|
|
20
20
|
def initialize
|
|
21
|
-
super
|
|
21
|
+
super
|
|
22
22
|
|
|
23
23
|
@summary_stats = {}
|
|
24
24
|
header_text 'Sprint burndown'
|
|
@@ -121,12 +121,14 @@ class SprintBurndown < ChartBase
|
|
|
121
121
|
|
|
122
122
|
# select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
|
|
123
123
|
def changes_for_one_issue issue:, sprint:
|
|
124
|
-
|
|
124
|
+
estimate = 0.0
|
|
125
125
|
ever_in_sprint = false
|
|
126
126
|
currently_in_sprint = false
|
|
127
127
|
change_data = []
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
estimate_display_name = current_board.estimation_configuration.display_name
|
|
130
|
+
|
|
131
|
+
issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
|
|
130
132
|
completed_has_been_tracked = false
|
|
131
133
|
|
|
132
134
|
issue.changes.each do |change|
|
|
@@ -140,26 +142,26 @@ class SprintBurndown < ChartBase
|
|
|
140
142
|
if currently_in_sprint == false && in_change_item
|
|
141
143
|
action = :enter_sprint
|
|
142
144
|
ever_in_sprint = true
|
|
143
|
-
value =
|
|
145
|
+
value = estimate
|
|
144
146
|
elsif currently_in_sprint && in_change_item == false
|
|
145
147
|
action = :leave_sprint
|
|
146
|
-
value = -
|
|
148
|
+
value = -estimate
|
|
147
149
|
end
|
|
148
150
|
currently_in_sprint = in_change_item
|
|
149
|
-
elsif change.
|
|
151
|
+
elsif change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time)
|
|
150
152
|
action = :story_points
|
|
151
|
-
|
|
152
|
-
value =
|
|
153
|
+
estimate = change.value.to_f
|
|
154
|
+
value = estimate - change.old_value.to_f
|
|
153
155
|
elsif completed_has_been_tracked == false && change.time == issue_completed_time
|
|
154
156
|
completed_has_been_tracked = true
|
|
155
157
|
action = :issue_stopped
|
|
156
|
-
value = -
|
|
158
|
+
value = -estimate
|
|
157
159
|
end
|
|
158
160
|
|
|
159
161
|
next unless action
|
|
160
162
|
|
|
161
163
|
change_data << SprintIssueChangeData.new(
|
|
162
|
-
time: change.time, issue: issue, action: action, value: value,
|
|
164
|
+
time: change.time, issue: issue, action: action, value: value, estimate: estimate
|
|
163
165
|
)
|
|
164
166
|
end
|
|
165
167
|
|
|
@@ -172,11 +174,11 @@ class SprintBurndown < ChartBase
|
|
|
172
174
|
change_item.raw['to'].split(/\s*,\s*/).any? { |id| id.to_i == sprint.id }
|
|
173
175
|
end
|
|
174
176
|
|
|
175
|
-
def data_set_by_story_points sprint:, change_data_for_sprint:
|
|
177
|
+
def data_set_by_story_points sprint:, change_data_for_sprint: # rubocop:disable Metrics/CyclomaticComplexity
|
|
176
178
|
summary_stats = SprintSummaryStats.new
|
|
177
179
|
summary_stats.completed = 0.0
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
estimate = 0.0
|
|
180
182
|
start_data_written = false
|
|
181
183
|
data_set = []
|
|
182
184
|
|
|
@@ -185,11 +187,11 @@ class SprintBurndown < ChartBase
|
|
|
185
187
|
change_data_for_sprint.each do |change_data|
|
|
186
188
|
if start_data_written == false && change_data.time >= sprint.start_time
|
|
187
189
|
data_set << {
|
|
188
|
-
y:
|
|
190
|
+
y: estimate,
|
|
189
191
|
x: chart_format(sprint.start_time),
|
|
190
|
-
title: "Sprint started with #{
|
|
192
|
+
title: "Sprint started with #{estimate} points"
|
|
191
193
|
}
|
|
192
|
-
summary_stats.started =
|
|
194
|
+
summary_stats.started = estimate
|
|
193
195
|
start_data_written = true
|
|
194
196
|
end
|
|
195
197
|
|
|
@@ -198,12 +200,12 @@ class SprintBurndown < ChartBase
|
|
|
198
200
|
case change_data.action
|
|
199
201
|
when :enter_sprint
|
|
200
202
|
issues_currently_in_sprint << change_data.issue.key
|
|
201
|
-
|
|
203
|
+
estimate += change_data.estimate
|
|
202
204
|
when :leave_sprint
|
|
203
205
|
issues_currently_in_sprint.delete change_data.issue.key
|
|
204
|
-
|
|
206
|
+
estimate -= change_data.estimate
|
|
205
207
|
when :story_points
|
|
206
|
-
|
|
208
|
+
estimate += change_data.value if issues_currently_in_sprint.include? change_data.issue.key
|
|
207
209
|
end
|
|
208
210
|
|
|
209
211
|
next unless change_data.time >= sprint.start_time
|
|
@@ -213,26 +215,26 @@ class SprintBurndown < ChartBase
|
|
|
213
215
|
when :story_points
|
|
214
216
|
next unless issues_currently_in_sprint.include? change_data.issue.key
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
message = "Story points changed from #{
|
|
218
|
+
old_estimate = change_data.estimate - change_data.value
|
|
219
|
+
message = "Story points changed from #{old_estimate} points to #{change_data.estimate} points"
|
|
218
220
|
summary_stats.points_values_changed = true
|
|
219
221
|
when :enter_sprint
|
|
220
|
-
message = "Added to sprint with #{change_data.
|
|
221
|
-
summary_stats.added += change_data.
|
|
222
|
+
message = "Added to sprint with #{change_data.estimate || 'no'} points"
|
|
223
|
+
summary_stats.added += change_data.estimate
|
|
222
224
|
when :issue_stopped
|
|
223
|
-
|
|
224
|
-
message = "Completed with #{change_data.
|
|
225
|
+
estimate -= change_data.estimate
|
|
226
|
+
message = "Completed with #{change_data.estimate || 'no'} points"
|
|
225
227
|
issues_currently_in_sprint.delete change_data.issue.key
|
|
226
|
-
summary_stats.completed += change_data.
|
|
228
|
+
summary_stats.completed += change_data.estimate
|
|
227
229
|
when :leave_sprint
|
|
228
|
-
message = "Removed from sprint with #{change_data.
|
|
229
|
-
summary_stats.removed += change_data.
|
|
230
|
+
message = "Removed from sprint with #{change_data.estimate || 'no'} points"
|
|
231
|
+
summary_stats.removed += change_data.estimate
|
|
230
232
|
else
|
|
231
233
|
raise "Unexpected action: #{change_data.action}"
|
|
232
234
|
end
|
|
233
235
|
|
|
234
236
|
data_set << {
|
|
235
|
-
y:
|
|
237
|
+
y: estimate,
|
|
236
238
|
x: chart_format(change_data.time),
|
|
237
239
|
title: "#{change_data.issue.key} #{message}"
|
|
238
240
|
}
|
|
@@ -241,27 +243,27 @@ class SprintBurndown < ChartBase
|
|
|
241
243
|
unless start_data_written
|
|
242
244
|
# There was nothing that triggered us to write the sprint started block so do it now.
|
|
243
245
|
data_set << {
|
|
244
|
-
y:
|
|
246
|
+
y: estimate,
|
|
245
247
|
x: chart_format(sprint.start_time),
|
|
246
|
-
title: "Sprint started with #{
|
|
248
|
+
title: "Sprint started with #{estimate} points"
|
|
247
249
|
}
|
|
248
|
-
summary_stats.started =
|
|
250
|
+
summary_stats.started = estimate
|
|
249
251
|
end
|
|
250
252
|
|
|
251
253
|
if sprint.completed_time
|
|
252
254
|
data_set << {
|
|
253
|
-
y:
|
|
255
|
+
y: estimate,
|
|
254
256
|
x: chart_format(sprint.completed_time),
|
|
255
|
-
title: "Sprint ended with #{
|
|
257
|
+
title: "Sprint ended with #{estimate} points unfinished"
|
|
256
258
|
}
|
|
257
|
-
summary_stats.remaining =
|
|
259
|
+
summary_stats.remaining = estimate
|
|
258
260
|
end
|
|
259
261
|
|
|
260
262
|
unless sprint.completed_at?(time_range.end)
|
|
261
263
|
data_set << {
|
|
262
|
-
y:
|
|
264
|
+
y: estimate,
|
|
263
265
|
x: chart_format(time_range.end),
|
|
264
|
-
title: "Sprint still active. #{
|
|
266
|
+
title: "Sprint still active. #{estimate} points still in progress."
|
|
265
267
|
}
|
|
266
268
|
end
|
|
267
269
|
|
|
@@ -4,14 +4,14 @@ require 'jirametrics/value_equality'
|
|
|
4
4
|
|
|
5
5
|
class SprintIssueChangeData
|
|
6
6
|
include ValueEquality
|
|
7
|
-
attr_reader :time, :action, :value, :issue, :
|
|
7
|
+
attr_reader :time, :action, :value, :issue, :estimate
|
|
8
8
|
|
|
9
|
-
def initialize time:, action:, value:, issue:,
|
|
9
|
+
def initialize time:, action:, value:, issue:, estimate:
|
|
10
10
|
@time = time
|
|
11
11
|
@action = action
|
|
12
12
|
@value = value
|
|
13
13
|
@issue = issue
|
|
14
|
-
@
|
|
14
|
+
@estimate = estimate
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def inspect
|
data/lib/jirametrics/status.rb
CHANGED
|
@@ -3,30 +3,65 @@
|
|
|
3
3
|
require 'jirametrics/value_equality'
|
|
4
4
|
|
|
5
5
|
class Status
|
|
6
|
-
|
|
7
|
-
attr_reader :id, :category_name, :category_id, :project_id
|
|
6
|
+
attr_reader :id, :project_id, :category
|
|
8
7
|
attr_accessor :name
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
class Category
|
|
10
|
+
attr_reader :id, :name, :key
|
|
11
|
+
|
|
12
|
+
def initialize id:, name:, key:
|
|
13
|
+
@id = id
|
|
14
|
+
@name = name
|
|
15
|
+
@key = key
|
|
16
|
+
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
def to_s
|
|
19
|
+
"#{name.inspect}:#{id.inspect}"
|
|
20
|
+
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
def <=> other
|
|
23
|
+
id <=> other.id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def == other
|
|
27
|
+
id == other.id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eql?(other) = id.eql?(other.id)
|
|
31
|
+
def hash = id.hash
|
|
32
|
+
|
|
33
|
+
def new? = (@key == 'new')
|
|
34
|
+
def indeterminate? = (@key == 'indeterminate')
|
|
35
|
+
def done? = (@key == 'done')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from_raw raw
|
|
39
|
+
raise "raw cannot be nil" if raw.nil?
|
|
22
40
|
|
|
23
41
|
category_config = raw['statusCategory']
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
|
|
43
|
+
|
|
44
|
+
Status.new(
|
|
45
|
+
name: raw['name'],
|
|
46
|
+
id: raw['id'].to_i,
|
|
47
|
+
category_name: category_config['name'],
|
|
48
|
+
category_id: category_config['id'].to_i,
|
|
49
|
+
category_key: category_config['key'],
|
|
50
|
+
project_id: raw['scope']&.[]('project')&.[]('id'),
|
|
51
|
+
artificial: false
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize name:, id:, category_name:, category_id:, category_key:, project_id: nil, artificial: true
|
|
56
|
+
# These checks are needed because nils used to be possible and now they aren't.
|
|
57
|
+
raise 'id cannot be nil' if id.nil?
|
|
58
|
+
raise 'category_id cannot be nil' if category_id.nil?
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@
|
|
60
|
+
@name = name
|
|
61
|
+
@id = id
|
|
62
|
+
@category = Category.new id: category_id, name: category_name, key: category_key
|
|
63
|
+
@project_id = project_id
|
|
64
|
+
@artificial = artificial
|
|
30
65
|
end
|
|
31
66
|
|
|
32
67
|
def project_scoped?
|
|
@@ -38,18 +73,38 @@ class Status
|
|
|
38
73
|
end
|
|
39
74
|
|
|
40
75
|
def to_s
|
|
41
|
-
|
|
42
|
-
" id=#{@id.inspect}," \
|
|
43
|
-
" category_name=#{@category_name.inspect}," \
|
|
44
|
-
" category_id=#{@category_id.inspect}," \
|
|
45
|
-
" project_id=#{@project_id}"
|
|
46
|
-
result << ' artificial' if artificial?
|
|
47
|
-
result << ')'
|
|
48
|
-
result
|
|
76
|
+
"#{name.inspect}:#{id.inspect}"
|
|
49
77
|
end
|
|
50
78
|
|
|
51
79
|
def artificial?
|
|
52
|
-
@
|
|
80
|
+
@artificial
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def == other
|
|
84
|
+
return false unless other.is_a? Status
|
|
85
|
+
|
|
86
|
+
@id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def eql?(other)
|
|
90
|
+
self == other
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def <=> other
|
|
94
|
+
result = @name.casecmp(other.name)
|
|
95
|
+
result = @id <=> other.id if result.zero?
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inspect
|
|
100
|
+
result = []
|
|
101
|
+
result << "Status(name: #{@name.inspect}"
|
|
102
|
+
result << "id: #{@id.inspect}"
|
|
103
|
+
result << "project_id: #{@project_id}" if @project_id
|
|
104
|
+
category = self.category
|
|
105
|
+
result << "category: {name:#{category.name.inspect}, id: #{category.id.inspect}, key: #{category.key.inspect}}"
|
|
106
|
+
result << 'artificial' if artificial?
|
|
107
|
+
result.join(', ') << ')'
|
|
53
108
|
end
|
|
54
109
|
|
|
55
110
|
def value_equality_ignored_variables
|
|
@@ -4,68 +4,106 @@ class StatusNotFoundError < StandardError
|
|
|
4
4
|
end
|
|
5
5
|
|
|
6
6
|
class StatusCollection
|
|
7
|
+
attr_reader :historical_status_mappings
|
|
8
|
+
|
|
7
9
|
def initialize
|
|
8
10
|
@list = []
|
|
11
|
+
@historical_status_mappings = {} # 'name:id' => category
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
# Return the status matching this id or nil if it can't be found.
|
|
15
|
+
def find_by_id id
|
|
16
|
+
@list.find { |status| status.id == id }
|
|
17
|
+
end
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
including.any? { |s| s.name == status.name }
|
|
18
|
-
keep = false if excluding.any? { |s| s.name == status.name }
|
|
19
|
+
def find_all_by_name identifier
|
|
20
|
+
name, id = parse_name_id identifier
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
if id
|
|
23
|
+
status = find_by_id id
|
|
24
|
+
return [] if status.nil?
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
names_or_ids = [names_or_ids] unless names_or_ids.is_a? Array
|
|
29
|
-
|
|
30
|
-
names_or_ids.each do |name_or_id|
|
|
31
|
-
status = @list.find { |s| s.name == name_or_id || s.id == name_or_id }
|
|
32
|
-
if status.nil?
|
|
33
|
-
if block_given?
|
|
34
|
-
yield name_or_id
|
|
35
|
-
next
|
|
36
|
-
else
|
|
37
|
-
all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
|
|
38
|
-
raise StatusNotFoundError, "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
|
|
39
|
-
end
|
|
26
|
+
if name && status.name != name
|
|
27
|
+
raise "Specified status ID of #{id} does not match specified name #{name.inspect}. " \
|
|
28
|
+
"You might have meant one of these: #{self}."
|
|
40
29
|
end
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
[status]
|
|
31
|
+
else
|
|
32
|
+
@list.select { |status| status.name == name }
|
|
43
33
|
end
|
|
44
|
-
result
|
|
45
34
|
end
|
|
46
35
|
|
|
47
|
-
def
|
|
48
|
-
|
|
36
|
+
def find_all_categories
|
|
37
|
+
@list
|
|
38
|
+
.collect(&:category)
|
|
39
|
+
.uniq
|
|
40
|
+
.sort_by(&:id)
|
|
49
41
|
end
|
|
50
42
|
|
|
51
|
-
def
|
|
52
|
-
|
|
43
|
+
def parse_name_id name
|
|
44
|
+
# Names could arrive in one of the following formats: "Done:3", "3", "Done"
|
|
45
|
+
if name =~ /^(.*):(\d+)$/
|
|
46
|
+
[$1, $2.to_i]
|
|
47
|
+
elsif name.match?(/^\d+$/)
|
|
48
|
+
[nil, name.to_i]
|
|
49
|
+
else
|
|
50
|
+
[name, nil]
|
|
51
|
+
end
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
def find_all_categories_by_name identifier
|
|
55
|
+
key = nil
|
|
56
|
+
id = nil
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if identifier.is_a? Symbol
|
|
59
|
+
key = identifier.to_s
|
|
60
|
+
else
|
|
61
|
+
name, id = parse_name_id identifier
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
find_all_categories.select { |c| c.id == id || c.name == name || c.key == key }
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
def find(&block)= @list.find(&block)
|
|
64
67
|
def collect(&block) = @list.collect(&block)
|
|
68
|
+
def find(&block) = @list.find(&block)
|
|
65
69
|
def each(&block) = @list.each(&block)
|
|
66
70
|
def select(&block) = @list.select(&block)
|
|
67
71
|
def <<(arg) = @list << arg
|
|
68
72
|
def empty? = @list.empty?
|
|
69
73
|
def clear = @list.clear
|
|
70
74
|
def delete(object) = @list.delete(object)
|
|
75
|
+
|
|
76
|
+
def to_s
|
|
77
|
+
"[#{@list.sort.join(', ')}]"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
"StatusCollection#{self}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fabricate_status_for id:, name:
|
|
85
|
+
category = @historical_status_mappings["#{name.inspect}:#{id.inspect}"]
|
|
86
|
+
category = in_progress_category if category.nil?
|
|
87
|
+
|
|
88
|
+
status = Status.new(
|
|
89
|
+
name: name,
|
|
90
|
+
id: id,
|
|
91
|
+
category_name: category.name,
|
|
92
|
+
category_id: category.id,
|
|
93
|
+
category_key: category.key,
|
|
94
|
+
artificial: true
|
|
95
|
+
)
|
|
96
|
+
@list << status
|
|
97
|
+
status
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Return the in-progress category or raise an error if we can't find one.
|
|
103
|
+
def in_progress_category
|
|
104
|
+
first_in_progress_status = find { |s| s.category.indeterminate? }
|
|
105
|
+
raise "Can't find even one in-progress status in #{self}" unless first_in_progress_status
|
|
106
|
+
|
|
107
|
+
first_in_progress_status.category
|
|
108
|
+
end
|
|
71
109
|
end
|
|
@@ -82,7 +82,7 @@ class ThroughputChart < ChartBase
|
|
|
82
82
|
def throughput_dataset periods:, completed_issues:
|
|
83
83
|
periods.collect do |period|
|
|
84
84
|
closed_issues = completed_issues.filter_map do |issue|
|
|
85
|
-
stop_date = issue.board.cycletime.
|
|
85
|
+
stop_date = issue.board.cycletime.started_stopped_dates(issue).last
|
|
86
86
|
[stop_date, issue] if stop_date && period.include?(stop_date)
|
|
87
87
|
end
|
|
88
88
|
|
|
@@ -9,9 +9,9 @@ module ValueEquality
|
|
|
9
9
|
names = object.instance_variables
|
|
10
10
|
if object.respond_to? :value_equality_ignored_variables
|
|
11
11
|
ignored_variables = object.value_equality_ignored_variables
|
|
12
|
-
names.reject! { |n| ignored_variables.include? n }
|
|
12
|
+
names.reject! { |n| ignored_variables.include? n.to_sym }
|
|
13
13
|
end
|
|
14
|
-
names.map { |variable| instance_variable_get variable }
|
|
14
|
+
names.map { |variable| object.instance_variable_get variable }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
code.call(self) == code.call(other)
|
data/lib/jirametrics.rb
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
|
|
5
5
|
class JiraMetrics < Thor
|
|
6
|
+
def self.exit_on_failure?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
map %w[--version -v] => :__print_version
|
|
11
|
+
|
|
12
|
+
desc '--version, -v', 'print the version'
|
|
13
|
+
def __print_version
|
|
14
|
+
puts Gem.loaded_specs['jirametrics'].version
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
option :config
|
|
7
18
|
option :name
|
|
8
|
-
desc 'export
|
|
19
|
+
desc 'export', "Export data into either reports or CSV's as per the configuration"
|
|
9
20
|
def export
|
|
10
21
|
load_config options[:config]
|
|
11
22
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
@@ -13,7 +24,7 @@ class JiraMetrics < Thor
|
|
|
13
24
|
|
|
14
25
|
option :config
|
|
15
26
|
option :name
|
|
16
|
-
desc 'download
|
|
27
|
+
desc 'download', 'Download data from Jira'
|
|
17
28
|
def download
|
|
18
29
|
load_config options[:config]
|
|
19
30
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -21,7 +32,7 @@ class JiraMetrics < Thor
|
|
|
21
32
|
|
|
22
33
|
option :config
|
|
23
34
|
option :name
|
|
24
|
-
desc '
|
|
35
|
+
desc 'go', 'Same as running download, followed by export'
|
|
25
36
|
def go
|
|
26
37
|
load_config options[:config]
|
|
27
38
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -30,6 +41,13 @@ class JiraMetrics < Thor
|
|
|
30
41
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
31
42
|
end
|
|
32
43
|
|
|
44
|
+
option :config
|
|
45
|
+
desc 'info', 'Dump information about one issue'
|
|
46
|
+
def info keys
|
|
47
|
+
load_config options[:config]
|
|
48
|
+
Exporter.instance.info(keys, name_filter: options[:name] || '*')
|
|
49
|
+
end
|
|
50
|
+
|
|
33
51
|
private
|
|
34
52
|
|
|
35
53
|
def load_config config_file
|
|
@@ -50,7 +68,6 @@ class JiraMetrics < Thor
|
|
|
50
68
|
require 'jirametrics/grouping_rules'
|
|
51
69
|
require 'jirametrics/daily_wip_chart'
|
|
52
70
|
require 'jirametrics/groupable_issue_chart'
|
|
53
|
-
require 'jirametrics/discard_changes_before'
|
|
54
71
|
require 'jirametrics/css_variable'
|
|
55
72
|
|
|
56
73
|
require 'jirametrics/aggregate_config'
|
|
@@ -69,6 +86,7 @@ class JiraMetrics < Thor
|
|
|
69
86
|
require 'jirametrics/daily_wip_by_parent_chart'
|
|
70
87
|
require 'jirametrics/aging_work_in_progress_chart'
|
|
71
88
|
require 'jirametrics/cycletime_scatterplot'
|
|
89
|
+
require 'jirametrics/flow_efficiency_scatterplot'
|
|
72
90
|
require 'jirametrics/sprint_issue_change_data'
|
|
73
91
|
require 'jirametrics/cycletime_histogram'
|
|
74
92
|
require 'jirametrics/daily_wip_by_blocked_stalled_chart'
|
|
@@ -94,9 +112,8 @@ class JiraMetrics < Thor
|
|
|
94
112
|
require 'jirametrics/download_config'
|
|
95
113
|
require 'jirametrics/columns_config'
|
|
96
114
|
require 'jirametrics/hierarchy_table'
|
|
115
|
+
require 'jirametrics/estimation_configuration'
|
|
97
116
|
require 'jirametrics/board'
|
|
98
117
|
load config_file
|
|
99
118
|
end
|
|
100
|
-
|
|
101
|
-
# Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
|
|
102
119
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.12pre9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2025-04-16 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: random-word
|
|
@@ -52,8 +51,7 @@ dependencies:
|
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: 1.2.2
|
|
55
|
-
description:
|
|
56
|
-
CSV files
|
|
54
|
+
description: Extract metrics from Jira and export to either a report or to CSV files
|
|
57
55
|
email: mbowler@gargoylesoftware.com
|
|
58
56
|
executables:
|
|
59
57
|
- jirametrics
|
|
@@ -71,6 +69,7 @@ files:
|
|
|
71
69
|
- lib/jirametrics/board.rb
|
|
72
70
|
- lib/jirametrics/board_column.rb
|
|
73
71
|
- lib/jirametrics/board_config.rb
|
|
72
|
+
- lib/jirametrics/board_movement_calculator.rb
|
|
74
73
|
- lib/jirametrics/change_item.rb
|
|
75
74
|
- lib/jirametrics/chart_base.rb
|
|
76
75
|
- lib/jirametrics/columns_config.rb
|
|
@@ -84,10 +83,10 @@ files:
|
|
|
84
83
|
- lib/jirametrics/daily_wip_chart.rb
|
|
85
84
|
- lib/jirametrics/data_quality_report.rb
|
|
86
85
|
- lib/jirametrics/dependency_chart.rb
|
|
87
|
-
- lib/jirametrics/discard_changes_before.rb
|
|
88
86
|
- lib/jirametrics/download_config.rb
|
|
89
87
|
- lib/jirametrics/downloader.rb
|
|
90
88
|
- lib/jirametrics/estimate_accuracy_chart.rb
|
|
89
|
+
- lib/jirametrics/estimation_configuration.rb
|
|
91
90
|
- lib/jirametrics/examples/aggregated_project.rb
|
|
92
91
|
- lib/jirametrics/examples/standard_project.rb
|
|
93
92
|
- lib/jirametrics/expedited_chart.rb
|
|
@@ -95,6 +94,7 @@ files:
|
|
|
95
94
|
- lib/jirametrics/file_config.rb
|
|
96
95
|
- lib/jirametrics/file_system.rb
|
|
97
96
|
- lib/jirametrics/fix_version.rb
|
|
97
|
+
- lib/jirametrics/flow_efficiency_scatterplot.rb
|
|
98
98
|
- lib/jirametrics/groupable_issue_chart.rb
|
|
99
99
|
- lib/jirametrics/grouping_rules.rb
|
|
100
100
|
- lib/jirametrics/hierarchy_table.rb
|
|
@@ -105,9 +105,9 @@ files:
|
|
|
105
105
|
- lib/jirametrics/html/cycletime_histogram.erb
|
|
106
106
|
- lib/jirametrics/html/cycletime_scatterplot.erb
|
|
107
107
|
- lib/jirametrics/html/daily_wip_chart.erb
|
|
108
|
-
- lib/jirametrics/html/data_quality_report.erb
|
|
109
108
|
- lib/jirametrics/html/estimate_accuracy_chart.erb
|
|
110
109
|
- lib/jirametrics/html/expedited_chart.erb
|
|
110
|
+
- lib/jirametrics/html/flow_efficiency_scatterplot.erb
|
|
111
111
|
- lib/jirametrics/html/hierarchy_table.erb
|
|
112
112
|
- lib/jirametrics/html/index.css
|
|
113
113
|
- lib/jirametrics/html/index.erb
|
|
@@ -130,15 +130,14 @@ files:
|
|
|
130
130
|
- lib/jirametrics/tree_organizer.rb
|
|
131
131
|
- lib/jirametrics/trend_line_calculator.rb
|
|
132
132
|
- lib/jirametrics/value_equality.rb
|
|
133
|
-
homepage: https://
|
|
133
|
+
homepage: https://jirametrics.org
|
|
134
134
|
licenses:
|
|
135
135
|
- Apache-2.0
|
|
136
136
|
metadata:
|
|
137
137
|
rubygems_mfa_required: 'true'
|
|
138
138
|
bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
|
|
139
|
-
changelog_uri: https://
|
|
140
|
-
documentation_uri: https://
|
|
141
|
-
post_install_message:
|
|
139
|
+
changelog_uri: https://jirametrics.org/changes
|
|
140
|
+
documentation_uri: https://jirametrics.org
|
|
142
141
|
rdoc_options: []
|
|
143
142
|
require_paths:
|
|
144
143
|
- lib
|
|
@@ -153,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
153
152
|
- !ruby/object:Gem::Version
|
|
154
153
|
version: '0'
|
|
155
154
|
requirements: []
|
|
156
|
-
rubygems_version: 3.
|
|
157
|
-
signing_key:
|
|
155
|
+
rubygems_version: 3.6.2
|
|
158
156
|
specification_version: 4
|
|
159
157
|
summary: Extract Jira metrics
|
|
160
158
|
test_files: []
|