jirametrics 2.12.1 → 2.22

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aging_work_bar_chart.rb +176 -134
  3. data/lib/jirametrics/anonymizer.rb +8 -6
  4. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  5. data/lib/jirametrics/bar_chart_range.rb +17 -0
  6. data/lib/jirametrics/board.rb +4 -0
  7. data/lib/jirametrics/board_config.rb +4 -1
  8. data/lib/jirametrics/change_item.rb +12 -4
  9. data/lib/jirametrics/chart_base.rb +36 -2
  10. data/lib/jirametrics/cycletime_config.rb +22 -4
  11. data/lib/jirametrics/cycletime_histogram.rb +3 -1
  12. data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
  13. data/lib/jirametrics/daily_view.rb +57 -53
  14. data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
  15. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
  16. data/lib/jirametrics/daily_wip_chart.rb +1 -1
  17. data/lib/jirametrics/data_quality_report.rb +8 -3
  18. data/lib/jirametrics/dependency_chart.rb +4 -1
  19. data/lib/jirametrics/downloader.rb +34 -70
  20. data/lib/jirametrics/downloader_for_cloud.rb +202 -0
  21. data/lib/jirametrics/downloader_for_data_center.rb +94 -0
  22. data/lib/jirametrics/examples/standard_project.rb +9 -9
  23. data/lib/jirametrics/expedited_chart.rb +1 -1
  24. data/lib/jirametrics/exporter.rb +12 -5
  25. data/lib/jirametrics/file_system.rb +24 -1
  26. data/lib/jirametrics/fix_version.rb +13 -0
  27. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  28. data/lib/jirametrics/groupable_issue_chart.rb +7 -1
  29. data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
  30. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  31. data/lib/jirametrics/html/aging_work_table.erb +2 -0
  32. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  33. data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
  34. data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
  35. data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
  36. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
  37. data/lib/jirametrics/html/expedited_chart.erb +3 -1
  38. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
  39. data/lib/jirametrics/html/index.css +21 -9
  40. data/lib/jirametrics/html/index.erb +5 -37
  41. data/lib/jirametrics/html/index.js +114 -0
  42. data/lib/jirametrics/html/sprint_burndown.erb +11 -3
  43. data/lib/jirametrics/html/throughput_chart.erb +2 -2
  44. data/lib/jirametrics/html_generator.rb +31 -0
  45. data/lib/jirametrics/html_report_config.rb +8 -25
  46. data/lib/jirametrics/issue.rb +128 -23
  47. data/lib/jirametrics/jira_gateway.rb +59 -17
  48. data/lib/jirametrics/project_config.rb +42 -5
  49. data/lib/jirametrics/raw_javascript.rb +13 -0
  50. data/lib/jirametrics/settings.json +3 -1
  51. data/lib/jirametrics/sprint.rb +12 -0
  52. data/lib/jirametrics/sprint_burndown.rb +6 -2
  53. data/lib/jirametrics/status_collection.rb +1 -0
  54. data/lib/jirametrics/stitcher.rb +75 -0
  55. data/lib/jirametrics.rb +26 -69
  56. metadata +11 -3
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ChangeItem
4
- attr_reader :field, :value_id, :old_value_id, :raw, :time, :author_raw
5
- attr_accessor :value, :old_value
4
+ attr_reader :field, :value_id, :old_value_id, :raw, :author_raw, :field_id
5
+ attr_accessor :value, :old_value, :time
6
6
 
7
7
  def initialize raw:, author_raw:, time:, artificial: false
8
8
  @raw = raw
@@ -13,9 +13,15 @@ class ChangeItem
13
13
 
14
14
  @field = @raw['field']
15
15
  @value = @raw['toString']
16
- @value_id = @raw['to'].to_i
17
16
  @old_value = @raw['fromString']
18
- @old_value_id = @raw['from']&.to_i
17
+ if sprint?
18
+ @value_id = @raw['to'].split(', ').collect(&:to_i)
19
+ @old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
20
+ else
21
+ @value_id = @raw['to'].to_i
22
+ @old_value_id = @raw['from']&.to_i
23
+ end
24
+ @field_id = @raw['fieldId']
19
25
  @artificial = artificial
20
26
  end
21
27
 
@@ -30,6 +36,7 @@ class ChangeItem
30
36
  def artificial? = @artificial
31
37
  def assignee? = (field == 'assignee')
32
38
  def comment? = (field == 'comment')
39
+ def description? = (field == 'description')
33
40
  def due_date? = (field == 'duedate')
