jirametrics 2.22 → 2.27

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +10 -2
  3. data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
  4. data/lib/jirametrics/aging_work_table.rb +4 -5
  5. data/lib/jirametrics/anonymizer.rb +74 -1
  6. data/lib/jirametrics/atlassian_document_format.rb +93 -93
  7. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  8. data/lib/jirametrics/board.rb +20 -8
  9. data/lib/jirametrics/board_feature.rb +14 -0
  10. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  11. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  12. data/lib/jirametrics/change_item.rb +4 -3
  13. data/lib/jirametrics/chart_base.rb +94 -2
  14. data/lib/jirametrics/css_variable.rb +1 -1
  15. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  16. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
  17. data/lib/jirametrics/cycletime_histogram.rb +15 -103
  18. data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
  19. data/lib/jirametrics/daily_view.rb +36 -12
  20. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  21. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
  22. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  23. data/lib/jirametrics/daily_wip_chart.rb +29 -7
  24. data/lib/jirametrics/data_quality_report.rb +38 -12
  25. data/lib/jirametrics/dependency_chart.rb +2 -2
  26. data/lib/jirametrics/download_config.rb +15 -0
  27. data/lib/jirametrics/downloader.rb +87 -5
  28. data/lib/jirametrics/downloader_for_cloud.rb +52 -10
  29. data/lib/jirametrics/downloader_for_data_center.rb +2 -1
  30. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  31. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  32. data/lib/jirametrics/examples/standard_project.rb +29 -19
  33. data/lib/jirametrics/expedited_chart.rb +3 -1
  34. data/lib/jirametrics/exporter.rb +3 -1
  35. data/lib/jirametrics/file_system.rb +35 -2
  36. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  37. data/lib/jirametrics/github_gateway.rb +115 -0
  38. data/lib/jirametrics/groupable_issue_chart.rb +4 -0
  39. data/lib/jirametrics/grouping_rules.rb +26 -4
  40. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
  41. data/lib/jirametrics/html/aging_work_table.erb +3 -0
  42. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
  44. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
  45. data/lib/jirametrics/html/expedited_chart.erb +3 -13
  46. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
  47. data/lib/jirametrics/html/index.css +117 -0
  48. data/lib/jirametrics/html/index.erb +6 -0
  49. data/lib/jirametrics/html/index.js +52 -2
  50. data/lib/jirametrics/html/sprint_burndown.erb +7 -13
  51. data/lib/jirametrics/html/throughput_chart.erb +40 -9
  52. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
  53. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
  54. data/lib/jirametrics/html_generator.rb +2 -1
  55. data/lib/jirametrics/html_report_config.rb +23 -16
  56. data/lib/jirametrics/issue.rb +101 -96
  57. data/lib/jirametrics/issue_printer.rb +97 -0
  58. data/lib/jirametrics/jira_gateway.rb +6 -3
  59. data/lib/jirametrics/mcp_server.rb +305 -0
  60. data/lib/jirametrics/project_config.rb +80 -7
  61. data/lib/jirametrics/pull_request.rb +30 -0
  62. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  63. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  64. data/lib/jirametrics/pull_request_review.rb +13 -0
  65. data/lib/jirametrics/raw_javascript.rb +4 -0
  66. data/lib/jirametrics/settings.json +3 -1
  67. data/lib/jirametrics/sprint_burndown.rb +3 -1
  68. data/lib/jirametrics/status.rb +1 -1
  69. data/lib/jirametrics/stitcher.rb +7 -1
  70. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  71. data/lib/jirametrics/throughput_chart.rb +73 -23
  72. data/lib/jirametrics/time_based_histogram.rb +139 -0
  73. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  74. data/lib/jirametrics.rb +28 -0
  75. metadata +47 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40a0ee85ee8d7d0d2ff071357afdea2aefdfeea8734f96eb789721d4a9f2607b
4
- data.tar.gz: 11008f97848d8e3034cf95c5f615496c5677d8057f40b06d2724247d6087318d
3
+ metadata.gz: c87212eebf4e22145b8e66741174658bffc3cb1e47bd45bfe3f7590586a42200
4
+ data.tar.gz: 00fb6c4375e7e6858359fa754af42c697028198dedd2ae8100c98d0564f59db8
5
5
  SHA512:
