jirametrics 2.20.1 → 2.23
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 +185 -132
- 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/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +21 -3
- data/lib/jirametrics/board_config.rb +2 -1
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/chart_base.rb +111 -1
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +4 -6
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +15 -85
- data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
- data/lib/jirametrics/daily_wip_chart.rb +28 -8
- data/lib/jirametrics/data_quality_report.rb +2 -0
- 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/standard_project.rb +15 -5
- data/lib/jirametrics/expedited_chart.rb +2 -0
- data/lib/jirametrics/exporter.rb +7 -3
- data/lib/jirametrics/file_system.rb +4 -0
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
- data/lib/jirametrics/github_gateway.rb +99 -0
- data/lib/jirametrics/groupable_issue_chart.rb +9 -1
- data/lib/jirametrics/grouping_rules.rb +1 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +2 -0
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +7 -4
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
- data/lib/jirametrics/html/expedited_chart.erb +6 -14
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
- data/lib/jirametrics/html/index.css +17 -0
- data/lib/jirametrics/html/index.erb +1 -1
- data/lib/jirametrics/html/index.js +24 -0
- data/lib/jirametrics/html/sprint_burndown.erb +12 -12
- data/lib/jirametrics/html/throughput_chart.erb +7 -10
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +8 -9
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +6 -24
- data/lib/jirametrics/issue.rb +133 -77
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/project_config.rb +44 -7
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +17 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +8 -2
- data/lib/jirametrics/stitcher.rb +76 -0
- 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
- data/lib/jirametrics.rb +8 -1
- metadata +15 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2145950e91bf010e3c2790151b32f859fc479a871002f2057e2070ef23a6e1ae
|
|
4
|
+
data.tar.gz: 00bff5cffee6fc49862ae15fdc78f142982337a03abb4ea33735b07a2f0f3426
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d16248d2502890619c3da712ebb7f0b6aa864994013a599d29e6795f687ccbd782d9e7f33f253f7f5db81a3c78f1be0898bf27cd4e8a981137f0a3ca55da8c8f
|
|
7
|
+
data.tar.gz: ee3e5d4fdd783a025c9666a6080f587f82cd70f0fb6aeeda9a12a862c454baab64fb635bdf2727b821760e75822638378c87b18e59b68fa8903b3bd87a05f9f8
|
|
@@ -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
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
|
+
require 'jirametrics/bar_chart_range'
|
|
4
5
|
|
|
5
6
|
class AgingWorkBarChart < ChartBase
|
|
6
7
|
def initialize block
|
|
7
8
|
super()
|
|
8
9
|
|
|
10
|
+
@age_cutoff = nil
|
|
9
11
|
header_text 'Aging Work Bar Chart'
|
|
10
12
|
description_text <<-HTML
|
|
11
13
|
<p>
|
|
@@ -13,16 +15,16 @@ class AgingWorkBarChart < ChartBase
|
|
|
13
15
|
newest at the bottom.
|
|
14
16
|
</p>
|
|
15
17
|
<p>
|
|
16
|
-
There are
|
|
17
|
-
information relevant to that. Hovering over any of the bars will provide more details.
|
|
18
|
+
There are three bars for each issue, and hovering over any of the bars will provide more details.
|
|
18
19
|
<ol>
|
|
19
|
-
<li>The
|
|
20
|
+
<li>Status: The status the issue was in at any time. The colour indicates the
|
|
20
21
|
status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
|
|
21
22
|
#{color_block '--status-category-inprogress-color'} In Progress,
|
|
22
23
|
or #{color_block '--status-category-done-color'} Done</li>
|
|
23
|
-
<li>
|
|
24
|
+
<li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
|
|
24
25
|
or #{color_block '--stalled-color'} stalled.</li>
|
|
25
|
-
<li>
|
|
26
|
+
<li>Priority: This shows the priority over time. If one of these priorities is considered expedited
|
|
27
|
+
then it will be drawn with diagonal lines.</li>
|
|
26
28
|
</ol>
|
|
27
29
|
</p>
|
|
28
30
|
#{describe_non_working_days}
|
|
@@ -36,6 +38,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
36
38
|
|
|
37
39
|
def run
|
|
38
40
|
aging_issues = select_aging_issues issues: @issues
|
|
41
|
+
adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
|
|
39
42
|
|
|
40
43
|
today = date_range.end
|
|
41
44
|
sort_by_age! issues: aging_issues, today: today
|
|
@@ -58,134 +61,134 @@ class AgingWorkBarChart < ChartBase
|
|
|
58
61
|
wrap_and_render(binding, __FILE__)
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
|
|
65
|
+
earliest_start_time = aging_issues.collect do |issue|
|
|
66
|
+
issue.board.cycletime.started_stopped_times(issue).first
|
|
67
|
+
end.min
|
|
68
|
+
return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
|
|
69
|
+
|
|
70
|
+
@time_range = earliest_start_time..@time_range.end
|
|
71
|
+
@date_range = @time_range.begin.to_date..@time_range.end.to_date
|
|
72
|
+
end
|
|
73
|
+
|
|
61
74
|
def data_sets_for_one_issue issue:, today:
|
|
62
75
|
cycletime = issue.board.cycletime
|
|
63
|
-
issue_start_time
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
issue_label: issue_label,
|
|
71
|
-
stack: 'blocked',
|
|
72
|
-
issue_start_time: issue_start_time
|
|
73
|
-
),
|
|
74
|
-
data_set_by_block(
|
|
75
|
-
issue: issue,
|
|
76
|
-
issue_label: issue_label,
|
|
77
|
-
title_label: 'Expedited',
|
|
78
|
-
stack: 'expedited',
|
|
79
|
-
color: CssVariable['--expedited-color'],
|
|
80
|
-
start_date: issue_start_date
|
|
81
|
-
) { |day| issue.expedited_on_date?(day) }
|
|
76
|
+
issue_start_time = cycletime.started_stopped_times(issue).first
|
|
77
|
+
end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
|
|
78
|
+
|
|
79
|
+
bar_data = [
|
|
80
|
+
['status', collect_status_ranges(issue: issue, now: end_of_today)],
|
|
81
|
+
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
82
|
+
['priority', collect_priority_ranges(issue: issue)]
|
|
82
83
|
]
|
|
84
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
85
|
+
|
|
86
|
+
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
87
|
+
|
|
88
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
89
|
+
bar_data.collect do |stack, ranges|
|
|
90
|
+
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
91
|
+
end
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
def sort_by_age! issues:, today:
|
|
86
95
|
issues.sort! do |a, b|
|
|
87
|
-
|
|
96
|
+
b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
|
|
88
97
|
end
|
|
89
98
|
end
|
|
90
99
|
|
|
91
100
|
def select_aging_issues issues:
|
|
92
101
|
issues.select do |issue|
|
|
93
102
|
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
94
|
-
started_time && stopped_time.nil?
|
|
103
|
+
next false unless started_time && stopped_time.nil?
|
|
104
|
+
|
|
105
|
+
age = (date_range.end - started_time.to_date).to_i + 1
|
|
106
|
+
!(@age_cutoff && @age_cutoff >= age)
|
|
95
107
|
end
|
|
96
108
|
end
|
|
97
109
|
|
|
98
110
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
99
|
-
px_per_bar =
|
|
111
|
+
px_per_bar = 10
|
|
100
112
|
bars_per_issue = 3
|
|
113
|
+
bars_per_issue += 1 if current_board.scrum?
|
|
114
|
+
|
|
101
115
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
102
116
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
103
117
|
end
|
|
104
118
|
|
|
105
|
-
def
|
|
106
|
-
|
|
119
|
+
def clip_ranges_to_start_time ranges:, issue_start_time:
|
|
120
|
+
return if issue_start_time.nil?
|
|
107
121
|
|
|
108
|
-
|
|
122
|
+
ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
|
|
123
|
+
ranges.reject! { |range| range.start >= range.stop }
|
|
124
|
+
end
|
|
109
125
|
|
|
126
|
+
def collect_status_ranges issue:, now:
|
|
127
|
+
ranges = []
|
|
128
|
+
issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
|
|
110
129
|
previous_start = nil
|
|
111
130
|
previous_status = nil
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
unless previous_start.nil? || previous_start < issue_started_time
|
|
120
|
-
hash = {
|
|
121
|
-
type: 'bar',
|
|
122
|
-
data: [{
|
|
123
|
-
x: [chart_format(previous_start), chart_format(change.time)],
|
|
124
|
-
y: label,
|
|
125
|
-
title: "#{issue.type} : #{change.value}"
|
|
126
|
-
}],
|
|
127
|
-
backgroundColor: status_category_color(status),
|
|
128
|
-
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
129
|
-
borderWidth: {
|
|
130
|
-
top: 0,
|
|
131
|
-
right: 1,
|
|
132
|
-
bottom: 0,
|
|
133
|
-
left: 0
|
|
134
|
-
},
|
|
135
|
-
stacked: true,
|
|
136
|
-
stack: 'status'
|
|
137
|
-
}
|
|
138
|
-
data_sets << hash if date_range.include?(change.time.to_date)
|
|
131
|
+
issue.status_changes.each do |change|
|
|
132
|
+
new_status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
133
|
+
if previous_start.nil?
|
|
134
|
+
previous_start = change.time
|
|
135
|
+
previous_status = new_status
|
|
136
|
+
next
|
|
139
137
|
end
|
|
140
138
|
|
|
139
|
+
previous_start = issue_started_time if issue_started_time > previous_start
|
|
140
|
+
|
|
141
|
+
ranges << BarChartRange.new(
|
|
142
|
+
start: previous_start,
|
|
143
|
+
stop: change.time,
|
|
144
|
+
color: status_category_color(previous_status),
|
|
145
|
+
title: previous_status.to_s
|
|
146
|
+
)
|
|
141
147
|
previous_start = change.time
|
|
142
|
-
previous_status =
|
|
148
|
+
previous_status = new_status
|
|
143
149
|
end
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
ranges << BarChartRange.new(
|
|
152
|
+
start: previous_start,
|
|
153
|
+
stop: now,
|
|
154
|
+
color: status_category_color(previous_status),
|
|
155
|
+
title: previous_status.to_s
|
|
156
|
+
)
|
|
157
|
+
ranges
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
|
|
161
|
+
ranges.filter_map do |bar_chart_range|
|
|
162
|
+
next if bar_chart_range.stop < issue_start_time
|
|
163
|
+
|
|
164
|
+
background_color = bar_chart_range.color
|
|
165
|
+
if bar_chart_range.highlight
|
|
166
|
+
background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
{
|
|
147
170
|
type: 'bar',
|
|
148
171
|
data: [{
|
|
149
|
-
x: [chart_format(
|
|
150
|
-
y:
|
|
151
|
-
title:
|
|
172
|
+
x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
|
|
173
|
+
y: y_value,
|
|
174
|
+
title: bar_chart_range.title
|
|
152
175
|
}],
|
|
153
|
-
backgroundColor:
|
|
176
|
+
backgroundColor: background_color,
|
|
177
|
+
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
178
|
+
borderWidth: {
|
|
179
|
+
top: 0,
|
|
180
|
+
right: 1,
|
|
181
|
+
bottom: 0,
|
|
182
|
+
left: 0
|
|
183
|
+
},
|
|
154
184
|
stacked: true,
|
|
155
|
-
stack:
|
|
185
|
+
stack: stack
|
|
156
186
|
}
|
|
157
187
|
end
|
|
158
|
-
|
|
159
|
-
data_sets
|
|
160
188
|
end
|
|
161
189
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
|
|
165
|
-
end
|
|
166
|
-
if settings['stalled_color']
|
|
167
|
-
file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
color = settings['blocked_color'] || '--blocked-color'
|
|
171
|
-
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
172
|
-
{
|
|
173
|
-
backgroundColor: CssVariable[color],
|
|
174
|
-
data: [
|
|
175
|
-
{
|
|
176
|
-
title: starting_change.reasons,
|
|
177
|
-
x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
|
|
178
|
-
y: issue_label
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
stack: stack,
|
|
182
|
-
stacked: true,
|
|
183
|
-
type: 'bar'
|
|
184
|
-
}
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def blocked_data_sets issue:, issue_label:, issue_start_time:, stack:
|
|
188
|
-
data_sets = []
|
|
190
|
+
def collect_blocked_stalled_ranges issue:, issue_start_time:
|
|
191
|
+
results = []
|
|
189
192
|
starting_change = nil
|
|
190
193
|
|
|
191
194
|
issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
|
|
@@ -195,58 +198,104 @@ class AgingWorkBarChart < ChartBase
|
|
|
195
198
|
end
|
|
196
199
|
|
|
197
200
|
if change.time >= issue_start_time
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
color = settings['blocked_color'] || '--blocked-color'
|
|
202
|
+
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
203
|
+
|
|
204
|
+
results << BarChartRange.new(
|
|
205
|
+
start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
|
|
201
206
|
)
|
|
202
207
|
end
|
|
203
208
|
|
|
204
209
|
starting_change = change
|
|
205
210
|
end
|
|
211
|
+
results
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def collect_priority_ranges issue:
|
|
215
|
+
expedited_priority_names = settings['expedited_priority_names']
|
|
216
|
+
|
|
217
|
+
previous_change = nil
|
|
218
|
+
results = []
|
|
219
|
+
|
|
220
|
+
issue.changes.each do |change|
|
|
221
|
+
next unless change.priority?
|
|
206
222
|
|
|
207
|
-
|
|
223
|
+
if previous_change.nil?
|
|
224
|
+
previous_change = change
|
|
225
|
+
next
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
results << create_range_for_priority(
|
|
229
|
+
previous_change: previous_change, stop_time: change.time,
|
|
230
|
+
expedited_priority_names: expedited_priority_names
|
|
231
|
+
)
|
|
232
|
+
previous_change = change
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
results << create_range_for_priority(
|
|
236
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
237
|
+
expedited_priority_names: expedited_priority_names
|
|
238
|
+
)
|
|
239
|
+
results
|
|
208
240
|
end
|
|
209
241
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
242
|
+
def collect_sprint_ranges issue:
|
|
243
|
+
results = []
|
|
244
|
+
open_sprints = {}
|
|
245
|
+
|
|
246
|
+
issue.changes.each do |change|
|
|
247
|
+
next unless change.sprint?
|
|
248
|
+
|
|
249
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
250
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
251
|
+
|
|
252
|
+
removed_sprint_ids.each do |id|
|
|
253
|
+
data = open_sprints.delete(id)
|
|
254
|
+
next unless data
|
|
255
|
+
|
|
256
|
+
completed = data[:sprint].completed_time
|
|
257
|
+
stop = completed ? [change.time, completed].min : change.time
|
|
258
|
+
results << BarChartRange.new(
|
|
259
|
+
start: data[:start_time], stop: stop,
|
|
260
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
added_sprint_ids.each do |id|
|
|
265
|
+
sprint = issue.board.sprints.find { |s| s.id == id }
|
|
266
|
+
next unless sprint
|
|
267
|
+
next if sprint.future?
|
|
268
|
+
|
|
269
|
+
start_time = [sprint.start_time, change.time].max
|
|
270
|
+
open_sprints[id] = { start_time: start_time, sprint: sprint }
|
|
230
271
|
end
|
|
231
272
|
end
|
|
232
273
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
274
|
+
open_sprints.each_value do |data|
|
|
275
|
+
next if data[:sprint].future?
|
|
276
|
+
|
|
277
|
+
stop = data[:sprint].completed_time || time_range.end
|
|
278
|
+
results << BarChartRange.new(
|
|
279
|
+
start: data[:start_time], stop: stop,
|
|
280
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
281
|
+
)
|
|
239
282
|
end
|
|
240
283
|
|
|
241
|
-
|
|
284
|
+
results
|
|
285
|
+
end
|
|
242
286
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
287
|
+
def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
|
|
288
|
+
expedited = expedited_priority_names.include?(previous_change.value)
|
|
289
|
+
title = "Priority: #{previous_change.value}"
|
|
290
|
+
title << ' (expedited)' if expedited
|
|
291
|
+
|
|
292
|
+
BarChartRange.new(
|
|
293
|
+
start: previous_change.time,
|
|
294
|
+
stop: stop_time,
|
|
295
|
+
color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
|
|
296
|
+
title: title,
|
|
297
|
+
highlight: expedited
|
|
298
|
+
)
|
|
250
299
|
end
|
|
251
300
|
|
|
252
301
|
def calculate_percent_line percentage: 85
|
|
@@ -255,4 +304,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
255
304
|
|
|
256
305
|
days[days.length * percentage / 100]
|
|
257
306
|
end
|
|
307
|
+
|
|
308
|
+
def age_cutoff days
|
|
309
|
+
@age_cutoff = days
|
|
310
|
+
end
|
|
258
311
|
end
|
|
@@ -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
|