jirametrics 2.12.1 → 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 +160 -0
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +4 -1
- data/lib/jirametrics/change_item.rb +12 -4
- data/lib/jirametrics/chart_base.rb +36 -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 +57 -53
- 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 -70
- 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 +21 -9
- data/lib/jirametrics/html/index.erb +5 -37
- 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 +128 -23
- data/lib/jirametrics/jira_gateway.rb +59 -17
- data/lib/jirametrics/project_config.rb +42 -5
- 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/status_collection.rb +1 -0
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +26 -69
- metadata +11 -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
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AtlassianDocumentFormat
|
|
4
|
+
attr_reader :users
|
|
5
|
+
|
|
6
|
+
def initialize users:, timezone_offset:
|
|
7
|
+
@users = users
|
|
8
|
+
@timezone_offset = timezone_offset
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_html input
|
|
12
|
+
if input.is_a? String
|
|
13
|
+
input
|
|
14
|
+
.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
|
|
15
|
+
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
16
|
+
.gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
17
|
+
.gsub("\n", '<br />')
|
|
18
|
+
elsif input&.[]('content')
|
|
19
|
+
input['content'].collect { |element| adf_node_to_html element }.join("\n")
|
|
20
|
+
else
|
|
21
|
+
# We have an actual ADF document with no content.
|
|
22
|
+
''
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ADF is Atlassian Document Format
|
|
27
|
+
# https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
|
28
|
+
def adf_node_to_html node # rubocop:disable Metrics/CyclomaticComplexity
|
|
29
|
+
closing_tag = nil
|
|
30
|
+
node_attrs = node['attrs']
|
|
31
|
+
|
|
32
|
+
result = +''
|
|
33
|
+
case node['type']
|
|
34
|
+
when 'blockquote'
|
|
35
|
+
result << '<blockquote>'
|
|
36
|
+
closing_tag = '</blockquote>'
|
|
37
|
+
when 'bulletList'
|
|
38
|
+
result << '<ul>'
|
|
39
|
+
closing_tag = '</ul>'
|
|
40
|
+
when 'codeBlock'
|
|
41
|
+
result << '<code>'
|
|
42
|
+
closing_tag = '</code>'
|
|
43
|
+
when 'date'
|
|
44
|
+
result << Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s
|
|
45
|
+
when 'decisionItem'
|
|
46
|
+
result << '<li>'
|
|
47
|
+
closing_tag = '</li>'
|
|
48
|
+
when 'decisionList'
|
|
49
|
+
result << '<div>Decisions<ul>'
|
|
50
|
+
closing_tag = '</ul></div>'
|
|
51
|
+
when 'emoji'
|
|
52
|
+
result << node_attrs['text']
|
|
53
|
+
when 'expand'
|
|
54
|
+
# TODO: Maybe, someday, make this actually expandable. For now it's always open
|
|
55
|
+
result << "<div>#{node_attrs['title']}</div>"
|
|
56
|
+
when 'hardBreak'
|
|
57
|
+
result << '<br />'
|
|
58
|
+
when 'heading'
|
|
59
|
+
level = node_attrs['level']
|
|
60
|
+
result << "<h#{level}>"
|
|
61
|
+
closing_tag = "</h#{level}>"
|
|
62
|
+
when 'inlineCard'
|
|
63
|
+
url = node_attrs['url']
|
|
64
|
+
result << "[Inline card]: <a href='#{url}'>#{url}</a>"
|
|
65
|
+
when 'listItem'
|
|
66
|
+
result << '<li>'
|
|
67
|
+
closing_tag = '</li>'
|
|
68
|
+
when 'media'
|
|
69
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
70
|
+
result << "Media: #{text}"
|
|
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>"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
node['content']&.each do |child|
|
|
120
|
+
result << adf_node_to_html(child)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result << closing_tag if closing_tag
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def adf_marks_to_html list
|
|
128
|
+
return [] if list.nil?
|
|
129
|
+
|
|
130
|
+
mappings = [
|
|
131
|
+
['strong', '<b>', '</b>'],
|
|
132
|
+
['code', '<code>', '</code>'],
|
|
133
|
+
['em', '<em>', '</em>'],
|
|
134
|
+
['strike', '<s>', '</s>'],
|
|
135
|
+
['underline', '<u>', '</u>']
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
list.filter_map do |mark|
|
|
139
|
+
type = mark['type']
|
|
140
|
+
if type == 'textColor'
|
|
141
|
+
color = mark['attrs']['color']
|
|
142
|
+
["<span style='color: #{color}'>", '</span>']
|
|
143
|
+
elsif type == 'link'
|
|
144
|
+
href = mark['attrs']['href']
|
|
145
|
+
title = mark['attrs']['title']
|
|
146
|
+
["<a href='#{href}' title='#{title}'>", '</a>']
|
|
147
|
+
else
|
|
148
|
+
line = mappings.find { |key, _open, _close| key == type }
|
|
149
|
+
[line[1], line[2]] if line
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def expand_account_id account_id
|
|
155
|
+
user = @users.find { |u| u.account_id == account_id }
|
|
156
|
+
text = account_id
|
|
157
|
+
text = "@#{user.display_name}" if user
|
|
158
|
+
"<span class='account_id'>#{text}</span>"
|
|
159
|
+
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
|
@@ -11,6 +11,7 @@ class BoardConfig
|
|
|
11
11
|
|
|
12
12
|
def run
|
|
13
13
|
@board = @project_config.all_boards[id]
|
|
14
|
+
raise "Can't find board #{id.inspect} in #{@project_config.all_boards.keys.inspect}" unless @board
|
|
14
15
|
|
|
15
16
|
instance_eval(&@block)
|
|
16
17
|
raise "Must specify a cycletime for board #{@id}" if @board.cycletime.nil?
|
|
@@ -23,7 +24,9 @@ class BoardConfig
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
@board.cycletime = CycleTimeConfig.new(
|
|
26
|
-
|
|
27
|
+
possible_statuses: project_config.possible_statuses,
|
|
28
|
+
label: label, block: block, file_system: project_config.file_system,
|
|
29
|
+
settings: project_config.settings
|
|
27
30
|
)
|
|
28
31
|
end
|
|
29
32
|
|