jirametrics 2.5 → 2.11

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +9 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +54 -7
  6. data/lib/jirametrics/blocked_stalled_change.rb +1 -1
  7. data/lib/jirametrics/board.rb +43 -13
  8. data/lib/jirametrics/board_config.rb +6 -2
  9. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  10. data/lib/jirametrics/change_item.rb +19 -6
  11. data/lib/jirametrics/chart_base.rb +63 -27
  12. data/lib/jirametrics/css_variable.rb +1 -1
  13. data/lib/jirametrics/cycletime_config.rb +59 -8
  14. data/lib/jirametrics/cycletime_histogram.rb +68 -3
  15. data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
  16. data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
  17. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
  18. data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
  19. data/lib/jirametrics/daily_wip_chart.rb +7 -9
  20. data/lib/jirametrics/data_quality_report.rb +219 -41
  21. data/lib/jirametrics/dependency_chart.rb +3 -4
  22. data/lib/jirametrics/download_config.rb +2 -2
  23. data/lib/jirametrics/downloader.rb +59 -47
  24. data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
  25. data/lib/jirametrics/examples/aggregated_project.rb +3 -6
  26. data/lib/jirametrics/examples/standard_project.rb +14 -13
  27. data/lib/jirametrics/expedited_chart.rb +7 -8
  28. data/lib/jirametrics/exporter.rb +28 -13
  29. data/lib/jirametrics/file_config.rb +23 -6
  30. data/lib/jirametrics/file_system.rb +39 -3
  31. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  32. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  33. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  34. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  35. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  36. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  37. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  38. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  39. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  40. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  41. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  42. data/lib/jirametrics/html/index.css +28 -5
  43. data/lib/jirametrics/html/index.erb +8 -4
  44. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  45. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  46. data/lib/jirametrics/html_report_config.rb +33 -23
  47. data/lib/jirametrics/issue.rb +205 -44
  48. data/lib/jirametrics/jira_gateway.rb +16 -3
  49. data/lib/jirametrics/project_config.rb +245 -134
  50. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  51. data/lib/jirametrics/settings.json +1 -0
  52. data/lib/jirametrics/sprint_burndown.rb +3 -3
  53. data/lib/jirametrics/status.rb +84 -19
  54. data/lib/jirametrics/status_collection.rb +77 -39
  55. data/lib/jirametrics/throughput_chart.rb +1 -1
  56. data/lib/jirametrics/value_equality.rb +2 -2
  57. data/lib/jirametrics.rb +22 -6
  58. metadata +10 -13
  59. data/lib/jirametrics/discard_changes_before.rb +0 -37
  60. data/lib/jirametrics/html/data_quality_report.erb +0 -126
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22daa10ce05dd421522069d3b5d73868f8f7bc1a6a53128acab515ca98644892
4
- data.tar.gz: 8d797de1b80fe8f35fde593bc578012af92080e32000a905ea90ed286754bccd
3
+ metadata.gz: 670df42835c7d41e30dcde01ead744a132f81313165297d434334e272de795c7
4
+ data.tar.gz: 12822842210b4aeddb7f59e6681a9f1bd25060bae0ff98e8490d14e895dea657
5
5
  SHA512:
6
- metadata.gz: b00a94c9c676625ff698ecf61211dea8b8bbc6d3fde400e23f349a888147b0245616f8944646eac4435106d1156dca08327fe596dcc0402e92169c67cc72cdb1
7
- data.tar.gz: c85706281f71b2b0adf5dae8f99bcab478dadeb86b014ce88874879051ac0e4288d3d7cda1e0fd949e03b70878969f918248649423704ae5d757f63c83b12733
6
+ metadata.gz: a4f36b8921c94a457710dc68251a9a2ddb38be2f0f7027991cf381fdf1cff28d2e549e508314733a3c753f583fd3c695b7e4979e1bb7be43c45de90ec001cbab
7
+ data.tar.gz: 28a2553b31cd4de5fce0c943da38643ad13dbfad3a8bddc77f4764e179394968966bb676e70daa75a4faeb42a67027fb7efb63895b56c01a3db563a029df66aa
@@ -41,7 +41,7 @@ class AggregateConfig
41
41
  def include_issues_from project_name
