jirametrics 2.7 → 2.8

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: 73e2f2e8408a55ccecb221ba0c6d241c77f19de18d2100a9b05213a6470ed65f
4
- data.tar.gz: d0f9056615c44e783c824ca1c6566c95bf68f16a26f81a596475c1d458fee9f1
3
+ metadata.gz: f5bec8084c47e2383d25676b969891c6fc8e427d06427a1caf195378a830ff4f
4
+ data.tar.gz: e9364c2b43f66d90a651f50e115751c144f8dfc6f7d247b47dfd17495bdf167e
5
5
  SHA512:
6
- metadata.gz: e58594175d798269e0e4b8ec1fd4c0f8b693767670222bc6144be19646b04f83e5edb508e4f933f8466865018e8802f789ecb348db368c3f626318df7774c0e0
7
- data.tar.gz: 4555628c33a398cd3aaedd6e36a074b54d89f5a431f47d8617344e4af45e389e529ba54dd9cbd7d9e7d8ad7f673a68f664d00b951451b433d47f90c6feb439a5
6
+ metadata.gz: 82c740fa8d23565eb33edf1e244cde1b8b8ef2d88f753091fd0f3cfb7e20aa67c32fc22dca70d4076c25ad40898a0872c13f8af14173a26b33054faf8838df14
7
+ data.tar.gz: 66d5db7495165aa4ac0ea36acd27e1b77d2d4b4d4ebb7912fbbffee57c9c160b8cee3fc919884564370db51618438d24fc6fde5e47e2343560aca52b53917d4b
@@ -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
@@ -86,7 +86,7 @@ class AggregateConfig
86
86
 
87
87
  private
88
88
 
89
- def log message
90
- @project_config.exporter.file_system.log message
89
+ def file_system
90
+ @project_config.exporter.file_system
91
91
  end
92
92
  end
@@ -116,7 +116,7 @@ class AgingWorkBarChart < ChartBase
116
116
  issue.changes.each do |change|
117
117
  next unless change.status?
118
118
 
119
- status = issue.find_status_by_name change.value
119
+ status = issue.find_status_by_id change.value_id, name: change.value
120
120
 
121
121
  unless previous_start.nil? || previous_start < issue_started_time
122
122
  hash = {
@@ -113,7 +113,7 @@ class AgingWorkInProgressChart < ChartBase
113
113
 
114
114
  def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
115
115
  issues.filter_map do |issue|
116
- stop = issue.first_time_in_status(*status_ids)
116
+ stop = issue.first_time_in_status(*status_ids)&.to_time
117
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.
@@ -4,7 +4,7 @@ class Board
4
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
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ChangeItem
4
- attr_reader :field, :value_id, :old_value_id, :raw, :author
5
- attr_accessor :value, :old_value, :time
4
+ attr_reader :field, :value_id, :old_value_id, :raw, :author, :time
5
+ attr_accessor :value, :old_value
6
6
 
7
7
  def initialize raw:, time:, author:, artificial: false
8
8
  @raw = raw
9
9
  @time = time
10
- raise "Time must be an object of type Time in the correct timezone: #{@time}" if @time.is_a? String
10
+ raise 'ChangeItem.new() time cannot be nil' if time.nil?
11
+ raise "Time must be an object of type Time in the correct timezone: #{@time.inspect}" unless @time.is_a? Time
11
12
 
12
- @field = field || @raw['field']
13
- @value = value || @raw['toString']
13
+ @field = @raw['field']
14
+ @value = @raw['toString']
14
15
  @value_id = @raw['to'].to_i
15
16
  @old_value = @raw['fromString']
16
17
  @old_value_id = @raw['from']&.to_i
@@ -34,6 +35,11 @@ class ChangeItem
34
35
 
35
36
  def link? = (field == 'Link')
36
37
 
38
+ def labels? = (field == 'labels')
39
+
40
+ # An alias for time so that logic accepting a Time, Date, or ChangeItem can all respond to :to_time
41
+ def to_time = @time
42
+
37
43
  def to_s
38
44
  message = +''
39
45
  message << "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: #{time_to_s(@time).inspect}"
@@ -25,6 +25,14 @@ class ChartBase
25
25
  @aggregated_project
26
26
  end
27
27
 
28
+ def html_directory
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
+ "#{pathname.dirname}/html"
34
+ end
35
+
28
36
  def render caller_binding, file
29
37
  pathname = Pathname.new(File.realpath(file))
30
38
  basename = pathname.basename.to_s
@@ -33,8 +41,8 @@ class ChartBase
33
41
  # Insert a incrementing chart_id so that all the chart names on the page are unique
34
42
  caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
35
43
 
36
- @html_directory = "#{pathname.dirname}/html"
37
- erb = ERB.new file_system.load "#{@html_directory}/#{$1}.erb"
44
+ # @html_directory = "#{pathname.dirname}/html"
45
+ erb = ERB.new file_system.load "#{html_directory}/#{$1}.erb"
38
46
  erb.result(caller_binding)
39
47
  end
40
48
 
@@ -100,7 +108,7 @@ class ChartBase
100
108
  issues_id = next_id
101
109
 
102
110
  issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
103
- erb = ERB.new file_system.load "#{@html_directory}/collapsible_issues_panel.erb"
111
+ erb = ERB.new file_system.load File.join(html_directory, 'collapsible_issues_panel.erb')
104
112
  erb.result(binding)
105
113
  end
106
114
 
@@ -161,13 +169,13 @@ class ChartBase
161
169
  end
162
170
  end
163
171
 
164
- def header_text text = nil
165
- @header_text = text if text
172
+ def header_text text = :none
173
+ @header_text = text unless text == :none
166
174
  @header_text
167
175
  end
168
176
 
169
- def description_text text = nil
170
- @description_text = text if text
177
+ def description_text text = :none
178
+ @description_text = text unless text == :none
171
179
  @description_text
172
180
  end
173
181
 
@@ -192,8 +200,8 @@ class ChartBase
192
200
  icon: ' 👀'
193
201
  )