6
- metadata.gz: 1e5ad6c1d5dddf5a89cc63498f1967f35c4761b6418c90019c8c6756599efb5b8badaf0ac4d50f94be4644508a97641430f178b3d40217cb02560aa017f33b80
7
- data.tar.gz: a6a7f74dadbb8a2f7961a02e396a39dd994ff1a11756f57a5eadfd60af5c71bd79c3df65b7bfd77ca998059018f60cd6306b2006512b7cf5f760d75e2d0dfdf4
6
+ metadata.gz: aad3747a78dcf530df02244720c676632816a30688045d95c9a701ba946a5573f81a2e3955c5035e745fb197f5c6cff58491a63d35db68b3abf35817c78dc0b8
7
+ data.tar.gz: fcf17a8c2a4c91e4de45ff15150d1de9147c890d9e80354b490bec2b27fb17ef2b20e7bc6073c503e88fd0ad188c414002d8312500b22dd5207b043d3c253be7
@@ -65,8 +65,16 @@ class AggregateConfig
65
65
 
66
66
  if issues.nil?
67
67
  file_system.warning "No issues found for #{project_name}"
68
- else
69
- @project_config.add_issues issues
68
+ return
69
+ end
70
+
71
+ @project_config.add_issues issues
72
+
73
+ # Bring fix versions over
74
+ project.fix_versions.each do |fix_version|
75
+ unless @project_config.fix_versions.find { |fv| fv.id == fix_version.id }
76
+ @project_config.fix_versions << fix_version
77
+ end
70
78
  end
71
79
  end
72
80
 
@@ -15,7 +15,7 @@ class AgingWorkBarChart < ChartBase
15
15
  newest at the bottom.
16
16
  </p>
17
17
  <p>
18
- There are three bars for each issue, and hovering over any of the bars will provide more details.
18
+ There are <%= (current_board.scrum? || aggregated_project?) ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
19
19
  <ol>
20
20
  <li>Status: The status the issue was in at any time. The colour indicates the
21
21
  status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
@@ -25,6 +25,9 @@ class AgingWorkBarChart < ChartBase
25
25
  or #{color_block '--stalled-color'} stalled.</li>
26
26
  <li>Priority: This shows the priority over time. If one of these priorities is considered expedited
27
27
  then it will be drawn with diagonal lines.</li>
28
+ <% if current_board.scrum? || aggregated_project? %>
29
+ <li>Sprints: The sprints that the issue was in.</li>
30
+ <% end %>
28
31
  </ol>
29
32
  </p>
30
33
  #{describe_non_working_days}
@@ -63,7 +66,7 @@ class AgingWorkBarChart < ChartBase
63
66
 
64
67
  def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
65
68
  earliest_start_time = aging_issues.collect do |issue|
66
- issue.board.cycletime.started_stopped_times(issue).first
69
+ issue.started_stopped_times.first
67
70
  end.min
68
71
  return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
69
72
 
@@ -81,7 +84,9 @@ class AgingWorkBarChart < ChartBase
81
84
  ['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
82
85
  ['priority', collect_priority_ranges(issue: issue)]
83
86
  ]
84
- bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
87
+ bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum? || aggregated_project?
88
+
89
+ bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
85
90
 
86
91
  issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
87
92
  bar_data.collect do |stack, ranges|
@@ -97,7 +102,7 @@ class AgingWorkBarChart < ChartBase
97
102
 
98
103
  def select_aging_issues issues:
99
104
  issues.select do |issue|
100
- started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
105
+ started_time, stopped_time = issue.started_stopped_times
101
106
  next false unless started_time && stopped_time.nil?
102
107
 
103
108
  age = (date_range.end - started_time.to_date).to_i + 1
@@ -108,15 +113,22 @@ class AgingWorkBarChart < ChartBase
108
113
  def grow_chart_height_if_too_many_issues aging_issue_count:
109
114
  px_per_bar = 10
110
115
  bars_per_issue = 3
111
- bars_per_issue += 1 if current_board.scrum?
116
+ bars_per_issue += 1 if current_board.scrum? || aggregated_project?
112
117
 
113
118
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
114
119
  @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
115
120
  end
116
121
 
