jirametrics 2.6 → 2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +6 -1
  3. data/lib/jirametrics/aging_work_bar_chart.rb +6 -6
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
  5. data/lib/jirametrics/aging_work_table.rb +4 -5
  6. data/lib/jirametrics/blocked_stalled_change.rb +1 -1
  7. data/lib/jirametrics/board.rb +14 -12
  8. data/lib/jirametrics/chart_base.rb +5 -7
  9. data/lib/jirametrics/cycletime_config.rb +26 -7
  10. data/lib/jirametrics/cycletime_histogram.rb +1 -1
  11. data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
  12. data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
  13. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
  14. data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
  15. data/lib/jirametrics/daily_wip_chart.rb +7 -9
  16. data/lib/jirametrics/data_quality_report.rb +45 -6
  17. data/lib/jirametrics/dependency_chart.rb +3 -4
  18. data/lib/jirametrics/discard_changes_before.rb +1 -1
  19. data/lib/jirametrics/downloader.rb +14 -13
  20. data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
  21. data/lib/jirametrics/examples/aggregated_project.rb +1 -1
  22. data/lib/jirametrics/examples/standard_project.rb +10 -7
  23. data/lib/jirametrics/expedited_chart.rb +1 -2
  24. data/lib/jirametrics/exporter.rb +25 -0
  25. data/lib/jirametrics/file_system.rb +1 -1
  26. data/lib/jirametrics/flow_efficiency_scatterplot.rb +113 -0
  27. data/lib/jirametrics/html/data_quality_report.erb +12 -0
  28. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  29. data/lib/jirametrics/html_report_config.rb +2 -1
  30. data/lib/jirametrics/issue.rb +62 -10
  31. data/lib/jirametrics/jira_gateway.rb +1 -1
  32. data/lib/jirametrics/project_config.rb +27 -19
  33. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  34. data/lib/jirametrics/sprint_burndown.rb +2 -2
  35. data/lib/jirametrics/status.rb +1 -1
  36. data/lib/jirametrics/status_collection.rb +4 -0
  37. data/lib/jirametrics/throughput_chart.rb +1 -1
  38. data/lib/jirametrics.rb +15 -5
  39. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 749f92d4329a18999bf8954aee5b41955047ef263d7a37fe766b2233d697a8c9
4
- data.tar.gz: f174b2241d23a3d7a9ff34b20093ff0716406197563df4b7f4f6447773fc7a51
3
+ metadata.gz: 73e2f2e8408a55ccecb221ba0c6d241c77f19de18d2100a9b05213a6470ed65f
4
+ data.tar.gz: d0f9056615c44e783c824ca1c6566c95bf68f16a26f81a596475c1d458fee9f1
5
5
  SHA512:
6
- metadata.gz: f89eb6c1a9655163d91b19d9f5b92f79a7d714e6b45fe53e972248f20c5d788a356e6d5be0cbfe24a70665381bf0a7a28ce898b0e31cef9c4c3349d8ce335558
7
- data.tar.gz: b95ee06e9b3ef78aba0635aa634fa56082da2aa8bfa759f7e38be2eb2b6889a04a23e99dc8aa9a021391d8db4fe797247fb2b9379c5801fc2c5ec7a3bc3ff103
6
+ metadata.gz: e58594175d798269e0e4b8ec1fd4c0f8b693767670222bc6144be19646b04f83e5edb508e4f933f8466865018e8802f789ecb348db368c3f626318df7774c0e0
7
+ data.tar.gz: 4555628c33a398cd3aaedd6e36a074b54d89f5a431f47d8617344e4af45e389e529ba54dd9cbd7d9e7d8ad7f673a68f664d00b951451b433d47f90c6feb439a5
@@ -62,7 +62,12 @@ class AggregateConfig
62
62
  'the first file section'
63
63
  end
64
64
  end
65
- @project_config.add_issues issues
65
+
66
+ if issues.nil?
67
+ log "No issues found for #{project_name}"
68
+ else
69
+ @project_config.add_issues issues
70
+ end
66
71
  end
67
72
 
68
73
  def find_time_range projects:
@@ -53,8 +53,8 @@ class AgingWorkBarChart < ChartBase
53
53
  percentage_line_x = date_range.end - calculate_percent_line if percentage