194
202
  end
195
- text = is_category ? status.category_name : status.name
196
- "<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
203
+ text = is_category ? status.category.name : status.name
204
+ "<span title='Category: #{status.category.name}'>#{color_block color.name} #{text}</span>#{visibility}"
197
205
  end
198
206
 
199
207
  def icon_span title:, icon:
@@ -201,11 +209,11 @@ class ChartBase
201
209
  end
202
210
 
203
211
  def status_category_color status
204
- case status.category_name
205
- when 'To Do' then CssVariable['--status-category-todo-color']
206
- when 'In Progress' then CssVariable['--status-category-inprogress-color']
207
- when 'Done' then CssVariable['--status-category-done-color']
208
- else 'black' # Theoretically impossible but seen in prod.
212
+ case status.category.key
213
+ when 'new' then CssVariable['--status-category-todo-color']
214
+ when 'indeterminate' then CssVariable['--status-category-inprogress-color']
215
+ when 'done' then CssVariable['--status-category-done-color']
216
+ else CssVariable['--status-category-unknown-color'] # Theoretically impossible but seen in prod.
209
217
  end
210
218
  end
211
219
 
@@ -44,18 +44,44 @@ class CycleTimeConfig
44
44
  started_stopped_times(issue).last
45
45
  end
46
46
 
47
- def started_stopped_times issue
47
+ def fabricate_change_item time
48
+ deprecated date: '2024-12-16', message: 'This method should now return a ChangeItem not a Time', depth: 4
49
+ raw = {
50
+ 'field' => 'Fabricated change',
51
+ 'to' => '0',
52
+ 'toString' => '',
53
+ 'from' => '0',
54
+ 'fromString' => ''
55
+ }
56
+ ChangeItem.new raw: raw, time: time, author: 'unknown', artificial: true
57
+ end
58
+
59
+ def started_stopped_changes issue
48
60
  started = @start_at.call(issue)
49
61
  stopped = @stop_at.call(issue)
50
62
 
63
+ # Obscure edge case where some of the start_at and stop_at blocks might return false in place of nil.
64
+ # If they are false then explicitly make them nil.
65
+ started ||= nil
66
+ stopped ||= nil
67
+
68
+ # These are only here for backwards compatibility. Hopefully nobody will ever need them.
69
+ started = fabricate_change_item(started) if !started.nil? && !started.is_a?(ChangeItem)
70
+ stopped = fabricate_change_item(stopped) if !stopped.nil? && !stopped.is_a?(ChangeItem)
71
+
51
72
  # In the case where started and stopped are exactly the same time, we pretend that
52
73
  # it just stopped and never started. This allows us to have logic like 'in or right of'
53
74
  # for the start and not have it conflict.
