jirametrics 2.11 → 2.12pre2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 670df42835c7d41e30dcde01ead744a132f81313165297d434334e272de795c7
4
- data.tar.gz: 12822842210b4aeddb7f59e6681a9f1bd25060bae0ff98e8490d14e895dea657
3
+ metadata.gz: 499b5a712159d628f0c19b54d826bcfff4ed52a7a74563f171505df6f9bb9059
4
+ data.tar.gz: 694f1973f07bbd88f062a2d1250690df2d43dc326d4368492ac58d89af5184c7
5
5
  SHA512:
6
- metadata.gz: a4f36b8921c94a457710dc68251a9a2ddb38be2f0f7027991cf381fdf1cff28d2e549e508314733a3c753f583fd3c695b7e4979e1bb7be43c45de90ec001cbab
7
- data.tar.gz: 28a2553b31cd4de5fce0c943da38643ad13dbfad3a8bddc77f4764e179394968966bb676e70daa75a4faeb42a67027fb7efb63895b56c01a3db563a029df66aa
6
+ metadata.gz: 1b349c33e08ca97f135fb0d1a2625a238b35948ca16aa2e421887a79cc18c6f2537790e8093ce47b9e1cdc5b1e694f533efa2663b8739e00e5518b61908bb08e
7
+ data.tar.gz: f0da1809b457e37e473e7893c86dfc3009738ba98e5a9eba913f9a61f7ccd533a9b09deb1f3fa0126bfadbe3909cd811d3f590293c9fd9128d811ba3bf8aa56a
@@ -117,4 +117,8 @@ class Board
117
117
  all_names << name
118
118
  end
119
119
  end
120
+
121
+ def estimation_configuration
122
+ EstimationConfiguration.new raw: raw['estimation']
123
+ end
120
124
  end
@@ -124,6 +124,12 @@ class BoardMovementCalculator
124
124
  column_name, entry_time = find_current_column_and_entry_time_in_column issue
125
125
  return [nil, 'This issue is not visible on the board. No way to predict when it will be done.'] if column_name.nil?
126
126
 
127
+ if entry_time.nil?
128
+ message = "Couldn't find the time issue #{issue.key} entered column #{column_name.inspect}"
129
+ puts message, issue.dump
130
+ return [nil, message]
131
+ end
132
+
127
133
  age_in_column = (today - entry_time.to_date).to_i + 1
128
134
 
129
135
  message = nil
@@ -31,8 +31,6 @@ class ChangeItem
31
31
 
32
32
  def sprint? = (field == 'Sprint')
33
33
 
34
- def story_points? = (field == 'Story Points')
35
-
36
34
  def link? = (field == 'Link')
37
35
 
38
36
  def labels? = (field == 'labels')
@@ -22,15 +22,18 @@ class EstimateAccuracyChart < ChartBase
22
22
  </div>
23
23
  HTML
24
24
 
25
- @y_axis_label = 'Story Point Estimates'
26
25
  @y_axis_type = 'linear'
27
- @y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
26
+ @y_axis_block = ->(issue, start_time) { estimate_at(issue: issue, start_time: start_time)&.to_f }
28
27
  @y_axis_sort_order = nil
29
28
 
30
29
  instance_eval(&configuration_block)
31
30
  end
32
31
 
33
32
  def run
33
+ if @y_axis_label.nil?
34
+ text = current_board.estimation_configuration.units == :story_points ? 'Story Points' : 'Days'
35
+ @y_axis_label = "Estimated #{text}"
36
+ end
34
37
  data_sets = scan_issues
35
38
 
36
39
  return '' if data_sets.empty?
@@ -41,6 +44,7 @@ class EstimateAccuracyChart < ChartBase
41
44
  def scan_issues
42
45
  completed_hash, aging_hash = split_into_completed_and_aging issues: issues
43
46
 
47
+ estimation_units = current_board.estimation_configuration.units
44
48
  @has_aging_data = !aging_hash.empty?
45
49
 