54
54
 
55
55
  if aging_issues.empty?
56
- @description_text = "<p>There is no aging work</p>"
57
- return render_top_text(binding) #if aging_issues.empty?
56
+ @description_text = '<p>There is no aging work</p>'
57
+ return render_top_text(binding)
58
58
  end
59
59
 
60
60
  wrap_and_render(binding, __FILE__)
@@ -62,7 +62,7 @@ class AgingWorkBarChart < ChartBase
62
62
 
63
63
  def data_sets_for_one_issue issue:, today:
64
64
  cycletime = issue.board.cycletime
65
- issue_start_time = cycletime.started_time(issue)
65
+ issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
66
66
  issue_start_date = issue_start_time.to_date
67
67
  issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
68
68
  [
@@ -92,8 +92,8 @@ class AgingWorkBarChart < ChartBase
92
92
 
93
93
  def select_aging_issues issues:
94
94
  issues.select do |issue|
95
- cycletime = issue.board.cycletime
96
- cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
95
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
96
+ started_time && stopped_time.nil?
97
97
  end
98
98
  end
99
99
 
@@ -107,7 +107,7 @@ class AgingWorkBarChart < ChartBase
107
107
  def status_data_sets issue:, label:, today:
108
108
  cycletime = issue.board.cycletime
109
109
 
110
- issue_started_time = cycletime.started_time(issue)
110
+ issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
111
111
 
112
112
  previous_start = nil
113
113
  previous_status = nil
@@ -114,7 +114,7 @@ class AgingWorkInProgressChart < ChartBase
114
114
  def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
115
115
  issues.filter_map do |issue|
116
116
  stop = issue.first_time_in_status(*status_ids)
117
- start = issue.board.cycletime.started_time(issue)
117
+ start, = issue.board.cycletime.started_stopped_times(issue)
118
118
 
119
119
  # Skip if either it hasn't crossed the boundary or we can't tell when it started.
120
120
  next if stop.nil? || start.nil?
@@ -36,16 +36,15 @@ class AgingWorkTable < ChartBase
36
36
 
37
37
  def expedited_but_not_started
38
38
  @issues.select do |issue|
39
- cycletime = issue.board.cycletime
40
- cycletime.started_time(issue).nil? && cycletime.stopped_time(issue).nil? && issue.expedited?
39
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
40
+ started_time.nil? && stopped_time.nil? && issue.expedited?
41
41
  end.sort_by(&:created)
42
42
  end
43
43
 
44
44
  def select_aging_issues
45
45
  aging_issues = @issues.select do |issue|
46
46
  cycletime = issue.board.cycletime
47
- started = cycletime.started_time(issue)
48
- stopped = cycletime.stopped_time(issue)
47
+ started, stopped = cycletime.started_stopped_times(issue)
49
48
  next false if started.nil? || stopped
50
49
  next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
51
50
 
@@ -64,7 +63,7 @@ class AgingWorkTable < ChartBase
64
63
  end
65
64
 
66
65
  def blocked_text issue
67
- started_time = issue.board.cycletime.started_time(issue)
66
+ started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
68
67
  return nil if started_time.nil?
69
68
 
70
69
  current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
@@ -47,7 +47,7 @@ class BlockedStalledChange
47
47
  end
48
48
 
49
49
  def inspect
50
- text = +"BlockedStalledChange(time: '#{@time}', "
50
+ text = "BlockedStalledChange(time: '#{@time}', "
51
51
  if active?
52
52
  text << 'Active'
53
53
  else
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Board
4
- attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :backlog_statuses
4
+ attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
5
5
  attr_accessor :cycletime, :project_config
6
6
 
7
7
  def initialize raw:, possible_statuses: StatusCollection.new
@@ -15,24 +15,26 @@ class Board
15
15
  # For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
16
16
  # visible on the board. If the board is configured to have a kanban backlog then it will have
17
17
  # statuses matched to it and otherwise, there will be no statuses.
18
- if kanban?
19
- @backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
20
- # There is a status defined as being 'backlog' that is no longer being returned in statuses.
21
- # We used to display a warning for this but honestly, there is nothing that anyone can do about it
22
- # so now we just quietly ignore it.
23
- end
24
- columns = columns[1..]
25
- else
26
- # We currently don't know how to get the backlog status for a Scrum board
27
- @backlog_statuses = []
28
- end
18
+ columns = columns[1..] if kanban?
29
19
 
20
+ @backlog_statuses = []
30
21
  @visible_columns = columns.filter_map do |column|
31
22
  # It's possible for a column to be defined without any statuses and in this case, it won't be visible.
32
23
  BoardColumn.new column unless status_ids_from_column(column).empty?
33
24
  end
34
25
  end
35
26
 
27
+ def backlog_statuses
28
+ if @backlog_statuses.empty? && kanban?
29
+ status_ids = status_ids_from_column raw['columnConfig']['columns'].first
30
+ @backlog_statuses = @possible_statuses.expand_statuses(status_ids) do |unknown_status|
31
+ # If a status is returned here that is no longer in the system then there's nothing useful
32
+ # we can do about it. Ignore it.
33
+ end
34
+ end
35
+ @backlog_statuses
36
+ end
37
+
36
38
  def server_url_prefix
37
39
  raise "Cannot parse self: #{@raw['self'].inspect}" unless @raw['self'] =~ /^(https?:\/\/.+)\/rest\//
38
40
 
@@ -144,8 +144,7 @@ class ChartBase
144
144
  def completed_issues_in_range include_unstarted: false
145
145
  issues.select do |issue|
146
146
  cycletime = issue.board.cycletime
147
- stopped_time = cycletime.stopped_time(issue)
148
- started_time = cycletime.started_time(issue)
147
+ started_time, stopped_time = cycletime.started_stopped_times(issue)
149
148
 
150
149
  stopped_time &&
151
150
  date_range.include?(stopped_time.to_date) && # Remove outside range
@@ -179,7 +178,7 @@ class ChartBase
179
178
  def format_status name_or_id, board:, is_category: false
180
179
  begin
181
180
  statuses = board.possible_statuses.expand_statuses([name_or_id])
182
- rescue StatusNotFoundError => e
181
+ rescue StatusNotFoundError
183
182
  return "<span style='color: red'>#{name_or_id}</span>"
184
183
  end
185
184
 
@@ -189,10 +188,9 @@ class ChartBase
189
188
  visibility = ''
190
189
  if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
191
190
  visibility = icon_span(
192
- title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
193
- icon: ' 👀'
194
- )
195
-
191
+ title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
192
+ icon: ' 👀'
193
+ )
196
194
  end