54
- started = nil if started == stopped
75
+ started = nil if started&.time == stopped&.time
55
76
 
56
77
  [started, stopped]
57
78
  end
58
79
 
80
+ def started_stopped_times issue
81
+ started, stopped = started_stopped_changes(issue)
82
+ [started&.time, stopped&.time]
83
+ end
84
+
59
85
  def started_stopped_dates issue
60
86
  started_time, stopped_time = started_stopped_times(issue)
61
87
  [started_time&.to_date, stopped_time&.to_date]
@@ -41,6 +41,8 @@ class CycletimeHistogram < ChartBase
41
41
  )
42
42
  end
43
43
 
44
+ return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
45
+
44
46
  wrap_and_render(binding, __FILE__)
45
47
  end
46
48
 
@@ -57,7 +57,25 @@ class DataQualityReport < ChartBase
57
57
  entries_with_problems = entries_with_problems()
58
58
  return '' if entries_with_problems.empty?
59
59
 
60
- wrap_and_render(binding, __FILE__)
60
+ caller_binding = binding
61
+ result = +''
62
+ result << render_top_text(caller_binding)
63
+
64
+ result << '<ul class="quality_report">'
65
+ result << render_problem_type(:discarded_changes)
66
+ result << render_problem_type(:completed_but_not_started)
67
+ result << render_problem_type(:status_changes_after_done)
68
+ result << render_problem_type(:backwards_through_status_categories)
69
+ result << render_problem_type(:backwords_through_statuses)
70
+ result << render_problem_type(:status_not_on_board)
71
+ result << render_problem_type(:created_in_wrong_status)
72
+ result << render_problem_type(:stopped_before_started)
73
+ result << render_problem_type(:issue_not_started_but_subtasks_have)
74
+ result << render_problem_type(:incomplete_subtasks_when_issue_done)
75
+ result << render_problem_type(:issue_on_multiple_boards)
76
+ result << '</ul>'
77
+
78
+ result
61
79
  end
62
80
 
63
81
  def problems_for key
@@ -70,6 +88,18 @@ class DataQualityReport < ChartBase
70
88
  result
71
89
  end
72
90
 
91
+ def render_problem_type problem_key
92
+ problems = problems_for problem_key
93
+ return '' if problems.empty?
94
+
95
+ <<-HTML
96
+ <li>
97
+ #{__send__ :"render_#{problem_key}", problems}
98
+ #{collapsible_issues_panel problems}
99
+ </li>
100
+ HTML
101
+ end
102
+
73
103
  # Return a format that's easier to assert against
74
104
  def testable_entries
75
105
  format = '%Y-%m-%d %H:%M:%S %z'
@@ -82,8 +112,8 @@ class DataQualityReport < ChartBase
82
112
  @entries.reject { |entry| entry.problems.empty? }
83
113
  end
84
114
 
85
- def category_name_for status_name:, board:
86
- board.possible_statuses.find { |status| status.name == status_name }&.category_name
115
+ def category_name_for status_id:, board:
116
+ board.possible_statuses.find_by_id(status_id)&.category&.name
87
117
  end
88
118
 
89
119
  def initialize_entries
@@ -154,7 +184,7 @@ class DataQualityReport < ChartBase
154
184
  index = entry.issue.board.visible_columns.find_index { |column| column.status_ids.include? change.value_id }
155
185
  if index.nil?
156
186
  # If it's a backlog status then ignore it. Not supposed to be visible.
157
- next if entry.issue.board.backlog_statuses.include? change.value_id
187
+ next if entry.issue.board.backlog_statuses.include?(board.possible_statuses.find_by_id(change.value_id))
158
188
 
159
189
  detail = "Status #{format_status change.value, board: board} is not on the board"
160
190
  if issue.board.possible_statuses.expand_statuses(change.value).empty?
@@ -168,8 +198,8 @@ class DataQualityReport < ChartBase
168
198
  elsif change.old_value.nil?
169
199
  # Do nothing
170
200
  elsif index < last_index
171
- new_category = category_name_for(status_name: change.value, board: board)
172
- old_category = category_name_for(status_name: change.old_value, board: board)
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)
173
203
 
174
204
  if new_category == old_category
175
205
  entry.report(
@@ -317,4 +347,94 @@ class DataQualityReport < ChartBase
317
347
  )
318
348
  end
319
349
  end
