jirametrics 2.12.1 → 2.20.1

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/anonymizer.rb +8 -6
  3. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  4. data/lib/jirametrics/board_config.rb +3 -1
  5. data/lib/jirametrics/change_item.rb +3 -2
  6. data/lib/jirametrics/chart_base.rb +5 -2
  7. data/lib/jirametrics/cycletime_config.rb +22 -3
  8. data/lib/jirametrics/cycletime_histogram.rb +3 -1
  9. data/lib/jirametrics/daily_view.rb +57 -53
  10. data/lib/jirametrics/data_quality_report.rb +6 -3
  11. data/lib/jirametrics/dependency_chart.rb +4 -1
  12. data/lib/jirametrics/downloader.rb +34 -70
  13. data/lib/jirametrics/downloader_for_cloud.rb +202 -0
  14. data/lib/jirametrics/downloader_for_data_center.rb +94 -0
  15. data/lib/jirametrics/examples/standard_project.rb +9 -9
  16. data/lib/jirametrics/expedited_chart.rb +1 -1
  17. data/lib/jirametrics/exporter.rb +10 -5
  18. data/lib/jirametrics/file_system.rb +24 -1
  19. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  20. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +1 -1
  21. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  22. data/lib/jirametrics/html/cycletime_histogram.erb +2 -2
  23. data/lib/jirametrics/html/index.css +5 -10
  24. data/lib/jirametrics/html/index.erb +4 -36
  25. data/lib/jirametrics/html/index.js +90 -0
  26. data/lib/jirametrics/html/sprint_burndown.erb +5 -3
  27. data/lib/jirametrics/html_report_config.rb +5 -3
  28. data/lib/jirametrics/issue.rb +32 -20
  29. data/lib/jirametrics/jira_gateway.rb +59 -17
  30. data/lib/jirametrics/project_config.rb +30 -3
  31. data/lib/jirametrics/settings.json +3 -1
  32. data/lib/jirametrics/status_collection.rb +1 -0
  33. data/lib/jirametrics.rb +19 -69
  34. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7565c5b6320b9c4375a3feb27fcaf80f96d9c058041b71584f287e6a3d7eb399
4
- data.tar.gz: 4554c7edbc7c42e01ad4838094ab922819c2e71818a46907f25740e35c327aa6
3
+ metadata.gz: cbe1101b082615d38939850c0adc688aedefe02a74537503bcd390cdf11d0d4e
4
+ data.tar.gz: eeffbda7c7ba8280273e0d749ede1ed3c1caa33d06a1f50a2c47b2331035d5af
5
5
  SHA512:
6
- metadata.gz: 021c1f989117b1fbb8708183d9ad8e79b049060989c1f20d31121a486d25f1ae2d1c036e013279479cd3ac6f5b101a48d8c9f4943570107c9cc4b40485ea6a4f
7
- data.tar.gz: 7f7e334a2161e23ef5ae8a754aa3f12524c1bb130893ca3df3e8574f7a44286ef33c8a03b2ac2790ccb8e8b792ce0fb6c17b1d69d2da068ea48dbce1b4c514a1
6
+ metadata.gz: b73533c90e457c2c5f7a7f2d1759ccd44d218ebe984052fcb581d8ab88225abf43b5612923217b4fa7467ed11e82ba5b4fe2cc656a324f269b71c2497bf67659
7
+ data.tar.gz: 57cbc54fe6c739d0c85f68cc0efcfbc6005975af0b40174ed9ee35790a83dac9b5e524601b770dffc6a3fefbd10f0d577d19b840560b3acfb63b0b542728d5fc
@@ -2,11 +2,12 @@
2
2
 
3
3
  require 'random-word'
4
4
 
5
- class Anonymizer
5
+ class Anonymizer < ChartBase
6
6
  # needed for testing
7
7
  attr_reader :project_config, :issues
8
8
 
9
9
  def initialize project_config:, date_adjustment: -200