34
41
  def flagged? = (field == 'Flagged')
35
42
  def issue_type? = field == 'issuetype'
@@ -53,6 +60,7 @@ class ChangeItem
53
60
  message << ':' << old_value_id.inspect if status?
54
61
  end
55
62
  message << ", time: #{time_to_s(@time).inspect}"
63
+ message << ", field_id: #{@field_id.inspect}" if @field_id
56
64
  message << ', artificial' if artificial?
57
65
  message << ')'
58
66
  message
@@ -2,7 +2,8 @@
2
2
 
3
3
  class ChartBase
4
4
  attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
5
- :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system, :users
5
+ :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
6
+ :atlassian_document_format
6
7
  attr_writer :aggregated_project
7
8
  attr_reader :canvas_width, :canvas_height
8
9
 
@@ -21,6 +22,14 @@ class ChartBase
21
22
  @canvas_responsive = true
22
23
  end
23
24
 
25
+ def call_before_run &proc
26
+ (@call_before_run_procs ||= []) << proc
27
+ end
28
+
29
+ def before_run
30
+ @call_before_run_procs&.each { |proc| proc.call }
31
+ end
32
+
24
33
  def aggregated_project?
25
34
  @aggregated_project
26
35
  end
@@ -44,7 +53,7 @@ class ChartBase
44
53
 
45
54
  def render_top_text caller_binding
46
55
  result = +''
47
- result << "<h1>#{@header_text}</h1>" if @header_text
56
+ result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
48
57
  result << ERB.new(@description_text).result(caller_binding) if @description_text
49
58
  result
50
59
  end
@@ -66,6 +75,8 @@ class ChartBase
66
75
  end
67
76
 
68
77
  def label_days days
78
+ return 'unknown' if days.nil?
79
+
69
80
  "#{days} day#{'s' unless days == 1}"
70
81
  end
71
82
 
@@ -276,4 +287,27 @@ class ChartBase
276
287
  </div>
277
288
  TEXT
278
289
  end
290
+
291
+ # Set a cycletime for just this one chart, overriding the one for the report.
292
+ def cycletime &block
293
+ call_before_run do
294
+ @cycletime = CycleTimeConfig.new(
295
+ possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system,
296
+ settings: settings
297
+ )
298
+ end
299
+ end
300
+
301
+ # Returns the cycletime in use right now, which may be specific to the chart or across the report.
302
+ def cycletime_for_issue issue
303
+ @cycletime || issue.board.cycletime
304
+ end
305
+
306
+ def seam_start type = 'chart'
307
+ "\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
308
+ end
309
+
310
+ def seam_end type = 'chart'
311
+ "\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
312
+ end
279
313
  end
@@ -6,12 +6,14 @@ require 'date'
6
6
  class CycleTimeConfig
7
7
  include SelfOrIssueDispatcher
8
8
 
9
- attr_reader :label, :parent_config
9
+ attr_reader :label, :possible_statuses, :settings, :file_system
10
10
 
11
- def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
12
- @parent_config = parent_config
11
+ def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
12
+
13
+ @possible_statuses = possible_statuses
13
14
  @label = label
14
15
  @today = today
16
+ @settings = settings
15
17
 
16
18
  # If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
17
19
  # may make it easier to find problems in the test code ;-)
@@ -63,6 +65,10 @@ class CycleTimeConfig
63
65
  end
64
66
 
65
67
  def started_stopped_changes issue
68
+ cache_key = "#{issue.key}:#{issue.board.id}"
69
+ last_result = (@cache ||= {})[cache_key]
70
+ return *last_result if last_result && settings['cache_cycletime_calculations']
71
+
66
72
  started = @start_at.call(issue)
67
73
  stopped = @stop_at.call(issue)
68
74
 
@@ -80,7 +86,15 @@ class CycleTimeConfig
80
86
  # for the start and not have it conflict.
81
87
  started = nil if started&.time == stopped&.time
82
88
 
83
- [started, stopped]
89
+ result = [started, stopped]
90
+ if last_result && result != last_result
91
+ @file_system.error(
92
+ "Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
93
+ "previous=#{last_result.inspect}"
94
+ )
95
+ end
96
+ @cache[cache_key] = result
97
+ result
84
98
  end
85
99
 
86
100
  def started_stopped_times issue
@@ -88,6 +102,10 @@ class CycleTimeConfig
88
102
  [started&.time, stopped&.time]
89
103
  end
90
104
 