350
+
351
+ def render_discarded_changes problems
352
+ <<-HTML
353
+ #{label_issues problems.size} have had information discarded. This configuration is set
354
+ 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
356
+ HTML
357
+ end
358
+
359
+ def render_completed_but_not_started problems
360
+ percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
361
+ html = <<-HTML
362
+ #{label_issues problems.size} were discarded from all charts using cycletime (scatterplot, histogram, etc)
363
+ as we couldn't determine when they started.
364
+ HTML
365
+ if percentage_work_included < 85
366
+ html << <<-HTML
367
+ Consider whether looking at only #{percentage_work_included}% of the total data points is enough
368
+ to come to any reasonable conclusions. See <a href="https://unconsciousagile.com/2024/11/19/survivor-bias.html">
369
+ Survivor Bias</a>.
370
+ HTML
371
+ end
372
+ html
373
+ end
374
+
375
+ def render_status_changes_after_done problems
376
+ <<-HTML
377
+ #{label_issues problems.size} had a status change after being identified as done. We should question
378
+ whether they were really done at that point or if we stopped the clock too early.
379
+ HTML
380
+ end
381
+
382
+ def render_backwards_through_status_categories problems
383
+ <<-HTML
384
+ #{label_issues problems.size} moved backwards across the board, <b>crossing status categories</b>.
385
+ This will almost certainly have impacted timings as the end times are often taken at status category
386
+ boundaries. You should assume that any timing measurements for this item are wrong.
387
+ HTML
388
+ end
389
+
390
+ def render_backwords_through_statuses problems
391
+ <<-HTML
392
+ #{label_issues problems.size} moved backwards across the board. Depending where we have set the
393
+ start and end points, this may give us incorrect timing data. Note that these items did not cross
394
+ a status category and may not have affected metrics.
395
+ HTML
396
+ end
397
+
398
+ def render_status_not_on_board problems
399
+ <<-HTML
400
+ #{label_issues problems.size} were not visible on the board for some period of time. This may impact
401
+ timings as the work was likely to have been forgotten if it wasn't visible.
402
+ HTML
403
+ end
404
+
405
+ def render_created_in_wrong_status problems
406
+ <<-HTML
407
+ #{label_issues problems.size} were created in a status not designated as Backlog. This will impact
408
+ the measurement of start times and will therefore impact whether it's shown as in progress or not.
409
+ HTML
410
+ end
411
+
412
+ def render_stopped_before_started problems
413
+ <<-HTML
414
+ #{label_issues problems.size} were stopped before they were started and this will play havoc with
415
+ any cycletime or WIP calculations. The most common case for this is when an item gets closed and
416
+ then moved back into an in-progress status.
417
+ HTML
418
+ end
419
+
420
+ def render_issue_not_started_but_subtasks_have problems
421
+ <<-HTML
422
+ #{label_issues problems.size} still showing 'not started' while sub-tasks underneath them have
423
+ started. This is almost always a mistake; if we're working on subtasks, the top level item should
424
+ also have started.
425
+ HTML
426
+ end
427
+
428
+ def render_incomplete_subtasks_when_issue_done problems
429
+ <<-HTML
430
+ #{label_issues problems.size} issues were marked as done while subtasks were still not done.
431
+ HTML
432
+ end
433
+
434
+ def render_issue_on_multiple_boards problems
435
+ <<-HTML
436
+ For #{label_issues problems.size}, we have an issue that shows up on more than one board. This
437
+ could result in more data points showing up on a chart then there really should be.
438
+ HTML
439
+ end
320
440
  end
@@ -20,8 +20,8 @@ class DownloadConfig
20
20
  @rolling_date_count
21
21
  end
22
22
 
23
- def no_earlier_than date = nil
24
- @no_earlier_than = Date.parse(date) unless date.nil?
23
+ def no_earlier_than date = :not_set
24
+ @no_earlier_than = Date.parse(date) unless date == :not_set
25
25
  @no_earlier_than
26
26
  end
27
27
 
@@ -39,6 +39,7 @@ class Downloader
39
39
  # board_ids = @download_config.board_ids
40
40
 
41
41
  remove_old_files
42
+ update_status_history_file
42
43
  download_statuses
43
44
  find_board_ids.each do |id|
44
45
  board = download_board_configuration board_id: id
@@ -66,7 +67,7 @@ class Downloader
66
67
 
67
68
  def download_issues board:
68
69
  log " Downloading primary issues for board #{board.id}", both: true