10
+ super()
10
11
  @project_config = project_config
11
12
  @issues = @project_config.issues
12
13
  @all_boards = @project_config.all_boards
@@ -130,18 +131,19 @@ class Anonymizer
130
131
  end
131
132
  end
132
133
 
133
- def shift_all_dates
134
- @file_system.log "Shifting all dates by #{@date_adjustment} days"
134
+ def shift_all_dates date_adjustment: @date_adjustment
135
+ adjustment_in_seconds = 60 * 60 * 24 * date_adjustment
136
+ @file_system.log "Shifting all dates by #{label_days date_adjustment}"
135
137
  @issues.each do |issue|
136
138
  issue.changes.each do |change|
137
- change.time = change.time + @date_adjustment
139
+ change.time = change.time + adjustment_in_seconds
138
140
  end
139
141
 
140
- issue.raw['fields']['updated'] = (issue.updated + @date_adjustment).to_s
142
+ issue.raw['fields']['updated'] = (issue.updated + adjustment_in_seconds).to_s
141
143
  end
142
144
 
143
145
  range = @project_config.time_range
144
- @project_config.time_range = (range.begin + @date_adjustment)..(range.end + @date_adjustment)
146
+ @project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
145
147
  end
146
148
 
147
149
  def random_name
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AtlassianDocumentFormat
4
+ attr_reader :users
5
+
6
+ def initialize users:, timezone_offset:
7
+ @users = users
8
+ @timezone_offset = timezone_offset
9
+ end
10
+
11
+ def to_html input
12
+ if input.is_a? String
13
+ input
14
+ .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
15
+ .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
16
+ .gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
17
+ .gsub("\n", '<br />')
18
+ elsif input&.[]('content')
19
+ input['content'].collect { |element| adf_node_to_html element }.join("\n")
20
+ else
21
+ # We have an actual ADF document with no content.
22
+ ''
23
+ end
24
+ end
25
+
26
+ # ADF is Atlassian Document Format
27
+ # https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
28
+ def adf_node_to_html node # rubocop:disable Metrics/CyclomaticComplexity
29
+ closing_tag = nil
30
+ node_attrs = node['attrs']
31
+
32
+ result = +''
33
+ case node['type']
34
+ when 'blockquote'
35
+ result << '<blockquote>'
36
+ closing_tag = '</blockquote>'
37
+ when 'bulletList'
38
+ result << '<ul>'
39
+ closing_tag = '</ul>'
40
+ when 'codeBlock'
41
+ result << '<code>'
42
+ closing_tag = '</code>'
43
+ when 'date'
44
+ result << Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s
45
+ when 'decisionItem'
46
+ result << '<li>'
47
+ closing_tag = '</li>'
48
+ when 'decisionList'
49
+ result << '<div>Decisions<ul>'
50
+ closing_tag = '</ul></div>'
51
+ when 'emoji'
52
+ result << node_attrs['text']
53
+ when 'expand'
54
+ # TODO: Maybe, someday, make this actually expandable. For now it's always open
55
+ result << "<div>#{node_attrs['title']}</div>"
56
+ when 'hardBreak'
57
+ result << '<br />'
58
+ when 'heading'
59
+ level = node_attrs['level']
60
+ result << "<h#{level}>"
61
+ closing_tag = "</h#{level}>"
62
+ when 'inlineCard'
63
+ url = node_attrs['url']
64
+ result << "[Inline card]: <a href='#{url}'>#{url}</a>"
65
+ when 'listItem'
66
+ result << '<li>'
67
+ closing_tag = '</li>'
68
+ when 'media'
69
+ text = node_attrs['alt'] || node_attrs['id']
70
+ result << "Media: #{text}"
71
+ when 'mediaSingle', 'mediaGroup'
72
+ result << '<div>'
73
+ closing_tag = '</div>'
74
+ when 'mention'
75
+ user = node_attrs['text']
76
+ result << "<b>#{user}</b>"
77
+ when 'orderedList'
78
+ result << '<ol>'
79
+ closing_tag = '</ol>'
80
+ when 'panel'
81
+ type = node_attrs['panelType']
82
+ result << "<div>#{type.upcase}</div>"
83
+ when 'paragraph'
84
+ result << '<p>'
85
+ closing_tag = '</p>'
86
+ when 'rule'
87
+ result << '<hr />'
88
+ when 'status'
89
+ text = node_attrs['text']
90
+ result << text
91
+ when 'table'
92
+ result << '<table>'
93
+ closing_tag = '</table>'
94
+ when 'tableCell'
95
+ result << '<td>'
96
+ closing_tag = '</td>'
97
+ when 'tableHeader'
98
+ result << '<th>'
99
+ closing_tag = '</th>'
100
+ when 'tableRow'
101
+ result << '<tr>'
102
+ closing_tag = '</tr>'
103
+ when 'text'
104
+ marks = adf_marks_to_html node['marks']
105
+ result << marks.collect(&:first).join
106
+ result << node['text']
107
+ result << marks.collect(&:last).join
108
+ when 'taskItem'
109
+ state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
110
+ result << "<li>#{state} "
111
+ closing_tag = '</li>'
112
+ when 'taskList'
113
+ result << "<ul class='taskList'>"
114
+ closing_tag = '</ul>'
115
+ else
116
+ result << "<p>Unparseable section: #{node['type']}</p>"
117
+ end
118
+
119
+ node['content']&.each do |child|
120
+ result << adf_node_to_html(child)
121
+ end
122
+
123
+ result << closing_tag if closing_tag
124
+ result
125
+ end
126
+
127
+ def adf_marks_to_html list
128
+ return [] if list.nil?
129
+
130
+ mappings = [
131
+ ['strong', '<b>', '</b>'],
132
+ ['code', '<code>', '</code>'],
133
+ ['em', '<em>', '</em>'],
134
+ ['strike', '<s>', '</s>'],
135
+ ['underline', '<u>', '</u>']
136
+ ]
137
+
138
+ list.filter_map do |mark|
139
+ type = mark['type']
140
+ if type == 'textColor'
141
+ color = mark['attrs']['color']
142
+ ["<span style='color: #{color}'>", '</span>']
143
+ elsif type == 'link'
144
+ href = mark['attrs']['href']
145
+ title = mark['attrs']['title']
146
+ ["<a href='#{href}' title='#{title}'>", '</a>']
147
+ else
148
+ line = mappings.find { |key, _open, _close| key == type }
149
+ [line[1], line[2]] if line
150
+ end
151
+ end
152
+ end
153
+
154
+ def expand_account_id account_id
155
+ user = @users.find { |u| u.account_id == account_id }
156
+ text = account_id
157
+ text = "@#{user.display_name}" if user
158
+ "<span class='account_id'>#{text}</span>"
159
+ end
160
+ end
@@ -11,6 +11,7 @@ class BoardConfig
11
11
 
