jirametrics 2.8 → 2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5bec8084c47e2383d25676b969891c6fc8e427d06427a1caf195378a830ff4f
4
- data.tar.gz: e9364c2b43f66d90a651f50e115751c144f8dfc6f7d247b47dfd17495bdf167e
3
+ metadata.gz: 892b3686d9d53b05f9dd064524e99d38753b7c79685794a6bd6234d2d538e52d
4
+ data.tar.gz: c9412dfecec193196d523e1d811e89793269568eb31708192a075440d2806cb0
5
5
  SHA512:
6
- metadata.gz: 82c740fa8d23565eb33edf1e244cde1b8b8ef2d88f753091fd0f3cfb7e20aa67c32fc22dca70d4076c25ad40898a0872c13f8af14173a26b33054faf8838df14
7
- data.tar.gz: 66d5db7495165aa4ac0ea36acd27e1b77d2d4b4d4ebb7912fbbffee57c9c160b8cee3fc919884564370db51618438d24fc6fde5e47e2343560aca52b53917d4b
6
+ metadata.gz: a1ccae2dfc047912ced09176cac3af057b66350eb1436fe053457ff299629dafe801acb337c9ffa3702ec7c1ef0d3dd2dad84c8e6d2ff2842f6814d1ef6e43f2
7
+ data.tar.gz: e5b635a59c4026b51bd84d65cf3230cd90964f82cbb752e04534f1a4d7c59447e174f3d0bf480babe229b4fb91244f89f897d6da2c7c855ab189a4e943cd0a6d
@@ -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
 
@@ -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_id change.value_id, 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?
@@ -27,9 +27,8 @@ class Board
27
27
  def backlog_statuses
28
28
  if @backlog_statuses.empty? && kanban?
29
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.
30
+ @backlog_statuses = status_ids.filter_map do |id|
31
+ @possible_statuses.find_by_id id
33
32
  end
34
33
  end
35
34
  @backlog_statuses
@@ -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
@@ -27,9 +27,6 @@ class ChartBase
27
27
 
28
28
  def html_directory
29
29
  pathname = Pathname.new(File.realpath(__FILE__))
30
- # basename = pathname.basename.to_s
31
- # raise "Unexpected filename #{basename.inspect}" unless basename.match?(/^(.+)\.rb$/)
32
-
33
30
  "#{pathname.dirname}/html"
34
31
  end
35
32
 
@@ -179,18 +176,33 @@ class ChartBase
179
176
  @description_text
180
177
  end
181
178
 
179
+ # Convert a number like 1234567 into the string "1,234,567"
182
180
  def format_integer number
183
181
  number.to_s.reverse.scan(/.{1,3}/).join(',').reverse
184
182
  end
185
183
 
186
- def format_status name_or_id, board:, is_category: false
187
- begin
188
- statuses = board.possible_statuses.expand_statuses([name_or_id])
189
- rescue StatusNotFoundError
190
- return "<span style='color: red'>#{name_or_id}</span>"
184
+ # object will be either a Status or a ChangeItem
185
+ # if it's a ChangeItem then use_old_status will specify whether we're using the new or old
186
+ # Either way, is_category will format the category rather than the status
187
+ def format_status object, board:, is_category: false, use_old_status: false
188
+ status = nil
189
+ error_message = nil
190
+
191
+ case object
192
+ when ChangeItem
193
+ id = use_old_status ? object.old_value_id : object.value_id
194
+ status = board.possible_statuses.find_by_id(id)
195
+ if status.nil?
196
+ error_message = use_old_status ? object.old_value : object.value
197
+ end
198
+ when Status
199
+ status = object
200
+ else
201
+ raise "Unexpected type: #{object.class}"
191
202
  end
192
203
 
193
- status = statuses.first
204
+ return "<span style='color: red'>#{error_message}</span>" if error_message
205
+
194
206
  color = status_category_color status
195
207
 
196
208
  visibility = ''
@@ -8,10 +8,14 @@ class CycleTimeConfig
8
8
 
