jirametrics 2.14 → 2.22

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aging_work_bar_chart.rb +176 -134
  3. data/lib/jirametrics/anonymizer.rb +8 -6
  4. data/lib/jirametrics/atlassian_document_format.rb +3 -3
  5. data/lib/jirametrics/bar_chart_range.rb +17 -0
  6. data/lib/jirametrics/board.rb +4 -0
  7. data/lib/jirametrics/board_config.rb +3 -1
  8. data/lib/jirametrics/change_item.rb +11 -4
  9. data/lib/jirametrics/chart_base.rb +34 -2
  10. data/lib/jirametrics/cycletime_config.rb +22 -4
  11. data/lib/jirametrics/cycletime_histogram.rb +3 -1
  12. data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
  13. data/lib/jirametrics/daily_view.rb +6 -20
  14. data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
  15. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
  16. data/lib/jirametrics/daily_wip_chart.rb +1 -1
  17. data/lib/jirametrics/data_quality_report.rb +8 -3
  18. data/lib/jirametrics/dependency_chart.rb +4 -1
  19. data/lib/jirametrics/downloader.rb +34 -99
  20. data/lib/jirametrics/downloader_for_cloud.rb +202 -0
  21. data/lib/jirametrics/downloader_for_data_center.rb +94 -0
  22. data/lib/jirametrics/examples/standard_project.rb +9 -9
  23. data/lib/jirametrics/expedited_chart.rb +1 -1
  24. data/lib/jirametrics/exporter.rb +12 -5
  25. data/lib/jirametrics/file_system.rb +24 -1
  26. data/lib/jirametrics/fix_version.rb +13 -0
  27. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  28. data/lib/jirametrics/groupable_issue_chart.rb +7 -1
  29. data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
  30. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  31. data/lib/jirametrics/html/aging_work_table.erb +2 -0
  32. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  33. data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
  34. data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
  35. data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
  36. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
  37. data/lib/jirametrics/html/expedited_chart.erb +3 -1
  38. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
  39. data/lib/jirametrics/html/index.css +16 -9
  40. data/lib/jirametrics/html/index.erb +3 -35
  41. data/lib/jirametrics/html/index.js +114 -0
  42. data/lib/jirametrics/html/sprint_burndown.erb +11 -3
  43. data/lib/jirametrics/html/throughput_chart.erb +2 -2
  44. data/lib/jirametrics/html_generator.rb +31 -0
  45. data/lib/jirametrics/html_report_config.rb +8 -25
  46. data/lib/jirametrics/issue.rb +125 -19
  47. data/lib/jirametrics/jira_gateway.rb +55 -17
  48. data/lib/jirametrics/project_config.rb +22 -2
  49. data/lib/jirametrics/raw_javascript.rb +13 -0
  50. data/lib/jirametrics/settings.json +3 -1
  51. data/lib/jirametrics/sprint.rb +12 -0
  52. data/lib/jirametrics/sprint_burndown.rb +6 -2
  53. data/lib/jirametrics/stitcher.rb +75 -0
  54. data/lib/jirametrics.rb +26 -70
  55. metadata +10 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5b1b9e8d837f6d74990d377db007ed5e55670de77738ab38a04c6d023d865c3
4
- data.tar.gz: 99e8ef3e85a3dfa2bd6d845a32d974718c0e9884d2f4750891ba491fd884ab0b
3
+ metadata.gz: 40a0ee85ee8d7d0d2ff071357afdea2aefdfeea8734f96eb789721d4a9f2607b
4
+ data.tar.gz: 11008f97848d8e3034cf95c5f615496c5677d8057f40b06d2724247d6087318d
5
5
  SHA512:
6
- metadata.gz: 804a25c8a19df9ae9862e2f95539183ece53e3841e590c64b8deb7048601bfb2b42ad5b75c075b85058abb7cea0cc875d517f4208bd5edbf98e2129e5567af59
7
- data.tar.gz: '0059cde9746423c5baf3ca1f47243b6489ccf77b8fd409255f7301f638eb1975b8b2815d266f720cd9f1cd51e2cf0bc9e33cbad3e34110ca1dedc4ad7fc9ff5b'
6
+ metadata.gz: 1e5ad6c1d5dddf5a89cc63498f1967f35c4761b6418c90019c8c6756599efb5b8badaf0ac4d50f94be4644508a97641430f178b3d40217cb02560aa017f33b80
7
+ data.tar.gz: a6a7f74dadbb8a2f7961a02e396a39dd994ff1a11756f57a5eadfd60af5c71bd79c3df65b7bfd77ca998059018f60cd6306b2006512b7cf5f760d75e2d0dfdf4
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jirametrics/chart_base'
4
+ require 'jirametrics/bar_chart_range'
4
5
 