105
+ def flush_cache
106
+ @cache = nil
107
+ end
108
+
91
109
  def started_stopped_dates issue
92
110
  started_time, stopped_time = started_stopped_times(issue)
93
111
  [started_time&.to_date, stopped_time&.to_date]
@@ -62,7 +62,9 @@ class CycletimeHistogram < ChartBase
62
62
  )
63
63
  end
64
64
 
65
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
65
+ if data_sets.empty?
66
+ return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>"
67
+ end
66
68
 
67
69
  wrap_and_render(binding, __FILE__)
68
70
  end
@@ -35,14 +35,33 @@ class CycletimeScatterplot < ChartBase
35
35
  end
36
36
 
37
37
  @percentage_lines = []
38
- @highest_cycletime = 0
38
+ @highest_y_value = 0
39
39
  end
40
40
 
41
- def run
42
- completed_issues = completed_issues_in_range include_unstarted: false
41
+ def all_items
42
+ completed_issues_in_range include_unstarted: false
43
+ end
44
+
45
+ def x_value item
46
+ item.board.cycletime.started_stopped_times(item).last
47
+ end
48
+
49
+ def y_value item
50
+ item.board.cycletime.cycletime(item)
51
+ end
43
52
 
44
- data_sets = create_datasets completed_issues
45
- overall_percent_line = calculate_percent_line(completed_issues)
53
+ def title_value item
54
+ "#{item.key} : #{item.summary} (#{label_days(y_value(item))})"
55
+ end
56
+
57
+ def y_axis_heading
58
+ 'Cycle time in days'
59
+ end
60
+
61
+ def run
62
+ items = all_items
63
+ data_sets = create_datasets items
64
+ overall_percent_line = calculate_percent_line(items)
46
65
  @percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
47
66
 
48
67
  return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
@@ -50,14 +69,14 @@ class CycletimeScatterplot < ChartBase
50
69
  wrap_and_render(binding, __FILE__)
51
70
  end
52
71
 
53
- def create_datasets completed_issues
72
+ def create_datasets items
54
73
  data_sets = []
55
74
 
56
- group_issues(completed_issues).each do |rules, completed_issues_by_type|
75
+ group_issues(items).each do |rules, completed_items_by_type|
57
76
  label = rules.label
58
77
  color = rules.color
59
- percent_line = calculate_percent_line completed_issues_by_type
60
- data = completed_issues_by_type.filter_map { |issue| data_for_issue(issue) }
78
+ percent_line = calculate_percent_line completed_items_by_type
79
+ data = completed_items_by_type.filter_map { |issue| data_for_issue(issue) }
61
80
  data_sets << {
62
81
  label: "#{label} (85% at #{label_days(percent_line)})",
63
82
  data: data,
@@ -86,7 +105,7 @@ class CycletimeScatterplot < ChartBase
86
105
  calculator = TrendLineCalculator.new(points)
87
106
  data_points = calculator.chart_datapoints(
88
107
  range: time_range.begin.to_i..time_range.end.to_i,
89
- max_y: @highest_cycletime
108
+ max_y: @highest_y_value
90
109
  )
91
110
  data_points.each do |point_hash|
92
111
  point_hash[:x] = chart_format Time.at(point_hash[:x])
@@ -106,21 +125,21 @@ class CycletimeScatterplot < ChartBase
106
125
  }
107
126
  end
108
127
 
109
- def data_for_issue issue
110
- cycle_time = issue.board.cycletime.cycletime(issue)
128
+ def data_for_issue item
129
+ cycle_time = y_value(item)
111
130
  return nil if cycle_time < 1 # These will get called out on the quality report
112
131
 
113
- @highest_cycletime = cycle_time if @highest_cycletime < cycle_time
132
+ @highest_y_value = cycle_time if @highest_y_value < cycle_time
114
133
 
115
134
  {
116
135
  y: cycle_time,
117
- x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
118
- title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
136
+ x: chart_format(x_value(item)),
137
+ title: [title_value(item)]
119
138
  }
120
139
  end
121
140
 
122
- def calculate_percent_line completed_issues
123
- times = completed_issues.collect { |issue| issue.board.cycletime.cycletime(issue) }
141
+ def calculate_percent_line items
142
+ times = items.collect { |item| y_value(item) }
124
143
  index = times.size * 85 / 100
125
144
  times.sort[index]
126
145
  end
@@ -23,7 +23,7 @@ class DailyView < ChartBase
23
23
  def run
24
24
  aging_issues = select_aging_issues
