jirametrics 2.22 → 2.24
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 +4 -4
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +15 -1
- data/lib/jirametrics/aging_work_table.rb +1 -1
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +104 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +20 -8
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +87 -1
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +8 -97
- data/lib/jirametrics/daily_view.rb +32 -9
- data/lib/jirametrics/daily_wip_chart.rb +27 -7
- data/lib/jirametrics/data_quality_report.rb +31 -7
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +76 -5
- data/lib/jirametrics/downloader_for_cloud.rb +39 -0
- data/lib/jirametrics/downloader_for_data_center.rb +2 -1
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +1 -1
- data/lib/jirametrics/examples/standard_project.rb +20 -9
- data/lib/jirametrics/expedited_chart.rb +2 -0
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +4 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
- data/lib/jirametrics/github_gateway.rb +106 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -0
- data/lib/jirametrics/grouping_rules.rb +21 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +5 -4
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/index.css +114 -0
- data/lib/jirametrics/html/index.erb +5 -0
- data/lib/jirametrics/html/index.js +52 -2
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +5 -8
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +57 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +3 -4
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +84 -95
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/project_config.rb +66 -6
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +2 -0
- data/lib/jirametrics/stitcher.rb +2 -1
- data/lib/jirametrics/throughput_chart.rb +7 -1
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +100 -0
- metadata +12 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ab33e299bd374f28754ecfbd750f6afd89861ecf64a323f28d4678d7a22959d
|
|
4
|
+
data.tar.gz: a0bf98e4d51b86eaa36aef8c6bfb69e8cdba958d3afeaf67da71a37ff6eb3ae5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c017b109ce143403190db03124148def9be7a92fe20961f6f7c42f459482a4056018601f131d6ca6275889e7231e54df54befbccb4279de0073838d50280ff4
|
|
7
|
+
data.tar.gz: 07b783818d028d3d95fd1cd861f272fe624c46fe3d4b671f3e2e6fede8fcdc694e5118da888c7c12113838e37711ccd6935a54261d4edf5ceaac4e29c6a08a73
|
|
@@ -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
|
-
|
|
69
|
-
|
|
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? ? '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? %>
|
|
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}
|
|
@@ -83,6 +86,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
83
86
|
]
|
|
84
87
|
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
85
88
|
|
|
89
|
+
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
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|
|
|
88
93
|
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
@@ -114,6 +119,13 @@ class AgingWorkBarChart < ChartBase
|
|
|
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
131
|
issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
|
|
@@ -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,
|
|
@@ -174,6 +174,6 @@ class AgingWorkTable < ChartBase
|
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def priority_text issue
|
|
177
|
-
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
|
|
177
|
+
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
|
|
178
178
|
end
|
|
179
179
|
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 +
|
|
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,106 @@ 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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' then ['<li>', '</li>']
|
|
48
|
+
when 'decisionList' then ['<div>Decisions<ul>', '</ul></div>']
|
|
49
|
+
when 'emoji' 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 'listItem' then ['<li>', '</li>']
|
|
59
|
+
when 'media'
|
|
60
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
61
|
+
["Media: #{text}", nil]
|
|
62
|
+
when 'mediaSingle', 'mediaGroup' then ['<div>', '</div>']
|
|
63
|
+
when 'mention' then ["<b>#{node_attrs['text']}</b>", nil]
|
|
64
|
+
when 'orderedList' then ['<ol>', '</ol>']
|
|
65
|
+
when 'panel' then ["<div>#{node_attrs['panelType'].upcase}</div>", nil]
|
|
66
|
+
when 'paragraph' then ['<p>', '</p>']
|
|
67
|
+
when 'rule' then ['<hr />', nil]
|
|
68
|
+
when 'status' then [node_attrs['text'], nil]
|
|
69
|
+
when 'table' then ['<table>', '</table>']
|
|
70
|
+
when 'tableCell' then ['<td>', '</td>']
|
|
71
|
+
when 'tableHeader' then ['<th>', '</th>']
|
|
72
|
+
when 'tableRow' then ['<tr>', '</tr>']
|
|
73
|
+
when 'text'
|
|
74
|
+
marks = adf_marks_to_html(n['marks'])
|
|
75
|
+
[marks.collect(&:first).join + n['text'], marks.collect(&:last).join]
|
|
76
|
+
when 'taskItem'
|
|
77
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
78
|
+
["<li>#{state} ", '</li>']
|
|
79
|
+
when 'taskList' then ["<ul class='taskList'>", '</ul>']
|
|
80
|
+
else
|
|
81
|
+
["<p>Unparseable section: #{n['type']}</p>", nil]
|
|
82
|
+
end
|
|
117
83
|
end
|
|
84
|
+
end
|
|
118
85
|
|
|
119
|
-
|
|
120
|
-
|
|
86
|
+
def adf_node_to_text node # rubocop:disable Metrics/CyclomaticComplexity
|
|
87
|
+
adf_node_render(node) do |n|
|
|
88
|
+
node_attrs = n['attrs']
|
|
89
|
+
case n['type']
|
|
90
|
+
when 'blockquote' then ['', nil]
|
|
91
|
+
when 'bulletList' then ['', nil]
|
|
92
|
+
when 'codeBlock' then ['', nil]
|
|
93
|
+
when 'date'
|
|
94
|
+
[Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s, nil]
|
|
95
|
+
when 'decisionItem' then ['- ', "\n"]
|
|
96
|
+
when 'decisionList' then ["Decisions:\n", nil]
|
|
97
|
+
when 'emoji' then [node_attrs['text'], nil]
|
|
98
|
+
when 'expand' then ["#{node_attrs['title']}\n", nil]
|
|
99
|
+
when 'hardBreak' then ["\n", nil]
|
|
100
|
+
when 'heading' then ['', "\n"]
|
|
101
|
+
when 'inlineCard' then [node_attrs['url'], nil]
|
|
102
|
+
when 'listItem' then ['- ', nil]
|
|
103
|
+
when 'media'
|
|
104
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
105
|
+
["Media: #{text}", nil]
|
|
106
|
+
when 'mediaSingle', 'mediaGroup' then ['', nil]
|
|
107
|
+
when 'mention' then [node_attrs['text'], nil]
|
|
108
|
+
when 'orderedList' then ['', nil]
|
|
109
|
+
when 'panel' then ["#{node_attrs['panelType'].upcase}\n", nil]
|
|
110
|
+
when 'paragraph' then ['', "\n"]
|
|
111
|
+
when 'rule' then ["---\n", nil]
|
|
112
|
+
when 'status' then [node_attrs['text'], nil]
|
|
113
|
+
when 'table' then ['', nil]
|
|
114
|
+
when 'tableCell' then ['', "\t"]
|
|
115
|
+
when 'tableHeader' then ['', "\t"]
|
|
116
|
+
when 'tableRow' then ['', "\n"]
|
|
117
|
+
when 'text' then [n['text'], nil]
|
|
118
|
+
when 'taskItem'
|
|
119
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
120
|
+
["#{state} ", "\n"]
|
|
121
|
+
when 'taskList' then ['', nil]
|
|
122
|
+
else
|
|
123
|
+
["[Unparseable: #{n['type']}]\n", nil]
|
|
124
|
+
end
|
|
121
125
|
end
|
|
122
|
-
|
|
123
|
-
result << closing_tag if closing_tag
|
|
124
|
-
result
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def adf_marks_to_html list
|
|
@@ -157,4 +158,14 @@ class AtlassianDocumentFormat
|
|
|
157
158
|
text = "@#{user.display_name}" if user
|
|
158
159
|
"<span class='account_id'>#{text}</span>"
|
|
159
160
|
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def adf_node_render node, &render_node
|
|
165
|
+
prefix, suffix = render_node.call(node)
|
|
166
|
+
result = +(prefix || '')
|
|
167
|
+
node['content']&.each { |child| result << adf_node_render(child, &render_node) }
|
|
168
|
+
result << suffix if suffix
|
|
169
|
+
result
|
|
170
|
+
end
|
|
160
171
|
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,
|
|
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?
|
data/lib/jirametrics/board.rb
CHANGED
|
@@ -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
|
|
16
|
-
# visible on the board.
|
|
17
|
-
|
|
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
|
-
|
|
71
|
-
def 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
|
|
@@ -18,7 +18,7 @@ class ChangeItem
|
|
|
18
18
|
@value_id = @raw['to'].split(', ').collect(&:to_i)
|
|
19
19
|
@old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
|
|
20
20
|
else
|
|
21
|
-
@value_id = @raw['to']
|
|
21
|
+
@value_id = @raw['to']&.to_i
|
|
22
22
|
@old_value_id = @raw['from']&.to_i
|
|
23
23
|
end
|
|
24
24
|
@field_id = @raw['fieldId']
|
|
@@ -46,6 +46,7 @@ class ChangeItem
|
|
|
46
46
|
def resolution? = (field == 'resolution')
|
|
47
47
|
def sprint? = (field == 'Sprint')
|
|
48
48
|
def status? = (field == 'status')
|
|
49
|
+
def fix_version? = (field == 'Fix Version')
|
|
49
50
|
|
|
50
51
|
# An alias for time so that logic accepting a Time, Date, or ChangeItem can all respond to :to_time
|
|
51
52
|
def to_time = @time
|
|
@@ -54,10 +55,10 @@ class ChangeItem
|
|
|
54
55
|
message = +''
|
|
55
56
|
message << "ChangeItem(field: #{field.inspect}"
|
|
56
57
|
message << ", value: #{value.inspect}"
|
|
57
|
-
message << ':' << value_id.inspect if
|
|
58
|
+
message << ':' << value_id.inspect if value_id
|
|
58
59
|
if old_value
|
|
59
60
|
message << ", old_value: #{old_value.inspect}"
|
|
60
|
-
message << ':' << old_value_id.inspect if
|
|
61
|
+
message << ':' << old_value_id.inspect if old_value_id
|
|
61
62
|
end
|
|
62
63
|
message << ", time: #{time_to_s(@time).inspect}"
|
|
63
64
|
message << ", field_id: #{@field_id.inspect}" if @field_id
|