46
50
  [
@@ -53,9 +57,13 @@ class EstimateAccuracyChart < ChartBase
53
57
  # We sort so that the smaller circles are in front of the bigger circles.
54
58
  data = hash.sort(&hash_sorter).collect do |key, values|
55
59
  estimate, cycle_time = *key
56
- estimate_label = "#{estimate}#{'pts' if @y_axis_type == 'linear'}"
57
- title = ["Estimate: #{estimate_label}, Cycletime: #{label_days(cycle_time)}, #{values.size} issues"] +
58
- values.collect { |issue| "#{issue.key}: #{issue.summary}" }
60
+
61
+ title = [
62
+ "Estimate: #{estimate_label(estimate: estimate, estimation_units: estimation_units)}, " \
63
+ "Cycletime: #{label_days(cycle_time)}, " \
64
+ "#{values.size} issues"
65
+ ] + values.collect { |issue| "#{issue.key}: #{issue.summary}" }
66
+
59
67
  {
60
68
  'x' => cycle_time,
61
69
  'y' => estimate,
@@ -77,6 +85,18 @@ class EstimateAccuracyChart < ChartBase
77
85
  end
78
86
  end
79
87
 
88
+ def estimate_label estimate:, estimation_units:
89
+ if @y_axis_type == 'linear'
90
+ if estimation_units == :story_points
91
+ estimate_label = "#{estimate}pts"
92
+ elsif estimation_units == :seconds
93
+ estimate_label = label_days estimate
94
+ end
95
+ end
96
+ estimate_label = estimate.to_s if estimate_label.nil?
97
+ estimate_label
98
+ end
99
+
80
100
  def split_into_completed_and_aging issues:
81
101
  aging_hash = {}
82
102
  completed_hash = {}
@@ -126,14 +146,18 @@ class EstimateAccuracyChart < ChartBase
126
146
  end
127
147
  end
128
148
 
129
- def story_points_at issue:, start_time:
130
- story_points = nil
149
+ def estimate_at issue:, start_time:, estimation_configuration: current_board.estimation_configuration
150
+ estimate = nil
151
+
131
152
  issue.changes.each do |change|
132
- return story_points if change.time >= start_time
153
+ return estimate if change.time >= start_time
133
154
 
134
- story_points = change.value if change.story_points?
155
+ if change.field == estimation_configuration.display_name || change.field == estimation_configuration.field_id
156
+ estimate = change.value
157
+ estimate = estimate.to_f / (24 * 60 * 60) if estimation_configuration.units == :seconds
158
+ end
135
159
  end
136
- story_points
160
+ estimate
137
161
  end
138
162
 
139
163
  def y_axis label:, sort_order: nil, &block
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EstimationConfiguration
4
+ attr_reader :units, :display_name, :field_id
5
+
6
+ def initialize raw:
7
+ @units = :story_points
8
+ @display_name = 'Story Points'
9
+
10
+ # If there wasn't an estimation section they rely on all defaults
11
+ return if raw.nil?
12
+
13
+ if raw['type'] == 'field'
14
+ @field_id = raw['field']['fieldId']
15
+ @display_name = raw['field']['displayName']
16
+ if @field_id == 'timeoriginalestimate'
17
+ @units = :seconds
18
+ @display_name = 'Original estimate'
19
+ end
20
+ elsif raw['type'] == 'issueCount'
21
+ @display_name = 'Issue Count'
22
+ @units = :issue_count
23
+ end
24
+ end
25
+ end
@@ -72,8 +72,12 @@ class Exporter
72
72
  grouping_rules do |issue, rules|
73
73
  if issue.resolution
74
74
  rules.label = "#{issue.status.name}:#{issue.resolution}"
75
- else
76
- rules.label = issue.status.name
75
+ if rules.label.start_with? 'Completed'
76
+ rules.label = 'Cancelled'
77
+ elsif rules.label.start_with? 'Closed'
78
+ rules.label = 'Done'
79
+ # rules.label = issue.status.name
80
+ end
77
81
  end
78
82
  end
79
83
  end
@@ -151,6 +151,8 @@ class ProjectConfig
151
151
  end
152
152
 
153
153
  def status_category_mapping status:, category:
154
+ return if @exporter.downloading?
155
+
154
156
  status, status_id = possible_statuses.parse_name_id status
155
157
  category, category_id = possible_statuses.parse_name_id category
156
158
 
@@ -121,11 +121,13 @@ 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
- story_points = 0.0
124
+ estimate = 0.0
125
125
  ever_in_sprint = false
126
126
  currently_in_sprint = false
127
127
  change_data = []
128
128
 
129
+ estimate_display_name = current_board.estimation_configuration.display_name
130
+
129
131
  issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
130
132
  completed_has_been_tracked = false
131
133
 
@@ -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 = story_points
145
+ value = estimate
144
146
  elsif currently_in_sprint && in_change_item == false
145
147
  action = :leave_sprint
146
- value = -story_points
148
+ value = -estimate
147
149
  end
148
150
  currently_in_sprint = in_change_item
149
- elsif change.story_points? && (issue_completed_time.nil? || change.time < issue_completed_time)
151
+ elsif change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time)
150
152
  action = :story_points
151
- story_points = change.value.to_f
152
- value = story_points - change.old_value.to_f
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 = -story_points
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, story_points: story_points
164
+ time: change.time, issue: issue, action: action, value: value, estimate: estimate
163
165
  )
