jirametrics 2.6 → 2.7

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 (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