42
42
  project = @project_config.exporter.project_configs.find { |p| p.name == project_name }
43
43
  if project.nil?
44
- log "Warning: Aggregated project #{@project_config.name.inspect} is attempting to load " \
44
+ file_system.warning "Aggregated project #{@project_config.name.inspect} is attempting to load " \
45
45
  "project #{project_name.inspect} but it can't be found. Is it disabled?"
46
46
  return
47
47
  end
@@ -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
+ file_system.warning "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:
@@ -81,7 +86,7 @@ class AggregateConfig
81
86
 
82
87
  private
83
88
 
84
- def log message
85
- @project_config.exporter.file_system.log message
89
+ def file_system
90
+ @project_config.exporter.file_system
86
91
  end
87
92
  end
@@ -3,8 +3,6 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class AgingWorkBarChart < ChartBase
6
- @@next_id = 0
7
-
8
6
  def initialize block
9
7
  super()
10
8
 
@@ -53,8 +51,8 @@ class AgingWorkBarChart < ChartBase
53
51
  percentage_line_x = date_range.end - calculate_percent_line if percentage
54
52
 
55
53
  if aging_issues.empty?
56
- @description_text = "<p>There is no aging work</p>"
57
- return render_top_text(binding) #if aging_issues.empty?
54
+ @description_text = '<p>There is no aging work</p>'
55
+ return render_top_text(binding)
58
56
  end
59
57
 
60
58
  wrap_and_render(binding, __FILE__)
@@ -62,7 +60,7 @@ class AgingWorkBarChart < ChartBase
62
60
 
63
61
  def data_sets_for_one_issue issue:, today:
64
62
  cycletime = issue.board.cycletime
65
- issue_start_time = cycletime.started_time(issue)
63
+ issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
66
64
  issue_start_date = issue_start_time.to_date
67
65
  issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
