jirametrics 2.2.1 → 2.4pre1
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 +13 -25
- data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +9 -26
- data/lib/jirametrics/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/board_config.rb +2 -2
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/chart_base.rb +27 -39
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +3 -16
- data/lib/jirametrics/daily_wip_chart.rb +1 -13
- data/lib/jirametrics/data_quality_report.rb +4 -1
- data/lib/jirametrics/dependency_chart.rb +1 -1
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
- data/lib/jirametrics/examples/standard_project.rb +1 -1
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +3 -3
- data/lib/jirametrics/file_config.rb +12 -8
- data/lib/jirametrics/file_system.rb +11 -2
- data/lib/jirametrics/groupable_issue_chart.rb +2 -4
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -3
- data/lib/jirametrics/html/index.erb +1 -0
- data/lib/jirametrics/html_report_config.rb +61 -74
- data/lib/jirametrics/issue.rb +129 -57
- data/lib/jirametrics/project_config.rb +13 -7
- data/lib/jirametrics/sprint_burndown.rb +11 -0
- data/lib/jirametrics/status_collection.rb +4 -1
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +1 -1
- metadata +5 -7
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b7d2b44b1c29a643b039bdb955fc41c666681c41a17ba608670ccdb9898b276
|
4
|
+
data.tar.gz: 97db49aaf2c001c685eb9a16ef7257972c8df717467e8638ea57bd619b62cf5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 704a16388b223c738d82f9f13341f23f6b40bb659506b3b6a843e05f8da7c74e1022e7a17d8077cf123bc35a28d14b53ecc5bb7c51d8978705ebea7034b78df3
|
7
|
+
data.tar.gz: 3be64f47f590cfe0011d9b9de8afc17ccf357a42a9377bec0d0729d789dcf352ffc1ed3858827268e80a85a72803a1f142702f9a73bb7dfc918941ae637a4125
|
@@ -19,14 +19,15 @@ class AggregateConfig
|
|
19
19
|
raise "#{@project_config.name}: When aggregating, you must include at least one other project"
|
20
20
|
end
|
21
21
|
|
22
|
-
# If the
|
22
|
+
# If the time range wasn't set then calculate it now
|
23
23
|
@project_config.time_range = find_time_range projects: @included_projects if @project_config.time_range.nil?
|
24
24
|
|
25
|
-
adjust_issue_links
|
25
|
+
adjust_issue_links issues: @project_config.issues
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
# IssueLinks just have a reference to the key. Walk through all of them to see if we have a full
|
29
|
+
# issue that we'd already loaded. If we do, then replace it.
|
30
|
+
def adjust_issue_links issues:
|
30
31
|
issues.each do |issue|
|
31
32
|
issue.issue_links.each do |link|
|
32
33
|
other_issue_key = link.other_issue.key
|
@@ -40,7 +41,7 @@ class AggregateConfig
|
|
40
41
|
def include_issues_from project_name
|
41
42
|
project = @project_config.exporter.project_configs.find { |p| p.name == project_name }
|
42
43
|
if project.nil?
|
43
|
-
|
44
|
+
log "Warning: Aggregated project #{@project_config.name.inspect} is attempting to load " \
|
44
45
|
"project #{project_name.inspect} but it can't be found. Is it disabled?"
|
45
46
|
return
|
46
47
|
end
|
@@ -57,30 +58,13 @@ class AggregateConfig
|
|
57
58
|
else
|
58
59
|
issues = project.file_configs.first.issues
|
59
60
|
if project.file_configs.size > 1
|
60
|
-
|
61
|
+
log 'More than one file section is defined. For the aggregated view, we always use ' \
|
61
62
|
'the first file section'
|
62
63
|
end
|
63
64
|
end
|
64
65
|
@project_config.add_issues issues
|
65
66
|
end
|
66
67
|
|
67
|
-
def date_range range
|
68
|
-
@project_config.time_range = date_range_to_time_range(
|
69
|
-
date_range: range, timezone_offset: project_config.exporter.timezone_offset
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
def date_range_to_time_range date_range:, timezone_offset:
|
74
|
-
start_of_first_day = Time.new(
|
75
|
-
date_range.begin.year, date_range.begin.month, date_range.begin.day, 0, 0, 0, timezone_offset
|
76
|
-
)
|
77
|
-
end_of_last_day = Time.new(
|
78
|
-
date_range.end.year, date_range.end.month, date_range.end.day, 23, 59, 59, timezone_offset
|
79
|
-
)
|
80
|
-
|
81
|
-
start_of_first_day..end_of_last_day
|
82
|
-
end
|
83
|
-
|
84
68
|
def find_time_range projects:
|
85
69
|
raise "Can't calculate aggregated range as no projects were included." if projects.empty?
|
86
70
|
|
@@ -92,8 +76,12 @@ class AggregateConfig
|
|
92
76
|
latest = range.end if latest.nil? || range.end > latest
|
93
77
|
end
|
94
78
|
|
95
|
-
raise "Can't calculate range" if earliest.nil? || latest.nil?
|
96
|
-
|
97
79
|
earliest..latest
|
98
80
|
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def log message
|
85
|
+
@project_config.exporter.file_system.log message
|
86
|
+
end
|
99
87
|
end
|
@@ -5,7 +5,7 @@ require 'jirametrics/chart_base'
|
|
5
5
|
class AgingWorkBarChart < ChartBase
|
6
6
|
@@next_id = 0
|
7
7
|
|
8
|
-
def initialize block
|
8
|
+
def initialize block
|
9
9
|
super()
|
10
10
|
|
11
11
|
header_text 'Aging Work Bar Chart'
|
@@ -27,61 +27,77 @@ class AgingWorkBarChart < ChartBase
|
|
27
27
|
<li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
|
28
28
|
</ol>
|
29
29
|
</p>
|
30
|
-
#{
|
30
|
+
#{describe_non_working_days}
|
31
31
|
HTML
|
32
32
|
|
33
33
|
# Because this one will size itself as needed, we start with a smaller default size
|
34
34
|
@canvas_height = 80
|
35
35
|
|
36
|
-
instance_eval(&block)
|
36
|
+
instance_eval(&block)
|
37
37
|
end
|
38
38
|
|
39
39
|
def run
|
40
|
-
aging_issues = @issues
|
41
|
-
cycletime = issue.board.cycletime
|
42
|
-
cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
|
43
|
-
end
|
44
|
-
|
45
|
-
grow_chart_height_if_too_many_issues aging_issues.size
|
40
|
+
aging_issues = select_aging_issues issues: @issues
|
46
41
|
|
47
42
|
today = date_range.end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
57
|
-
[
|
58
|
-
status_data_sets(issue: issue, label: issue_label, today: today),
|
59
|
-
blocked_data_sets(
|
60
|
-
issue: issue,
|
61
|
-
issue_label: issue_label,
|
62
|
-
stack: 'blocked',
|
63
|
-
issue_start_time: issue_start_time
|
64
|
-
),
|
65
|
-
data_set_by_block(
|
66
|
-
issue: issue,
|
67
|
-
issue_label: issue_label,
|
68
|
-
title_label: 'Expedited',
|
69
|
-
stack: 'expedited',
|
70
|
-
color: CssVariable['--expedited-color'],
|
71
|
-
start_date: issue_start_date
|
72
|
-
) { |day| issue.expedited_on_date?(day) }
|
73
|
-
].compact.flatten.each do |data|
|
74
|
-
data_sets << data
|
75
|
-
end
|
76
|
-
end
|
43
|
+
sort_by_age! issues: aging_issues, today: today
|
44
|
+
|
45
|
+
grow_chart_height_if_too_many_issues aging_issue_count: aging_issues.size
|
46
|
+
|
47
|
+
data_sets = aging_issues
|
48
|
+
.collect { |issue| data_sets_for_one_issue issue: issue, today: today }
|
49
|
+
.flatten
|
50
|
+
.compact
|
77
51
|
|
78
52
|
percentage = calculate_percent_line
|
79
53
|
percentage_line_x = date_range.end - calculate_percent_line if percentage
|
80
54
|
|
55
|
+
if aging_issues.empty?
|
56
|
+
@description_text = "<p>There is no aging work</p>"
|
57
|
+
return render_top_text(binding) #if aging_issues.empty?
|
58
|
+
end
|
59
|
+
|
81
60
|
wrap_and_render(binding, __FILE__)
|
82
61
|
end
|
83
62
|
|
84
|
-
def
|
63
|
+
def data_sets_for_one_issue issue:, today:
|
64
|
+
cycletime = issue.board.cycletime
|
65
|
+
issue_start_time = cycletime.started_time(issue)
|
66
|
+
issue_start_date = issue_start_time.to_date
|
67
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
68
|
+
[
|
69
|
+
status_data_sets(issue: issue, label: issue_label, today: today),
|
70
|
+
blocked_data_sets(
|
71
|
+
issue: issue,
|
72
|
+
issue_label: issue_label,
|
73
|
+
stack: 'blocked',
|
74
|
+
issue_start_time: issue_start_time
|
75
|
+
),
|
76
|
+
data_set_by_block(
|
77
|
+
issue: issue,
|
78
|
+
issue_label: issue_label,
|
79
|
+
title_label: 'Expedited',
|
80
|
+
stack: 'expedited',
|
81
|
+
color: CssVariable['--expedited-color'],
|
82
|
+
start_date: issue_start_date
|
83
|
+
) { |day| issue.expedited_on_date?(day) }
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
def sort_by_age! issues:, today:
|
88
|
+
issues.sort! do |a, b|
|
89
|
+
a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def select_aging_issues issues:
|
94
|
+
issues.select do |issue|
|
95
|
+
cycletime = issue.board.cycletime
|
96
|
+
cycletime.started_time(issue) && cycletime.stopped_time(issue).nil?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def grow_chart_height_if_too_many_issues aging_issue_count:
|
85
101
|
px_per_bar = 8
|
86
102
|
bars_per_issue = 3
|
87
103
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
@@ -220,6 +236,8 @@ class AgingWorkBarChart < ChartBase
|
|
220
236
|
}
|
221
237
|
end
|
222
238
|
|
239
|
+
return [] if data.empty?
|
240
|
+
|
223
241
|
{
|
224
242
|
type: 'bar',
|
225
243
|
data: data,
|
@@ -4,6 +4,7 @@ require 'jirametrics/chart_base'
|
|
4
4
|
|
5
5
|
class AgingWorkTable < ChartBase
|
6
6
|
attr_accessor :today, :board_id
|
7
|
+
attr_reader :any_scrum_boards
|
7
8
|
|
8
9
|
def initialize block
|
9
10
|
super()
|
@@ -23,20 +24,21 @@ class AgingWorkTable < ChartBase
|
|
23
24
|
</p>
|
24
25
|
TEXT
|
25
26
|
|
26
|
-
instance_eval(&block)
|
27
|
+
instance_eval(&block)
|
27
28
|
end
|
28
29
|
|
29
30
|
def run
|
30
31
|
@today = date_range.end
|
31
|
-
aging_issues = select_aging_issues
|
32
|
+
aging_issues = select_aging_issues + expedited_but_not_started
|
32
33
|
|
33
|
-
|
34
|
+
wrap_and_render(binding, __FILE__)
|
35
|
+
end
|
36
|
+
|
37
|
+
def expedited_but_not_started
|
38
|
+
@issues.select do |issue|
|
34
39
|
cycletime = issue.board.cycletime
|
35
40
|
cycletime.started_time(issue).nil? && cycletime.stopped_time(issue).nil? && issue.expedited?
|
36
|
-
end
|
37
|
-
aging_issues += expedited_but_not_started.sort_by(&:created)
|
38
|
-
|
39
|
-
wrap_and_render(binding, __FILE__)
|
41
|
+
end.sort_by(&:created)
|
40
42
|
end
|
41
43
|
|
42
44
|
def select_aging_issues
|
@@ -54,10 +56,6 @@ class AgingWorkTable < ChartBase
|
|
54
56
|
aging_issues.sort { |a, b| b.board.cycletime.age(b, today: @today) <=> a.board.cycletime.age(a, today: @today) }
|
55
57
|
end
|
56
58
|
|
57
|
-
def icon_span title:, icon:
|
58
|
-
"<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
|
59
|
-
end
|
60
|
-
|
61
59
|
def expedited_text issue
|
62
60
|
return unless issue.expedited?
|
63
61
|
|
@@ -85,13 +83,6 @@ class AgingWorkTable < ChartBase
|
|
85
83
|
end
|
86
84
|
end
|
87
85
|
|
88
|
-
def unmapped_status_text issue
|
89
|
-
icon_span(
|
90
|
-
title: "Not visible: The status #{issue.status.name.inspect} is not mapped to any column and will not be visible",
|
91
|
-
icon: ' 👀'
|
92
|
-
)
|
93
|
-
end
|
94
|
-
|
95
86
|
def fix_versions_text issue
|
96
87
|
issue.fix_versions.collect do |fix|
|
97
88
|
if fix.released?
|
@@ -124,19 +115,11 @@ class AgingWorkTable < ChartBase
|
|
124
115
|
end.join('<br />')
|
125
116
|
end
|
126
117
|
|
127
|
-
def current_status_visible? issue
|
128
|
-
issue.board.visible_columns.any? { |column| column.status_ids.include? issue.status.id }
|
129
|
-
end
|
130
|
-
|
131
118
|
def age_cutoff age = nil
|
132
119
|
@age_cutoff = age.to_i if age
|
133
120
|
@age_cutoff
|
134
121
|
end
|
135
122
|
|
136
|
-
def any_scrum_boards?
|
137
|
-
@any_scrum_boards
|
138
|
-
end
|
139
|
-
|
140
123
|
def parent_hierarchy issue
|
141
124
|
result = []
|
142
125
|
|
@@ -15,12 +15,12 @@ class BlockedStalledChange
|
|
15
15
|
@time = time
|
16
16
|
end
|
17
17
|
|
18
|
-
def blocked? = @flag || blocked_by_status? || @blocking_issue_keys
|
19
|
-
def stalled? = @stalled_days || stalled_by_status?
|
18
|
+
def blocked? = !!(@flag || blocked_by_status? || @blocking_issue_keys)
|
19
|
+
def stalled? = !!(@stalled_days || stalled_by_status?)
|
20
20
|
def active? = !blocked? && !stalled?
|
21
21
|
|
22
|
-
def blocked_by_status? = @status && @status_is_blocking
|
23
|
-
def stalled_by_status? = @status && !@status_is_blocking
|
22
|
+
def blocked_by_status? = !!(@status && @status_is_blocking)
|
23
|
+
def stalled_by_status? = !!(@status && !@status_is_blocking)
|
24
24
|
|
25
25
|
def reasons
|
26
26
|
result = []
|
@@ -35,4 +35,24 @@ class BlockedStalledChange
|
|
35
35
|
end
|
36
36
|
result.join(', ')
|
37
37
|
end
|
38
|
+
|
39
|
+
def as_symbol
|
40
|
+
if blocked?
|
41
|
+
:blocked
|
42
|
+
elsif stalled?
|
43
|
+
:stalled
|
44
|
+
else
|
45
|
+
:active
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
text = +"BlockedStalledChange(time: '#{@time}', "
|
51
|
+
if active?
|
52
|
+
text << 'Active'
|
53
|
+
else
|
54
|
+
text << reasons
|
55
|
+
end
|
56
|
+
text << ')'
|
57
|
+
end
|
38
58
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class BoardConfig
|
4
|
-
attr_reader :id, :project_config
|
4
|
+
attr_reader :id, :project_config, :board
|
5
5
|
|
6
6
|
def initialize id:, block:, project_config:
|
7
7
|
@id = id
|
@@ -26,6 +26,6 @@ class BoardConfig
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def expedited_priority_names *priority_names
|
29
|
-
@board.expedited_priority_names = priority_names
|
29
|
+
@board.expedited_priority_names = priority_names
|
30
30
|
end
|
31
31
|
end
|
@@ -5,7 +5,6 @@ class ChangeItem
|
|
5
5
|
attr_accessor :value, :old_value, :time
|
6
6
|
|
7
7
|
def initialize raw:, time:, author:, artificial: false
|
8
|
-
# raw will only ever be nil in a test and in that case field and value should be passed in
|
9
8
|
@raw = raw
|
10
9
|
@time = time
|
11
10
|
raise "Time must be an object of type Time in the correct timezone: #{@time}" if @time.is_a? String
|
@@ -36,16 +35,17 @@ class ChangeItem
|
|
36
35
|
def link? = (field == 'Link')
|
37
36
|
|
38
37
|
def to_s
|
39
|
-
message =
|
40
|
-
message
|
41
|
-
message
|
38
|
+
message = +''
|
39
|
+
message << "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: #{time_to_s(@time).inspect}"
|
40
|
+
message << ', artificial' if artificial?
|
41
|
+
message << ')'
|
42
42
|
message
|
43
43
|
end
|
44
44
|
|
45
45
|
def inspect = to_s
|
46
46
|
|
47
47
|
def == other
|
48
|
-
field.eql?(other.field) && value.eql?(other.value) && time.
|
48
|
+
field.eql?(other.field) && value.eql?(other.value) && time_to_s(time).eql?(time_to_s(other.time))
|
49
49
|
end
|
50
50
|
|
51
51
|
def current_status_matches *status_names_or_ids
|
@@ -77,4 +77,12 @@ class ChangeItem
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def time_to_s time
|
84
|
+
# MRI and JRuby return different strings for to_s() so we have to explicitly provide a full
|
85
|
+
# format so that tests work under both environments.
|
86
|
+
time.strftime '%Y-%m-%d %H:%M:%S %z'
|
87
|
+
end
|
80
88
|
end
|
@@ -2,9 +2,9 @@
|
|
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
|
5
|
+
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system
|
6
6
|
attr_writer :aggregated_project
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :canvas_width, :canvas_height
|
8
8
|
|
9
9
|
@@chart_counter = 0
|
10
10
|
|
@@ -34,7 +34,7 @@ class ChartBase
|
|
34
34
|
caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
|
35
35
|
|
36
36
|
@html_directory = "#{pathname.dirname}/html"
|
37
|
-
erb = ERB.new
|
37
|
+
erb = ERB.new file_system.load "#{@html_directory}/#{$1}.erb"
|
38
38
|
erb.result(caller_binding)
|
39
39
|
end
|
40
40
|
|
@@ -100,7 +100,7 @@ class ChartBase
|
|
100
100
|
issues_id = next_id
|
101
101
|
|
102
102
|
issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
|
103
|
-
erb = ERB.new
|
103
|
+
erb = ERB.new file_system.load "#{@html_directory}/collapsible_issues_panel.erb"
|
104
104
|
erb.result(binding)
|
105
105
|
end
|
106
106
|
|
@@ -153,17 +153,6 @@ class ChartBase
|
|
153
153
|
end
|
154
154
|
end
|
155
155
|
|
156
|
-
def sprints_in_time_range board
|
157
|
-
board.sprints.select do |sprint|
|
158
|
-
sprint_end_time = sprint.completed_time || sprint.end_time
|
159
|
-
sprint_start_time = sprint.start_time
|
160
|
-
next false if sprint_start_time.nil?
|
161
|
-
|
162
|
-
time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) ||
|
163
|
-
(sprint_start_time < time_range.begin && sprint_end_time > time_range.end)
|
164
|
-
end || []
|
165
|
-
end
|
166
|
-
|
167
156
|
def chart_format object
|
168
157
|
if object.is_a? Time
|
169
158
|
# "2022-04-09T11:38:30-07:00"
|
@@ -173,12 +162,14 @@ class ChartBase
|
|
173
162
|
end
|
174
163
|
end
|
175
164
|
|
176
|
-
def header_text text
|
177
|
-
@header_text = text
|
165
|
+
def header_text text = nil
|
166
|
+
@header_text = text if text
|
167
|
+
@header_text
|
178
168
|
end
|
179
169
|
|
180
|
-
def description_text text
|
181
|
-
@description_text = text
|
170
|
+
def description_text text = nil
|
171
|
+
@description_text = text if text
|
172
|
+
@description_text
|
182
173
|
end
|
183
174
|
|
184
175
|
def format_integer number
|
@@ -188,26 +179,35 @@ class ChartBase
|
|
188
179
|
def format_status name_or_id, board:, is_category: false
|
189
180
|
begin
|
190
181
|
statuses = board.possible_statuses.expand_statuses([name_or_id])
|
191
|
-
rescue
|
192
|
-
return "<span style='color: red'>#{name_or_id}</span>"
|
193
|
-
|
194
|
-
throw e
|
182
|
+
rescue StatusNotFoundError => e
|
183
|
+
return "<span style='color: red'>#{name_or_id}</span>"
|
195
184
|
end
|
196
|
-
raise "Expected exactly one match and got #{statuses.inspect} for #{name_or_id.inspect}" if statuses.size > 1
|
197
185
|
|
198
186
|
status = statuses.first
|
199
187
|
color = status_category_color status
|
200
188
|
|
189
|
+
visibility = ''
|
190
|
+
if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
|
191
|
+
visibility = icon_span(
|
192
|
+
title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
|
193
|
+
icon: ' 👀'
|
194
|
+
)
|
195
|
+
|
196
|
+
end
|
201
197
|
text = is_category ? status.category_name : status.name
|
202
|
-
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span
|
198
|
+
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
|
199
|
+
end
|
200
|
+
|
201
|
+
def icon_span title:, icon:
|
202
|
+
"<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>"
|
203
203
|
end
|
204
204
|
|
205
205
|
def status_category_color status
|
206
206
|
case status.category_name
|
207
|
-
when nil then 'black'
|
208
207
|
when 'To Do' then CssVariable['--status-category-todo-color']
|
209
208
|
when 'In Progress' then CssVariable['--status-category-inprogress-color']
|
210
209
|
when 'Done' then CssVariable['--status-category-done-color']
|
210
|
+
else 'black' # Theoretically impossible but seen in prod.
|
211
211
|
end
|
212
212
|
end
|
213
213
|
|
@@ -225,18 +225,6 @@ class ChartBase
|
|
225
225
|
@canvas_responsive
|
226
226
|
end
|
227
227
|
|
228
|
-
def filter_issues &block
|
229
|
-
@filter_issues_block = block
|
230
|
-
end
|
231
|
-
|
232
|
-
def issues= issues
|
233
|
-
@issues = issues
|
234
|
-
return unless @filter_issues_block
|
235
|
-
|
236
|
-
@issues = issues.filter_map { |i| @filter_issues_block.call(i) }.uniq
|
237
|
-
puts @issues.collect(&:key).join(', ')
|
238
|
-
end
|
239
|
-
|
240
228
|
def color_block color, title: nil
|
241
229
|
result = +''
|
242
230
|
result << "<div class='color_block' style='background: var(#{color});'"
|
@@ -252,5 +240,5 @@ class ChartBase
|
|
252
240
|
and any other holidays mentioned in the configuration.
|
253
241
|
</div>
|
254
242
|
TEXT
|
255
|
-
end
|
243
|
+
end
|
256
244
|
end
|
@@ -34,6 +34,10 @@ class ColumnsConfig
|
|
34
34
|
@columns << [:string, label, proc]
|
35
35
|
end
|
36
36
|
|
37
|
+
def integer label, proc
|
38
|
+
@columns << [:integer, label, proc]
|
39
|
+
end
|
40
|
+
|
37
41
|
def column_entry_times board_id: nil
|
38
42
|
@file_config.project_config.find_board_by_id(board_id).visible_columns.each do |column|
|
39
43
|
date column.name, first_time_in_status(*column.status_ids)
|
@@ -38,25 +38,12 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
38
38
|
HTML
|
39
39
|
end
|
40
40
|
|
41
|
-
def key_blocked_stalled_change issue:, date:, end_time:
|
42
|
-
stalled_change = nil
|
43
|
-
blocked_change = nil
|
44
|
-
|
45
|
-
issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
|
46
|
-
blocked_change = change if change.blocked?
|
47
|
-
stalled_change = change if change.stalled?
|
48
|
-
end
|
49
|
-
|
50
|
-
return blocked_change if blocked_change
|
51
|
-
return stalled_change if stalled_change
|
52
|
-
|
53
|
-
nil
|
54
|
-
end
|
55
|
-
|
56
41
|
def default_grouping_rules issue:, rules:
|
57
42
|
started = issue.board.cycletime.started_time(issue)
|
58
43
|
stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
|
59
|
-
|
44
|
+
|
45
|
+
date = rules.current_date
|
46
|
+
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
60
47
|
|
61
48
|
stopped_today = stopped_date == rules.current_date
|
62
49
|
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
class DailyWipChart < ChartBase
|
15
15
|
attr_accessor :possible_statuses
|
16
16
|
|
17
|
-
def initialize block
|
17
|
+
def initialize block
|
18
18
|
super()
|
19
19
|
|
20
20
|
header_text default_header_text
|
@@ -27,9 +27,6 @@ class DailyWipChart < ChartBase
|
|
27
27
|
default_grouping_rules issue: issue, rules: rules
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
31
|
-
# Because this one will size itself as needed, we start with a smaller default size
|
32
|
-
# @canvas_height = 80
|
33
30
|
end
|
34
31
|
|
35
32
|
def run
|
@@ -45,8 +42,6 @@ class DailyWipChart < ChartBase
|
|
45
42
|
end + data_sets
|
46
43
|
end
|
47
44
|
|
48
|
-
# grow_chart_height_if_too_many_data_sets data_sets.size
|
49
|
-
|
50
45
|
wrap_and_render(binding, __FILE__)
|
51
46
|
end
|
52
47
|
|
@@ -174,11 +169,4 @@ class DailyWipChart < ChartBase
|
|
174
169
|
hidden: false
|
175
170
|
}
|
176
171
|
end
|
177
|
-
|
178
|
-
def grow_chart_height_if_too_many_data_sets count
|
179
|
-
px_per_bar = 8
|
180
|
-
bars_per_issue = 0.5
|
181
|
-
preferred_height = count * px_per_bar * bars_per_issue
|
182
|
-
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
183
|
-
end
|
184
172
|
end
|
@@ -71,7 +71,10 @@ class DataQualityReport < ChartBase
|
|
71
71
|
|
72
72
|
# Return a format that's easier to assert against
|
73
73
|
def testable_entries
|
74
|
-
|
74
|
+
format = '%Y-%m-%d %H:%M:%S %z'
|
75
|
+
@entries.collect do |entry|
|
76
|
+
[entry.started&.strftime(format) || '', entry.stopped&.strftime(format) || '', entry.issue]
|
77
|
+
end
|
75
78
|
end
|
76
79
|
|
77
80
|
def entries_with_problems
|
@@ -52,7 +52,7 @@ class DependencyChart < ChartBase
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def run
|
55
|
-
instance_eval(&@rules_block)
|
55
|
+
instance_eval(&@rules_block)
|
56
56
|
|
57
57
|
dot_graph = build_dot_graph
|
58
58
|
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
|