9
9
  attr_reader :label, :parent_config
10
10
 
11
- def initialize parent_config:, label:, block:, today: Date.today
11
+ def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
12
12
  @parent_config = parent_config
13
13
  @label = label
14
14
  @today = today
15
+
16
+ # If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
17
+ # may make it easier to find problems in the test code ;-)
18
+ @file_system = file_system
15
19
  instance_eval(&block) unless block.nil?
16
20
  end
17
21
 
@@ -35,17 +39,19 @@ class CycleTimeConfig
35
39
  end
36
40
 
37
41
  def started_time issue
38
- deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
42
+ @file_system.deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
39
43
  started_stopped_times(issue).first
40
44
  end
41
45
 
42
46
  def stopped_time issue
43
- deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
47
+ @file_system.deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
44
48
  started_stopped_times(issue).last
45
49
  end
46
50
 
47
51
  def fabricate_change_item time
48
- deprecated date: '2024-12-16', message: 'This method should now return a ChangeItem not a Time', depth: 4
52
+ @file_system.deprecated(
53
+ date: '2024-12-16', message: "This method should now return a ChangeItem not a #{time.class}", depth: 4
54
+ )
49
55
  raw = {
50
56
  'field' => 'Fabricated change',
51
57
  'to' => '0',
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DataQualityReport < ChartBase
4
- attr_reader :original_issue_times # For testing purposes only
4
+ attr_reader :discarded_changes_data, :entries # Both for testing purposes only
5
5
  attr_accessor :board_id
6
6
 
7
7
  class Entry
@@ -19,10 +19,10 @@ class DataQualityReport < ChartBase
19
19
  end
20
20
  end
21
21
 
22
- def initialize original_issue_times
22
+ def initialize discarded_changes_data
23
23
  super()
24
24
 
25
- @original_issue_times = original_issue_times
25
+ @discarded_changes_data = discarded_changes_data
26
26
 
27
27
  header_text 'Data Quality Report'
28
28
  description_text <<-HTML
@@ -50,6 +50,7 @@ class DataQualityReport < ChartBase
50
50
  scan_for_issues_not_started_with_subtasks_that_have entry: entry
51
51
  scan_for_incomplete_subtasks_when_issue_done entry: entry
52
52
  scan_for_discarded_data entry: entry
53
+ scan_for_items_blocked_on_closed_tickets entry: entry
53
54
  end
54
55
 
55
56
  scan_for_issues_on_multiple_boards entries: @entries
@@ -73,6 +74,7 @@ class DataQualityReport < ChartBase
73
74
  result << render_problem_type(:issue_not_started_but_subtasks_have)
74
75
  result << render_problem_type(:incomplete_subtasks_when_issue_done)
75
76
  result << render_problem_type(:issue_on_multiple_boards)
77
+ result << render_problem_type(:items_blocked_on_closed_tickets)
76
78
  result << '</ul>'
77
79
 
78
80
  result
@@ -102,9 +104,13 @@ class DataQualityReport < ChartBase
102
104
 
103
105
  # Return a format that's easier to assert against
104
106
  def testable_entries
105
- format = '%Y-%m-%d %H:%M:%S %z'
107
+ formatter = ->(time) { time&.strftime('%Y-%m-%d %H:%M:%S %z') || '' }
106
108
  @entries.collect do |entry|
107
- [entry.started&.strftime(format) || '', entry.stopped&.strftime(format) || '', entry.issue]
109
+ [
110
+ formatter.call(entry.started),
111
+ formatter.call(entry.stopped),
112
+ entry.issue
113
+ ]
108
114
  end
109
115
  end
110
116
 
@@ -112,10 +118,6 @@ class DataQualityReport < ChartBase
112
118
  @entries.reject { |entry| entry.problems.empty? }
113
119
  end
114
120
 
115
- def category_name_for status_id:, board:
116
- board.possible_statuses.find_by_id(status_id)&.category&.name
117
- end
118
-
119
121
  def initialize_entries
120
122
  @entries = @issues.filter_map do |issue|
121
123
  started, stopped = issue.board.cycletime.started_stopped_times(issue)
@@ -139,10 +141,8 @@ class DataQualityReport < ChartBase
139
141
  def scan_for_completed_issues_without_a_start_time entry:
140
142
  return unless entry.stopped && entry.started.nil?
141
143
 
142
- status_names = entry.issue.changes.filter_map do |change|
143
- next unless change.status?
144
-
145
- format_status change.value, board: entry.issue.board
144
+ status_names = entry.issue.status_changes.filter_map do |change|
145
+ format_status change, board: entry.issue.board
146
146
  end
147
147
 
148
148
  entry.report(
@@ -157,14 +157,14 @@ class DataQualityReport < ChartBase
157
157
  changes_after_done = entry.issue.changes.select do |change|
158
158
  change.status? && change.time >= entry.stopped
159
159
  end
160
- done_status = changes_after_done.shift.value
160
+ done_status = changes_after_done.shift
161
161
 
162
162
  return if changes_after_done.empty?
163
163
 
164
164
  board = entry.issue.board
165
165
  problem = "Completed on #{entry.stopped.to_date} with status #{format_status done_status, board: board}."
166
166
  changes_after_done.each do |change|
167
- problem << " Changed to #{format_status change.value, board: board} on #{change.time.to_date}."
167
+ problem << " Changed to #{format_status change, board: board} on #{change.time.to_date}."
168
168
  end
169
169
  entry.report(
170
170
  problem_key: :status_changes_after_done,
@@ -186,9 +186,9 @@ class DataQualityReport < ChartBase
186
186
  # If it's a backlog status then ignore it. Not supposed to be visible.
187
187
  next if entry.issue.board.backlog_statuses.include?(board.possible_statuses.find_by_id(change.value_id))
188
188
 
189
- detail = "Status #{format_status change.value, board: board} is not on the board"
190
- if issue.board.possible_statuses.expand_statuses(change.value).empty?
191
- detail = "Status #{format_status change.value, board: board} cannot be found at all. Was it deleted?"
189
+ detail = "Status #{format_status change, board: board} is not on the board"
190
+ if issue.board.possible_statuses.find_by_id(change.value_id).nil?
191
+ detail = "Status #{format_status change, board: board} cannot be found at all. Was it deleted?"
192
192
  end
193
193
 
194
194
  # If it's been moved back to backlog then it's on a different report. Ignore it here.
@@ -198,24 +198,24 @@ class DataQualityReport < ChartBase
198
198
  elsif change.old_value.nil?
199
199
  # Do nothing
200
200
  elsif index < last_index
201
- new_category = category_name_for(status_id: change.value_id, board: board)
202
- old_category = category_name_for(status_id: change.old_value_id, board: board)
201
+ new_category = board.possible_statuses.find_by_id(change.value_id).category.name
202
+ old_category = board.possible_statuses.find_by_id(change.old_value_id).category.name
203
203
 
204
204
  if new_category == old_category
205
205
  entry.report(
206
206
  problem_key: :backwords_through_statuses,
207
- detail: "Moved from #{format_status change.old_value, board: board}" \
208
- " to #{format_status change.value, board: board}" \
207
+ detail: "Moved from #{format_status change, use_old_status: true, board: board}" \
208
+ " to #{format_status change, board: board}" \
209
209
  " on #{change.time.to_date}"
210
210
  )
211
211
  else
212
212
  entry.report(
213
213
  problem_key: :backwards_through_status_categories,
214
- detail: "Moved from #{format_status change.old_value, board: board}" \
215
- " to #{format_status change.value, board: board}" \
216
- " on #{change.time.to_date}, " \
217
- " crossing from category #{format_status old_category, board: board, is_category: true}" \
218
- " to #{format_status new_category, board: board, is_category: true}."
214
+ detail: "Moved from #{format_status change, use_old_status: true, board: board}" \
215
+ " to #{format_status change, board: board}" \
216
+ " on #{change.time.to_date}," \
217
+ " crossing from category #{format_status change, use_old_status: true, board: board, is_category: true}" \
218
+ " to #{format_status change, board: board, is_category: true}."
219
219
  )
220
220
  end
221
221
  end
@@ -224,16 +224,14 @@ class DataQualityReport < ChartBase
224
224
  end
225
225
 
226
226
  def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
227
- return if backlog_statuses.empty?
228
-
229
227
  creation_change = entry.issue.changes.find { |issue| issue.status? }
230
228
 
231
229
  return if backlog_statuses.any? { |status| status.id == creation_change.value_id }
232
230
 
233
- status_string = backlog_statuses.collect { |s| format_status s.name, board: entry.issue.board }.join(', ')
231
+ status_string = backlog_statuses.collect { |s| format_status s, board: entry.issue.board }.join(', ')
234
232
  entry.report(
235
233
  problem_key: :created_in_wrong_status,
236
- detail: "Created in #{format_status creation_change.value, board: entry.issue.board}, " \
234
+ detail: "Created in #{format_status creation_change, board: entry.issue.board}, " \
237
235
  "which is not one of the backlog statuses for this board: #{status_string}"
238
236
  )
239
237
  end
@@ -266,6 +264,20 @@ class DataQualityReport < ChartBase
266
264
  )
267
265
  end
268
266
 
267
+ def scan_for_items_blocked_on_closed_tickets entry:
268
+ entry.issue.issue_links.each do |link|
269
+ this_active = !entry.stopped
270
+ other_active = !link.other_issue.board.cycletime.started_stopped_times(link.other_issue).last
271
+ next unless this_active && !other_active
272
+
273
+ entry.report(
274
+ problem_key: :items_blocked_on_closed_tickets,
275
+ detail: "#{entry.issue.key} thinks it's blocked on #{link.other_issue.key}, " \
276
+ "except #{link.other_issue.key} is closed."
277
+ )
278
+ end
279
+ end
280
+
269
281
  def subtask_label subtask
270
282
  "<img src='#{subtask.type_icon_url}' /> #{link_to_issue(subtask)} #{subtask.summary[..50].inspect}"
271
283
  end
@@ -314,10 +326,10 @@ class DataQualityReport < ChartBase
314
326
  end
315
327
 
316
328
  def scan_for_discarded_data entry:
317
- hash = @original_issue_times[entry.issue]
329
+ hash = @discarded_changes_data&.find { |a| a[:issue] == entry.issue }
318
330
  return if hash.nil?
319
331
 
320
- old_start_time = hash[:started_time]
332
+ old_start_time = hash[:original_start_time]
321
333
  cutoff_time = hash[:cutoff_time]
322
334
 
323
335
  old_start_date = old_start_time.to_date
@@ -352,7 +364,7 @@ class DataQualityReport < ChartBase
352
364
  <<-HTML
353
365
  #{label_issues problems.size} have had information discarded. This configuration is set
354
366
  to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
355
- information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b> HTML
367
+ information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b>
356
368
  HTML
357
369
  end
358
370
 
@@ -437,4 +449,11 @@ class DataQualityReport < ChartBase
437
449
  could result in more data points showing up on a chart then there really should be.
438
450
  HTML
439
451
  end
452
+
453
+ def render_items_blocked_on_closed_tickets problems
454
+ <<-HTML
455
+ For #{label_issues problems.size}, the issue is identified as being blocked by another issue. Yet,
456
+ that other issue is already completed so, by definition, it can't still be blocking.
457
+ HTML
458
+ end
440
459
  end
@@ -28,8 +28,8 @@ class Exporter
28
28
  block = boards[board_id]
29
29
  if block == :default
30
30
  block = lambda do |_|
31
- start_at first_time_in_status_category('In Progress')
32
- stop_at still_in_status_category('Done')
31
+ start_at first_time_in_status_category(:indeterminate)
32
+ stop_at still_in_status_category(:done)
33
33
  end
34
34
  end
35
35
  board id: board_id do
@@ -2,18 +2,6 @@
2
2
 
3
3
  require 'fileutils'
4
4
 
5
- class Object
6
- def deprecated message:, date:, depth: 2
7
- text = +''
8
- text << "Deprecated(#{date}): "
9
- text << message
10
- caller(1..depth).each do |line|
11
- text << "\n-> Called from #{line}"
12
- end
13
- warn text
14
- end
15
- end
16
-
17
5
  class Exporter
18
6
  attr_reader :project_configs
19
7
  attr_accessor :file_system
@@ -85,6 +85,11 @@ class FileConfig
85
85
 
86
86
  def html_report &block
87
87
  assert_only_one_filetype_config_set
88
+ if block.nil?
89
+ project_config.file_system.warning 'No charts were specified for the report. This is almost certainly a mistake.'
90
+ block = ->(_) {}
91
+ end
92
+
88
93
  @html_report = HtmlReportConfig.new file_config: self, block: block
89
94
  end
90
95
 
@@ -120,4 +125,11 @@ class FileConfig
120
125
  @file_suffix = suffix unless suffix.nil?
121
126
  @file_suffix
122
127
  end
128
+
129
+ def children
130
+ result = []
131
+ result << @columns if @columns
132
+ result << @html_report if @html_report
133
+ result
134
+ end
123
135
  end
@@ -31,13 +31,18 @@ class FileSystem
31
31
  File.write(filename, content)
32
32
  end
33
33
 
34
- def warning message
35
- log "Warning: #{message}", also_write_to_stderr: true
34
+ def warning message, more: nil
35
+ log "Warning: #{message}", more: more, also_write_to_stderr: true
36
36
  end
37
37
 
38
- def log message, also_write_to_stderr: false
38
+ def log message, more: nil, also_write_to_stderr: false
39
+ message += " See #{logfile_name} for more details about this message." if more
40
+
39
41
  logfile.puts message
40
- $stderr.puts message if also_write_to_stderr # rubocop:disable Style/StderrPuts
42
+ logfile.puts more if more
43
+ return unless also_write_to_stderr
44
+
45
+ $stderr.puts message # rubocop:disable Style/StderrPuts
41
46
  end
42
47
 
43
48
  # In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
@@ -59,4 +64,14 @@ class FileSystem
59
64
  def file_exist? filename
60
65
  File.exist? filename
61
66
  end
67
+
68
+ def deprecated message:, date:, depth: 2
69
+ text = +''
70
+ text << "Deprecated(#{date}): "
71
+ text << message
72
+ caller(1..depth).each do |line|
73
+ text << "\n-> Called from #{line}"
74
+ end
75
+ log text, also_write_to_stderr: true
76
+ end
62
77
  end
@@ -6,9 +6,7 @@ require 'jirametrics/grouping_rules'
6
6
  module GroupableIssueChart
7
7
  def init_configuration_block user_provided_block, &default_block
8
8
  instance_eval(&user_provided_block)
9
- return if @group_by_block
10
-
11
- instance_eval(&default_block)
9
+ instance_eval(&default_block) unless @group_by_block
12
10
  end
13
11
 
14
12
  def grouping_rules &block
@@ -40,7 +40,7 @@
40
40
  </div>
41
41
  <% end %>
42
42
  </td>
43
- <td><%= format_status issue.status.name, board: issue.board %></td>
43
+ <td><%= format_status issue.status, board: issue.board %></td>
44
44
  <td><%= fix_versions_text(issue) %></td>
45
45
  <% if any_scrum_boards %>
46
46
  <td><%= sprints_text(issue) %></td>
@@ -22,7 +22,7 @@
22
22
  </span>
23
23
  </td>
24
24
  <td><span style="color: <%= color %>; font-style: italic;"><%= issue.summary[0..80] %></span></td>
25
- <td><%= format_status issue.status.name, board: issue.board %></td>
25
+ <td><%= format_status issue.status, board: issue.board %></td>
26
26
  </tr>
27
27
  <% end %>
28
28
  </tbody>
@@ -5,16 +5,15 @@ require 'jirametrics/self_or_issue_dispatcher'
5
5
 
6
6
  class HtmlReportConfig
7
7
  include SelfOrIssueDispatcher
8
- include DiscardChangesBefore
9
8
 
10
- attr_reader :file_config, :sections
9
+ attr_reader :file_config, :sections, :charts
11
10
 
12
11
  def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
13
12
  lines = []
14
13
  lines << "def #{name} &block"
15
14
  lines << ' block = ->(_) {} unless block'
16
15
  if deprecated_warning
17
- lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
16
+ lines << " file_system.deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
18
17
  end
19
18
  lines << " execute_chart #{classname}.new(block)"
20
19
  lines << 'end'
@@ -43,14 +42,15 @@ class HtmlReportConfig
43
42
  def initialize file_config:, block:
44
43
  @file_config = file_config
45
44
  @block = block
46
- @sections = []
45
+ @sections = [] # Where we store the chunks of text that will be assembled into the HTML
46
+ @charts = [] # Where we store all the charts we executed so we can assert against them.
47
47
  end
48
48
 
49
49
  def cycletime label = nil, &block
50
50
  @file_config.project_config.all_boards.each_value do |board|
51
51
  raise 'Multiple cycletimes not supported' if board.cycletime
52
52
 
53
- board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
53
+ board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
54
54
  end
55
55
  end
56
56
 
@@ -64,7 +64,7 @@ class HtmlReportConfig
64
64
 
65
65
  # The quality report has to be generated last because otherwise cycletime won't have been
66
66
  # set. Then we have to rotate it to the first position so it's at the top of the report.
67
- execute_chart DataQualityReport.new(@original_issue_times || {})
67
+ execute_chart DataQualityReport.new(file_config.project_config.discarded_changes_data)
68
68
  @sections.rotate!(-1)
69
69
 
70
70
  html create_footer
@@ -101,9 +101,8 @@ class HtmlReportConfig
101
101
  base_css
102
102
  end
103
103
 
104
- def board_id id = nil
105
- @board_id = id unless id.nil?
106
- @board_id
104
+ def board_id id
105
+ @board_id = id
107
106
  end
108
107
 
109
108
  def timezone_offset
@@ -143,19 +142,6 @@ class HtmlReportConfig
143
142
  end
144
143
  end
145
144
 
146
- def discard_changes_before_hook issues_cutoff_times
147
- # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
148
-
149
- @original_issue_times = {}
150
- issues_cutoff_times.each do |issue, cutoff_time|
151
- started = issue.board.cycletime.started_stopped_times(issue).first
152
- if started && started <= cutoff_time
153
- # We only need to log this if data was discarded
154
- @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
155
- end
156
- end
157
- end
158
-
159
145
  def dependency_chart &block
160
146
  execute_chart DependencyChart.new block
161
147
  end
@@ -175,7 +161,7 @@ class HtmlReportConfig
175
161
  chart.settings = settings
176
162
 
177
163
  chart.all_boards = project_config.all_boards
178
- chart.board_id = find_board_id if chart.respond_to? :board_id=
164
+ chart.board_id = find_board_id
179
165
  chart.holiday_dates = project_config.exporter.holiday_dates
180
166
 
181
167
  time_range = @file_config.project_config.time_range
@@ -184,6 +170,7 @@ class HtmlReportConfig
184
170
 
185
171
  after_init_block&.call chart
186
172
 
173
+ @charts << chart
187
174
  html chart.run
188
175
  end
189
176
 
@@ -216,4 +203,12 @@ class HtmlReportConfig
216
203
  </section>
217
204
  HTML
218
205
  end
206
+
207
+ def discard_changes_before status_becomes: nil, &block
208
+ file_system.deprecated(
209
+ date: '2025-01-09',
210
+ message: 'discard_changes_before is now only supported at the project level'
211
+ )
212
+ file_config.project_config.discard_changes_before status_becomes: status_becomes, &block
213
+ end
219
214
  end