69
- path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
70
+ path = File.join(@target_path, "#{file_prefix}_issues/")
70
71
  unless Dir.exist?(path)
71
72
  log " Creating path #{path}"
72
73
  Dir.mkdir(path)
@@ -153,30 +154,58 @@ class Downloader
153
154
 
154
155
  @file_system.save_json(
155
156
  json: json,
156
- filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
157
+ filename: File.join(@target_path, "#{file_prefix}_statuses.json")
157
158
  )
158
159
  end
159
160
 
161
+ def update_status_history_file
162
+ status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
163
+ return unless file_system.file_exist? status_filename
164
+
165
+ status_json = file_system.load_json(status_filename)
166
+
167
+ history_filename = File.join(@target_path, "#{file_prefix}_status_history.json")
168
+ history_json = file_system.load_json(history_filename) if file_system.file_exist? history_filename
169
+
170
+ if history_json
171
+ file_system.log ' Updating status history file', also_write_to_stderr: true
172
+ else
173
+ file_system.log ' Creating status history file', also_write_to_stderr: true
174
+ history_json = []
175
+ end
176
+
177
+ status_json.each do |status_item|
178
+ id = status_item['id']
179
+ history_item = history_json.find { |s| s['id'] == id }
180
+ history_json.delete(history_item) if history_item
181
+ history_json << status_item
182
+ end
183
+
184
+ file_system.save_json(filename: history_filename, json: history_json)
185
+ end
186
+
160
187
  def download_board_configuration board_id:
161
188
  log " Downloading board configuration for board #{board_id}", both: true
162
189
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
163
190
 
164
191
  exit_if_call_failed json
165
192
 
166
- file_prefix = @download_config.project_config.file_prefix
167
- @file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
193
+ @file_system.save_json(
194
+ json: json,
195
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_configuration.json")
196
+ )
168
197
 
169
198
  # We have a reported bug that blew up on this line. Moved it after the save so we can
170
199
  # actually look at the returned json.
171
200
  @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
172
201
 
173
202
  download_sprints board_id: board_id if json['type'] == 'scrum'
174
- Board.new raw: json
203
+ # TODO: Should be passing actual statuses, not empty list
204
+ Board.new raw: json, possible_statuses: StatusCollection.new
175
205
  end
176
206
 
177
207
  def download_sprints board_id:
178
208
  log " Downloading sprints for board #{board_id}", both: true
179
- file_prefix = @download_config.project_config.file_prefix
180
209
  max_results = 100
181
210
  start_at = 0
182
211
  is_last = false
@@ -188,7 +217,7 @@ class Downloader
188
217
 
189
218
  @file_system.save_json(
190
219
  json: json,
191
- filename: "#{@target_path}#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json"
220
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json")
192
221
  )
193
222
  is_last = json['isLast']
194
223
  max_results = json['maxResults']
@@ -201,7 +230,7 @@ class Downloader
201
230
  end
202
231
 
203
232
  def metadata_pathname
204
- "#{@target_path}#{@download_config.project_config.file_prefix}_meta.json"
233
+ File.join(@target_path, "#{file_prefix}_meta.json")
205
234
  end
206
235
 
207
236
  def load_metadata
@@ -244,17 +273,17 @@ class Downloader
244
273
  end
245
274
 
246
275
  def remove_old_files
247
- file_prefix = @download_config.project_config.file_prefix
248
276
  Dir.foreach @target_path do |file|
249
277
  next unless file.match?(/^#{file_prefix}_\d+\.json$/)
278
+ next if file == "#{file_prefix}_status_history.json"
250
279
 
251
- File.unlink "#{@target_path}#{file}"
280
+ File.unlink File.join(@target_path, file)
252
281
  end
253
282
 
254
283
  return if @cached_data_format_is_current
255
284
 
256
285
  # Also throw away all the previously downloaded issues.
257
- path = File.join @target_path, "#{file_prefix}_issues"
286
+ path = File.join(@target_path, "#{file_prefix}_issues")
258
287
  return unless File.exist? path
259
288
 
260
289
  Dir.foreach path do |file|
@@ -292,4 +321,8 @@ class Downloader
292
321
 
293
322
  segments.join ' AND '
294
323
  end
324
+
325
+ def file_prefix
326
+ @download_config.project_config.get_file_prefix
327
+ end
295
328
  end
@@ -3,8 +3,6 @@
3
3
  # This file is really intended to give you ideas about how you might configure your own reports, not
4
4
  # as a complete setup that will work in every case.
5
5
  #
6
- # See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more details
7
- #
8
6
  # The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a
9
7
  # S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a
10
8
  # single team. For that reason, we look at slightly different things that we would on a single team board.
@@ -13,6 +11,7 @@ class Exporter
13
11
  def aggregated_project name:, project_names:, settings: {}
14
12
  project name: name do
15
13
  puts name
14
+ file_prefix name
16
15
  self.settings.merge! settings
17
16
 
18
17
  aggregate do
@@ -21,8 +20,6 @@ class Exporter
21
20
  end
22
21
  end
23
22
 
24
- file_prefix name
25
-
26
23
  file do
27
24
  file_suffix '.html'
28
25
  issues.reject! do |issue|
@@ -34,7 +31,7 @@ class Exporter
34
31
  board_lines = []
35
32
  included_projects.each do |project|
36
33
  project.all_boards.each_value do |board|
37
- board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
34
+ board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}"
38
35
  end
