jirametrics 2.20.1 → 2.25
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 +189 -133
- 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/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +24 -8
- data/lib/jirametrics/board_config.rb +2 -1
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +103 -0
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/chart_base.rb +124 -1
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +200 -0
- 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_view.rb +35 -11
- data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +30 -8
- data/lib/jirametrics/data_quality_report.rb +37 -11
- data/lib/jirametrics/dependency_chart.rb +1 -1
- 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 +28 -18
- data/lib/jirametrics/expedited_chart.rb +3 -1
- 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 +5 -1
- data/lib/jirametrics/github_gateway.rb +106 -0
- data/lib/jirametrics/groupable_issue_chart.rb +9 -1
- data/lib/jirametrics/grouping_rules.rb +21 -3
- 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 +5 -0
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +504 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
- 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 +134 -0
- data/lib/jirametrics/html/index.erb +6 -1
- data/lib/jirametrics/html/index.js +76 -2
- data/lib/jirametrics/html/sprint_burndown.erb +12 -12
- data/lib/jirametrics/html/throughput_chart.erb +42 -11
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -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 +26 -39
- data/lib/jirametrics/issue.rb +186 -88
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/project_config.rb +78 -8
- 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 +81 -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 +9 -3
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +76 -0
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +56 -22
- 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 +22 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 615528aa77577881d7658b0ce059e9e3972ce4285aeb3c1081c4347dd1a20ca4
|
|
4
|
+
data.tar.gz: 483cc9b7535da95ca2249813e5ad8ff924334f65c8b7fa84c5705367a9d1b147
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36acebeef4d036c6ed043f0073177944c390d5c80ba6ef162319f6a9bbb67bb2762ca4a345f4160d0f1d3fdc7047e257474920c09da9c5770c8a0e45a4748e59
|
|
7
|
+
data.tar.gz: 2c6f92cdb1d61b49ed7b318bc5c2108649ad387e9af51c312ade43f1522f3cc842dc949dde7d0373d41dc25630bbf9e5a3f883fc9255423ecd6e19c9008b3a4f
|
|
@@ -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,19 @@ 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 <%= current_board.scrum? ? 'four' : '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>
|
|
28
|
+
<% if current_board.scrum? %>
|
|
29
|
+
<li>Sprints: The sprints that the issue was in.</li>
|
|
30
|
+
<% end %>
|
|
26
31
|
</ol>
|
|
27
32
|
</p>
|
|
28
33
|
#{describe_non_working_days}
|
|
@@ -36,6 +41,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
36
41
|
|
|
37
42
|
def run
|
|
38
43
|
aging_issues = select_aging_issues issues: @issues
|
|
44
|
+
adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
|
|
39
45
|
|
|
40
46
|
today = date_range.end
|
|
41
47
|
sort_by_age! issues: aging_issues, today: today
|
|
@@ -58,134 +64,134 @@ class AgingWorkBarChart < ChartBase
|
|
|
58
64
|
wrap_and_render(binding, __FILE__)
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
|
|
68
|
+
earliest_start_time = aging_issues.collect do |issue|
|
|
69
|
+
issue.started_stopped_times.first
|
|
70
|
+
end.min
|
|
71
|
+
return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
|
|
72
|
+
|
|
73
|
+
@time_range = earliest_start_time..@time_range.end
|
|
74
|
+
@date_range = @time_range.begin.to_date..@time_range.end.to_date
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
def data_sets_for_one_issue issue:, today:
|
|
62
78
|
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) }
|
|
79
|
+
issue_start_time = cycletime.started_stopped_times(issue).first
|
|
80
|
+
end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
|
|
81
|
+
|
|
82
|
+
bar_data = [
|
|
83
|
+
['status', collect_status_ranges(issue: issue, now: end_of_today)],
|
|
84
|
+
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
85
|
+
['priority', collect_priority_ranges(issue: issue)]
|
|
82
86
|
]
|
|
87
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
88
|
+
|
|
89
|
+
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
90
|
+
|
|
91
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
92
|
+
bar_data.collect do |stack, ranges|
|
|
93
|
+
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
94
|
+
end
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def sort_by_age! issues:, today:
|
|
86
98
|
issues.sort! do |a, b|
|
|
87
|
-
|
|
99
|
+
b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
|
|
88
100
|
end
|
|
89
101
|
end
|
|
90
102
|
|
|
91
103
|
def select_aging_issues issues:
|
|
92
104
|
issues.select do |issue|
|
|
93
|
-
started_time, stopped_time = issue.
|
|
94
|
-
started_time && stopped_time.nil?
|
|
105
|
+
started_time, stopped_time = issue.started_stopped_times
|
|
106
|
+
next false unless started_time && stopped_time.nil?
|
|
107
|
+
|
|
108
|
+
age = (date_range.end - started_time.to_date).to_i + 1
|
|
109
|
+
!(@age_cutoff && @age_cutoff >= age)
|
|
95
110
|
end
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
99
|
-
px_per_bar =
|
|
114
|
+
px_per_bar = 10
|
|
100
115
|
bars_per_issue = 3
|
|
116
|
+
bars_per_issue += 1 if current_board.scrum?
|
|
117
|
+
|
|
101
118
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
102
119
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
103
120
|
end
|
|
104
121
|
|
|
105
|
-
def
|
|
106
|
-
|
|
122
|
+
def clip_ranges_to_start_time ranges:, issue_start_time:
|
|
123
|
+
return if issue_start_time.nil?
|
|
107
124
|
|
|
108
|
-
|
|
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
|
|
109
128
|
|
|
129
|
+
def collect_status_ranges issue:, now:
|
|
130
|
+
ranges = []
|
|
131
|
+
issue_started_time = issue.started_stopped_times.first
|
|
110
132
|
previous_start = nil
|
|
111
133
|
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)
|
|
134
|
+
issue.status_changes.each do |change|
|
|
135
|
+
new_status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
136
|
+
if previous_start.nil?
|
|
137
|
+
previous_start = change.time
|
|
138
|
+
previous_status = new_status
|
|
139
|
+
next
|
|
139
140
|
end
|
|
140
141
|
|
|
142
|
+
previous_start = issue_started_time if issue_started_time > previous_start
|
|
143
|
+
|
|
144
|
+
ranges << BarChartRange.new(
|
|
145
|
+
start: previous_start,
|
|
146
|
+
stop: change.time,
|
|
147
|
+
color: status_category_color(previous_status),
|
|
148
|
+
title: previous_status.to_s
|
|
149
|
+
)
|
|
141
150
|
previous_start = change.time
|
|
142
|
-
previous_status =
|
|
151
|
+
previous_status = new_status
|
|
143
152
|
end
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
ranges << BarChartRange.new(
|
|
155
|
+
start: previous_start,
|
|
156
|
+
stop: now,
|
|
157
|
+
color: status_category_color(previous_status),
|
|
158
|
+
title: previous_status.to_s
|
|
159
|
+
)
|
|
160
|
+
ranges
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
|
|
164
|
+
ranges.filter_map do |bar_chart_range|
|
|
165
|
+
next if bar_chart_range.stop < issue_start_time
|
|
166
|
+
|
|
167
|
+
background_color = bar_chart_range.color
|
|
168
|
+
if bar_chart_range.highlight
|
|
169
|
+
background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
{
|
|
147
173
|
type: 'bar',
|
|
148
174
|
data: [{
|
|
149
|
-
x: [chart_format(
|
|
150
|
-
y:
|
|
151
|
-
title:
|
|
175
|
+
x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
|
|
176
|
+
y: y_value,
|
|
177
|
+
title: bar_chart_range.title
|
|
152
178
|
}],
|
|
153
|
-
backgroundColor:
|
|
179
|
+
backgroundColor: background_color,
|
|
180
|
+
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
181
|
+
borderWidth: {
|
|
182
|
+
top: 0,
|
|
183
|
+
right: 1,
|
|
184
|
+
bottom: 0,
|
|
185
|
+
left: 0
|
|
186
|
+
},
|
|
154
187
|
stacked: true,
|
|
155
|
-
stack:
|
|
188
|
+
stack: stack
|
|
156
189
|
}
|
|
157
190
|
end
|
|
158
|
-
|
|
159
|
-
data_sets
|
|
160
191
|
end
|
|
161
192
|
|
|
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 = []
|
|
193
|
+
def collect_blocked_stalled_ranges issue:, issue_start_time:
|
|
194
|
+
results = []
|
|
189
195
|
starting_change = nil
|
|
190
196
|
|
|
191
197
|
issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
|
|
@@ -195,58 +201,104 @@ class AgingWorkBarChart < ChartBase
|
|
|
195
201
|
end
|
|
196
202
|
|
|
197
203
|
if change.time >= issue_start_time
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
color = settings['blocked_color'] || '--blocked-color'
|
|
205
|
+
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
206
|
+
|
|
207
|
+
results << BarChartRange.new(
|
|
208
|
+
start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
|
|
201
209
|
)
|
|
202
210
|
end
|
|
203
211
|
|
|
204
212
|
starting_change = change
|
|
205
213
|
end
|
|
214
|
+
results
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def collect_priority_ranges issue:
|
|
218
|
+
expedited_priority_names = settings['expedited_priority_names']
|
|
219
|
+
|
|
220
|
+
previous_change = nil
|
|
221
|
+
results = []
|
|
222
|
+
|
|
223
|
+
issue.changes.each do |change|
|
|
224
|
+
next unless change.priority?
|
|
206
225
|
|
|
207
|
-
|
|
226
|
+
if previous_change.nil?
|
|
227
|
+
previous_change = change
|
|
228
|
+
next
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
results << create_range_for_priority(
|
|
232
|
+
previous_change: previous_change, stop_time: change.time,
|
|
233
|
+
expedited_priority_names: expedited_priority_names
|
|
234
|
+
)
|
|
235
|
+
previous_change = change
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
results << create_range_for_priority(
|
|
239
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
240
|
+
expedited_priority_names: expedited_priority_names
|
|
241
|
+
)
|
|
242
|
+
results
|
|
208
243
|
end
|
|
209
244
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
def collect_sprint_ranges issue:
|
|
246
|
+
results = []
|
|
247
|
+
open_sprints = {}
|
|
248
|
+
|
|
249
|
+
issue.changes.each do |change|
|
|
250
|
+
next unless change.sprint?
|
|
251
|
+
|
|
252
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
253
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
254
|
+
|
|
255
|
+
removed_sprint_ids.each do |id|
|
|
256
|
+
data = open_sprints.delete(id)
|
|
257
|
+
next unless data
|
|
258
|
+
|
|
259
|
+
completed = data[:sprint].completed_time
|
|
260
|
+
stop = completed ? [change.time, completed].min : change.time
|
|
261
|
+
results << BarChartRange.new(
|
|
262
|
+
start: data[:start_time], stop: stop,
|
|
263
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
added_sprint_ids.each do |id|
|
|
268
|
+
sprint = issue.board.sprints.find { |s| s.id == id }
|
|
269
|
+
next unless sprint
|
|
270
|
+
next if sprint.future?
|
|
271
|
+
|
|
272
|
+
start_time = [sprint.start_time, change.time].max
|
|
273
|
+
open_sprints[id] = { start_time: start_time, sprint: sprint }
|
|
230
274
|
end
|
|
231
275
|
end
|
|
232
276
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
277
|
+
open_sprints.each_value do |data|
|
|
278
|
+
next if data[:sprint].future?
|
|
279
|
+
|
|
280
|
+
stop = data[:sprint].completed_time || time_range.end
|
|
281
|
+
results << BarChartRange.new(
|
|
282
|
+
start: data[:start_time], stop: stop,
|
|
283
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
284
|
+
)
|
|
239
285
|
end
|
|
240
286
|
|
|
241
|
-
|
|
287
|
+
results
|
|
288
|
+
end
|
|
242
289
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
290
|
+
def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
|
|
291
|
+
expedited = expedited_priority_names.include?(previous_change.value)
|
|
292
|
+
title = "Priority: #{previous_change.value}"
|
|
293
|
+
title << ' (expedited)' if expedited
|
|
294
|
+
|
|
295
|
+
BarChartRange.new(
|
|
296
|
+
start: previous_change.time,
|
|
297
|
+
stop: stop_time,
|
|
298
|
+
color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
|
|
299
|
+
title: title,
|
|
300
|
+
highlight: expedited
|
|
301
|
+
)
|
|
250
302
|
end
|
|
251
303
|
|
|
252
304
|
def calculate_percent_line percentage: 85
|
|
@@ -255,4 +307,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
255
307
|
|
|
256
308
|
days[days.length * percentage / 100]
|
|
257
309
|
end
|
|
310
|
+
|
|
311
|
+
def age_cutoff days
|
|
312
|
+
@age_cutoff = days
|
|
313
|
+
end
|
|
258
314
|
end
|
|
@@ -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
|