12
12
  def run
13
13
  @board = @project_config.all_boards[id]
14
+ raise "Can't find board #{id.inspect} in #{@project_config.all_boards.keys.inspect}" unless @board
14
15
 
15
16
  instance_eval(&@block)
16
17
  raise "Must specify a cycletime for board #{@id}" if @board.cycletime.nil?
@@ -23,7 +24,8 @@ class BoardConfig
23
24
  end
24
25
 
25
26
  @board.cycletime = CycleTimeConfig.new(
26
- parent_config: self, label: label, block: block, file_system: project_config.file_system
27
+ parent_config: self, label: label, block: block, file_system: project_config.file_system,
28
+ settings: project_config.settings
27
29
  )
28
30
  end
29
31
 
@@ -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
5
+ attr_accessor :value, :old_value, :time
6
6
 
7
7
  def initialize raw:, author_raw:, time:, artificial: false
8
8
  @raw = raw
@@ -30,6 +30,7 @@ class ChangeItem
30
30
  def artificial? = @artificial
31
31
  def assignee? = (field == 'assignee')
32
32
  def comment? = (field == 'comment')
33
+ def description? = (field == 'description')
33
34
  def due_date? = (field == 'duedate')
34
35
  def flagged? = (field == 'Flagged')
35
36
  def issue_type? = field == 'issuetype'