5
6
  class AgingWorkBarChart < ChartBase
6
7
  def initialize block
7
8
  super()
8
9
 
10
+ @age_cutoff = nil
9
11
  header_text 'Aging Work Bar Chart'
10
12
  description_text <<-HTML
11
13
  <p>
@@ -13,16 +15,16 @@ class AgingWorkBarChart < ChartBase
13
15
  newest at the bottom.
14
16
  </p>
15
17
  <p>
16
- There are potentially three bars for each issue, although a bar may be missing if the issue has no
17
- information relevant to that. Hovering over any of the bars will provide more details.
18
+ There are three bars for each issue, and hovering over any of the bars will provide more details.
18
19
  <ol>
19
- <li>The top bar tells you what status the issue is in at any time. The colour indicates the
20
+ <li>Status: The status the issue was in at any time. The colour indicates the
20
21
  status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
21
22
  #{color_block '--status-category-inprogress-color'} In Progress,
22
23
  or #{color_block '--status-category-done-color'} Done</li>
23
- <li>The middle bar indicates #{color_block '--blocked-color'} blocked
24
+ <li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
24
25
  or #{color_block '--stalled-color'} stalled.</li>
25
- <li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
26
+ <li>Priority: This shows the priority over time. If one of these priorities is considered expedited
27
+ then it will be drawn with diagonal lines.</li>
26
28
  </ol>
27
29
  </p>
28
30
  #{describe_non_working_days}
@@ -36,6 +38,7 @@ class AgingWorkBarChart < ChartBase
36
38
 
37
39
  def run
38
40
  aging_issues = select_aging_issues issues: @issues
41
+ adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
39
42
 
40
43
  today = date_range.end
41
44
  sort_by_age! issues: aging_issues, today: today
@@ -58,134 +61,125 @@ class AgingWorkBarChart < ChartBase
58
61
  wrap_and_render(binding, __FILE__)
59
62
  end
60
63
 
64
+ def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
65
+ earliest_start_time = aging_issues.collect do |issue|
66
+ issue.board.cycletime.started_stopped_times(issue).first
67
+ end.min
68
+ return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
69
+
70
+ @time_range = earliest_start_time..@time_range.end
71
+ @date_range = @time_range.begin.to_date..@time_range.end.to_date
72
+ end
73
+
61
74
  def data_sets_for_one_issue issue:, today:
62
75
  cycletime = issue.board.cycletime
