jirametrics 2.4 → 2.11
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 +9 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
- data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
- data/lib/jirametrics/aging_work_table.rb +54 -7
- data/lib/jirametrics/blocked_stalled_change.rb +1 -1
- data/lib/jirametrics/board.rb +44 -15
- data/lib/jirametrics/board_config.rb +7 -3
- data/lib/jirametrics/board_movement_calculator.rb +147 -0
- data/lib/jirametrics/change_item.rb +19 -6
- data/lib/jirametrics/chart_base.rb +63 -27
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +68 -3
- data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
- data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
- data/lib/jirametrics/daily_wip_chart.rb +7 -9
- data/lib/jirametrics/data_quality_report.rb +219 -41
- data/lib/jirametrics/dependency_chart.rb +37 -10
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +68 -50
- data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
- data/lib/jirametrics/examples/aggregated_project.rb +7 -21
- data/lib/jirametrics/examples/standard_project.rb +18 -34
- data/lib/jirametrics/expedited_chart.rb +8 -9
- data/lib/jirametrics/exporter.rb +28 -11
- data/lib/jirametrics/file_config.rb +23 -6
- data/lib/jirametrics/file_system.rb +39 -3
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
- data/lib/jirametrics/html/aging_work_table.erb +6 -4
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +28 -5
- data/lib/jirametrics/html/index.erb +8 -4
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +33 -23
- data/lib/jirametrics/issue.rb +232 -47
- data/lib/jirametrics/jira_gateway.rb +16 -3
- data/lib/jirametrics/project_config.rb +245 -134
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +5 -2
- data/lib/jirametrics/sprint_burndown.rb +3 -3
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +77 -39
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +22 -6
- metadata +10 -13
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 670df42835c7d41e30dcde01ead744a132f81313165297d434334e272de795c7
|
|
4
|
+
data.tar.gz: 12822842210b4aeddb7f59e6681a9f1bd25060bae0ff98e8490d14e895dea657
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4f36b8921c94a457710dc68251a9a2ddb38be2f0f7027991cf381fdf1cff28d2e549e508314733a3c753f583fd3c695b7e4979e1bb7be43c45de90ec001cbab
|
|
7
|
+
data.tar.gz: 28a2553b31cd4de5fce0c943da38643ad13dbfad3a8bddc77f4764e179394968966bb676e70daa75a4faeb42a67027fb7efb63895b56c01a3db563a029df66aa
|
|
@@ -41,7 +41,7 @@ class AggregateConfig
|
|
|
41
41
|
def include_issues_from project_name
|
|
42
42
|
project = @project_config.exporter.project_configs.find { |p| p.name == project_name }
|
|
43
43
|
if project.nil?
|
|
44
|
-
|
|
44
|
+
file_system.warning "Aggregated project #{@project_config.name.inspect} is attempting to load " \
|
|
45
45
|
"project #{project_name.inspect} but it can't be found. Is it disabled?"
|
|
46
46
|
return
|
|
47
47
|
end
|
|
@@ -62,7 +62,12 @@ class AggregateConfig
|
|
|
62
62
|
'the first file section'
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
if issues.nil?
|
|
67
|
+
file_system.warning "No issues found for #{project_name}"
|
|
68
|
+
else
|
|
69
|
+
@project_config.add_issues issues
|
|
70
|
+
end
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
def find_time_range projects:
|
|
@@ -81,7 +86,7 @@ class AggregateConfig
|
|
|
81
86
|
|
|
82
87
|
private
|
|
83
88
|
|
|
84
|
-
def
|
|
85
|
-
@project_config.exporter.file_system
|
|
89
|
+
def file_system
|
|
90
|
+
@project_config.exporter.file_system
|
|
86
91
|
end
|
|
87
92
|
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
4
|
|
|
5
5
|
class AgingWorkBarChart < ChartBase
|
|
6
|
-
@@next_id = 0
|
|
7
|
-
|
|
8
6
|
def initialize block
|
|
9
7
|
super()
|
|
10
8
|
|
|
@@ -53,8 +51,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
53
51
|
percentage_line_x = date_range.end - calculate_percent_line if percentage
|
|
54
52
|
|
|
55
53
|
if aging_issues.empty?
|
|
56
|
-
@description_text =
|
|
57
|
-
return render_top_text(binding)
|
|
54
|
+
@description_text = '<p>There is no aging work</p>'
|
|
55
|
+
return render_top_text(binding)
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
wrap_and_render(binding, __FILE__)
|
|
@@ -62,7 +60,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
62
60
|
|
|
63
61
|
def data_sets_for_one_issue issue:, today:
|
|
64
62
|
cycletime = issue.board.cycletime
|
|
65
|
-
issue_start_time = cycletime.
|
|
63
|
+
issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
|
|
66
64
|
issue_start_date = issue_start_time.to_date
|
|
67
65
|
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
68
66
|
[
|
|
@@ -92,8 +90,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
92
90
|
|
|
93
91
|
def select_aging_issues issues:
|
|
94
92
|
issues.select do |issue|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
94
|
+
started_time && stopped_time.nil?
|
|
97
95
|
end
|
|
98
96
|
end
|
|
99
97
|
|
|
@@ -107,7 +105,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
107
105
|
def status_data_sets issue:, label:, today:
|
|
108
106
|
cycletime = issue.board.cycletime
|
|
109
107
|
|
|
110
|
-
issue_started_time = cycletime.
|
|
108
|
+
issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
|
|
111
109
|
|
|
112
110
|
previous_start = nil
|
|
113
111
|
previous_status = nil
|
|
@@ -116,7 +114,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
116
114
|
issue.changes.each do |change|
|
|
117
115
|
next unless change.status?
|
|
118
116
|
|
|
119
|
-
status = issue.
|
|
117
|
+
status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
120
118
|
|
|
121
119
|
unless previous_start.nil? || previous_start < issue_started_time
|
|
122
120
|
hash = {
|
|
@@ -162,8 +160,12 @@ class AgingWorkBarChart < ChartBase
|
|
|
162
160
|
end
|
|
163
161
|
|
|
164
162
|
def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
167
169
|
|
|
168
170
|
color = settings['blocked_color'] || '--blocked-color'
|
|
169
171
|
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
4
|
require 'jirametrics/groupable_issue_chart'
|
|
5
|
+
require 'jirametrics/board_movement_calculator'
|
|
5
6
|
|
|
6
7
|
class AgingWorkInProgressChart < ChartBase
|
|
7
8
|
include GroupableIssueChart
|
|
@@ -16,13 +17,33 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
16
17
|
This chart shows only work items that have started but not completed, grouped by the column
|
|
17
18
|
they're currently in. Hovering over a dot will show you the ID of that work item.
|
|
18
19
|
</p>
|
|
19
|
-
<
|
|
20
|
-
The
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
<p>
|
|
21
|
+
The shaded areas indicate what percentage of the work has passed that column within that time.
|
|
22
|
+
Notes:
|
|
23
|
+
<ul>
|
|
24
|
+
<li>It only shows columns that are considered "in progress". If you see a column that wouldn't normally
|
|
25
|
+
be thought of that way, then likely issues were moving backwards or continued to progress after hitting
|
|
26
|
+
that column.</li>
|
|
27
|
+
<li>If you see a colour group that drops as it moves to the right, that generally indicates that
|
|
28
|
+
a different number of data points is being included in each column. Probably because tickets moved
|
|
29
|
+
backwards athough it could also indicate that a ticket jumped over columns as it moved to the right.
|
|
30
|
+
</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</p>
|
|
33
|
+
<div style="border: 1px solid gray; padding: 0.2em">
|
|
34
|
+
<% @percentiles.keys.sort.reverse.each do |percent| %>
|
|
35
|
+
<span style="padding-left: 0.5em; padding-right: 0.5em; vertical-align: middle;"><%= color_block @percentiles[percent] %> <%= percent %>%</span>
|
|
36
|
+
<% end %>
|
|
24
37
|
</div>
|
|
25
38
|
HTML
|
|
39
|
+
percentiles(
|
|
40
|
+
50 => '--aging-work-in-progress-chart-shading-50-color',
|
|
41
|
+
85 => '--aging-work-in-progress-chart-shading-85-color',
|
|
42
|
+
98 => '--aging-work-in-progress-chart-shading-98-color',
|
|
43
|
+
100 => '--aging-work-in-progress-chart-shading-100-color'
|
|
44
|
+
)
|
|
45
|
+
show_all_columns false
|
|
46
|
+
|
|
26
47
|
init_configuration_block(block) do
|
|
27
48
|
grouping_rules do |issue, rule|
|
|
28
49
|
rule.label = issue.type
|
|
@@ -36,13 +57,17 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
36
57
|
|
|
37
58
|
@header_text += " on board: #{@all_boards[@board_id].name}"
|
|
38
59
|
data_sets = make_data_sets
|
|
39
|
-
column_headings = @board_columns.collect(&:name)
|
|
40
60
|
|
|
41
|
-
adjust_visibility_of_unmapped_status_column data_sets: data_sets
|
|
61
|
+
adjust_visibility_of_unmapped_status_column data_sets: data_sets
|
|
62
|
+
adjust_chart_height
|
|
42
63
|
|
|
43
64
|
wrap_and_render(binding, __FILE__)
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
def show_all_columns show = true # rubocop:disable Style/OptionalBooleanParameter
|
|
68
|
+
@show_all_columns = show
|
|
69
|
+
end
|
|
70
|
+
|
|
46
71
|
def determine_board_columns
|
|
47
72
|
unmapped_statuses = current_board.possible_statuses.collect(&:id)
|
|
48
73
|
|
|
@@ -51,7 +76,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
51
76
|
|
|
52
77
|
@fake_column = BoardColumn.new({
|
|
53
78
|
'name' => '[Unmapped Statuses]',
|
|
54
|
-
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
|
|
79
|
+
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq # rubocop:disable Performance/ChainArrayAllocation
|
|
55
80
|
})
|
|
56
81
|
@board_columns = columns + [@fake_column]
|
|
57
82
|
end
|
|
@@ -62,7 +87,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
62
87
|
board.id == @board_id && board.cycletime.in_progress?(issue)
|
|
63
88
|
end
|
|
64
89
|
|
|
65
|
-
|
|
90
|
+
@max_age = 20
|
|
66
91
|
rules_to_issues = group_issues aging_issues
|
|
67
92
|
data_sets = rules_to_issues.keys.collect do |rules|
|
|
68
93
|
{
|
|
@@ -73,7 +98,10 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
73
98
|
column = column_for issue: issue
|
|
74
99
|
next if column.nil?
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
@max_age = age if age > @max_age
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
'y' => age,
|
|
77
105
|
'x' => column.name,
|
|
78
106
|
'title' => ["#{issue.key} : #{issue.summary} (#{label_days age})"]
|
|
79
107
|
}
|
|
@@ -83,45 +111,70 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
83
111
|
'backgroundColor' => rules.color
|
|
84
112
|
}
|
|
85
113
|
end
|
|
86
|
-
data_sets << {
|
|
87
|
-
'type' => 'bar',
|
|
88
|
-
'label' => "#{percentage}%",
|
|
89
|
-
'barPercentage' => 1.0,
|
|
90
|
-
'categoryPercentage' => 1.0,
|
|
91
|
-
'backgroundColor' => CssVariable['--aging-work-in-progress-chart-shading-color'],
|
|
92
|
-
'data' => days_at_percentage_threshold_for_all_columns(percentage: percentage, issues: @issues).drop(1)
|
|
93
|
-
}
|
|
94
|
-
end
|
|
95
114
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
|
|
116
|
+
|
|
117
|
+
column_indexes_to_remove = []
|
|
118
|
+
unless @show_all_columns
|
|
119
|
+
column_indexes_to_remove = indexes_of_leading_and_trailing_zeros(calculator.age_data_for(percentage: 100))
|
|
120
|
+
|
|
121
|
+
column_indexes_to_remove.reverse_each do |index|
|
|
122
|
+
@board_columns.delete_at index
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@row_index_offset = data_sets.size
|
|
127
|
+
|
|
128
|
+
bar_data = []
|
|
129
|
+
calculator.stacked_age_data_for(percentages: @percentiles.keys).each do |percentage, data|
|
|
130
|
+
column_indexes_to_remove.reverse_each { |index| data.delete_at index }
|
|
131
|
+
color = @percentiles[percentage]
|
|
132
|
+
|
|
133
|
+
data_sets << {
|
|
134
|
+
'type' => 'bar',
|
|
135
|
+
'label' => "#{percentage}%",
|
|
136
|
+
'barPercentage' => 1.0,
|
|
137
|
+
'categoryPercentage' => 1.0,
|
|
138
|
+
'backgroundColor' => color,
|
|
139
|
+
'data' => data
|
|
140
|
+
}
|
|
141
|
+
bar_data << data
|
|
101
142
|
end
|
|
143
|
+
@bar_data = adjust_bar_data bar_data
|
|
144
|
+
|
|
145
|
+
data_sets
|
|
102
146
|
end
|
|
103
147
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
def adjust_bar_data input
|
|
149
|
+
return [] if input.empty?
|
|
150
|
+
|
|
151
|
+
row_size = input.first.size
|
|
108
152
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
153
|
+
output = []
|
|
154
|
+
output << input.first
|
|
155
|
+
input.drop(1).each do |row|
|
|
156
|
+
previous_row = output.last
|
|
157
|
+
output << 0.upto(row_size - 1).collect { |i| row[i] + previous_row[i] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
output
|
|
112
161
|
end
|
|
113
162
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
def indexes_of_leading_and_trailing_zeros list
|
|
164
|
+
result = []
|
|
165
|
+
0.upto(list.size - 1) do |index|
|
|
166
|
+
break unless list[index].zero?
|
|
118
167
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
next if stop < start
|
|
168
|
+
result << index
|
|
169
|
+
end
|
|
122
170
|
|
|
123
|
-
|
|
171
|
+
stop_at = result.empty? ? 0 : (result.last + 1)
|
|
172
|
+
(list.size - 1).downto(stop_at).each do |index|
|
|
173
|
+
break unless list[index].zero?
|
|
174
|
+
|
|
175
|
+
result << index if list[index].zero?
|
|
124
176
|
end
|
|
177
|
+
result
|
|
125
178
|
end
|
|
126
179
|
|
|
127
180
|
def column_for issue:
|
|
@@ -130,7 +183,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
130
183
|
end
|
|
131
184
|
end
|
|
132
185
|
|
|
133
|
-
def adjust_visibility_of_unmapped_status_column data_sets
|
|
186
|
+
def adjust_visibility_of_unmapped_status_column data_sets:
|
|
134
187
|
column_name = @fake_column.name
|
|
135
188
|
|
|
136
189
|
has_unmapped = data_sets.any? do |set|
|
|
@@ -143,8 +196,19 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
143
196
|
@description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
|
|
144
197
|
'board but are still active. Most likely everyone has forgotten about them.</p>'
|
|
145
198
|
else
|
|
146
|
-
column_headings.pop
|
|
199
|
+
# @column_headings.pop
|
|
147
200
|
@board_columns.pop
|
|
148
201
|
end
|
|
149
202
|
end
|
|
203
|
+
|
|
204
|
+
def percentiles percentile_color_hash
|
|
205
|
+
@percentiles = percentile_color_hash.transform_values { |value| CssVariable[value] }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def adjust_chart_height
|
|
209
|
+
min_height = @max_age * 5
|
|
210
|
+
|
|
211
|
+
@canvas_height = min_height if min_height > @canvas_height
|
|
212
|
+
@canvas_height = 400 if min_height > 400
|
|
213
|
+
end
|
|
150
214
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
4
|
|
|
5
5
|
class AgingWorkTable < ChartBase
|
|
6
|
-
attr_accessor :today
|
|
6
|
+
attr_accessor :today
|
|
7
7
|
attr_reader :any_scrum_boards
|
|
8
8
|
|
|
9
9
|
def initialize block
|
|
@@ -22,30 +22,43 @@ class AgingWorkTable < ChartBase
|
|
|
22
22
|
If there are expedited items that haven't yet started then they're at the bottom of the table.
|
|
23
23
|
By the very definition of expedited, if we haven't started them already, we'd better get on that.
|
|
24
24
|
</p>
|
|
25
|
+
<p>
|
|
26
|
+
Legend:
|
|
27
|
+
<ul>
|
|
28
|
+
<li><b>E:</b> Whether this item is <b>E</b>xpedited.</li>
|
|
29
|
+
<li><b>B/S:</b> Whether this item is either <b>B</b>locked or <b>S</b>talled.</li>
|
|
30
|
+
<li><b>Forecast:</b> A forecast of how long it is likely to take to finish this work item.</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</p>
|
|
25
33
|
TEXT
|
|
26
34
|
|
|
27
35
|
instance_eval(&block)
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
def run
|
|
31
|
-
|
|
39
|
+
initialize_calculator
|
|
32
40
|
aging_issues = select_aging_issues + expedited_but_not_started
|
|
33
41
|
|
|
34
42
|
wrap_and_render(binding, __FILE__)
|
|
35
43
|
end
|
|
36
44
|
|
|
45
|
+
# This is its own method simply so the tests can initialize the calculator without doing a full run.
|
|
46
|
+
def initialize_calculator
|
|
47
|
+
@today = date_range.end
|
|
48
|
+
@calculator = BoardMovementCalculator.new board: current_board, issues: issues, today: @today
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
def expedited_but_not_started
|
|
38
52
|
@issues.select do |issue|
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
54
|
+
started_time.nil? && stopped_time.nil? && issue.expedited?
|
|
41
55
|
end.sort_by(&:created)
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
def select_aging_issues
|
|
45
59
|
aging_issues = @issues.select do |issue|
|
|
46
60
|
cycletime = issue.board.cycletime
|
|
47
|
-
started = cycletime.
|
|
48
|
-
stopped = cycletime.stopped_time(issue)
|
|
61
|
+
started, stopped = cycletime.started_stopped_times(issue)
|
|
49
62
|
next false if started.nil? || stopped
|
|
50
63
|
next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
|
|
51
64
|
|
|
@@ -64,7 +77,7 @@ class AgingWorkTable < ChartBase
|
|
|
64
77
|
end
|
|
65
78
|
|
|
66
79
|
def blocked_text issue
|
|
67
|
-
started_time = issue.board.cycletime.
|
|
80
|
+
started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
68
81
|
return nil if started_time.nil?
|
|
69
82
|
|
|
70
83
|
current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
|
|
@@ -115,6 +128,40 @@ class AgingWorkTable < ChartBase
|
|
|
115
128
|
end.join('<br />')
|
|
116
129
|
end
|
|
117
130
|
|
|
131
|
+
def dates_text issue
|
|
132
|
+
date = date_range.end
|
|
133
|
+
due = issue.due_date
|
|
134
|
+
message = nil
|
|
135
|
+
|
|
136
|
+
days_remaining, error = @calculator.forecasted_days_remaining_and_message issue: issue, today: @today
|
|
137
|
+
|
|
138
|
+
unless error
|
|
139
|
+
if due
|
|
140
|
+
if due < date
|
|
141
|
+
message = "Due: <b>#{due}</b> (#{label_days (@today - due).to_i} ago)"
|
|
142
|
+
error = 'Overdue'
|
|
143
|
+
elsif due == date
|
|
144
|
+
message = 'Due: <b>today</b>'
|
|
145
|
+
else
|
|
146
|
+
error = 'Due date at risk' if date_range.end + days_remaining > due
|
|
147
|
+
message = "Due: <b>#{due}</b> (#{label_days (due - @today).to_i})"
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
"#{label_days days_remaining} left."
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
text = +''
|
|
155
|
+
text << "<span title='#{error}' style='color: red'>ⓘ </span>" if error
|
|
156
|
+
if days_remaining
|
|
157
|
+
text << "#{label_days days_remaining} left"
|
|
158
|
+
else
|
|
159
|
+
text << 'Unable to forecast'
|
|
160
|
+
end
|
|
161
|
+
text << ' | ' << message if message
|
|
162
|
+
text
|
|
163
|
+
end
|
|
164
|
+
|
|
118
165
|
def age_cutoff age = nil
|
|
119
166
|
@age_cutoff = age.to_i if age
|
|
120
167
|
@age_cutoff
|
data/lib/jirametrics/board.rb
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Board
|
|
4
|
-
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :
|
|
5
|
-
attr_accessor :cycletime, :project_config
|
|
4
|
+
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
|
|
5
|
+
attr_accessor :cycletime, :project_config
|
|
6
6
|
|
|
7
|
-
def initialize raw:, possible_statuses:
|
|
7
|
+
def initialize raw:, possible_statuses:
|
|
8
8
|
@raw = raw
|
|
9
9
|
@board_type = raw['type']
|
|
10
10
|
@possible_statuses = possible_statuses
|
|
11
11
|
@sprints = []
|
|
12
|
-
@expedited_priority_names = []
|
|
13
12
|
|
|
14
13
|
columns = raw['columnConfig']['columns']
|
|
14
|
+
ensure_uniqueness_of_column_names! columns
|
|
15
15
|
|
|
16
16
|
# For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
|
|
17
17
|
# visible on the board. If the board is configured to have a kanban backlog then it will have
|
|
18
18
|
# statuses matched to it and otherwise, there will be no statuses.
|
|
19
|
-
if kanban?
|
|
20
|
-
@backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
|
|
21
|
-
# There is a status defined as being 'backlog' that is no longer being returned in statuses.
|
|
22
|
-
# We used to display a warning for this but honestly, there is nothing that anyone can do about it
|
|
23
|
-
# so now we just quietly ignore it.
|
|
24
|
-
end
|
|
25
|
-
columns = columns[1..]
|
|
26
|
-
else
|
|
27
|
-
# We currently don't know how to get the backlog status for a Scrum board
|
|
28
|
-
@backlog_statuses = []
|
|
29
|
-
end
|
|
19
|
+
columns = columns.drop(1) if kanban?
|
|
30
20
|
|
|
21
|
+
@backlog_statuses = []
|
|
31
22
|
@visible_columns = columns.filter_map do |column|
|
|
32
23
|
# It's possible for a column to be defined without any statuses and in this case, it won't be visible.
|
|
33
24
|
BoardColumn.new column unless status_ids_from_column(column).empty?
|
|
34
25
|
end
|
|
35
26
|
end
|
|
36
27
|
|
|
28
|
+
def backlog_statuses
|
|
29
|
+
if @backlog_statuses.empty? && kanban?
|
|
30
|
+
status_ids = status_ids_from_column raw['columnConfig']['columns'].first
|
|
31
|
+
@backlog_statuses = status_ids.filter_map do |id|
|
|
32
|
+
@possible_statuses.find_by_id id
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
@backlog_statuses
|
|
36
|
+
end
|
|
37
|
+
|
|
37
38
|
def server_url_prefix
|
|
38
39
|
raise "Cannot parse self: #{@raw['self'].inspect}" unless @raw['self'] =~ /^(https?:\/\/.+)\/rest\//
|
|
39
40
|
|
|
@@ -88,4 +89,32 @@ class Board
|
|
|
88
89
|
def name
|
|
89
90
|
@raw['name']
|
|
90
91
|
end
|
|
92
|
+
|
|
93
|
+
def accumulated_status_ids_per_column
|
|
94
|
+
accumulated_status_ids = []
|
|
95
|
+
visible_columns.reverse.filter_map do |column|
|
|
96
|
+
next if column == @fake_column
|
|
97
|
+
|
|
98
|
+
accumulated_status_ids += column.status_ids
|
|
99
|
+
[column.name, accumulated_status_ids.dup]
|
|
100
|
+
end.reverse
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ensure_uniqueness_of_column_names! json
|
|
104
|
+
all_names = []
|
|
105
|
+
json.each do |column_json|
|
|
106
|
+
name = column_json['name']
|
|
107
|
+
if all_names.include? name
|
|
108
|
+
(2..).each do |i|
|
|
109
|
+
new_name = "#{name}-#{i}"
|
|
110
|
+
next if all_names.include?(new_name)
|
|
111
|
+
|
|
112
|
+
name = new_name
|
|
113
|
+
column_json['name'] = new_name
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
all_names << name
|
|
118
|
+
end
|
|
119
|
+
end
|
|
91
120
|
end
|
|
@@ -11,7 +11,6 @@ class BoardConfig
|
|
|
11
11
|
|
|
12
12
|
def run
|
|
13
13
|
@board = @project_config.all_boards[id]
|
|
14
|
-
@board.expedited_priority_names = []
|
|
15
14
|
|
|
16
15
|
instance_eval(&@block)
|
|
17
16
|
end
|
|
@@ -22,10 +21,15 @@ class BoardConfig
|
|
|
22
21
|
'If so, remove it from there.'
|
|
23
22
|
end
|
|
24
23
|
|
|
25
|
-
@board.cycletime = CycleTimeConfig.new(
|
|
24
|
+
@board.cycletime = CycleTimeConfig.new(
|
|
25
|
+
parent_config: self, label: label, block: block, file_system: project_config.file_system
|
|
26
|
+
)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def expedited_priority_names *priority_names
|
|
29
|
-
|
|
30
|
+
project_config.exporter.file_system.deprecated(
|
|
31
|
+
date: '2024-09-15', message: 'Expedited priority names are now specified in settings'
|
|
32
|
+
)
|
|
33
|
+
@project_config.settings['expedited_priority_names'] = priority_names
|
|
30
34
|
end
|
|
31
35
|
end
|