jirametrics 1.4 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/jirametrics +1 -0
- data/lib/jirametrics/aggregate_config.rb +2 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +3 -3
- data/lib/jirametrics/aging_work_in_progress_chart.rb +6 -6
- data/lib/jirametrics/anonymizer.rb +3 -3
- data/lib/jirametrics/blocked_stalled_change.rb +5 -10
- data/lib/jirametrics/board.rb +11 -13
- data/lib/jirametrics/chart_base.rb +5 -5
- data/lib/jirametrics/cycletime_histogram.rb +2 -2
- data/lib/jirametrics/cycletime_scatterplot.rb +5 -2
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +4 -4
- data/lib/jirametrics/dependency_chart.rb +5 -4
- data/lib/jirametrics/download_config.rb +0 -19
- data/lib/jirametrics/downloader.rb +32 -74
- data/lib/jirametrics/examples/aggregated_project.rb +61 -3
- data/lib/jirametrics/examples/standard_project.rb +3 -3
- data/lib/jirametrics/expedited_chart.rb +4 -4
- data/lib/jirametrics/experimental/generator.rb +5 -4
- data/lib/jirametrics/experimental/info.rb +2 -2
- data/lib/jirametrics/exporter.rb +16 -30
- data/lib/jirametrics/file_system.rb +36 -0
- data/lib/jirametrics/groupable_issue_chart.rb +0 -9
- data/lib/jirametrics/hierarchy_table.rb +1 -1
- data/lib/jirametrics/html_report_config.rb +5 -30
- data/lib/jirametrics/issue.rb +1 -7
- data/lib/jirametrics/issue_link.rb +0 -7
- data/lib/jirametrics/jira_gateway.rb +59 -0
- data/lib/jirametrics/project_config.rb +78 -88
- data/lib/jirametrics/rules.rb +1 -20
- data/lib/jirametrics/sprint_burndown.rb +7 -6
- data/lib/jirametrics/sprint_issue_change_data.rb +4 -9
- data/lib/jirametrics/status.rb +24 -20
- data/lib/jirametrics/status_collection.rb +2 -2
- data/lib/jirametrics/story_point_accuracy_chart.rb +2 -7
- data/lib/jirametrics/throughput_chart.rb +2 -2
- data/lib/jirametrics/trend_line_calculator.rb +4 -4
- data/lib/jirametrics/value_equality.rb +23 -0
- data/lib/jirametrics.rb +3 -1
- metadata +5 -17
- data/lib/jirametrics/json_file_loader.rb +0 -9
@@ -8,10 +8,10 @@ class ProjectConfig
|
|
8
8
|
|
9
9
|
attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
|
10
10
|
:download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
|
11
|
-
:settings
|
12
|
-
attr_accessor :time_range, :jira_url, :
|
11
|
+
:settings, :id
|
12
|
+
attr_accessor :time_range, :jira_url, :id
|
13
13
|
|
14
|
-
def initialize exporter:, jira_config:, block:, target_path: '.', name: ''
|
14
|
+
def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
|
15
15
|
@exporter = exporter
|
16
16
|
@block = block
|
17
17
|
@file_configs = []
|
@@ -21,6 +21,7 @@ class ProjectConfig
|
|
21
21
|
@possible_statuses = StatusCollection.new
|
22
22
|
@name = name
|
23
23
|
@board_configs = []
|
24
|
+
@all_boards = {}
|
24
25
|
@settings = {
|
25
26
|
'stalled_threshold' => 5,
|
26
27
|
'blocked_statuses' => [],
|
@@ -32,6 +33,7 @@ class ProjectConfig
|
|
32
33
|
'blocked' => '#FF7400'
|
33
34
|
}
|
34
35
|
}
|
36
|
+
@id = id
|
35
37
|
end
|
36
38
|
|
37
39
|
def evaluate_next_level
|
@@ -41,7 +43,7 @@ class ProjectConfig
|
|
41
43
|
def run
|
42
44
|
unless aggregated_project?
|
43
45
|
load_all_boards
|
44
|
-
@
|
46
|
+
@id = guess_project_id
|
45
47
|
load_status_category_mappings
|
46
48
|
load_project_metadata
|
47
49
|
load_sprints
|
@@ -54,7 +56,23 @@ class ProjectConfig
|
|
54
56
|
@file_configs.each do |file_config|
|
55
57
|
file_config.run
|
56
58
|
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def guess_project_id
|
62
|
+
return @id if @id
|
63
|
+
|
64
|
+
previous_id = nil
|
65
|
+
@all_boards.each_value do |board|
|
66
|
+
project_id = board.project_id
|
67
|
+
|
68
|
+
# If the id is ambiguous then return nil for now. The user will get an error later
|
69
|
+
# in the case where we need it to be unambiguous. Sometimes we don't care and there's
|
70
|
+
# no point forcing the user to enter a project id if we don't need it.
|
71
|
+
return nil if previous_id && project_id && previous_id != project_id
|
57
72
|
|
73
|
+
previous_id = project_id if project_id
|
74
|
+
end
|
75
|
+
previous_id
|
58
76
|
end
|
59
77
|
|
60
78
|
def aggregated_project?
|
@@ -92,15 +110,7 @@ class ProjectConfig
|
|
92
110
|
@file_prefix
|
93
111
|
end
|
94
112
|
|
95
|
-
def status_category_mapping status:, category
|
96
|
-
puts "Deprecated: ProjectConfig.status_category_mapping no longer needs a type: #{type.inspect}" if type
|
97
|
-
|
98
|
-
status_object = find_status(name: status)
|
99
|
-
if status_object
|
100
|
-
puts "Status/Category mapping was already present. Ignoring redefinition: #{status_object}"
|
101
|
-
return
|
102
|
-
end
|
103
|
-
|
113
|
+
def status_category_mapping status:, category:
|
104
114
|
add_possible_status Status.new(name: status, category_name: category)
|
105
115
|
end
|
106
116
|
|
@@ -111,7 +121,7 @@ class ProjectConfig
|
|
111
121
|
board_id = $1.to_i
|
112
122
|
load_board board_id: board_id, filename: "#{@target_path}#{file}"
|
113
123
|
end
|
114
|
-
raise "No boards found in #{@target_path.inspect}" if @all_boards.
|
124
|
+
raise "No boards found in #{@target_path.inspect}" if @all_boards.empty?
|
115
125
|
end
|
116
126
|
|
117
127
|
def load_board board_id:, filename:
|
@@ -119,38 +129,38 @@ class ProjectConfig
|
|
119
129
|
raw: JSON.parse(File.read(filename)), possible_statuses: @possible_statuses
|
120
130
|
)
|
121
131
|
board.project_config = self
|
122
|
-
|
132
|
+
@all_boards[board_id] = board
|
123
133
|
end
|
124
134
|
|
125
|
-
def raise_with_message_about_missing_category_information
|
126
|
-
message =
|
127
|
-
message <<
|
128
|
-
|
129
|
-
'
|
130
|
-
" 'status_category_mapping' declaration in your config to manually add one.\n\n" \
|
131
|
-
' The mappings we do know about are below:'
|
135
|
+
def raise_with_message_about_missing_category_information all_issues = @issues
|
136
|
+
message = +''
|
137
|
+
message << "Could not determine categories for some of the statuses used in this data set.\n" \
|
138
|
+
"Use the 'status_category_mapping' declaration in your config to manually add one.\n" \
|
139
|
+
'The mappings we do know about are below:'
|
132
140
|
|
133
141
|
@possible_statuses.each do |status|
|
134
|
-
message << "\n
|
135
|
-
"category: #{status.category_name.inspect}'"
|
142
|
+
message << "\n status: #{status.name.inspect}, category: #{status.category_name.inspect}"
|
136
143
|
end
|
137
144
|
|
138
145
|
message << "\n\nThe ones we're missing are the following:"
|
139
146
|
|
147
|
+
find_statuses_with_no_category_information(all_issues).each do |status_name|
|
148
|
+
message << "\n status: #{status_name.inspect}, category: <unknown>"
|
149
|
+
end
|
150
|
+
|
151
|
+
raise message
|
152
|
+
end
|
153
|
+
|
154
|
+
def find_statuses_with_no_category_information all_issues
|
140
155
|
missing_statuses = []
|
141
|
-
|
156
|
+
all_issues.each do |issue|
|
142
157
|
issue.changes.each do |change|
|
143
158
|
next unless change.status?
|
144
159
|
|
145
160
|
missing_statuses << change.value unless find_status(name: change.value)
|
146
161
|
end
|
147
162
|
end
|
148
|
-
|
149
|
-
missing_statuses.uniq.each do |status_name|
|
150
|
-
message << "\n status: #{status_name.inspect}, category: <unknown>"
|
151
|
-
end
|
152
|
-
|
153
|
-
raise message
|
163
|
+
missing_statuses.uniq
|
154
164
|
end
|
155
165
|
|
156
166
|
def load_status_category_mappings
|
@@ -158,22 +168,14 @@ class ProjectConfig
|
|
158
168
|
# We may not always have this file. Load it if we can.
|
159
169
|
return unless File.exist? filename
|
160
170
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
else
|
170
|
-
# Response from /api/2/status
|
171
|
-
status_json_snippets = json
|
172
|
-
end
|
173
|
-
|
174
|
-
status_json_snippets.each do |snippet|
|
175
|
-
add_possible_status Status.new(raw: snippet)
|
176
|
-
end
|
171
|
+
statuses = JSON.parse(File.read(filename))
|
172
|
+
.map { |snippet| Status.new(raw: snippet) }
|
173
|
+
statuses
|
174
|
+
.find_all { |status| status.global? }
|
175
|
+
.each { |status| add_possible_status status }
|
176
|
+
statuses
|
177
|
+
.find_all { |status| status.project_scoped? }
|
178
|
+
.each { |status| add_possible_status status }
|
177
179
|
end
|
178
180
|
|
179
181
|
def load_sprints
|
@@ -193,33 +195,40 @@ class ProjectConfig
|
|
193
195
|
end
|
194
196
|
|
195
197
|
def add_possible_status status
|
196
|
-
# If it's project scoped and it's not this project, just ignore it.
|
197
|
-
return if status.project_id && (@project_id.nil? || status.project_id != @project_id)
|
198
|
-
|
199
198
|
existing_status = find_status(name: status.name)
|
200
199
|
|
201
|
-
|
202
|
-
|
200
|
+
if status.project_scoped?
|
201
|
+
# If the project specific status doesn't change anything then we don't care whether it's
|
202
|
+
# our project or not.
|
203
|
+
return if existing_status && existing_status.category_name == status.category_name
|
203
204
|
|
204
|
-
|
205
|
-
# No need to check categories as status_category_mapping can't add a project_id so by definition
|
206
|
-
# this data came from Jira.
|
207
|
-
return if existing_status && existing_status.project_id
|
205
|
+
raise_ambiguous_project_id if @id.nil?
|
208
206
|
|
209
|
-
|
210
|
-
|
211
|
-
|
207
|
+
# Not our project, ignore it.
|
208
|
+
return unless status.project_id == @id
|
209
|
+
|
210
|
+
# Replace the old one with this
|
212
211
|
@possible_statuses.delete(existing_status)
|
213
212
|
@possible_statuses << status
|
214
213
|
return
|
215
214
|
end
|
216
215
|
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
|
216
|
+
# If it isn't there, add it and go.
|
217
|
+
return @possible_statuses << status unless existing_status
|
218
|
+
|
219
|
+
# We're registering the same one twice. Shouldn't be possible with the new status API but it
|
220
|
+
# did happen with the project specific one.
|
221
|
+
return if status.category_name == existing_status.category_name
|
221
222
|
|
222
|
-
|
223
|
+
# If we got this far then someone has called status_category_mapping and is attempting to
|
224
|
+
# change the category.
|
225
|
+
raise "Redefining status category #{status} with #{existing_status}. Was one set in the config?"
|
226
|
+
end
|
227
|
+
|
228
|
+
def raise_ambiguous_project_id
|
229
|
+
raise 'Ambiguous project id: There is a project specific status that could affect out calculations. ' \
|
230
|
+
'We are unable to automatically detect the id of the project so you will have to set it manually ' \
|
231
|
+
'in the configuration like: "project id: 5"'
|
223
232
|
end
|
224
233
|
|
225
234
|
def find_status name:
|
@@ -243,7 +252,7 @@ class ProjectConfig
|
|
243
252
|
end
|
244
253
|
|
245
254
|
def to_time string
|
246
|
-
string = "#{string}T00:00:00#{@timezone_offset}" if string
|
255
|
+
string = "#{string}T00:00:00#{@timezone_offset}" if string.match?(/^\d{4}-\d{2}\d{2}$/)
|
247
256
|
Time.parse string
|
248
257
|
end
|
249
258
|
|
@@ -275,7 +284,7 @@ class ProjectConfig
|
|
275
284
|
# To be used by the aggregate_config only. Not intended to be part of the public API
|
276
285
|
def add_issues issues_list
|
277
286
|
@issues = [] if @issues.nil?
|
278
|
-
@all_boards = {}
|
287
|
+
@all_boards = {}
|
279
288
|
|
280
289
|
issues_list.each do |issue|
|
281
290
|
@issues << issue
|
@@ -298,10 +307,8 @@ class ProjectConfig
|
|
298
307
|
issues_path = "#{@target_path}#{file_prefix}_issues/"
|
299
308
|
if File.exist?(issues_path) && File.directory?(issues_path)
|
300
309
|
issues = load_issues_from_issues_directory path: issues_path, timezone_offset: timezone_offset
|
301
|
-
elsif File.exist?(@target_path) && File.directory?(@target_path)
|
302
|
-
issues = load_issues_from_target_directory path: @target_path, timezone_offset: timezone_offset
|
303
310
|
else
|
304
|
-
puts "Can't find issues in
|
311
|
+
puts "Can't find issues in #{path}. Has a download been done?"
|
305
312
|
end
|
306
313
|
|
307
314
|
# Attach related issues
|
@@ -347,29 +354,12 @@ class ProjectConfig
|
|
347
354
|
raise "No boards found for project #{name.inspect}" if all_boards.empty?
|
348
355
|
|
349
356
|
if all_boards.size != 1
|
350
|
-
puts "Multiple boards are in use for project #{name.inspect}.
|
357
|
+
puts "Multiple boards are in use for project #{name.inspect}. " \
|
358
|
+
"Picked #{default_board.name.inspect} to attach issues to."
|
351
359
|
end
|
352
360
|
default_board
|
353
361
|
end
|
354
362
|
|
355
|
-
def load_issues_from_target_directory path:, timezone_offset:
|
356
|
-
puts "Deprecated: issues in the target directory for project #{@name}. " \
|
357
|
-
'Download again and this should fix itself.'
|
358
|
-
|
359
|
-
default_board = find_default_board
|
360
|
-
|
361
|
-
issues = []
|
362
|
-
Dir.foreach(path) do |filename|
|
363
|
-
if filename =~ /#{file_prefix}_\d+\.json/
|
364
|
-
content = JSON.parse File.read("#{path}#{filename}")
|
365
|
-
content['issues'].each do |issue|
|
366
|
-
issues << Issue.new(raw: issue, timezone_offset: timezone_offset, board: default_board)
|
367
|
-
end
|
368
|
-
end
|
369
|
-
end
|
370
|
-
issues
|
371
|
-
end
|
372
|
-
|
373
363
|
def load_issues_from_issues_directory path:, timezone_offset:
|
374
364
|
issues = []
|
375
365
|
default_board = nil
|
data/lib/jirametrics/rules.rb
CHANGED
@@ -9,26 +9,7 @@ class Rules
|
|
9
9
|
@ignore == true
|
10
10
|
end
|
11
11
|
|
12
|
-
def eql?(other)
|
13
|
-
(other.class == self.class) && (other.state == state)
|
14
|
-
end
|
15
|
-
|
16
|
-
def state
|
17
|
-
instance_variables.map { |variable| instance_variable_get variable }
|
18
|
-
end
|
19
|
-
|
20
12
|
def hash
|
21
|
-
2 # TODO: While this
|
13
|
+
2 # TODO: While this works, it's not performant
|
22
14
|
end
|
23
|
-
|
24
|
-
def inspect
|
25
|
-
result = String.new
|
26
|
-
result << "#{self.class}("
|
27
|
-
result << instance_variables.collect do |variable|
|
28
|
-
"#{variable}=#{instance_variable_get(variable).inspect}"
|
29
|
-
end.join(', ')
|
30
|
-
result << ')'
|
31
|
-
result
|
32
|
-
end
|
33
|
-
|
34
15
|
end
|
@@ -54,19 +54,20 @@ class SprintBurndown < ChartBase
|
|
54
54
|
change_data_by_sprint[sprint] = change_data.sort_by(&:time)
|
55
55
|
end
|
56
56
|
|
57
|
-
result =
|
57
|
+
result = +''
|
58
58
|
result << '<h1>Sprint Burndowns</h1>'
|
59
59
|
|
60
|
+
possible_colours = %w[blue orange green red brown]
|
60
61
|
charts_to_generate = []
|
61
62
|
charts_to_generate << [:data_set_by_story_points, 'Story Points'] if @use_story_points
|
62
63
|
charts_to_generate << [:data_set_by_story_counts, 'Story Count'] if @use_story_counts
|
63
|
-
charts_to_generate.each do |data_method, y_axis_title|
|
64
|
+
charts_to_generate.each do |data_method, y_axis_title| # rubocop:disable Style/HashEachMethods
|
64
65
|
@summary_stats.clear
|
65
66
|
data_sets = []
|
66
67
|
sprints.each_with_index do |sprint, index|
|
67
|
-
color =
|
68
|
+
color = possible_colours[index % 5]
|
68
69
|
label = sprint.name
|
69
|
-
data = send(data_method,
|
70
|
+
data = send(data_method, sprint: sprint, change_data_for_sprint: change_data_by_sprint[sprint])
|
70
71
|
data_sets << {
|
71
72
|
label: label,
|
72
73
|
data: data,
|
@@ -130,8 +131,8 @@ class SprintBurndown < ChartBase
|
|
130
131
|
currently_in_sprint = in_change_item
|
131
132
|
elsif change.story_points? && (issue_completed_time.nil? || change.time < issue_completed_time)
|
132
133
|
action = :story_points
|
133
|
-
story_points = change.value
|
134
|
-
value = story_points -
|
134
|
+
story_points = change.value.to_f
|
135
|
+
value = story_points - change.old_value.to_f
|
135
136
|
elsif completed_has_been_tracked == false && change.time == issue_completed_time
|
136
137
|
completed_has_been_tracked = true
|
137
138
|
action = :issue_stopped
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'jirametrics/value_equality'
|
4
|
+
|
3
5
|
class SprintIssueChangeData
|
6
|
+
include ValueEquality
|
4
7
|
attr_reader :time, :action, :value, :issue, :story_points
|
5
8
|
|
6
9
|
def initialize time:, action:, value:, issue:, story_points:
|
@@ -11,16 +14,8 @@ class SprintIssueChangeData
|
|
11
14
|
@story_points = story_points
|
12
15
|
end
|
13
16
|
|
14
|
-
def eql?(other)
|
15
|
-
(other.class == self.class) && (other.state == state)
|
16
|
-
end
|
17
|
-
|
18
|
-
def state
|
19
|
-
instance_variables.map { |variable| instance_variable_get variable }
|
20
|
-
end
|
21
|
-
|
22
17
|
def inspect
|
23
|
-
result =
|
18
|
+
result = +''
|
24
19
|
result << 'SprintIssueChangeData('
|
25
20
|
result << instance_variables.collect do |variable|
|
26
21
|
"#{variable}=#{instance_variable_get(variable).inspect}"
|
data/lib/jirametrics/status.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'jirametrics/value_equality'
|
4
|
+
|
3
5
|
class Status
|
4
|
-
|
6
|
+
include ValueEquality
|
7
|
+
attr_reader :id, :category_name, :category_id, :project_id
|
5
8
|
attr_accessor :name
|
6
9
|
|
7
10
|
def initialize name: nil, id: nil, category_name: nil, category_id: nil, project_id: nil, raw: nil
|
@@ -11,20 +14,27 @@ class Status
|
|
11
14
|
@category_id = category_id
|
12
15
|
@project_id = project_id
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
return unless raw
|
18
|
+
|
19
|
+
@raw = raw
|
20
|
+
@name = raw['name']
|
21
|
+
@id = raw['id'].to_i
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
category_config = raw['statusCategory']
|
24
|
+
@category_name = category_config['name']
|
25
|
+
@category_id = category_config['id'].to_i
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
+
# If this is a NextGen project then this status may be project specific. When this field is
|
28
|
+
# nil then the status is global.
|
29
|
+
@project_id = raw['scope']&.[]('project')&.[]('id')
|
30
|
+
end
|
27
31
|
|
32
|
+
def project_scoped?
|
33
|
+
!!@project_id
|
34
|
+
end
|
35
|
+
|
36
|
+
def global?
|
37
|
+
!project_scoped?
|
28
38
|
end
|
29
39
|
|
30
40
|
def to_s
|
@@ -32,13 +42,7 @@ class Status
|
|
32
42
|
" category_name=#{@category_name.inspect}, category_id=#{@category_id.inspect}, project_id=#{@project_id})"
|
33
43
|
end
|
34
44
|
|
35
|
-
def
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
def state
|
40
|
-
instance_variables
|
41
|
-
.reject {|variable| variable == :@raw}
|
42
|
-
.map { |variable| instance_variable_get variable }
|
45
|
+
def value_equality_ignored_variables
|
46
|
+
[:@raw]
|
43
47
|
end
|
44
48
|
end
|
@@ -9,13 +9,13 @@ class StatusCollection
|
|
9
9
|
including = expand_statuses including
|
10
10
|
excluding = expand_statuses excluding
|
11
11
|
|
12
|
-
@list.
|
12
|
+
@list.filter_map do |status|
|
13
13
|
keep = status.category_name == category_name ||
|
14
14
|
including.any? { |s| s.name == status.name }
|
15
15
|
keep = false if excluding.any? { |s| s.name == status.name }
|
16
16
|
|
17
17
|
status.name if keep
|
18
|
-
end
|
18
|
+
end
|
19
19
|
end
|
20
20
|
|
21
21
|
def expand_statuses names_or_ids
|
@@ -58,7 +58,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
58
58
|
[
|
59
59
|
[completed_hash, 'Completed', '#66FF99', 'green', false],
|
60
60
|
[aging_hash, 'Still in progress', '#FFCCCB', 'red', true]
|
61
|
-
].
|
61
|
+
].filter_map do |hash, label, fill_color, border_color, starts_hidden|
|
62
62
|
# We sort so that the smaller circles are in front of the bigger circles.
|
63
63
|
data = hash.sort(&hash_sorter).collect do |key, values|
|
64
64
|
estimate, cycle_time = *key
|
@@ -71,7 +71,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
71
71
|
'r' => values.size * 2,
|
72
72
|
'title' => title
|
73
73
|
}
|
74
|
-
end
|
74
|
+
end
|
75
75
|
next if data.empty?
|
76
76
|
|
77
77
|
{
|
@@ -121,11 +121,6 @@ class StoryPointAccuracyChart < ChartBase
|
|
121
121
|
story_points
|
122
122
|
end
|
123
123
|
|
124
|
-
def grouping range:, color: # rubocop:disable Lint/UnusedMethodArgument
|
125
|
-
deprecated message: 'The grouping declaration is no longer supported on the StoryPointEstimateChart ' \
|
126
|
-
'as we now use a bubble chart rather than colors'
|
127
|
-
end
|
128
|
-
|
129
124
|
def y_axis label:, sort_order: nil, &block
|
130
125
|
@y_axis_sort_order = sort_order
|
131
126
|
@y_axis_label = label
|
@@ -73,10 +73,10 @@ class ThroughputChart < ChartBase
|
|
73
73
|
|
74
74
|
def throughput_dataset periods:, completed_issues:
|
75
75
|
periods.collect do |period|
|
76
|
-
closed_issues = completed_issues.
|
76
|
+
closed_issues = completed_issues.filter_map do |issue|
|
77
77
|
stop_date = issue.board.cycletime.stopped_time(issue)&.to_date
|
78
78
|
[stop_date, issue] if stop_date && period.include?(stop_date)
|
79
|
-
end
|
79
|
+
end
|
80
80
|
|
81
81
|
date_label = "on #{period.end}"
|
82
82
|
date_label = "between #{period.begin} and #{period.end}" unless period.begin == period.end
|
@@ -8,10 +8,10 @@ class TrendLineCalculator
|
|
8
8
|
@valid = points.size >= 2
|
9
9
|
return unless valid?
|
10
10
|
|
11
|
-
sum_of_x = points.
|
12
|
-
sum_of_y = points.
|
13
|
-
sum_of_xy = points.
|
14
|
-
sum_of_x2 = points.
|
11
|
+
sum_of_x = points.sum { |x, _y| x }
|
12
|
+
sum_of_y = points.sum { |_x, y| y }
|
13
|
+
sum_of_xy = points.sum { |x, y| x * y }
|
14
|
+
sum_of_x2 = points.sum { |x, _y| x * x }
|
15
15
|
n = points.size.to_f
|
16
16
|
|
17
17
|
@slope = ((n * sum_of_xy) - (sum_of_x * sum_of_y)) / ((n * sum_of_x2) - (sum_of_x * sum_of_x))
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Perform an equality check based on whether the two objects have the same values
|
4
|
+
module ValueEquality
|
5
|
+
def ==(other)
|
6
|
+
return false unless other.class == self.class
|
7
|
+
|
8
|
+
code = lambda do |object|
|
9
|
+
names = object.instance_variables
|
10
|
+
if object.respond_to? :value_equality_ignored_variables
|
11
|
+
ignored_variables = object.value_equality_ignored_variables
|
12
|
+
names.reject! { |n| ignored_variables.include? n }
|
13
|
+
end
|
14
|
+
names.map { |variable| instance_variable_get variable }
|
15
|
+
end
|
16
|
+
|
17
|
+
code.call(self) == code.call(other)
|
18
|
+
end
|
19
|
+
|
20
|
+
def eql?(other)
|
21
|
+
self == other
|
22
|
+
end
|
23
|
+
end
|
data/lib/jirametrics.rb
CHANGED
@@ -44,6 +44,7 @@ class JiraMetrics < Thor
|
|
44
44
|
exit 1
|
45
45
|
end
|
46
46
|
|
47
|
+
require 'jirametrics/value_equality'
|
47
48
|
require 'jirametrics/chart_base'
|
48
49
|
require 'jirametrics/rules'
|
49
50
|
require 'jirametrics/grouping_rules'
|
@@ -55,6 +56,7 @@ class JiraMetrics < Thor
|
|
55
56
|
require 'jirametrics/expedited_chart'
|
56
57
|
require 'jirametrics/board_config'
|
57
58
|
require 'jirametrics/file_config'
|
59
|
+
require 'jirametrics/jira_gateway'
|
58
60
|
require 'jirametrics/trend_line_calculator'
|
59
61
|
require 'jirametrics/status'
|
60
62
|
require 'jirametrics/issue_link'
|
@@ -81,7 +83,7 @@ class JiraMetrics < Thor
|
|
81
83
|
require 'jirametrics/self_or_issue_dispatcher'
|
82
84
|
require 'jirametrics/throughput_chart'
|
83
85
|
require 'jirametrics/exporter'
|
84
|
-
require 'jirametrics/
|
86
|
+
require 'jirametrics/file_system'
|
85
87
|
require 'jirametrics/blocked_stalled_change'
|
86
88
|
require 'jirametrics/board_column'
|
87
89
|
require 'jirametrics/anonymizer'
|
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: '
|
4
|
+
version: '2.0'
|
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-04-
|
11
|
+
date: 2024-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: random-word
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 1.2.2
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rspec
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '3.4'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '3.4'
|
69
55
|
description: Tool to extract metrics from Jira and export to either a report or to
|
70
56
|
CSV files
|
71
57
|
email: mbowler@gargoylesoftware.com
|
@@ -106,6 +92,7 @@ files:
|
|
106
92
|
- lib/jirametrics/experimental/info.rb
|
107
93
|
- lib/jirametrics/exporter.rb
|
108
94
|
- lib/jirametrics/file_config.rb
|
95
|
+
- lib/jirametrics/file_system.rb
|
109
96
|
- lib/jirametrics/fix_version.rb
|
110
97
|
- lib/jirametrics/groupable_issue_chart.rb
|
111
98
|
- lib/jirametrics/grouping_rules.rb
|
@@ -127,7 +114,7 @@ files:
|
|
127
114
|
- lib/jirametrics/html_report_config.rb
|
128
115
|
- lib/jirametrics/issue.rb
|
129
116
|
- lib/jirametrics/issue_link.rb
|
130
|
-
- lib/jirametrics/
|
117
|
+
- lib/jirametrics/jira_gateway.rb
|
131
118
|
- lib/jirametrics/project_config.rb
|
132
119
|
- lib/jirametrics/rules.rb
|
133
120
|
- lib/jirametrics/self_or_issue_dispatcher.rb
|
@@ -140,6 +127,7 @@ files:
|
|
140
127
|
- lib/jirametrics/throughput_chart.rb
|
141
128
|
- lib/jirametrics/tree_organizer.rb
|
142
129
|
- lib/jirametrics/trend_line_calculator.rb
|
130
|
+
- lib/jirametrics/value_equality.rb
|
143
131
|
homepage: https://github.com/mikebowler/jirametrics
|
144
132
|
licenses:
|
145
133
|
- Apache-2.0
|