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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
- data/lib/jirametrics/aging_work_table.rb +4 -5
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +93 -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/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +94 -2
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- 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 +13 -98
- data/lib/jirametrics/daily_view.rb +36 -12
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +29 -7
- data/lib/jirametrics/data_quality_report.rb +38 -12
- data/lib/jirametrics/dependency_chart.rb +2 -2
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +87 -5
- data/lib/jirametrics/downloader_for_cloud.rb +52 -10
- 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 +2 -2
- data/lib/jirametrics/examples/standard_project.rb +29 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +35 -2
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +4 -0
- data/lib/jirametrics/grouping_rules.rb +26 -4
- 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/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
- 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 +117 -0
- data/lib/jirametrics/html/index.erb +6 -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 +40 -9
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
- data/lib/jirametrics/html_generator.rb +2 -1
- data/lib/jirametrics/html_report_config.rb +23 -16
- data/lib/jirametrics/issue.rb +101 -96
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/mcp_server.rb +305 -0
- data/lib/jirametrics/project_config.rb +80 -7
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -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 +3 -1
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +7 -1
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics.rb +28 -0
- metadata +47 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c87212eebf4e22145b8e66741174658bffc3cb1e47bd45bfe3f7590586a42200
|
|
4
|
+
data.tar.gz: 00fb6c4375e7e6858359fa754af42c697028198dedd2ae8100c98d0564f59db8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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? || 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 +
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
120
|
-
|
|
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,
|
|
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
|
|
@@ -10,7 +10,7 @@ class BoardMovementCalculator
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def moves_backwards? issue
|
|
13
|
-
started, stopped = 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.
|
|
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?
|