jirametrics 1.5 → 2.0
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/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
|