164
166
  end
165
167
 
@@ -176,7 +178,7 @@ class SprintBurndown < ChartBase
176
178
  summary_stats = SprintSummaryStats.new
177
179
  summary_stats.completed = 0.0
178
180
 
179
- story_points = 0.0
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: story_points,
190
+ y: estimate,
189
191
  x: chart_format(sprint.start_time),
190
- title: "Sprint started with #{story_points} points"
192
+ title: "Sprint started with #{estimate} points"
191
193
  }
192
- summary_stats.started = story_points
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
- story_points += change_data.story_points
203
+ estimate += change_data.estimate
202
204
  when :leave_sprint
203
205
  issues_currently_in_sprint.delete change_data.issue.key
204
- story_points -= change_data.story_points
206
+ estimate -= change_data.estimate
205
207
  when :story_points
206
- story_points += change_data.value if issues_currently_in_sprint.include? change_data.issue.key
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
- old_story_points = change_data.story_points - change_data.value
217
- message = "Story points changed from #{old_story_points} points to #{change_data.story_points} points"
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.story_points || 'no'} points"
221
- summary_stats.added += change_data.story_points
222
+ message = "Added to sprint with #{change_data.estimate || 'no'} points"
223
+ summary_stats.added += change_data.estimate
222
224
  when :issue_stopped
223
- story_points -= change_data.story_points
224
- message = "Completed with #{change_data.story_points || 'no'} points"
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.story_points
228
+ summary_stats.completed += change_data.estimate
227
229
  when :leave_sprint
228
- message = "Removed from sprint with #{change_data.story_points || 'no'} points"
229
- summary_stats.removed += change_data.story_points
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: story_points,
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: story_points,
246
+ y: estimate,
245
247
  x: chart_format(sprint.start_time),
246
- title: "Sprint started with #{story_points} points"
248
+ title: "Sprint started with #{estimate} points"
247
249
  }
248
- summary_stats.started = story_points
250
+ summary_stats.started = estimate
249
251
  end
250
252
 
251
253
  if sprint.completed_time
252
254
  data_set << {
253
- y: story_points,
255
+ y: estimate,
254
256
  x: chart_format(sprint.completed_time),
255
- title: "Sprint ended with #{story_points} points unfinished"
257
+ title: "Sprint ended with #{estimate} points unfinished"
256
258
  }
257
- summary_stats.remaining = story_points
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: story_points,
264
+ y: estimate,
263
265
  x: chart_format(time_range.end),
264
- title: "Sprint still active. #{story_points} points still in progress."
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, :story_points
7
+ attr_reader :time, :action, :value, :issue, :estimate
8
8
 
9
- def initialize time:, action:, value:, issue:, story_points:
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
- @story_points = story_points
14
+ @estimate = estimate
15
15
  end
16
16
 
17
17
  def inspect
data/lib/jirametrics.rb CHANGED
@@ -112,6 +112,7 @@ class JiraMetrics < Thor
112
112
  require 'jirametrics/download_config'
113
113
  require 'jirametrics/columns_config'
114
114
  require 'jirametrics/hierarchy_table'
115
+ require 'jirametrics/estimation_configuration'
115
116
  require 'jirametrics/board'
116
117
  load config_file
117
118
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.11'
4
+ version: 2.12pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-11 00:00:00.000000000 Z
10
+ date: 2025-04-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -86,6 +86,7 @@ files:
86
86
  - lib/jirametrics/download_config.rb
87
87
  - lib/jirametrics/downloader.rb
88
88
  - lib/jirametrics/estimate_accuracy_chart.rb
89
+ - lib/jirametrics/estimation_configuration.rb
89
90
  - lib/jirametrics/examples/aggregated_project.rb
90
91
  - lib/jirametrics/examples/standard_project.rb
91
92
  - lib/jirametrics/expedited_chart.rb