68
66
  [
@@ -92,8 +90,8 @@ class AgingWorkBarChart < ChartBase
92
90
 
93
91
  def select_aging_issues issues:
94
92
  issues.select do |issue|
95
- cycletime = issue.board.cycletime
96
- cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
93
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
94
+ started_time && stopped_time.nil?
97
95
  end
98
96
  end
99
97
 
@@ -107,7 +105,7 @@ class AgingWorkBarChart < ChartBase
107
105
  def status_data_sets issue:, label:, today:
108
106
  cycletime = issue.board.cycletime
109
107
 
110
- issue_started_time = cycletime.started_time(issue)
108
+ issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
111
109
 
112
110
  previous_start = nil
113
111
  previous_status = nil
@@ -116,7 +114,7 @@ class AgingWorkBarChart < ChartBase
116
114
  issue.changes.each do |change|
117
115
  next unless change.status?
118
116
 
119
- status = issue.find_status_by_name change.value
117
+ status = issue.find_or_create_status id: change.value_id, name: change.value
120
118
 
121
119
  unless previous_start.nil? || previous_start < issue_started_time
122
120
  hash = {
@@ -162,8 +160,12 @@ class AgingWorkBarChart < ChartBase
162
160
  end
163
161
 
164
162
  def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
165
- deprecated message: 'blocked color should be set via css now', date: '2024-05-03' if settings['blocked_color']
166
- deprecated message: 'blocked color should be set via css now', date: '2024-05-03' if settings['stalled_color']
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
167
169
 
168
170
  color = settings['blocked_color'] || '--blocked-color'
169
171
  color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'jirametrics/chart_base'
4
4
  require 'jirametrics/groupable_issue_chart'
5
+ require 'jirametrics/board_movement_calculator'
5
6
 
6
7
  class AgingWorkInProgressChart < ChartBase
7
8
  include GroupableIssueChart
@@ -16,13 +17,33 @@ class AgingWorkInProgressChart < ChartBase
16
17
  This chart shows only work items that have started but not completed, grouped by the column
17
18
  they're currently in. Hovering over a dot will show you the ID of that work item.
18
19
  </p>
19
- <div>
20
- The #{color_block '--non-working-days-color'} shaded area indicates the 85%
21
- mark for work items that have passed through here; 85% of
22
- previous work items left this column while still inside the shaded area. Any work items above
23
- the shading are outliers and they are the items that you should pay special attention to.
20
+ <p>
21
+ The shaded areas indicate what percentage of the work has passed that column within that time.
22
+ Notes:
23
+ <ul>
24
+ <li>It only shows columns that are considered "in progress". If you see a column that wouldn't normally
25
+ be thought of that way, then likely issues were moving backwards or continued to progress after hitting
26
+ that column.</li>
27
+ <li>If you see a colour group that drops as it moves to the right, that generally indicates that
28
+ a different number of data points is being included in each column. Probably because tickets moved
29
+ backwards athough it could also indicate that a ticket jumped over columns as it moved to the right.
30
+ </li>
31
+ </ul>
32
+ </p>
33
+ <div style="border: 1px solid gray; padding: 0.2em">
34
+ <% @percentiles.keys.sort.reverse.each do |percent| %>
35
+ <span style="padding-left: 0.5em; padding-right: 0.5em; vertical-align: middle;"><%= color_block @percentiles[percent] %> <%= percent %>%</span>
36
+ <% end %>
24
37
  </div>
25
38
  HTML
39
+ percentiles(
40
+ 50 => '--aging-work-in-progress-chart-shading-50-color',
41
+ 85 => '--aging-work-in-progress-chart-shading-85-color',
42
+ 98 => '--aging-work-in-progress-chart-shading-98-color',
43
+ 100 => '--aging-work-in-progress-chart-shading-100-color'
44
+ )
45
+ show_all_columns false
46
+
26
47
  init_configuration_block(block) do
27
48
  grouping_rules do |issue, rule|
28
49
  rule.label = issue.type
@@ -36,13 +57,17 @@ class AgingWorkInProgressChart < ChartBase
36
57
 
37
58
  @header_text += " on board: #{@all_boards[@board_id].name}"
38
59
  data_sets = make_data_sets
39
- column_headings = @board_columns.collect(&:name)
40
60
 
41
- adjust_visibility_of_unmapped_status_column data_sets: data_sets, column_headings: column_headings
61
+ adjust_visibility_of_unmapped_status_column data_sets: data_sets
62
+ adjust_chart_height
42
63
 
43
64
  wrap_and_render(binding, __FILE__)
44
65
  end
45
66
 
67
+ def show_all_columns show = true # rubocop:disable Style/OptionalBooleanParameter
68
+ @show_all_columns = show
69
+ end
70
+
46
71
  def determine_board_columns
47
72
  unmapped_statuses = current_board.possible_statuses.collect(&:id)
48
73
 
@@ -51,7 +76,7 @@ class AgingWorkInProgressChart < ChartBase
51
76
 
52
77
  @fake_column = BoardColumn.new({
53
78
  'name' => '[Unmapped Statuses]',
54
- 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
79
+ 'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq # rubocop:disable Performance/ChainArrayAllocation
55
80
  })
56
81
  @board_columns = columns + [@fake_column]
57
82
  end
@@ -62,7 +87,7 @@ class AgingWorkInProgressChart < ChartBase
62
87
  board.id == @board_id && board.cycletime.in_progress?(issue)
63
88
  end
64
89
 
65
- percentage = 85
90
+ @max_age = 20
66
91
  rules_to_issues = group_issues aging_issues
67
92
  data_sets = rules_to_issues.keys.collect do |rules|
68
93
  {
@@ -73,7 +98,10 @@ class AgingWorkInProgressChart < ChartBase
73
98
  column = column_for issue: issue
74
99
  next if column.nil?
75
100
 
76
- { 'y' => age,
101
+ @max_age = age if age > @max_age
102
+
103
+ {
104
+ 'y' => age,
77
105
  'x' => column.name,
78
106
  'title' => ["#{issue.key} : #{issue.summary} (#{label_days age})"]
79
107
  }
@@ -83,45 +111,70 @@ class AgingWorkInProgressChart < ChartBase
83
111
  'backgroundColor' => rules.color
84
112
  }
85
113
  end
86
- data_sets << {
87
- 'type' => 'bar',
88
- 'label' => "#{percentage}%",
89
- 'barPercentage' => 1.0,
90
- 'categoryPercentage' => 1.0,
91
- 'backgroundColor' => CssVariable['--aging-work-in-progress-chart-shading-color'],
92
- 'data' => days_at_percentage_threshold_for_all_columns(percentage: percentage, issues: @issues).drop(1)
93
- }
94
- end
95
114
 
96
- def days_at_percentage_threshold_for_all_columns percentage:, issues:
97
- accumulated_status_ids_per_column.collect do |_column, status_ids|
98
- ages = ages_of_issues_that_crossed_column_boundary issues: issues, status_ids: status_ids
99
- index = ages.size * percentage / 100
100
- ages.sort[index.to_i] || 0
115
+ calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
116
+
117
+ column_indexes_to_remove = []
118
+ unless @show_all_columns
119
+ column_indexes_to_remove = indexes_of_leading_and_trailing_zeros(calculator.age_data_for(percentage: 100))
120
+
121
+ column_indexes_to_remove.reverse_each do |index|
122
+ @board_columns.delete_at index
123
+ end
124
+ end
125
+
126
+ @row_index_offset = data_sets.size
127
+
128
+ bar_data = []
129
+ calculator.stacked_age_data_for(percentages: @percentiles.keys).each do |percentage, data|
130
+ column_indexes_to_remove.reverse_each { |index| data.delete_at index }
131
+ color = @percentiles[percentage]
132
+
133
+ data_sets << {
134
+ 'type' => 'bar',
135
+ 'label' => "#{percentage}%",
136
+ 'barPercentage' => 1.0,
137
+ 'categoryPercentage' => 1.0,
138
+ 'backgroundColor' => color,
139
+ 'data' => data
140
+ }
141
+ bar_data << data
101
142
  end
143
+ @bar_data = adjust_bar_data bar_data
144
+
145
+ data_sets
102
146
  end
103
147
 
104
- def accumulated_status_ids_per_column
105
- accumulated_status_ids = []
106
- @board_columns.reverse.filter_map do |column|
107
- next if column == @fake_column
148
+ def adjust_bar_data input
149
+ return [] if input.empty?
150
+
151
+ row_size = input.first.size
108
152
 
109
- accumulated_status_ids += column.status_ids
110
- [column.name, accumulated_status_ids.dup]
111
- end.reverse
153
+ output = []
154
+ output << input.first
155
+ input.drop(1).each do |row|
156
+ previous_row = output.last
157
+ output << 0.upto(row_size - 1).collect { |i| row[i] + previous_row[i] }
158
+ end
159
+
160
+ output
112
161
  end
113
162
 
114
- def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
115
- issues.filter_map do |issue|
116
- stop = issue.first_time_in_status(*status_ids)
117
- start = issue.board.cycletime.started_time(issue)
163
+ def indexes_of_leading_and_trailing_zeros list
164
+ result = []
165
+ 0.upto(list.size - 1) do |index|
166
+ break unless list[index].zero?
118
167
 
119
- # Skip if either it hasn't crossed the boundary or we can't tell when it started.
120
- next if stop.nil? || start.nil?
121
- next if stop < start
168
+ result << index
169
+ end
122
170
 
123
- (stop.to_date - start.to_date).to_i + 1
171
+ stop_at = result.empty? ? 0 : (result.last + 1)
172
+ (list.size - 1).downto(stop_at).each do |index|
173
+ break unless list[index].zero?
174
+
175
+ result << index if list[index].zero?
124
176
  end
177
+ result
125
178
  end
126
179
 
127
180
  def column_for issue:
@@ -130,7 +183,7 @@ class AgingWorkInProgressChart < ChartBase
130
183
  end
131
184
  end
132
185
 
133
- def adjust_visibility_of_unmapped_status_column data_sets:, column_headings:
186
+ def adjust_visibility_of_unmapped_status_column data_sets:
134
187
  column_name = @fake_column.name
135
188
 
136
189
  has_unmapped = data_sets.any? do |set|
@@ -143,8 +196,19 @@ class AgingWorkInProgressChart < ChartBase
143
196
  @description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
144
197
  'board but are still active. Most likely everyone has forgotten about them.</p>'
145
198
  else
146
- column_headings.pop
199
+ # @column_headings.pop
147
200
  @board_columns.pop
148
201
  end
149
202
  end
203
+
204
+ def percentiles percentile_color_hash
205
+ @percentiles = percentile_color_hash.transform_values { |value| CssVariable[value] }
206
+ end
207
+
208
+ def adjust_chart_height
209
+ min_height = @max_age * 5
210
+
211
+ @canvas_height = min_height if min_height > @canvas_height
212
+ @canvas_height = 400 if min_height > 400
213
+ end
150
214
  end
@@ -3,7 +3,7 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class AgingWorkTable < ChartBase
6
- attr_accessor :today, :board_id
6
+ attr_accessor :today
7
7
  attr_reader :any_scrum_boards
8
8
 
9
9
  def initialize block
@@ -22,30 +22,43 @@ class AgingWorkTable < ChartBase
22
22
  If there are expedited items that haven't yet started then they're at the bottom of the table.
23
23
  By the very definition of expedited, if we haven't started them already, we'd better get on that.
24
24
  </p>
25
+ <p>
26
+ Legend:
27
+ <ul>
28
+ <li><b>E:</b> Whether this item is <b>E</b>xpedited.</li>
29
+ <li><b>B/S:</b> Whether this item is either <b>B</b>locked or <b>S</b>talled.</li>
30
+ <li><b>Forecast:</b> A forecast of how long it is likely to take to finish this work item.</li>
31
+ </ul>
32
+ </p>
25
33
  TEXT
26
34
 
27
35
  instance_eval(&block)
28
36
  end
29
37
 
30
38
  def run
31
- @today = date_range.end
39
+ initialize_calculator
32
40
  aging_issues = select_aging_issues + expedited_but_not_started
33
41
 
34
42
  wrap_and_render(binding, __FILE__)
35
43
  end
36
44
 
45
+ # This is its own method simply so the tests can initialize the calculator without doing a full run.
46
+ def initialize_calculator
47
+ @today = date_range.end
48
+ @calculator = BoardMovementCalculator.new board: current_board, issues: issues, today: @today
49
+ end
50
+
37
51
  def expedited_but_not_started
38
52
  @issues.select do |issue|
39
- cycletime = issue.board.cycletime
40
- cycletime.started_time(issue).nil? && cycletime.stopped_time(issue).nil? && issue.expedited?
53
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
54
+ started_time.nil? && stopped_time.nil? && issue.expedited?
41
55
  end.sort_by(&:created)
42
56
  end
43
57
 
44
58
  def select_aging_issues
45
59
  aging_issues = @issues.select do |issue|
46
60
  cycletime = issue.board.cycletime
47
- started = cycletime.started_time(issue)
48
- stopped = cycletime.stopped_time(issue)
61
+ started, stopped = cycletime.started_stopped_times(issue)
49
62
  next false if started.nil? || stopped
50
63
  next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
51
64
 
@@ -64,7 +77,7 @@ class AgingWorkTable < ChartBase
64
77
  end
65
78
 
66
79
  def blocked_text issue
67
- started_time = issue.board.cycletime.started_time(issue)
80
+ started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
68
81
  return nil if started_time.nil?
69
82
 
70
83
  current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
@@ -115,6 +128,40 @@ class AgingWorkTable < ChartBase
115
128
  end.join('<br />')
116
129
  end
117
130
 
131
+ def dates_text issue
132
+ date = date_range.end
133
+ due = issue.due_date
134
+ message = nil
135
+
136
+ days_remaining, error = @calculator.forecasted_days_remaining_and_message issue: issue, today: @today
137
+
138
+ unless error
139
+ if due
140
+ if due < date
141
+ message = "Due: <b>#{due}</b> (#{label_days (@today - due).to_i} ago)"
142
+ error = 'Overdue'
143
+ elsif due == date
144
+ message = 'Due: <b>today</b>'
145
+ else
146
+ error = 'Due date at risk' if date_range.end + days_remaining > due
147
+ message = "Due: <b>#{due}</b> (#{label_days (due - @today).to_i})"
148
+ end
149
+ else
150
+ "#{label_days days_remaining} left."
151
+ end
152
+ end
153
+
154
+ text = +''
155
+ text << "<span title='#{error}' style='color: red'>ⓘ </span>" if error
156
+ if days_remaining
157
+ text << "#{label_days days_remaining} left"
158
+ else
159
+ text << 'Unable to forecast'
160
+ end
161
+ text << ' | ' << message if message
162
+ text
163
+ end
164
+
118
165
  def age_cutoff age = nil
119
166
  @age_cutoff = age.to_i if age
120
167
  @age_cutoff
@@ -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,38 +1,40 @@
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
- def initialize raw:, possible_statuses: StatusCollection.new
7
+ def initialize raw:, possible_statuses:
8
8
  @raw = raw
9
9
  @board_type = raw['type']
10
10
  @possible_statuses = possible_statuses
11
11
  @sprints = []
12
12
 
13
13
  columns = raw['columnConfig']['columns']
14
+ ensure_uniqueness_of_column_names! columns
14
15
 
15
16
  # For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
16
17
  # visible on the board. If the board is configured to have a kanban backlog then it will have
17
18
  # 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
19
+ columns = columns.drop(1) if kanban?
29
20
 
21
+ @backlog_statuses = []
30
22
  @visible_columns = columns.filter_map do |column|
31
23
  # It's possible for a column to be defined without any statuses and in this case, it won't be visible.
32
24
  BoardColumn.new column unless status_ids_from_column(column).empty?
33
25
  end
34
26
  end
35
27
 
28
+ def backlog_statuses
29
+ if @backlog_statuses.empty? && kanban?
30
+ status_ids = status_ids_from_column raw['columnConfig']['columns'].first
31
+ @backlog_statuses = status_ids.filter_map do |id|
32
+ @possible_statuses.find_by_id id
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
 
@@ -87,4 +89,32 @@ class Board
87
89
  def name
88
90
  @raw['name']
89
91
  end
92
+
93
+ def accumulated_status_ids_per_column
94
+ accumulated_status_ids = []
95
+ visible_columns.reverse.filter_map do |column|
96
+ next if column == @fake_column
97
+
98
+ accumulated_status_ids += column.status_ids
99
+ [column.name, accumulated_status_ids.dup]
100
+ end.reverse
101
+ end
102
+
103
+ def ensure_uniqueness_of_column_names! json
104
+ all_names = []
105
+ json.each do |column_json|
106
+ name = column_json['name']
107
+ if all_names.include? name
108
+ (2..).each do |i|
109
+ new_name = "#{name}-#{i}"
110
+ next if all_names.include?(new_name)
111
+
112
+ name = new_name
113
+ column_json['name'] = new_name
114
+ break
115
+ end
116
+ end
117
+ all_names << name
118
+ end
119
+ end
90
120
  end
@@ -21,11 +21,15 @@ class BoardConfig
21
21
  'If so, remove it from there.'
22
22
  end
23
23
 
24
- @board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
24
+ @board.cycletime = CycleTimeConfig.new(
25
+ parent_config: self, label: label, block: block, file_system: project_config.file_system
26
+ )
25
27
  end
26
28
 
27
29
  def expedited_priority_names *priority_names
28
- deprecated date: '2024-09-15', message: 'Expedited priority names are now specified in settings'
30
+ project_config.exporter.file_system.deprecated(
31
+ date: '2024-09-15', message: 'Expedited priority names are now specified in settings'
32
+ )
29
33
  @project_config.settings['expedited_priority_names'] = priority_names
30
34
  end
31
35
  end