39
36
  end
40
37
  board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
@@ -2,8 +2,6 @@
2
2
 
3
3
  # This file is really intended to give you ideas about how you might configure your own reports, not
4
4
  # as a complete setup that will work in every case.
5
- #
6
- # See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
7
5
  class Exporter
8
6
  def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
9
7
  default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
@@ -12,15 +10,15 @@ class Exporter
12
10
 
13
11
  project name: name do
14
12
  puts name
15
- self.anonymize if anonymize
13
+ file_prefix file_prefix
16
14
 
15
+ self.anonymize if anonymize
17
16
  self.settings.merge! settings
18
17
 
19
18
  status_category_mappings.each do |status, category|
20
19
  status_category_mapping status: status, category: category
21
20
  end
22
21
 
23
- file_prefix file_prefix
24
22
  download do
25
23
  self.rolling_date_count(rolling_date_count) if rolling_date_count
26
24
  self.no_earlier_than(no_earlier_than) if no_earlier_than
@@ -63,7 +63,7 @@ class ExpeditedChart < ChartBase
63
63
  next unless change.priority?
64
64
 
65
65
  if expedited_priority_names.include? change.value
66
- expedite_start = change.time
66
+ expedite_start = change.time.to_date
67
67
  elsif expedite_start
68
68
  start_date = expedite_start.to_date
69
69
  stop_date = change.time.to_date
@@ -72,7 +72,7 @@ class ExpeditedChart < ChartBase
72
72
  (start_date < date_range.begin && stop_date > date_range.end)
73
73
 
74
74
  result << [expedite_start, :expedite_start]
75
- result << [change.time, :expedite_stop]
75
+ result << [change.time.to_date, :expedite_stop]
76
76
  end
77
77
  expedite_start = nil
78
78
  end
@@ -109,11 +109,11 @@ 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, stopped_time = cycletime.started_stopped_times(issue)
112
+ started_date, stopped_date = cycletime.started_stopped_dates(issue)
113
113
 
114
- expedite_data << [started_time, :issue_started] if started_time
115
- expedite_data << [stopped_time, :issue_stopped] if stopped_time
116
- expedite_data.sort_by! { |a| a[0] }
114
+ expedite_data << [started_date, :issue_started] if started_date
115
+ expedite_data << [stopped_date, :issue_stopped] if stopped_date
116
+ expedite_data.sort_by!(&:first)
117
117
 
118
118
  # If none of the data would be visible on the chart then skip it.
119
119
  return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
@@ -150,7 +150,7 @@ class ExpeditedChart < ChartBase
150
150
 
151
151
  unless expedite_data.empty?
152
152
  last_change_time = expedite_data[-1][0].to_date
153
- if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
153
+ if last_change_time && last_change_time <= date_range.end && stopped_date.nil?
154
154
  data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
155
155
  dot_colors << '' # It won't be visible so it doesn't matter
156
156
  point_styles << 'dash'
@@ -3,11 +3,11 @@
3
3
  require 'fileutils'
4
4
 
5
5
  class Object
6
- def deprecated message:, date:
6
+ def deprecated message:, date:, depth: 2
7
7
  text = +''
8
8
  text << "Deprecated(#{date}): "
9
9
  text << message
10
- caller(1..2).each do |line|
10
+ caller(1..depth).each do |line|
11
11
  text << "\n-> Called from #{line}"
12
12
  end
13
13
  warn text