197
195
  text = is_category ? status.category_name : status.name
198
196
  "<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
@@ -26,31 +26,50 @@ class CycleTimeConfig
26
26
  end
27
27
 
28
28
  def in_progress? issue
29
- started_time(issue) && stopped_time(issue).nil?
29
+ started_time, stopped_time = started_stopped_times(issue)
30
+ started_time && stopped_time.nil?
30
31
  end
31
32
 
32
33
  def done? issue
33
- stopped_time(issue)
34
+ started_stopped_times(issue).last
34
35
  end
35
36
 
36
37
  def started_time issue
37
- @start_at.call(issue)
38
+ deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
39
+ started_stopped_times(issue).first
38
40
  end
39
41
 
40
42
  def stopped_time issue
41
- @stop_at.call(issue)
43
+ deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
44
+ started_stopped_times(issue).last
45
+ end
46
+
47
+ def started_stopped_times issue
48
+ started = @start_at.call(issue)
49
+ stopped = @stop_at.call(issue)
50
+
51
+ # In the case where started and stopped are exactly the same time, we pretend that
52
+ # it just stopped and never started. This allows us to have logic like 'in or right of'
53
+ # for the start and not have it conflict.
54
+ started = nil if started == stopped
55
+
56
+ [started, stopped]
57
+ end
58
+
59
+ def started_stopped_dates issue
60
+ started_time, stopped_time = started_stopped_times(issue)
61
+ [started_time&.to_date, stopped_time&.to_date]
42
62
  end
43
63
 
44
64
  def cycletime issue
45
- start = started_time(issue)
46
- stop = stopped_time(issue)
65
+ start, stop = started_stopped_times(issue)
47
66
  return nil if start.nil? || stop.nil?