25
25
 
26
- return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
26
+ return "<h1 class='foldable'>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
27
27
 
28
28
  result = +''
29
29
  result << render_top_text(binding)
@@ -78,7 +78,7 @@ class DailyView < ChartBase
78
78
  blocked_stalled = issue.blocked_stalled_by_date(
79
79
  date_range: today..today, chart_end_time: time_range.end, settings: settings
80
80
  )[today]
81
- return [] unless blocked_stalled
81
+ return [] if blocked_stalled.active?
82
82
 
83
83
  lines = []
84
84
  if blocked_stalled.blocked?
@@ -98,15 +98,18 @@ class DailyView < ChartBase
98
98
  lines
99
99
  end
100
100
 
101
- def make_issue_label issue
102
- "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> " \
103
- "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
101
+ def make_issue_label issue:, done:
102
+ label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
103
+ label << '<s>' if done
104
+ label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
105
+ label << '</s>' if done
106
+ label
104
107
  end
105
108
 
106
- def make_title_line issue
109
+ def make_title_line issue:, done:
107
110
  title_line = +''
108
111
  title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
109
- title_line << make_issue_label(issue)
112
+ title_line << make_issue_label(issue: issue, done: done)
110
113
  title_line
111
114
  end
112
115
 
@@ -115,20 +118,25 @@ class DailyView < ChartBase
115
118
  parent_key = issue.parent_key
116
119
  if parent_key
117
120
  parent = issues.find_by_key key: parent_key, include_hidden: true
118
- text = parent ? make_issue_label(parent) : parent_key
121
+ text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
119
122
  lines << ["Parent: #{text}"]
120
123
  end
121
124
  lines
122
125
  end
123
126
 
124
- def make_stats_lines issue
127
+ def make_stats_lines issue:, done:
125
128
  line = []
126
129
 
127
130
  line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
128
131
 
129
- age = issue.board.cycletime.age(issue, today: date_range.end)
130
- line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
132
+ if done
133
+ cycletime = issue.board.cycletime.cycletime(issue)
131
134
 
135
+ line << "Cycletime: <b>#{label_days cycletime}</b>"
136
+ else
137
+ age = issue.board.cycletime.age(issue, today: date_range.end)
138
+ line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
139
+ end
132
140
  line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
133
141
 
134
142
  column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
@@ -154,45 +162,26 @@ class DailyView < ChartBase
154
162
 
155
163
  def make_child_lines issue
156
164
  lines = []
157
- subtasks = issue.subtasks.reject { |i| i.done? }
165
+ subtasks = issue.subtasks
158
166
 
159
- unless subtasks.empty?
160
- icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
161
- lines << (icon_urls << 'Incomplete child issues')
162
- lines += subtasks
163
- end
164
- lines
165
- end
167
+ return lines if subtasks.empty?
166
168
 
167
- def jira_rich_text_to_html text
168
- text
169
- .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
170
- .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
171
- .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
172
- .gsub("\n", '<br />')
173
- end
169
+ lines << '<section><div class="foldable">Child issues</div>'
170
+ lines += subtasks
171
+ lines << '</section>'
174
172
 
175
- def expand_account_id account_id
176
- user = @users.find { |u| u.account_id == account_id }
177
- text = account_id
178
- text = "@#{user.display_name}" if user
179
- "<span class='account_id'>#{text}</span>"
173
+ lines
180
174
  end
181
175
 
182
176
  def make_history_lines issue
183
177
  history = issue.changes.reverse
184
178
  lines = []
185
179
 
186
- id = next_id
187
- lines << [
188
- "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
189
- "<span id='open#{id}'>▶ Issue History</span>" \
190
- "<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
191
- ]
180
+ lines << '<section><div class="foldable startFolded">Issue history</div>'
192
181
  table = +''
193
- table << "<table id='table#{id}' style='display: none'>"
182
+ table << '<table>'
194
183
  history.each do |c|
195
- time = c.time.strftime '%b %d, %I:%M%P'
184
+ time = c.time.strftime '%b %d, %Y @ %I:%M%P'
196
185
 
197
186
  table << '<tr>'
198
187
  table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
@@ -203,23 +192,24 @@ class DailyView < ChartBase
203
192
  end
204
193
  table << '</table>'
205
194
  lines << [table]
195
+ lines << '</section>'
206
196
  lines
207
197
  end
208
198
 
209
199
  def history_text change:, board:
210
- if change.comment?
211
- jira_rich_text_to_html(change.value)
212
- elsif change.status?
213
- convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
214
- to = convertor.call(change.value_id)
200
+ convertor = ->(value, _id) { value.inspect }
201
+ convertor = ->(_value, id) { format_status(board.possible_statuses.find_by_id(id), board: board) } if change.status?
202
+
203
+ if change.comment? || change.description?
204
+ atlassian_document_format.to_html(change.value)
205
+ elsif %w[status priority assignee duedate issuetype].include?(change.field)
206
+ to = convertor.call(change.value, change.value_id)
215
207
  if change.old_value
216
- from = convertor.call(change.old_value_id)
208
+ from = convertor.call(change.old_value, change.old_value_id)
217
209
  "Changed from #{from} to #{to}"
218
210
  else
219
211
  "Set to #{to}"
220
212
  end
221
- elsif %w[priority assignee duedate issuetype].include?(change.field)
222
- "Changed from \"#{change.old_value}\" to \"#{change.value}\""
223
213
  elsif change.flagged?
224
214
  change.value == '' ? 'Off' : 'On'
225
215
  else
@@ -245,15 +235,27 @@ class DailyView < ChartBase
245
235
  .join(' ')]]
246
236
  end
247
237
 
238
+ def make_description_lines issue
239
+ description = issue.raw['fields']['description']
240
+ result = []
241
+ result << [atlassian_document_format.to_html(description)] if description
242
+ result
243
+ end
244
+
248
245
  def assemble_issue_lines issue, child:
246
+ done = issue.done?
247
+
249
248
  lines = []
250
- lines << [make_title_line(issue)]
249
+ lines << [make_title_line(issue: issue, done: done)]
251
250
  lines += make_parent_lines(issue) unless child
252
- lines += make_stats_lines(issue)
253
- lines += make_sprints_lines(issue)
254
- lines += make_blocked_stalled_lines(issue)
255
- lines += make_child_lines(issue)
256
- lines += make_history_lines(issue)
251
+ lines += make_stats_lines(issue: issue, done: done)
252
+ unless done
253
+ lines += make_description_lines(issue)
254
+ lines += make_sprints_lines(issue)
255
+ lines += make_blocked_stalled_lines(issue)
256
+ lines += make_child_lines(issue)
257
+ lines += make_history_lines(issue)
258
+ end
257
259
  lines
258
260
  end
259
261
 
@@ -264,6 +266,8 @@ class DailyView < ChartBase
264
266
  assemble_issue_lines(issue, child: child).each do |row|
265
267
  if row.is_a? Issue
266
268
  result << render_issue(row, child: true)
269
+ elsif row.is_a?(String)
270
+ result << row
267
271
  else
268
272
  result << '<div class="heading">'
269
273
  row.each do |chunk|
@@ -51,8 +51,6 @@ class DailyWipByAgeChart < DailyWipChart
51
51
  def default_grouping_rules issue:, rules:
52
52
  started, stopped = issue.board.cycletime.started_stopped_dates(issue)
53
53
 
54
- rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
55
-
56
54
  if stopped && started.nil? # We can't tell when it started
57
55
  @has_completed_but_not_started = true
58
56
  not_started stopped: stopped, rules: rules, created: issue.created.to_date
@@ -72,7 +70,7 @@ class DailyWipByAgeChart < DailyWipChart
72
70
  rules.label = 'Start date unknown'
73
71
  rules.color = '--body-background'
74
72
  rules.group_priority = 11
75
- created_days = rules.current_date - created + 1
73
+ created_days = rules.current_date - created
76
74
  rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
77
75
  end
78
76
  end
@@ -84,7 +82,8 @@ class DailyWipByAgeChart < DailyWipChart
84
82
  end
85
83
 
86
84
  def group_by_age started:, rules:
87
- age = rules.current_date - started + 1
85
+ age = (rules.current_date - started).to_i + 1
86
+ rules.issue_hint = "(age: #{label_days age})"
88
87
 
89
88
  case age
90
89
  when 1
@@ -41,21 +41,30 @@ class DailyWipByBlockedStalledChart < DailyWipChart
41
41
  def default_grouping_rules issue:, rules:
42
42
  started, stopped = issue.board.cycletime.started_stopped_times(issue)
43
43
  stopped_date = stopped&.to_date
44
+ started_date = started&.to_date
44
45
 
45
46
  date = rules.current_date
46
47
  change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
47
-
48
48
  stopped_today = stopped_date == rules.current_date
49
49
 