@@ -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
 
@@ -44,7 +45,7 @@ class ChartBase
44
45
 
45
46
  def render_top_text caller_binding
46
47
  result = +''
47
- result << "<h1>#{@header_text}</h1>" if @header_text
48
+ result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
48
49
  result << ERB.new(@description_text).result(caller_binding) if @description_text
49
50
  result
50
51
  end
@@ -66,6 +67,8 @@ class ChartBase
66
67
  end
67
68
 
68
69
  def label_days days
70
+ return 'unknown' if days.nil?
71
+
69
72
  "#{days} day#{'s' unless days == 1}"
70
73
  end
71
74
 
@@ -6,12 +6,15 @@ require 'date'
6
6
  class CycleTimeConfig
7
7
  include SelfOrIssueDispatcher
8
8
 
9
- attr_reader :label, :parent_config
9
+ attr_reader :label, :parent_config, :settings, :file_system
10
+
11
+ def initialize parent_config:, label:, block:, settings:, file_system: nil, today: Date.today
10
12
 
11
- def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
12
13
  @parent_config = parent_config
13
14
  @label = label
14
15
  @today = today
16
+ @settings = settings
17
+ @cache_cycletime_calculations = settings['cache_cycletime_calculations']
15
18
 
16
19
  # If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
17
20
  # may make it easier to find problems in the test code ;-)
@@ -63,6 +66,10 @@ class CycleTimeConfig
63
66
  end
64
67
 
65
68
  def started_stopped_changes issue
69
+ cache_key = "#{issue.key}:#{issue.board.id}"
70
+ last_result = (@cache ||= {})[cache_key]
71
+ return *last_result if last_result && @cache_cycletime_calculations
72
+
66
73
  started = @start_at.call(issue)
67
74
  stopped = @stop_at.call(issue)
68
75
 
@@ -80,7 +87,15 @@ class CycleTimeConfig
80
87
  # for the start and not have it conflict.
81
88
  started = nil if started&.time == stopped&.time
82
89
 
83
- [started, stopped]
90
+ result = [started, stopped]
91
+ if last_result && result != last_result
92
+ @file_system.error(
93
+ "Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
94
+ "previous=#{last_result.inspect}"
95
+ )
96
+ end
97
+ @cache[cache_key] = result
98
+ result
84
99
  end
85
100
 
86
101
  def started_stopped_times issue
@@ -88,6 +103,10 @@ class CycleTimeConfig
88
103
  [started&.time, stopped&.time]
89
104
  end
90
105
 
106
+ def flush_cache
107
+ @cache = nil
108
+ end
109
+
91
110
  def started_stopped_dates issue
92
111
  started_time, stopped_time = started_stopped_times(issue)
93
112
  [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
@@ -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|
@@ -410,14 +410,17 @@ class DataQualityReport < ChartBase
410
410
  def render_status_not_on_board problems
411
411
  <<-HTML
412
412
  #{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.
413
+ timings as the work was likely to have been forgotten if it wasn't visible. What does "not visible"
414
+ mean in this context? The issue was in a status that is not mapped to any visible column on the board.
415
+ Look in "unmapped statuses" on your board.
414
416
  HTML
415
417
  end
416
418
 
417
419
  def render_created_in_wrong_status problems
418
420
  <<-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.
421
+ #{label_issues problems.size} were created in a status that is not considered to be some varient
422
+ of To Do. Most likely this means that the issue was created from one of the columns on the board,
423
+ rather than in the backlog. Why Jira allows this is still a mystery.
421
424
  HTML
422
425
  end
423
426
 
@@ -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}"