122
+ def clip_ranges_to_start_time ranges:, issue_start_time:
123
+ return if issue_start_time.nil?
124
+
125
+ ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
126
+ ranges.reject! { |range| range.start >= range.stop }
127
+ end
128
+
117
129
  def collect_status_ranges issue:, now:
118
130
  ranges = []
119
- issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
131
+ issue_started_time = issue.started_stopped_times.first
120
132
  previous_start = nil
121
133
  previous_status = nil
122
134
  issue.status_changes.each do |change|
@@ -263,6 +275,8 @@ class AgingWorkBarChart < ChartBase
263
275
  end
264
276
 
265
277
  open_sprints.each_value do |data|
278
+ next if data[:sprint].future?
279
+
266
280
  stop = data[:sprint].completed_time || time_range.end
267
281
  results << BarChartRange.new(
268
282
  start: data[:start_time], stop: stop,
@@ -50,15 +50,14 @@ class AgingWorkTable < ChartBase
50
50
 
51
51
  def expedited_but_not_started
52
52
  @issues.select do |issue|
53
- started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
53
+ started_time, stopped_time = issue.started_stopped_times
54
54
  started_time.nil? && stopped_time.nil? && issue.expedited?
55
55
  end.sort_by(&:created)
56
56
  end
57
57
 
58
58
  def select_aging_issues
59
59
  aging_issues = @issues.select do |issue|
60
- cycletime = issue.board.cycletime
61
- started, stopped = cycletime.started_stopped_times(issue)
60
+ started, stopped = issue.started_stopped_times
62
61
  next false if started.nil? || stopped
63
62
  next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
64
63
 
@@ -77,7 +76,7 @@ class AgingWorkTable < ChartBase
77
76
  end
78
77
 
79
78
  def blocked_text issue
80
- started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
79
+ started_time, _stopped_time = issue.started_stopped_times
81
80
  return nil if started_time.nil?
82
81
 
83
82
  current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
@@ -174,6 +173,6 @@ class AgingWorkTable < ChartBase
174
173
  end
175
174
 
176
175
  def priority_text issue
177
- "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
176
+ "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
178
177
  end
179
178
  end
@@ -21,6 +21,10 @@ class Anonymizer < ChartBase
21
21
  anonymize_column_names
22
22
  # anonymize_issue_statuses
23
23
  anonymize_board_names
24
+ anonymize_labels_and_components
25
+ anonymize_sprints
26
+ anonymize_fix_versions
27
+ anonymize_server_url
24
28
  shift_all_dates unless @date_adjustment.zero?
25
29
  @file_system.log 'Anonymize done'
26
30
  end
@@ -38,13 +42,25 @@ class Anonymizer < ChartBase
38
42
 
39
43
  def anonymize_issue_keys_and_titles issues: @issues
40
44
  counter = 0
45
+ seen_author_raws = {}
41
46
  issues.each do |issue|
42
47
  new_key = "ANON-#{counter += 1}"
43
48
 
44
49
  issue.raw['key'] = new_key
45
50
  issue.raw['fields']['summary'] = random_phrase
51
+ issue.raw['fields']['description'] = nil
46
52
  issue.raw['fields']['assignee']['displayName'] = random_name unless issue.raw['fields']['assignee'].nil?
47
53
 
54
+ anonymize_author_raw(issue.raw['fields']['creator'], seen_author_raws)
55
+
56
+ issue.changes.each do |change|
57
+ anonymize_author_raw(change.author_raw, seen_author_raws)
58
+ if change.comment? || change.description?
59
+ change.value = nil
60
+ change.old_value = nil
61
+ end
62
+ end
63
+
48
64
  issue.issue_links.each do |link|
49
65
  other_issue = link.other_issue
50
66
  next if other_issue.key.match?(/^ANON-\d+$/) # Already anonymized?
@@ -55,6 +71,49 @@ class Anonymizer < ChartBase
55
71
  end
56
72
  end
57
73
 
74
+ def anonymize_labels_and_components
75
+ @issues.each do |issue|
76
+ issue.raw['fields']['labels'] = []
77
+ issue.raw['fields']['components'] = []
78
+ end
79
+ end
80
+
81
+ def anonymize_sprints
82
+ sprint_counter = 0
83
+ sprint_name_map = {}
84
+ @all_boards.each_value do |board|
85
+ board.sprints.each do |sprint|
86
+ name = sprint.raw['name']
87
+ unless sprint_name_map[name]
88
+ sprint_counter += 1
89
+ sprint_name_map[name] = "Sprint-#{sprint_counter}"
90
+ end
91
+ sprint.raw['name'] = sprint_name_map[name]
92
+ end
93
+ end
94
+ end
95
+
96
+ def anonymize_fix_versions
97
+ version_counter = 0
98
+ version_name_map = {}
99
+ @issues.each do |issue|
100
+ issue.raw['fields']['fixVersions']&.each do |fix_version|
101
+ name = fix_version['name']
102
+ unless version_name_map[name]
103
+ version_counter += 1
104
+ version_name_map[name] = "Version-#{version_counter}"
105
+ end
106
+ fix_version['name'] = version_name_map[name]
107
+ end
108
+ end
109
+ end
110
+
111
+ def anonymize_server_url
112
+ @all_boards.each_value do |board|
113
+ board.raw['self'] = board.raw['self']&.sub(/^https?:\/\/[^\/]+/, 'https://anon.example.com')
114
+ end
115
+ end
116
+
58
117
  def anonymize_column_names
59
118
  @all_boards.each_key do |board_id|
60
119
  @file_system.log "Anonymizing column names for board #{board_id}"
@@ -143,7 +202,7 @@ class Anonymizer < ChartBase
143
202
  end
144
203
 
145
204
  range = @project_config.time_range
146
- @project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
205
+ @project_config.time_range = (range.begin + adjustment_in_seconds)..(range.end + adjustment_in_seconds)
147
206
  end
148
207
 
149
208
  def random_name
@@ -186,4 +245,18 @@ class Anonymizer < ChartBase
186
245
  board.raw['name'] = "#{random_phrase} board"
187
246
  end
188
247
  end
248
+
249
+ private
250
+
251
+ def anonymize_author_raw author_raw, seen
252
+ return unless author_raw
253
+ return if seen[author_raw.object_id]
254
+
255
+ seen[author_raw.object_id] = true
256
+ name = random_name
257
+ author_raw['displayName'] = name
258
+ author_raw['name'] = name
259
+ author_raw.delete('emailAddress')
260
+ author_raw.delete('avatarUrls')
261
+ end
189
262
  end
@@ -23,105 +23,95 @@ class AtlassianDocumentFormat
23
23
  end
24
24
  end
25
25
 
26
+ def to_text input
27
+ if input.is_a? String
28
+ input
29
+ elsif input&.[]('content')
30
+ input['content'].collect { |element| adf_node_to_text element }.join
31
+ else
32
+ ''
33
+ end
34
+ end
35
+
26
36
  # ADF is Atlassian Document Format
27
37
  # https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
28
38
  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>"
39
+ adf_node_render(node) do |n|
40
+ node_attrs = n['attrs']
41
+ case n['type']
42
+ when 'blockquote' then ['<blockquote>', '</blockquote>']
43
+ when 'bulletList' then ['<ul>', '</ul>']
44
+ when 'codeBlock' then ['<code>', '</code>']
45
+ when 'date'
46
+ [Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s, nil]
47
+ when 'decisionItem', 'listItem' then ['<li>', '</li>']
48
+ when 'decisionList' then ['<div>Decisions<ul>', '</ul></div>']
49
+ when 'emoji', 'status' then [node_attrs['text'], nil]
50
+ when 'expand' then ["<div>#{node_attrs['title']}</div>", nil]
51
+ when 'hardBreak' then ['<br />', nil]
52
+ when 'heading'
53
+ level = node_attrs['level']
54
+ ["<h#{level}>", "</h#{level}>"]
55
+ when 'inlineCard'
56
+ url = node_attrs['url']
57
+ ["[Inline card]: <a href='#{url}'>#{url}</a>", nil]
58
+ when 'media'
59
+ text = node_attrs['alt'] || node_attrs['id']
60
+ ["Media: #{text}", nil]
61
+ when 'mediaSingle', 'mediaGroup' then ['<div>', '</div>']
62
+ when 'mention' then ["<b>#{node_attrs['text']}</b>", nil]
63
+ when 'orderedList' then ['<ol>', '</ol>']
64
+ when 'panel' then ["<div>#{node_attrs['panelType'].upcase}</div>", nil]
65
+ when 'paragraph' then ['<p>', '</p>']
66
+ when 'rule' then ['<hr />', nil]
67
+ when 'table' then ['<table>', '</table>']
68
+ when 'tableCell' then ['<td>', '</td>']
69
+ when 'tableHeader' then ['<th>', '</th>']
70
+ when 'tableRow' then ['<tr>', '</tr>']
71
+ when 'text'
72
+ marks = adf_marks_to_html(n['marks'])
73
+ [marks.collect(&:first).join + n['text'], marks.collect(&:last).join]
74
+ when 'taskItem'
75
+ state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
76
+ ["<li>#{state} ", '</li>']
77
+ when 'taskList' then ["<ul class='taskList'>", '</ul>']
78
+ else
79
+ ["<p>Unparseable section: #{n['type']}</p>", nil]
80
+ end
117
81
  end
82
+ end
118
83
 
119
- node['content']&.each do |child|
120
- result << adf_node_to_html(child)
84
+ def adf_node_to_text node # rubocop:disable Metrics/CyclomaticComplexity
85
+ adf_node_render(node) do |n|
86
+ node_attrs = n['attrs']
87
+ case n['type']
88
+ when 'blockquote', 'bulletList', 'codeBlock',
89
+ 'mediaSingle', 'mediaGroup',
90
+ 'orderedList', 'table', 'taskList' then ['', nil]
91
+ when 'date'
92
+ [Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s, nil]
93
+ when 'decisionItem' then ['- ', "\n"]
94
+ when 'decisionList' then ["Decisions:\n", nil]
95
+ when 'emoji', 'mention', 'status' then [node_attrs['text'], nil]
96
+ when 'expand' then ["#{node_attrs['title']}\n", nil]
97
+ when 'hardBreak' then ["\n", nil]
98
+ when 'heading', 'paragraph', 'tableRow' then ['', "\n"]
99
+ when 'inlineCard' then [node_attrs['url'], nil]
100
+ when 'listItem' then ['- ', nil]
101
+ when 'media'
102
+ text = node_attrs['alt'] || node_attrs['id']
103
+ ["Media: #{text}", nil]
104
+ when 'panel' then ["#{node_attrs['panelType'].upcase}\n", nil]
105
+ when 'rule' then ["---\n", nil]
106
+ when 'tableCell', 'tableHeader' then ['', "\t"]
107
+ when 'text' then [n['text'], nil]
108
+ when 'taskItem'
109
+ state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
110
+ ["#{state} ", "\n"]
111
+ else
112
+ ["[Unparseable: #{n['type']}]\n", nil]
113
+ end
121
114
  end
122
-
123
- result << closing_tag if closing_tag
124
- result
125
115
  end
126
116
 
127
117
  def adf_marks_to_html list
@@ -157,4 +147,14 @@ class AtlassianDocumentFormat
157
147
  text = "@#{user.display_name}" if user
158
148
  "<span class='account_id'>#{text}</span>"
159
149
  end
150
+
151
+ private
152
+
153
+ def adf_node_render node, &render_node
154
+ prefix, suffix = render_node.call(node)
155
+ result = +(prefix || '')
156
+ node['content']&.each { |child| result << adf_node_render(child, &render_node) }
157
+ result << suffix if suffix
158
+ result
159
+ end
160
160
  end
@@ -4,10 +4,12 @@ require 'jirametrics/value_equality'
4
4
 
5
5
  class BlockedStalledChange
6
6
  include ValueEquality
7
- attr_reader :time, :blocking_issue_keys, :flag, :status, :stalled_days, :status_is_blocking
7
+ attr_reader :time, :blocking_issue_keys, :flag, :flag_reason, :status, :stalled_days, :status_is_blocking
8
8
 
9
- def initialize time:, flagged: nil, status: nil, status_is_blocking: true, blocking_issue_keys: nil, stalled_days: nil
9
+ def initialize time:, flagged: nil, flag_reason: nil, status: nil, status_is_blocking: true,
10
+ blocking_issue_keys: nil, stalled_days: nil
10
11
  @flag = flagged
12
+ @flag_reason = flag_reason
11
13
  @status = status
12
14
  @status_is_blocking = status_is_blocking
13
15
  @blocking_issue_keys = blocking_issue_keys
@@ -25,7 +27,7 @@ class BlockedStalledChange
25
27
  def reasons
26
28
  result = []
27
29
  if blocked?
28
- result << 'Blocked by flag' if @flag
30
+ result << (@flag_reason ? "Blocked by flag: #{@flag_reason}" : 'Blocked by flag') if @flag
29
31
  result << "Blocked by status: #{@status}" if blocked_by_status?
30
32
  result << "Blocked by issues: #{@blocking_issue_keys.join(', ')}" if @blocking_issue_keys
31
33
  elsif stalled_by_status?
@@ -4,18 +4,18 @@ class Board
4
4
  attr_reader :visible_columns, :raw, :possible_statuses, :sprints
5
5
  attr_accessor :cycletime, :project_config
6
6
 
7
- def initialize raw:, possible_statuses:
7
+ def initialize raw:, possible_statuses:, features: []
8
8
  @raw = raw
9
9
  @possible_statuses = possible_statuses
10
10
  @sprints = []
11
+ @features = features
11
12
 
12
13
  columns = raw['columnConfig']['columns']
13
14
  ensure_uniqueness_of_column_names! columns
14
15
 
15
- # For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
16
- # visible on the board. If the board is configured to have a kanban backlog then it will have
17
- # statuses matched to it and otherwise, there will be no statuses.
18
- columns = columns.drop(1) if kanban?
16
+ # For a classic Kanban board (type 'kanban'), the first column will always be called 'Backlog'
17
+ # and will NOT be visible on the board. This does not apply to team-managed boards (type 'simple').
18
+ columns = columns.drop(1) if board_type == 'kanban'
19
19
 
20
20
  @backlog_statuses = []
21
21
  @visible_columns = columns.filter_map do |column|
@@ -25,7 +25,7 @@ class Board
25
25
  end
26
26
 
27
27
  def backlog_statuses
28
- if @backlog_statuses.empty? && kanban?
28
+ if @backlog_statuses.empty? && board_type == 'kanban'
29
29
  status_ids = status_ids_from_column raw['columnConfig']['columns'].first
30
30
  @backlog_statuses = status_ids.filter_map do |id|
31
31
  @possible_statuses.find_by_id id
@@ -67,8 +67,20 @@ class Board
67
67
  end
68
68
 
69
69
  def board_type = raw['type']
70
- def kanban? = (board_type == 'kanban')
71
- def scrum? = (board_type == 'scrum')
70
+
71
+ def scrum?
72
+ return true if board_type == 'scrum'
73
+ return false unless board_type == 'simple'
74
+
75
+ @features.any? { |f| f.name == 'jsw.agility.sprints' && f.enabled? }
76
+ end
77
+
78
+ def kanban?
79
+ return true if board_type == 'kanban'
80
+ return false unless board_type == 'simple'
81
+
82
+ !scrum?
83
+ end
72
84
 
73
85
  def id
74
86
  @raw['id'].to_i
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BoardFeature
4
+ def initialize raw:
5
+ @raw = raw
6
+ end
7
+
8
+ def name = @raw['feature']
9
+ def enabled? = (@raw['state'] == 'ENABLED')
10
+
11
+ def self.from_raw features_json
12
+ features_json['features']&.map { |f| new(raw: f) } || []
13
+ end
14
+ end
@@ -10,7 +10,7 @@ class BoardMovementCalculator
10
10
  end
11
11
 
12
12
  def moves_backwards? issue
13
- started, stopped = issue.board.cycletime.started_stopped_times(issue)
13
+ started, stopped = issue.started_stopped_times
14
14
  return false unless started
15
15
 
16
16
  previous_column = nil
@@ -70,7 +70,7 @@ class BoardMovementCalculator
70
70
  @issues.filter_map do |issue|
71
71
  this_column_start = issue.first_time_in_or_right_of_column(this_column.name)&.time
72
72
  next_column_start = next_column.nil? ? nil : issue.first_time_in_or_right_of_column(next_column.name)&.time
73
- issue_start, issue_done = issue.board.cycletime.started_stopped_times(issue)
73
+ issue_start, issue_done = issue.started_stopped_times
74
74
 
75
75
  # Skip if we can't tell when it started.
76
76
  next if issue_start.nil?