50
+ days = nil
51
+ if started_date && stopped_date
52
+ days = (stopped_date - started_date).to_i + 1 # cycletime
53
+ elsif started_date
54
+ days = (time_range.end.to_date - started_date).to_i + 1 # age
55
+ end
56
+
50
57
  if stopped_today && started.nil?
51
58
  @has_completed_but_not_started = true
52
59
  rules.label = 'Completed but not started'
53
60
  rules.color = '--wip-chart-completed-but-not-started-color'
54
61
  rules.group_priority = -1
62
+ rules.issue_hint = '(Cycle time: Unknown)'
55
63
  elsif stopped_today
56
64
  rules.label = 'Completed'
57
65
  rules.color = '--wip-chart-completed-color'
58
66
  rules.group_priority = -2
67
+ rules.issue_hint = "(Cycle time: #{label_days days})"
59
68
  elsif started.nil?
60
69
  rules.label = 'Start date unknown'
61
70
  rules.color = '--body-background'
@@ -64,16 +73,17 @@ class DailyWipByBlockedStalledChart < DailyWipChart
64
73
  rules.label = 'Blocked'
65
74
  rules.color = '--blocked-color'
66
75
  rules.group_priority = 1
67
- rules.issue_hint = "(#{change.reasons})"
76
+ rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
68
77
  elsif change&.stalled?
69
78
  rules.label = 'Stalled'
70
79
  rules.color = '--stalled-color'
71
80
  rules.group_priority = 2
72
- rules.issue_hint = "(#{change.reasons})"
81
+ rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
73
82
  else
74
83
  rules.label = 'Active'
75
84
  rules.color = '--wip-chart-active-color'
76
85
  rules.group_priority = 3
86
+ rules.issue_hint = "(Age: #{label_days days})"
77
87
  end
78
88
  end
79
89
  end
@@ -66,7 +66,7 @@ class DailyWipChart < ChartBase
66
66
  hash = {}
67
67
 
68
68
  @issues.each do |issue|
69
- start, stop = issue.board.cycletime.started_stopped_dates(issue)
69
+ start, stop = cycletime_for_issue(issue).started_stopped_dates(issue)
70
70
  next if start.nil? && stop.nil?
71
71
 
72
72
  # If it stopped but never started then assume it started at creation so the data points
@@ -266,6 +266,8 @@ class DataQualityReport < ChartBase
266
266
 
267
267
  def scan_for_items_blocked_on_closed_tickets entry:
268
268
  entry.issue.issue_links.each do |link|
269
+ next unless settings['blocked_link_text'].include?(link.label)
270
+
269
271
  this_active = !entry.stopped
270
272
  other_active = !link.other_issue.board.cycletime.started_stopped_times(link.other_issue).last
271
273
  next unless this_active && !other_active
@@ -410,14 +412,17 @@ class DataQualityReport < ChartBase
410
412
  def render_status_not_on_board problems
411
413
  <<-HTML
412
414
  #{label_issues problems.size} were not visible on the board for some period of time. This may impact
413
- timings as the work was likely to have been forgotten if it wasn't visible.
415
+ timings as the work was likely to have been forgotten if it wasn't visible. What does "not visible"
416
+ mean in this context? The issue was in a status that is not mapped to any visible column on the board.
417
+ Look in "unmapped statuses" on your board.
414
418
  HTML
415
419
  end
416
420
 
417
421
  def render_created_in_wrong_status problems
418
422
  <<-HTML
419
- #{label_issues problems.size} were created in a status not designated as Backlog. This will impact
420
- the measurement of start times and will therefore impact whether it's shown as in progress or not.
423
+ #{label_issues problems.size} were created in a status that is not considered to be some varient
424
+ of To Do. Most likely this means that the issue was created from one of the columns on the board,
425
+ rather than in the backlog. Why Jira allows this is still a mystery.
421
426
  HTML
422
427
  end
423
428
 
@@ -51,7 +51,10 @@ class DependencyChart < ChartBase
51
51
  instance_eval(&@rules_block) if @rules_block
52
52
 
53
53
  dot_graph = build_dot_graph
54
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
54
+ if dot_graph.nil?
55
+ return "<h1 class='foldable'>#{@header_text}</h1>" \
56
+ '<div>No data matched the selected criteria. Nothing to show.</div>'
57
+ end
55
58
 
56
59
  svg = execute_graphviz(dot_graph.join("\n"))
57
60
  "<h1>#{@header_text}</h1><div>#{@description_text}</div>#{shrink_svg svg}"