jirametrics 2.14 → 2.22
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/aging_work_bar_chart.rb +176 -134
- data/lib/jirametrics/anonymizer.rb +8 -6
- data/lib/jirametrics/atlassian_document_format.rb +3 -3
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +3 -1
- data/lib/jirametrics/change_item.rb +11 -4
- data/lib/jirametrics/chart_base.rb +34 -2
- data/lib/jirametrics/cycletime_config.rb +22 -4
- data/lib/jirametrics/cycletime_histogram.rb +3 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
- data/lib/jirametrics/daily_view.rb +6 -20
- 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 +1 -1
- data/lib/jirametrics/data_quality_report.rb +8 -3
- data/lib/jirametrics/dependency_chart.rb +4 -1
- data/lib/jirametrics/downloader.rb +34 -99
- data/lib/jirametrics/downloader_for_cloud.rb +202 -0
- data/lib/jirametrics/downloader_for_data_center.rb +94 -0
- data/lib/jirametrics/examples/standard_project.rb +9 -9
- data/lib/jirametrics/expedited_chart.rb +1 -1
- data/lib/jirametrics/exporter.rb +12 -5
- data/lib/jirametrics/file_system.rb +24 -1
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
- data/lib/jirametrics/groupable_issue_chart.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
- data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
- data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
- data/lib/jirametrics/html/expedited_chart.erb +3 -1
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
- data/lib/jirametrics/html/index.css +16 -9
- data/lib/jirametrics/html/index.erb +3 -35
- data/lib/jirametrics/html/index.js +114 -0
- data/lib/jirametrics/html/sprint_burndown.erb +11 -3
- data/lib/jirametrics/html/throughput_chart.erb +2 -2
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +8 -25
- data/lib/jirametrics/issue.rb +125 -19
- data/lib/jirametrics/jira_gateway.rb +55 -17
- data/lib/jirametrics/project_config.rb +22 -2
- data/lib/jirametrics/raw_javascript.rb +13 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +6 -2
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +26 -70
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40a0ee85ee8d7d0d2ff071357afdea2aefdfeea8734f96eb789721d4a9f2607b
|
|
4
|
+
data.tar.gz: 11008f97848d8e3034cf95c5f615496c5677d8057f40b06d2724247d6087318d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e5ad6c1d5dddf5a89cc63498f1967f35c4761b6418c90019c8c6756599efb5b8badaf0ac4d50f94be4644508a97641430f178b3d40217cb02560aa017f33b80
|
|
7
|
+
data.tar.gz: a6a7f74dadbb8a2f7961a02e396a39dd994ff1a11756f57a5eadfd60af5c71bd79c3df65b7bfd77ca998059018f60cd6306b2006512b7cf5f760d75e2d0dfdf4
|
|
@@ -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,125 @@ 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
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
87
|
+
bar_data.collect do |stack, ranges|
|
|
88
|
+
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
89
|
+
end
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
def sort_by_age! issues:, today:
|
|
86
93
|
issues.sort! do |a, b|
|
|
87
|
-
|
|
94
|
+
b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
|
|
88
95
|
end
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def select_aging_issues issues:
|
|
92
99
|
issues.select do |issue|
|
|
93
100
|
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
94
|
-
started_time && stopped_time.nil?
|
|
101
|
+
next false unless started_time && stopped_time.nil?
|
|
102
|
+
|
|
103
|
+
age = (date_range.end - started_time.to_date).to_i + 1
|
|
104
|
+
!(@age_cutoff && @age_cutoff >= age)
|
|
95
105
|
end
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
99
|
-
px_per_bar =
|
|
109
|
+
px_per_bar = 10
|
|
100
110
|
bars_per_issue = 3
|
|
111
|
+
bars_per_issue += 1 if current_board.scrum?
|
|
112
|
+
|
|
101
113
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
102
114
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
103
115
|
end
|
|
104
116
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
|
|
109
|
-
|
|
117
|
+
def collect_status_ranges issue:, now:
|
|
118
|
+
ranges = []
|
|
119
|
+
issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
|
|
110
120
|
previous_start = nil
|
|
111
121
|
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)
|
|
122
|
+
issue.status_changes.each do |change|
|
|
123
|
+
new_status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
124
|
+
if previous_start.nil?
|
|
125
|
+
previous_start = change.time
|
|
126
|
+
previous_status = new_status
|
|
127
|
+
next
|
|
139
128
|
end
|
|
140
129
|
|
|
130
|
+
previous_start = issue_started_time if issue_started_time > previous_start
|
|
131
|
+
|
|
132
|
+
ranges << BarChartRange.new(
|
|
133
|
+
start: previous_start,
|
|
134
|
+
stop: change.time,
|
|
135
|
+
color: status_category_color(previous_status),
|
|
136
|
+
title: previous_status.to_s
|
|
137
|
+
)
|
|
141
138
|
previous_start = change.time
|
|
142
|
-
previous_status =
|
|
139
|
+
previous_status = new_status
|
|
143
140
|
end
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
ranges << BarChartRange.new(
|
|
143
|
+
start: previous_start,
|
|
144
|
+
stop: now,
|
|
145
|
+
color: status_category_color(previous_status),
|
|
146
|
+
title: previous_status.to_s
|
|
147
|
+
)
|
|
148
|
+
ranges
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
|
|
152
|
+
ranges.filter_map do |bar_chart_range|
|
|
153
|
+
next if bar_chart_range.stop < issue_start_time
|
|
154
|
+
|
|
155
|
+
background_color = bar_chart_range.color
|
|
156
|
+
if bar_chart_range.highlight
|
|
157
|
+
background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
{
|
|
147
161
|
type: 'bar',
|
|
148
162
|
data: [{
|
|
149
|
-
x: [chart_format(
|
|
150
|
-
y:
|
|
151
|
-
title:
|
|
163
|
+
x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
|
|
164
|
+
y: y_value,
|
|
165
|
+
title: bar_chart_range.title
|
|
152
166
|
}],
|
|
153
|
-
backgroundColor:
|
|
167
|
+
backgroundColor: background_color,
|
|
168
|
+
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
169
|
+
borderWidth: {
|
|
170
|
+
top: 0,
|
|
171
|
+
right: 1,
|
|
172
|
+
bottom: 0,
|
|
173
|
+
left: 0
|
|
174
|
+
},
|
|
154
175
|
stacked: true,
|
|
155
|
-
stack:
|
|
176
|
+
stack: stack
|
|
156
177
|
}
|
|
157
178
|
end
|
|
158
|
-
|
|
159
|
-
data_sets
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
|
|
163
|
-
if settings['blocked_color']
|
|
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
179
|
end
|
|
186
180
|
|
|
187
|
-
def
|
|
188
|
-
|
|
181
|
+
def collect_blocked_stalled_ranges issue:, issue_start_time:
|
|
182
|
+
results = []
|
|
189
183
|
starting_change = nil
|
|
190
184
|
|
|
191
185
|
issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
|
|
@@ -195,58 +189,102 @@ class AgingWorkBarChart < ChartBase
|
|
|
195
189
|
end
|
|
196
190
|
|
|
197
191
|
if change.time >= issue_start_time
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
192
|
+
color = settings['blocked_color'] || '--blocked-color'
|
|
193
|
+
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
194
|
+
|
|
195
|
+
results << BarChartRange.new(
|
|
196
|
+
start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
|
|
201
197
|
)
|
|
202
198
|
end
|
|
203
199
|
|
|
204
200
|
starting_change = change
|
|
205
201
|
end
|
|
202
|
+
results
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def collect_priority_ranges issue:
|
|
206
|
+
expedited_priority_names = settings['expedited_priority_names']
|
|
207
|
+
|
|
208
|
+
previous_change = nil
|
|
209
|
+
results = []
|
|
210
|
+
|
|
211
|
+
issue.changes.each do |change|
|
|
212
|
+
next unless change.priority?
|
|
213
|
+
|
|
214
|
+
if previous_change.nil?
|
|
215
|
+
previous_change = change
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
results << create_range_for_priority(
|
|
220
|
+
previous_change: previous_change, stop_time: change.time,
|
|
221
|
+
expedited_priority_names: expedited_priority_names
|
|
222
|
+
)
|
|
223
|
+
previous_change = change
|
|
224
|
+
end
|
|
206
225
|
|
|
207
|
-
|
|
226
|
+
results << create_range_for_priority(
|
|
227
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
228
|
+
expedited_priority_names: expedited_priority_names
|
|
229
|
+
)
|
|
230
|
+
results
|
|
208
231
|
end
|
|
209
232
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
def collect_sprint_ranges issue:
|
|
234
|
+
results = []
|
|
235
|
+
open_sprints = {}
|
|
236
|
+
|
|
237
|
+
issue.changes.each do |change|
|
|
238
|
+
next unless change.sprint?
|
|
239
|
+
|
|
240
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
241
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
242
|
+
|
|
243
|
+
removed_sprint_ids.each do |id|
|
|
244
|
+
data = open_sprints.delete(id)
|
|
245
|
+
next unless data
|
|
246
|
+
|
|
247
|
+
completed = data[:sprint].completed_time
|
|
248
|
+
stop = completed ? [change.time, completed].min : change.time
|
|
249
|
+
results << BarChartRange.new(
|
|
250
|
+
start: data[:start_time], stop: stop,
|
|
251
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
added_sprint_ids.each do |id|
|
|
256
|
+
sprint = issue.board.sprints.find { |s| s.id == id }
|
|
257
|
+
next unless sprint
|
|
258
|
+
next if sprint.future?
|
|
259
|
+
|
|
260
|
+
start_time = [sprint.start_time, change.time].max
|
|
261
|
+
open_sprints[id] = { start_time: start_time, sprint: sprint }
|
|
230
262
|
end
|
|
231
263
|
end
|
|
232
264
|
|
|
233
|
-
|
|
234
|
-
data
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
open_sprints.each_value do |data|
|
|
266
|
+
stop = data[:sprint].completed_time || time_range.end
|
|
267
|
+
results << BarChartRange.new(
|
|
268
|
+
start: data[:start_time], stop: stop,
|
|
269
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
270
|
+
)
|
|
239
271
|
end
|
|
240
272
|
|
|
241
|
-
|
|
273
|
+
results
|
|
274
|
+
end
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
|
|
277
|
+
expedited = expedited_priority_names.include?(previous_change.value)
|
|
278
|
+
title = "Priority: #{previous_change.value}"
|
|
279
|
+
title << ' (expedited)' if expedited
|
|
280
|
+
|
|
281
|
+
BarChartRange.new(
|
|
282
|
+
start: previous_change.time,
|
|
283
|
+
stop: stop_time,
|
|
284
|
+
color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
|
|
285
|
+
title: title,
|
|
286
|
+
highlight: expedited
|
|
287
|
+
)
|
|
250
288
|
end
|
|
251
289
|
|
|
252
290
|
def calculate_percent_line percentage: 85
|
|
@@ -255,4 +293,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
255
293
|
|
|
256
294
|
days[days.length * percentage / 100]
|
|
257
295
|
end
|
|
296
|
+
|
|
297
|
+
def age_cutoff days
|
|
298
|
+
@age_cutoff = days
|
|
299
|
+
end
|
|
258
300
|
end
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'random-word'
|
|
4
4
|
|
|
5
|
-
class Anonymizer
|
|
5
|
+
class Anonymizer < ChartBase
|
|
6
6
|
# needed for testing
|
|
7
7
|
attr_reader :project_config, :issues
|
|
8
8
|
|
|
9
9
|
def initialize project_config:, date_adjustment: -200
|
|
10
|
+
super()
|
|
10
11
|
@project_config = project_config
|
|
11
12
|
@issues = @project_config.issues
|
|
12
13
|
@all_boards = @project_config.all_boards
|
|
@@ -130,18 +131,19 @@ class Anonymizer
|
|
|
130
131
|
end
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
def shift_all_dates
|
|
134
|
-
|
|
134
|
+
def shift_all_dates date_adjustment: @date_adjustment
|
|
135
|
+
adjustment_in_seconds = 60 * 60 * 24 * date_adjustment
|
|
136
|
+
@file_system.log "Shifting all dates by #{label_days date_adjustment}"
|
|
135
137
|
@issues.each do |issue|
|
|
136
138
|
issue.changes.each do |change|
|
|
137
|
-
change.time = change.time +
|
|
139
|
+
change.time = change.time + adjustment_in_seconds
|
|
138
140
|
end
|
|
139
141
|
|
|
140
|
-
issue.raw['fields']['updated'] = (issue.updated +
|
|
142
|
+
issue.raw['fields']['updated'] = (issue.updated + adjustment_in_seconds).to_s
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
range = @project_config.time_range
|
|
144
|
-
@project_config.time_range = (range.begin +
|
|
146
|
+
@project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
def random_name
|
|
@@ -13,9 +13,9 @@ class AtlassianDocumentFormat
|
|
|
13
13
|
input
|
|
14
14
|
.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
|
|
15
15
|
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
16
|
-
.gsub(/\[([
|
|
16
|
+
.gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
17
17
|
.gsub("\n", '<br />')
|
|
18
|
-
elsif input['content'
|
|
18
|
+
elsif input&.[]('content')
|
|
19
19
|
input['content'].collect { |element| adf_node_to_html element }.join("\n")
|
|
20
20
|
else
|
|
21
21
|
# We have an actual ADF document with no content.
|
|
@@ -157,4 +157,4 @@ class AtlassianDocumentFormat
|
|
|
157
157
|
text = "@#{user.display_name}" if user
|
|
158
158
|
"<span class='account_id'>#{text}</span>"
|
|
159
159
|
end
|
|
160
|
-
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/value_equality'
|
|
4
|
+
|
|
5
|
+
class BarChartRange
|
|
6
|
+
include ValueEquality
|
|
7
|
+
|
|
8
|
+
attr_accessor :start, :stop, :color, :title, :highlight
|
|
9
|
+
|
|
10
|
+
def initialize start:, stop:, color:, title:, highlight: false
|
|
11
|
+
@start = start
|
|
12
|
+
@stop = stop
|
|
13
|
+
@color = color
|
|
14
|
+
@title = title
|
|
15
|
+
@highlight = highlight
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/jirametrics/board.rb
CHANGED
|
@@ -24,7 +24,9 @@ class BoardConfig
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
@board.cycletime = CycleTimeConfig.new(
|
|
27
|
-
|
|
27
|
+
possible_statuses: project_config.possible_statuses,
|
|
28
|
+
label: label, block: block, file_system: project_config.file_system,
|
|
29
|
+
settings: project_config.settings
|
|
28
30
|
)
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ChangeItem
|
|
4
|
-
attr_reader :field, :value_id, :old_value_id, :raw, :
|
|
5
|
-
attr_accessor :value, :old_value
|
|
4
|
+
attr_reader :field, :value_id, :old_value_id, :raw, :author_raw, :field_id
|
|
5
|
+
attr_accessor :value, :old_value, :time
|
|
6
6
|
|
|
7
7
|
def initialize raw:, author_raw:, time:, artificial: false
|
|
8
8
|
@raw = raw
|
|
@@ -13,9 +13,15 @@ class ChangeItem
|
|
|
13
13
|
|
|
14
14
|
@field = @raw['field']
|
|
15
15
|
@value = @raw['toString']
|
|
16
|
-
@value_id = @raw['to'].to_i
|
|
17
16
|
@old_value = @raw['fromString']
|
|
18
|
-
|
|
17
|
+
if sprint?
|
|
18
|
+
@value_id = @raw['to'].split(', ').collect(&:to_i)
|
|
19
|
+
@old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
|
|
20
|
+
else
|
|
21
|
+
@value_id = @raw['to'].to_i
|
|
22
|
+
@old_value_id = @raw['from']&.to_i
|
|
23
|
+
end
|
|
24
|
+
@field_id = @raw['fieldId']
|
|
19
25
|
@artificial = artificial
|
|
20
26
|
end
|
|
21
27
|
|
|
@@ -54,6 +60,7 @@ class ChangeItem
|
|
|
54
60
|
message << ':' << old_value_id.inspect if status?
|
|
55
61
|
end
|
|
56
62
|
message << ", time: #{time_to_s(@time).inspect}"
|
|
63
|
+
message << ", field_id: #{@field_id.inspect}" if @field_id
|
|
57
64
|
message << ', artificial' if artificial?
|
|
58
65
|
message << ')'
|
|
59
66
|
message
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class ChartBase
|
|
4
4
|
attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
|
|
5
|
-
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
|
|
5
|
+
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
|
|
6
|
+
:atlassian_document_format
|
|
6
7
|
attr_writer :aggregated_project
|
|
7
8
|
attr_reader :canvas_width, :canvas_height
|
|
8
9
|
|
|
@@ -21,6 +22,14 @@ class ChartBase
|
|
|
21
22
|
@canvas_responsive = true
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def call_before_run &proc
|
|
26
|
+
(@call_before_run_procs ||= []) << proc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def before_run
|
|
30
|
+
@call_before_run_procs&.each { |proc| proc.call }
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
def aggregated_project?
|
|
25
34
|
@aggregated_project
|
|
26
35
|
end
|
|
@@ -44,7 +53,7 @@ class ChartBase
|
|
|
44
53
|
|
|
45
54
|
def render_top_text caller_binding
|
|
46
55
|
result = +''
|
|
47
|
-
result << "<h1>#{@header_text}</h1>" if @header_text
|
|
56
|
+
result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
|
|
48
57
|
result << ERB.new(@description_text).result(caller_binding) if @description_text
|
|
49
58
|
result
|
|
50
59
|
end
|
|
@@ -278,4 +287,27 @@ class ChartBase
|
|
|
278
287
|
</div>
|
|
279
288
|
TEXT
|
|
280
289
|
end
|
|
290
|
+
|
|
291
|
+
# Set a cycletime for just this one chart, overriding the one for the report.
|
|
292
|
+
def cycletime &block
|
|
293
|
+
call_before_run do
|
|
294
|
+
@cycletime = CycleTimeConfig.new(
|
|
295
|
+
possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system,
|
|
296
|
+
settings: settings
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Returns the cycletime in use right now, which may be specific to the chart or across the report.
|
|
302
|
+
def cycletime_for_issue issue
|
|
303
|
+
@cycletime || issue.board.cycletime
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def seam_start type = 'chart'
|
|
307
|
+
"\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def seam_end type = 'chart'
|
|
311
|
+
"\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
312
|
+
end
|
|
281
313
|
end
|
|
@@ -6,12 +6,14 @@ require 'date'
|
|
|
6
6
|
class CycleTimeConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
|
-
attr_reader :label, :
|
|
9
|
+
attr_reader :label, :possible_statuses, :settings, :file_system
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
12
|
-
|
|
11
|
+
def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
|
|
12
|
+
|
|
13
|
+
@possible_statuses = possible_statuses
|
|
13
14
|
@label = label
|
|
14
15
|
@today = today
|
|
16
|
+
@settings = settings
|
|
15
17
|
|
|
16
18
|
# If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
|
|
17
19
|
# may make it easier to find problems in the test code ;-)
|
|
@@ -63,6 +65,10 @@ class CycleTimeConfig
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def started_stopped_changes issue
|
|
68
|
+
cache_key = "#{issue.key}:#{issue.board.id}"
|
|
69
|
+
last_result = (@cache ||= {})[cache_key]
|
|
70
|
+
return *last_result if last_result && settings['cache_cycletime_calculations']
|
|
71
|
+
|
|
66
72
|
started = @start_at.call(issue)
|
|
67
73
|
stopped = @stop_at.call(issue)
|
|
68
74
|
|
|
@@ -80,7 +86,15 @@ class CycleTimeConfig
|
|
|
80
86
|
# for the start and not have it conflict.
|
|
81
87
|
started = nil if started&.time == stopped&.time
|
|
82
88
|
|
|
83
|
-
[started, stopped]
|
|
89
|
+
result = [started, stopped]
|
|
90
|
+
if last_result && result != last_result
|
|
91
|
+
@file_system.error(
|
|
92
|
+
"Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
|
|
93
|
+
"previous=#{last_result.inspect}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
@cache[cache_key] = result
|
|
97
|
+
result
|
|
84
98
|
end
|
|
85
99
|
|
|
86
100
|
def started_stopped_times issue
|
|
@@ -88,6 +102,10 @@ class CycleTimeConfig
|
|
|
88
102
|
[started&.time, stopped&.time]
|
|
89
103
|
end
|
|
90
104
|
|
|
105
|
+
def flush_cache
|
|
106
|
+
@cache = nil
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
def started_stopped_dates issue
|
|
92
110
|
started_time, stopped_time = started_stopped_times(issue)
|
|
93
111
|
[started_time&.to_date, stopped_time&.to_date]
|