48
67
 
49
68
  (stop.to_date - start.to_date).to_i + 1
50
69
  end
51
70
 
52
71
  def age issue, today: nil
53
- start = started_time(issue)
72
+ start = started_stopped_times(issue).first
54
73
  stop = today || @today || Date.today
55
74
  return nil if start.nil? || stop.nil?
56
75
 
@@ -30,7 +30,7 @@ class CycletimeHistogram < ChartBase
30
30
  stopped_issues = completed_issues_in_range include_unstarted: true
31
31
 
32
32
  # For the histogram, we only want to consider items that have both a start and a stop time.
33
- histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_time(issue) }
33
+ histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_stopped_times(issue).first }
34
34
  rules_to_issues = group_issues histogram_issues
35
35
 
36
36
  data_sets = rules_to_issues.keys.collect do |rules|
@@ -24,7 +24,7 @@ class CycletimeScatterplot < ChartBase
24
24
  predict that most work of this type will complete in <%= overall_percent_line %> days or
25
25
  less. The other lines reflect the 85% line for that respective type of work.
26
26
  </div>
27
- #{ describe_non_working_days }
27
+ #{describe_non_working_days}
28
28
  HTML
29
29
 
30
30
  init_configuration_block block do
@@ -53,10 +53,7 @@ class CycletimeScatterplot < ChartBase
53
53
  def create_datasets completed_issues
54
54
  data_sets = []
55
55
 
56
- groups = group_issues completed_issues
57
-
58
- groups.each_key do |rules|
59
- completed_issues_by_type = groups[rules]
56
+ group_issues(completed_issues).each do |rules, completed_issues_by_type|
60
57
  label = rules.label
61
58
  color = rules.color
62
59
  percent_line = calculate_percent_line completed_issues_by_type
@@ -117,7 +114,7 @@ class CycletimeScatterplot < ChartBase
117
114
 
118
115
  {
119
116
  y: cycle_time,
120
- x: chart_format(issue.board.cycletime.stopped_time(issue)),
117
+ x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
121
118
  title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
122
119
  }
123
120
  end
@@ -4,7 +4,7 @@ require 'jirametrics/daily_wip_chart'
4
4
 
5
5
  class DailyWipByAgeChart < DailyWipChart
6
6
  def initialize block
7
- super(block)
7
+ super
8
8
 
9
9
  add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
10
10
  'Less than a day',
@@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart
49
49
  end
50
50
 
51
51
  def default_grouping_rules issue:, rules:
52
- cycletime = issue.board.cycletime
53
- started = cycletime.started_time(issue)&.to_date
54
- stopped = cycletime.stopped_time(issue)&.to_date
52
+ started, stopped = issue.board.cycletime.started_stopped_dates(issue)
55
53
 
56
54
  rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
57
55
 
@@ -39,8 +39,8 @@ class DailyWipByBlockedStalledChart < DailyWipChart
39
39
  end
40
40
 
41
41
  def default_grouping_rules issue:, rules:
42
- started = issue.board.cycletime.started_time(issue)
43
- stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
42
+ started, stopped = issue.board.cycletime.started_stopped_times(issue)
43
+ stopped_date = stopped&.to_date
44
44
 
45
45
  date = rules.current_date
46
46
  change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
@@ -3,10 +3,6 @@
3
3
  require 'jirametrics/daily_wip_chart'
4
4
 
5
5
  class DailyWipByParentChart < DailyWipChart
6
- def initialize block
7
- super(block)
8
- end
9
-
10
6
  def default_header_text
11
7
  'Daily WIP, grouped by the parent ticket (Epic, Feature, etc)'
12
8
  end
@@ -6,7 +6,7 @@ class DailyGroupingRules < GroupingRules
6
6
  attr_accessor :current_date, :group_priority, :issue_hint
7
7
 
8
8
  def initialize
9
- super()
9
+ super
10
10
  @group_priority = 0
11
11
  end
12
12
  end
@@ -22,10 +22,10 @@ class DailyWipChart < ChartBase
22
22
 
23
23
  instance_eval(&block) if block
24
24
 