63
- issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
64
- issue_start_date = issue_start_time.to_date
65
- issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
66
- [
67
- status_data_sets(issue: issue, label: issue_label, today: today),
68
- blocked_data_sets(
69
- issue: issue,
70
- issue_label: issue_label,
71
- stack: 'blocked',
72
- issue_start_time: issue_start_time
73
- ),
74
- data_set_by_block(
75
- issue: issue,
76
- issue_label: issue_label,
77
- title_label: 'Expedited',
78
- stack: 'expedited',
79
- color: CssVariable['--expedited-color'],
80
- start_date: issue_start_date
81
- ) { |day| issue.expedited_on_date?(day) }
76
+ issue_start_time = cycletime.started_stopped_times(issue).first
77
+ end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
78
+
79
+ bar_data = [
80
+ ['status', collect_status_ranges(issue: issue, now: end_of_today)],
81
+ ['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
82
+ ['priority', collect_priority_ranges(issue: issue)]
82
83
  ]
84
+ bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
85
+
86
+ issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
87
+ bar_data.collect do |stack, ranges|
88
+ bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
89
+ end
83
90
  end
84
91
 
85
92
  def sort_by_age! issues:, today:
86
93
  issues.sort! do |a, b|
87
- a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
94
+ b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
88
95
  end
89
96
  end
90
97
 
91
98
  def select_aging_issues issues:
92
99
  issues.select do |issue|
93
100
  started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
94
- started_time && stopped_time.nil?
101
+ next false unless started_time && stopped_time.nil?
102
+
103
+ age = (date_range.end - started_time.to_date).to_i + 1
104
+ !(@age_cutoff && @age_cutoff >= age)
95
105
  end
96
106
  end
97
107
 
98
108
  def grow_chart_height_if_too_many_issues aging_issue_count:
99
- px_per_bar = 8
109
+ px_per_bar = 10
100
110
  bars_per_issue = 3
111
+ bars_per_issue += 1 if current_board.scrum?
112
+
101
113
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
102
114
  @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
103
115
  end
104
116
 
105
- def status_data_sets issue:, label:, today:
106
- cycletime = issue.board.cycletime
107
-
108
- issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
109
-
117
+ def collect_status_ranges issue:, now:
118
+ ranges = []
119
+ issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
110
120
  previous_start = nil
111
121
  previous_status = nil
112
-
113
- data_sets = []
114
- issue.changes.each do |change|
115
- next unless change.status?
116
-
117
- status = issue.find_or_create_status id: change.value_id, name: change.value
118
-
119
- unless previous_start.nil? || previous_start < issue_started_time
120
- hash = {
121
- type: 'bar',
122
- data: [{
123
- x: [chart_format(previous_start), chart_format(change.time)],
124
- y: label,
125
- title: "#{issue.type} : #{change.value}"
126
- }],
127
- backgroundColor: status_category_color(status),
128
- borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
129
- borderWidth: {
130
- top: 0,
131
- right: 1,
132
- bottom: 0,
133
- left: 0
134
- },
135
- stacked: true,
136
- stack: 'status'
137
- }
138
- data_sets << hash if date_range.include?(change.time.to_date)
122
+ issue.status_changes.each do |change|
123
+ new_status = issue.find_or_create_status id: change.value_id, name: change.value
124
+ if previous_start.nil?
125
+ previous_start = change.time
126
+ previous_status = new_status
127
+ next
139
128
  end
140
129
 
130
+ previous_start = issue_started_time if issue_started_time > previous_start
131
+
132
+ ranges << BarChartRange.new(
133
+ start: previous_start,
134
+ stop: change.time,
135
+ color: status_category_color(previous_status),
136
+ title: previous_status.to_s
137
+ )
141
138
  previous_start = change.time
142
- previous_status = status
139
+ previous_status = new_status
143
140
  end
144
141
 
145
- if previous_start
146
- data_sets << {
142
+ ranges << BarChartRange.new(
143
+ start: previous_start,
144
+ stop: now,
145
+ color: status_category_color(previous_status),
146
+ title: previous_status.to_s
147
+ )
148
+ ranges
149
+ end
150
+
151
+ def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
152
+ ranges.filter_map do |bar_chart_range|
153
+ next if bar_chart_range.stop < issue_start_time
154
+
155
+ background_color = bar_chart_range.color
156
+ if bar_chart_range.highlight
157
+ background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
158
+ end
159
+
160
+ {
147
161
  type: 'bar',
148
162
  data: [{
149
- x: [chart_format(previous_start), chart_format("#{today}T00:00:00#{@timezone_offset}")],
150
- y: label,
151
- title: "#{issue.type} : #{previous_status.name}"
163
+ x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
164
+ y: y_value,
165
+ title: bar_chart_range.title
152
166
  }],
153
- backgroundColor: status_category_color(previous_status),
167
+ backgroundColor: background_color,
168
+ borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
169
+ borderWidth: {
170
+ top: 0,
171
+ right: 1,
172
+ bottom: 0,
173
+ left: 0
174
+ },
154
175
  stacked: true,
155
- stack: 'status'
176
+ stack: stack
156
177
  }
157
178
  end
158
-
159
- data_sets
160
- end
161
-
162
- def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
163
- if settings['blocked_color']
164
- file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
165
- end
166
- if settings['stalled_color']
167
- file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
168
- end
169
-
170
- color = settings['blocked_color'] || '--blocked-color'
171
- color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
172
- {
173
- backgroundColor: CssVariable[color],
174
- data: [
175
- {
176
- title: starting_change.reasons,
177
- x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
178
- y: issue_label
179
- }
180
- ],
181
- stack: stack,
182
- stacked: true,
183
- type: 'bar'
184
- }
185
179
  end
186
180
 
187
- def blocked_data_sets issue:, issue_label:, issue_start_time:, stack:
188
- data_sets = []
181
+ def collect_blocked_stalled_ranges issue:, issue_start_time:
182
+ results = []
189
183
  starting_change = nil
190
184
 
191
185
  issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
@@ -195,58 +189,102 @@ class AgingWorkBarChart < ChartBase
195
189
  end
196
190
 
197
191
  if change.time >= issue_start_time
198
- data_sets << one_block_change_data_set(
199
- starting_change: starting_change, ending_time: change.time,
200
- issue_label: issue_label, stack: stack, issue_start_time: issue_start_time
192
+ color = settings['blocked_color'] || '--blocked-color'
193
+ color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
194
+
195
+ results << BarChartRange.new(
196
+ start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
201
197
  )
202
198
  end
203
199
 
204
200
  starting_change = change
205
201
  end
202
+ results
203
+ end
204
+
205
+ def collect_priority_ranges issue:
206
+ expedited_priority_names = settings['expedited_priority_names']
207
+
208
+ previous_change = nil
209
+ results = []
210
+
211
+ issue.changes.each do |change|
212
+ next unless change.priority?
213
+
214
+ if previous_change.nil?
215
+ previous_change = change
216
+ next
217
+ end
218
+
219
+ results << create_range_for_priority(
220
+ previous_change: previous_change, stop_time: change.time,
221
+ expedited_priority_names: expedited_priority_names
222
+ )
223
+ previous_change = change
224
+ end
206
225
 
207
- data_sets
226
+ results << create_range_for_priority(
227
+ previous_change: previous_change, stop_time: time_range.end,
228
+ expedited_priority_names: expedited_priority_names
229
+ )
230
+ results
208
231
  end
209
232
 
210
- def data_set_by_block(
211
- issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
212
- )
213
- started = nil
214
- ended = nil
215
- data = []
216
-
217
- (start_date..end_date).each do |day|
218
- if yield(day)
219
- started = day if started.nil?
220
- ended = day
221
- elsif ended
222
- data << {
223
- x: [chart_format(started), chart_format(ended)],
224
- y: issue_label,
225
- title: "#{issue.type} : #{title_label} #{label_days (ended - started).to_i + 1}"
226
- }
227
-
228
- started = nil
229
- ended = nil
233
+ def collect_sprint_ranges issue:
234
+ results = []
235
+ open_sprints = {}
236
+
237
+ issue.changes.each do |change|
238
+ next unless change.sprint?
239
+
240
+ removed_sprint_ids = change.old_value_id - change.value_id
241
+ added_sprint_ids = change.value_id - change.old_value_id
242
+
243
+ removed_sprint_ids.each do |id|
244
+ data = open_sprints.delete(id)
245
+ next unless data
246
+
247
+ completed = data[:sprint].completed_time
248
+ stop = completed ? [change.time, completed].min : change.time
249
+ results << BarChartRange.new(
250
+ start: data[:start_time], stop: stop,
251
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
252
+ )
253
+ end
254
+
255
+ added_sprint_ids.each do |id|
256
+ sprint = issue.board.sprints.find { |s| s.id == id }
257
+ next unless sprint
258
+ next if sprint.future?
259
+
260
+ start_time = [sprint.start_time, change.time].max
261
+ open_sprints[id] = { start_time: start_time, sprint: sprint }
230
262
  end
231
263
  end
232
264
 
233
- if started
234
- data << {
235
- x: [chart_format(started), chart_format(ended)],
236
- y: issue_label,
237
- title: "#{issue.type} : #{title_label} #{label_days (end_date - started).to_i + 1}"
238
- }
265
+ open_sprints.each_value do |data|
266
+ stop = data[:sprint].completed_time || time_range.end
267
+ results << BarChartRange.new(
268
+ start: data[:start_time], stop: stop,
269
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
270
+ )
239
271
  end
240
272
 
241
- return [] if data.empty?
273
+ results
274
+ end
242
275
 
243
- {
244
- type: 'bar',
245
- data: data,
246
- backgroundColor: color,
247
- stacked: true,
248
- stack: stack
249
- }
276
+ def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
277
+ expedited = expedited_priority_names.include?(previous_change.value)
278
+ title = "Priority: #{previous_change.value}"
279
+ title << ' (expedited)' if expedited
280
+
281
+ BarChartRange.new(
282
+ start: previous_change.time,
283
+ stop: stop_time,
284
+ color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
285
+ title: title,
286
+ highlight: expedited
287
+ )
250
288
  end
251
289
 
252
290
  def calculate_percent_line percentage: 85
@@ -255,4 +293,8 @@ class AgingWorkBarChart < ChartBase
255
293
 
256
294
  days[days.length * percentage / 100]
257
295
  end
296
+
297
+ def age_cutoff days
298
+ @age_cutoff = days
299
+ end
258
300
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  require 'random-word'
4
4
 
5
- class Anonymizer
5
+ class Anonymizer < ChartBase
6
6
  # needed for testing
7
7
  attr_reader :project_config, :issues
8
8
 
9
9
  def initialize project_config:, date_adjustment: -200
10
+ super()
10
11
  @project_config = project_config
11
12
  @issues = @project_config.issues
12
13
  @all_boards = @project_config.all_boards
@@ -130,18 +131,19 @@ class Anonymizer
130
131
  end
131
132
  end
132
133
 
133
- def shift_all_dates
134
- @file_system.log "Shifting all dates by #{@date_adjustment} days"
134
+ def shift_all_dates date_adjustment: @date_adjustment
135
+ adjustment_in_seconds = 60 * 60 * 24 * date_adjustment
136
+ @file_system.log "Shifting all dates by #{label_days date_adjustment}"
135
137
  @issues.each do |issue|
136
138
  issue.changes.each do |change|
137
- change.time = change.time + @date_adjustment
139
+ change.time = change.time + adjustment_in_seconds
138
140
  end
139
141
 
140
- issue.raw['fields']['updated'] = (issue.updated + @date_adjustment).to_s
142
+ issue.raw['fields']['updated'] = (issue.updated + adjustment_in_seconds).to_s
141
143
  end
142
144
 
143
145
  range = @project_config.time_range
144
- @project_config.time_range = (range.begin + @date_adjustment)..(range.end + @date_adjustment)
146
+ @project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
145
147
  end
146
148
 
147
149
  def random_name
@@ -13,9 +13,9 @@ class AtlassianDocumentFormat
13
13
  input
14
14
  .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
15
15
  .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
16
- .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
16
+ .gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
17
17
  .gsub("\n", '<br />')
18
- elsif input['content']
18
+ elsif input&.[]('content')
19
19
  input['content'].collect { |element| adf_node_to_html element }.join("\n")
20
20
  else
21
21
  # We have an actual ADF document with no content.
@@ -157,4 +157,4 @@ class AtlassianDocumentFormat
157
157
  text = "@#{user.display_name}" if user
158
158
  "<span class='account_id'>#{text}</span>"
159
159
  end
160
- end
160
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/value_equality'
4
+
5
+ class BarChartRange
6
+ include ValueEquality
7
+
8
+ attr_accessor :start, :stop, :color, :title, :highlight
9
+
10
+ def initialize start:, stop:, color:, title:, highlight: false
11
+ @start = start
12
+ @stop = stop
13
+ @color = color
14
+ @title = title
15
+ @highlight = highlight
16
+ end
17
+ end
@@ -116,4 +116,8 @@ class Board
116
116
  def estimation_configuration
117
117
  EstimationConfiguration.new raw: raw['estimation']
118
118
  end
119
+
120
+ def inspect
121
+ "Board(id: #{id}, name: #{name.inspect}, board_type: #{board_type.inspect})"
122
+ end
119
123
  end
@@ -24,7 +24,9 @@ class BoardConfig
24
24
  end
25
25
 
26
26
  @board.cycletime = CycleTimeConfig.new(
27
- parent_config: self, label: label, block: block, file_system: project_config.file_system
27
+ possible_statuses: project_config.possible_statuses,
28
+ label: label, block: block, file_system: project_config.file_system,
29
+ settings: project_config.settings
28
30
  )
29
31
  end
30
32
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ChangeItem
4
- attr_reader :field, :value_id, :old_value_id, :raw, :time, :author_raw
5
- attr_accessor :value, :old_value
4
+ attr_reader :field, :value_id, :old_value_id, :raw, :author_raw, :field_id
5
+ attr_accessor :value, :old_value, :time
6
6
 
7
7
  def initialize raw:, author_raw:, time:, artificial: false
8
8
  @raw = raw
@@ -13,9 +13,15 @@ class ChangeItem
13
13
 
14
14
  @field = @raw['field']
15
15
  @value = @raw['toString']
16
- @value_id = @raw['to'].to_i
17
16
  @old_value = @raw['fromString']
18
- @old_value_id = @raw['from']&.to_i
17
+ if sprint?
18
+ @value_id = @raw['to'].split(', ').collect(&:to_i)
19
+ @old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
20
+ else
21
+ @value_id = @raw['to'].to_i
22
+ @old_value_id = @raw['from']&.to_i
23
+ end
24
+ @field_id = @raw['fieldId']
19
25
  @artificial = artificial
20
26
  end
21
27
 
@@ -54,6 +60,7 @@ class ChangeItem
54
60
  message << ':' << old_value_id.inspect if status?
55
61
  end
56
62
  message << ", time: #{time_to_s(@time).inspect}"
63
+ message << ", field_id: #{@field_id.inspect}" if @field_id
57
64
  message << ', artificial' if artificial?
58
65
  message << ')'
59
66
  message
@@ -2,7 +2,8 @@
2
2
 
3
3
  class ChartBase
4
4
  attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
5
- :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system, :users
5
+ :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
6
+ :atlassian_document_format
6
7
  attr_writer :aggregated_project
7
8
  attr_reader :canvas_width, :canvas_height
8
9
 
@@ -21,6 +22,14 @@ class ChartBase
21
22
  @canvas_responsive = true
22
23
  end
23
24
 
25
+ def call_before_run &proc
26
+ (@call_before_run_procs ||= []) << proc
27
+ end
28
+
29
+ def before_run
30
+ @call_before_run_procs&.each { |proc| proc.call }
31
+ end
32
+
24
33
  def aggregated_project?
25
34
  @aggregated_project
26
35
  end
@@ -44,7 +53,7 @@ class ChartBase
44
53
 
45
54
  def render_top_text caller_binding
46
55
  result = +''
47
- result << "<h1>#{@header_text}</h1>" if @header_text
56
+ result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
48
57
  result << ERB.new(@description_text).result(caller_binding) if @description_text
49
58
  result
50
59
  end
@@ -278,4 +287,27 @@ class ChartBase
278
287
  </div>
279
288
  TEXT
280
289
  end
290
+
291
+ # Set a cycletime for just this one chart, overriding the one for the report.
292
+ def cycletime &block
293
+ call_before_run do
294
+ @cycletime = CycleTimeConfig.new(
295
+ possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system,
296
+ settings: settings
297
+ )
298
+ end
299
+ end
300
+
301
+ # Returns the cycletime in use right now, which may be specific to the chart or across the report.
302
+ def cycletime_for_issue issue
303
+ @cycletime || issue.board.cycletime
304
+ end
305
+
306
+ def seam_start type = 'chart'
307
+ "\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
308
+ end
309
+
310
+ def seam_end type = 'chart'
311
+ "\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
312
+ end
281
313
  end
@@ -6,12 +6,14 @@ require 'date'
6
6
  class CycleTimeConfig
7
7
  include SelfOrIssueDispatcher
8
8
 
9
- attr_reader :label, :parent_config
9
+ attr_reader :label, :possible_statuses, :settings, :file_system
10
10
 
11
- def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
12
- @parent_config = parent_config
11
+ def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
12
+
13
+ @possible_statuses = possible_statuses
13
14
  @label = label
14
15
  @today = today
16
+ @settings = settings
15
17
 
16
18
  # If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
17
19
  # may make it easier to find problems in the test code ;-)
@@ -63,6 +65,10 @@ class CycleTimeConfig
63
65
  end
64
66
 
65
67
  def started_stopped_changes issue
68
+ cache_key = "#{issue.key}:#{issue.board.id}"
69
+ last_result = (@cache ||= {})[cache_key]
70
+ return *last_result if last_result && settings['cache_cycletime_calculations']
71
+
66
72
  started = @start_at.call(issue)
67
73
  stopped = @stop_at.call(issue)
68
74
 
@@ -80,7 +86,15 @@ class CycleTimeConfig
80
86
  # for the start and not have it conflict.
81
87
  started = nil if started&.time == stopped&.time
82
88
 
83
- [started, stopped]
89
+ result = [started, stopped]
90
+ if last_result && result != last_result
91
+ @file_system.error(
92
+ "Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
93
+ "previous=#{last_result.inspect}"
94
+ )
95
+ end
96
+ @cache[cache_key] = result
97
+ result
84
98
  end
85
99
 
86
100
  def started_stopped_times issue
@@ -88,6 +102,10 @@ class CycleTimeConfig
88
102
  [started&.time, stopped&.time]
89
103
  end
90
104
 
105
+ def flush_cache
106
+ @cache = nil
107
+ end
108
+
91
109
  def started_stopped_dates issue
92
110
  started_time, stopped_time = started_stopped_times(issue)
93
111
  [started_time&.to_date, stopped_time&.to_date]