25
- unless @group_by_block
26
- grouping_rules do |issue, rules|
27
- default_grouping_rules issue: issue, rules: rules
28
- end
25
+ return if @group_by_block
26
+
27
+ grouping_rules do |issue, rules|
28
+ default_grouping_rules issue: issue, rules: rules
29
29
  end
30
30
  end
31
31
 
@@ -66,9 +66,7 @@ class DailyWipChart < ChartBase
66
66
  hash = {}
67
67
 
68
68
  @issues.each do |issue|
69
- cycletime = issue.board.cycletime
70
- start = cycletime.started_time(issue)&.to_date
71
- stop = cycletime.stopped_time(issue)&.to_date
69
+ start, stop = issue.board.cycletime.started_stopped_dates(issue)
72
70
  next if start.nil? && stop.nil?
73
71
 
74
72
  # If it stopped but never started then assume it started at creation so the data points
@@ -158,7 +156,7 @@ class DailyWipChart < ChartBase
158
156
 
159
157
  {
160
158
  type: 'line',
161
- label: "Trendline",
159
+ label: 'Trendline',
162
160
  data: data_points,
163
161
  fill: false,
164
162
  borderWidth: 1,
@@ -48,6 +48,7 @@ class DataQualityReport < ChartBase
48
48
  scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
49
49
  scan_for_stopped_before_started entry: entry
50
50
  scan_for_issues_not_started_with_subtasks_that_have entry: entry
51
+ scan_for_incomplete_subtasks_when_issue_done entry: entry
51
52
  scan_for_discarded_data entry: entry
52
53
  end
53
54
 
@@ -87,9 +88,7 @@ class DataQualityReport < ChartBase
87
88
 
88
89
  def initialize_entries
89
90
  @entries = @issues.filter_map do |issue|
90
- cycletime = issue.board.cycletime
91
- started = cycletime.started_time(issue)
92
- stopped = cycletime.stopped_time(issue)
91
+ started, stopped = issue.board.cycletime.started_stopped_times(issue)
93
92
  next if stopped && stopped < time_range.begin
94
93
  next if started && started > time_range.end
95
94
 
@@ -223,14 +222,13 @@ class DataQualityReport < ChartBase
223
222
 
224
223
  started_subtasks = []
225
224
  entry.issue.subtasks.each do |subtask|
226
- started_subtasks << subtask if subtask.board.cycletime.started_time(subtask)
225
+ started_subtasks << subtask if subtask.board.cycletime.started_stopped_times(subtask).first
227
226
  end
228
227
 
229
228
  return if started_subtasks.empty?
230
229
 
231
230
  subtask_labels = started_subtasks.collect do |subtask|
232
- "Started subtask: #{link_to_issue(subtask)} (#{format_status subtask.status.name, board: entry.issue.board}) " \
233
- "#{subtask.summary[..50].inspect}"
231
+ subtask_label(subtask)
234
232
  end
235
233
  entry.report(
236
234
  problem_key: :issue_not_started_but_subtasks_have,
@@ -238,6 +236,47 @@ class DataQualityReport < ChartBase
238
236
  )
239
237
  end
240
238
 
239
+ def subtask_label subtask
240
+ "<img src='#{subtask.type_icon_url}' /> #{link_to_issue(subtask)} #{subtask.summary[..50].inspect}"
241
+ end
242
+
243
+ def time_as_english(from_time, to_time)
244
+ delta = (to_time - from_time).to_i
245
+ return "#{delta} seconds" if delta < 60
246
+
247
+ delta /= 60
248
+ return "#{delta} minutes" if delta < 60
249
+
250
+ delta /= 60
251
+ return "#{delta} hours" if delta < 24
252
+
253
+ delta /= 24
254
+ "#{delta} days"
255
+ end
256
+
257
+ def scan_for_incomplete_subtasks_when_issue_done entry:
258
+ return unless entry.stopped
259
+
260
+ subtask_labels = entry.issue.subtasks.filter_map do |subtask|
261
+ subtask_started, subtask_stopped = subtask.board.cycletime.started_stopped_times(subtask)
262
+
263
+ if !subtask_started && !subtask_stopped
264
+ "#{subtask_label subtask} (Not even started)"
265
+ elsif !subtask_stopped
266
+ "#{subtask_label subtask} (Still not done)"
267
+ elsif subtask_stopped > entry.stopped
268
+ "#{subtask_label subtask} (Closed #{time_as_english entry.stopped, subtask_stopped} later)"
269
+ end
270
+ end
271
+
272
+ return if subtask_labels.empty?
273
+
274
+ entry.report(
275
+ problem_key: :incomplete_subtasks_when_issue_done,
276
+ detail: subtask_labels.join('<br />')
277
+ )
278
+ end
279
+
241
280
  def label_issues number
242
281
  return '1 item' if number == 1
243
282
 
@@ -183,9 +183,8 @@ class DependencyChart < ChartBase
183
183
  return stdout.read
184
184
  end
185
185
  rescue # rubocop:disable Style/RescueStandardError
186
- message = "Unable to execute the command 'dot' which is part of graphviz. " \
187
- 'Ensure that graphviz is installed and that dot is in your path.'
188
- puts message
186
+ message = 'Unable to generate the dependency chart because graphviz could not be found in the path.'
187
+ file_system.log message, also_write_to_stderr: true
189
188
  message
190
189
  end
191
190
 
@@ -229,7 +228,7 @@ class DependencyChart < ChartBase
229
228
  elsif is_done
230
229
  line2 << 'Done'
231
230
  else
232
- started_at = issue.board.cycletime.started_time(issue)
231
+ started_at = issue.board.cycletime.started_stopped_times(issue).first
233
232
  if started_at.nil?
234
233
  line2 << 'Not started'
235
234
  else
@@ -31,7 +31,7 @@ module DiscardChangesBefore
31
31
  discard_changes_before_hook issues_cutoff_times
32
32
 
33
33
  issues_cutoff_times.each do |issue, cutoff_time|
34
- issue.changes.reject! { |change| change.status? && change.time <= cutoff_time && change.artificial? == false }
34
+ issue.discard_changes_before cutoff_time
35
35
  end
36
36
  end
37
37
  end
@@ -41,8 +41,8 @@ class Downloader
41
41
  remove_old_files
42
42
  download_statuses
43
43
  find_board_ids.each do |id|
44
- download_board_configuration board_id: id
45
- download_issues board_id: id
44
+ board = download_board_configuration board_id: id
45
+ download_issues board: board
46
46
  end
47
47
 
48
48
  save_metadata
@@ -64,19 +64,19 @@ class Downloader
64
64
  ids
65
65
  end
66
66
 
67
- def download_issues board_id:
68
- log " Downloading primary issues for board #{board_id}", both: true
67
+ def download_issues board:
68
+ log " Downloading primary issues for board #{board.id}", both: true
69
69
  path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
70
70
  unless Dir.exist?(path)
71
71
  log " Creating path #{path}"
72
72
  Dir.mkdir(path)
73
73
  end
74
74
 
75
- filter_id = @board_id_to_filter_id[board_id]
75
+ filter_id = @board_id_to_filter_id[board.id]
76
76
  jql = make_jql(filter_id: filter_id)
77
- jira_search_by_jql(jql: jql, initial_query: true, board_id: board_id, path: path)
77
+ jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
78
78
 
79
- log " Downloading linked issues for board #{board_id}", both: true
79
+ log " Downloading linked issues for board #{board.id}", both: true
80
80
  loop do
81
81
  @issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
82
82
  break if @issue_keys_pending_download.empty?
@@ -84,11 +84,11 @@ class Downloader
84
84
  keys_to_request = @issue_keys_pending_download[0..99]
85
85
  @issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
86
86
  jql = "key in (#{keys_to_request.join(', ')})"
87
- jira_search_by_jql(jql: jql, initial_query: false, board_id: board_id, path: path)
87
+ jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
88
88
  end
89
89
  end
90
90
 
91
- def jira_search_by_jql jql:, initial_query:, board_id:, path:
91
+ def jira_search_by_jql jql:, initial_query:, board:, path:
92
92
  intercept_jql = @download_config.project_config.settings['intercept_jql']
93
93
  jql = intercept_jql.call jql if intercept_jql
94
94
 
@@ -108,8 +108,8 @@ class Downloader
108
108
  issue_json['exporter'] = {
109
109
  'in_initial_query' => initial_query
110
110
  }
111
- identify_other_issues_to_be_downloaded issue_json
112
- file = "#{issue_json['key']}-#{board_id}.json"
111
+ identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
112
+ file = "#{issue_json['key']}-#{board.id}.json"
113
113
 
114
114
  @file_system.save_json(json: issue_json, filename: File.join(path, file))
115
115
  end
@@ -124,8 +124,8 @@ class Downloader
124
124
  end
125
125
  end
126
126
 
127
- def identify_other_issues_to_be_downloaded raw_issue
128
- issue = Issue.new raw: raw_issue, board: nil
127
+ def identify_other_issues_to_be_downloaded raw_issue:, board:
128
+ issue = Issue.new raw: raw_issue, board: board
129
129
  @issue_keys_downloaded_in_current_run << issue.key
130
130
 
131
131
  # Parent
@@ -171,6 +171,7 @@ class Downloader
171
171
  @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
172
172
 
173
173
  download_sprints board_id: board_id if json['type'] == 'scrum'
174
+ Board.new raw: json
174
175
  end
175
176
 
176
177
  def download_sprints board_id:
@@ -83,8 +83,7 @@ class EstimateAccuracyChart < ChartBase
83
83
 
84
84
  issues.each do |issue|
85
85
  cycletime = issue.board.cycletime
86
- start_time = cycletime.started_time(issue)
87
- stop_time = cycletime.stopped_time(issue)
86
+ start_time, stop_time = cycletime.started_stopped_times(issue)
88
87
 
89
88
  next unless start_time
90
89
 
@@ -33,7 +33,7 @@ class Exporter
33
33
  html '<h1>Boards included in this report</h1><ul>', type: :header
34
34
  board_lines = []
35
35
  included_projects.each do |project|
36
- project.all_boards.values.each do |board|
36
+ project.all_boards.each_value do |board|
37
37
  board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
38
38
  end
39
39
  end
@@ -7,7 +7,8 @@
7
7
  class Exporter
8
8
  def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
9
9
  default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
10
- rolling_date_count: 90, no_earlier_than: nil
10
+ rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],
11
+ show_experimental_charts: false
11
12
 
12
13
  project name: name do
13
14
  puts name
@@ -38,11 +39,14 @@ class Exporter
38
39
  end
39
40
  end
40
41
 
42
+ issues.reject! do |issue|
43
+ ignore_types.include? issue.type
44
+ end
45
+
46
+ discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
47
+
41
48
  file do
42
49
  file_suffix '.html'
43
- issues.reject! do |issue|
44
- %w[Sub-task Epic].include? issue.type
45
- end
46
50
 
47
51
  issues.reject! { |issue| ignore_issues.include? issue.key } if ignore_issues
48
52
 
@@ -52,12 +56,10 @@ class Exporter
52
56
  html "<H1>#{name}</H1>", type: :header
53
57
  boards.each_key do |id|
54
58
  board = find_board id
55
- html "<div><a href='#{board.url}'>#{id} #{board.name}</a></div>",
59
+ html "<div><a href='#{board.url}'>#{id} #{board.name}</a> (#{board.board_type})</div>",
56
60
  type: :header
57
61
  end
58
62
 
59
- discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
60
-
61
63
  cycletime_scatterplot do
62
64
  show_trend_lines
63
65
  end
@@ -84,6 +86,7 @@ class Exporter
84
86
  daily_wip_by_age_chart
85
87
  daily_wip_by_blocked_stalled_chart
86
88
  daily_wip_by_parent_chart
89
+ flow_efficiency_scatterplot if show_experimental_charts
87
90
  expedited_chart
88
91
  sprint_burndown
89
92
  estimate_accuracy_chart
@@ -109,8 +109,7 @@ class ExpeditedChart < ChartBase
109
109
 
110
110
  def make_expedite_lines_data_set issue:, expedite_data:
111
111
  cycletime = issue.board.cycletime
112
- started_time = cycletime.started_time(issue)
113
- stopped_time = cycletime.stopped_time(issue)
112
+ started_time, stopped_time = cycletime.started_stopped_times(issue)
114
113
 
115
114
  expedite_data << [started_time, :issue_started] if started_time
116
115
  expedite_data << [stopped_time, :issue_stopped] if stopped_time