jirametrics 2.0 → 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 +19 -26
- data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
- data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
- data/lib/jirametrics/aging_work_table.rb +78 -43
- data/lib/jirametrics/anonymizer.rb +6 -5
- data/lib/jirametrics/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/board.rb +44 -15
- data/lib/jirametrics/board_config.rb +8 -4
- data/lib/jirametrics/board_movement_calculator.rb +147 -0
- data/lib/jirametrics/change_item.rb +31 -10
- data/lib/jirametrics/chart_base.rb +102 -61
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/css_variable.rb +33 -0
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +69 -4
- data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
- data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
- data/lib/jirametrics/daily_wip_chart.rb +61 -14
- data/lib/jirametrics/data_quality_report.rb +222 -41
- data/lib/jirametrics/dependency_chart.rb +54 -23
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +76 -57
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
- data/lib/jirametrics/examples/aggregated_project.rb +22 -39
- data/lib/jirametrics/examples/standard_project.rb +25 -49
- data/lib/jirametrics/expedited_chart.rb +28 -25
- data/lib/jirametrics/exporter.rb +59 -32
- data/lib/jirametrics/file_config.rb +34 -13
- data/lib/jirametrics/file_system.rb +48 -3
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -6
- data/lib/jirametrics/grouping_rules.rb +7 -1
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
- data/lib/jirametrics/html/aging_work_table.erb +19 -25
- data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
- data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
- data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
- data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
- data/lib/jirametrics/html/expedited_chart.erb +10 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +2 -2
- data/lib/jirametrics/html/index.css +209 -0
- data/lib/jirametrics/html/index.erb +16 -39
- data/lib/jirametrics/html/sprint_burndown.erb +10 -14
- data/lib/jirametrics/html/throughput_chart.erb +10 -13
- data/lib/jirametrics/html_report_config.rb +108 -86
- data/lib/jirametrics/issue.rb +357 -96
- data/lib/jirametrics/jira_gateway.rb +29 -11
- data/lib/jirametrics/project_config.rb +256 -144
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +10 -0
- data/lib/jirametrics/sprint_burndown.rb +24 -7
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +80 -39
- data/lib/jirametrics/throughput_chart.rb +12 -4
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +25 -7
- metadata +16 -17
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'random-word'
|
|
4
|
-
require 'require_all'
|
|
5
|
-
require_all 'lib'
|
|
6
|
-
|
|
7
|
-
def to_time date
|
|
8
|
-
Time.new date.year, date.month, date.day, rand(0..23), rand(0..59), rand(0..59)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
class FakeIssue
|
|
12
|
-
@@issue_number = 1
|
|
13
|
-
attr_reader :effort, :raw, :worker
|
|
14
|
-
|
|
15
|
-
def initialize date:, type:, worker:
|
|
16
|
-
@raw = {
|
|
17
|
-
key: "FAKE-#{@@issue_number += 1}",
|
|
18
|
-
changelog: {
|
|
19
|
-
histories: []
|
|
20
|
-
},
|
|
21
|
-
fields: {
|
|
22
|
-
created: to_time(date),
|
|
23
|
-
updated: to_time(date),
|
|
24
|
-
creator: {
|
|
25
|
-
displayName: 'George Jetson'
|
|
26
|
-
},
|
|
27
|
-
issuetype: {
|
|
28
|
-
name: type
|
|
29
|
-
},
|
|
30
|
-
status: {
|
|
31
|
-
name: 'To Do',
|
|
32
|
-
id: 1,
|
|
33
|
-
statusCategory: {
|
|
34
|
-
id: 2,
|
|
35
|
-
name: 'To Do'
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
priority: {
|
|
39
|
-
name: ''
|
|
40
|
-
},
|
|
41
|
-
summary: RandomWord.phrases.next.gsub(_, ' '),
|
|
42
|
-
issuelinks: [],
|
|
43
|
-
fixVersions: []
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@workers = [worker]
|
|
48
|
-
@effort = case type
|
|
49
|
-
when 'Story'
|
|
50
|
-
[1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 6].sample
|
|
51
|
-
else
|
|
52
|
-
[1, 2, 3].sample
|
|
53
|
-
end
|
|
54
|
-
unblock
|
|
55
|
-
@done = false
|
|
56
|
-
@last_status = 'To Do'
|
|
57
|
-
@last_status_id = 1
|
|
58
|
-
change_status new_status: 'In Progress', new_status_id: 3, date: date
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def blocked? = @blocked
|
|
62
|
-
def block = @blocked = true
|
|
63
|
-
def unblock = @blocked = false
|
|
64
|
-
|
|
65
|
-
def key = @raw[:key]
|
|
66
|
-
|
|
67
|
-
def do_work date:, effort:
|
|
68
|
-
raise 'Already done' if done?
|
|
69
|
-
|
|
70
|
-
@effort -= effort
|
|
71
|
-
return unless done?
|
|
72
|
-
|
|
73
|
-
change_status new_status: 'Done', new_status_id: 5, date: date
|
|
74
|
-
# fix_change_timestamps
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def fix_change_timestamps
|
|
78
|
-
# since the timestamps have random hours, it's possible for them to be issued out of order. Sort them now
|
|
79
|
-
changes = @raw[:changelog][:histories]
|
|
80
|
-
times = [@raw[:fields][:created]] + changes.collect { |change| change[:created] }
|
|
81
|
-
times.sort!
|
|
82
|
-
|
|
83
|
-
@raw[:fields][:created] = times.shift
|
|
84
|
-
@raw[:fields][:updated] = times[-1]
|
|
85
|
-
changes.each do |change|
|
|
86
|
-
change[:created] = times.shift
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def done? = @effort <= 0
|
|
91
|
-
|
|
92
|
-
def change_status date:, new_status:, new_status_id:
|
|
93
|
-
@raw[:changelog][:histories] << {
|
|
94
|
-
author: {
|
|
95
|
-
emailAddress: 'george@jetson.com',
|
|
96
|
-
displayName: 'George Jetson'
|
|
97
|
-
},
|
|
98
|
-
created: to_time(date),
|
|
99
|
-
items: [
|
|
100
|
-
{
|
|
101
|
-
field: 'status',
|
|
102
|
-
fieldtype: 'jira',
|
|
103
|
-
fieldId: 'status',
|
|
104
|
-
from: @last_status_id,
|
|
105
|
-
fromString: @last_status,
|
|
106
|
-
to: new_status_id,
|
|
107
|
-
toString: new_status
|
|
108
|
-
}
|
|
109
|
-
]
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
@last_status = new_status
|
|
113
|
-
@last_status_id = new_status_id
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
class Worker
|
|
118
|
-
attr_accessor :issue
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
class Generator
|
|
122
|
-
def initialize
|
|
123
|
-
@random = Random.new
|
|
124
|
-
@file_prefix = 'fake'
|
|
125
|
-
@target_path = 'target/'
|
|
126
|
-
|
|
127
|
-
# @probability_work_will_be_pushed = 20
|
|
128
|
-
@probability_unblocked_work_becomes_blocked = 20
|
|
129
|
-
@probability_blocked_work_becomes_unblocked = 20
|
|
130
|
-
@date_range = (Date.today - 500)..Date.today
|
|
131
|
-
@issues = []
|
|
132
|
-
@workers = []
|
|
133
|
-
5.times { @workers << Worker.new }
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def run
|
|
137
|
-
remove_old_files
|
|
138
|
-
@date_range.each_with_index do |date, day|
|
|
139
|
-
yield date, day if block_given?
|
|
140
|
-
process_date(date, day) if (1..5).cover? date.wday # Weekday
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
@issues.each do |issue|
|
|
144
|
-
issue.fix_change_timestamps
|
|
145
|
-
File.open "target/fake_issues/#{issue.key}.json", 'w' do |file|
|
|
146
|
-
file.puts JSON.pretty_generate(issue.raw)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
File.write 'target/fake_meta.json', JSON.pretty_generate({
|
|
151
|
-
time_start: (@date_range.end - 90).to_time,
|
|
152
|
-
time_end: @date_range.end.to_time,
|
|
153
|
-
'no-download': true
|
|
154
|
-
})
|
|
155
|
-
puts "Created #{@issues.size} fake issues"
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def remove_old_files
|
|
159
|
-
path = "#{@target_path}#{@file_prefix}_issues"
|
|
160
|
-
Dir.foreach path do |file|
|
|
161
|
-
next unless file.match?(/-\d+\.json$/)
|
|
162
|
-
|
|
163
|
-
filename = "#{path}/#{file}"
|
|
164
|
-
File.unlink filename
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def lucky? probability
|
|
169
|
-
@random.rand(1..100) <= probability
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def next_issue_for worker:, date:, type:
|
|
173
|
-
# First look for something I already started
|
|
174
|
-
issue = @issues.find { |i| i.worker == worker && !i.done? && !i.blocked? }
|
|
175
|
-
|
|
176
|
-
# Then look for something that someone else started
|
|
177
|
-
issue = @issues.find { |i| i.worker != worker && !i.done? && !i.blocked? } if issue.nil? && lucky?(40)
|
|
178
|
-
|
|
179
|
-
# Then start new work
|
|
180
|
-
issue = FakeIssue.new(date: date, type: type, worker: worker) if issue.nil?
|
|
181
|
-
|
|
182
|
-
issue
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def process_date date, _simulation_day
|
|
186
|
-
@issues.each do |issue|
|
|
187
|
-
if issue.blocked?
|
|
188
|
-
issue.unblock if lucky? @probability_blocked_work_becomes_unblocked
|
|
189
|
-
elsif lucky? @probability_unblocked_work_becomes_blocked
|
|
190
|
-
issue.block
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
possible_capacities = [0, 1, 1, 1, 2]
|
|
195
|
-
@workers.each do |worker|
|
|
196
|
-
worker_capacity = possible_capacities.sample
|
|
197
|
-
if worker.issue.nil? || worker.issue.done?
|
|
198
|
-
type = lucky?(89) ? 'Story' : 'Bug'
|
|
199
|
-
worker.issue = next_issue_for worker: worker, date: date, type: type
|
|
200
|
-
@issues << worker.issue
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
worker.issue = next_issue_for worker: worker, date: date, type: type if worker.issue.blocked?
|
|
204
|
-
worker.issue.do_work date: date, effort: worker_capacity
|
|
205
|
-
worker.issue = nil if worker.issue.done?
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
Generator.new.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'require_all'
|
|
4
|
-
require_all 'lib'
|
|
5
|
-
|
|
6
|
-
class InfoDumper
|
|
7
|
-
def initialize
|
|
8
|
-
@target_dir = 'target/'
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def run key
|
|
12
|
-
find_file_prefixes.each do |prefix|
|
|
13
|
-
path = "#{@target_dir}#{prefix}_issues/#{key}.json"
|
|
14
|
-
path = "#{@target_dir}#{prefix}_issues"
|
|
15
|
-
Dir.foreach path do |file|
|
|
16
|
-
if file.match?(/^#{key}.+\.json$/)
|
|
17
|
-
issue = Issue.new raw: JSON.parse(File.read(File.join(path, file))), board: nil
|
|
18
|
-
dump issue
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def find_file_prefixes
|
|
25
|
-
prefixes = []
|
|
26
|
-
Dir.foreach @target_dir do |file|
|
|
27
|
-
prefixes << $1 if file =~ /^(.+)_issues$/
|
|
28
|
-
end
|
|
29
|
-
prefixes
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def dump issue
|
|
33
|
-
puts "#{issue.key} (#{issue.type}): #{compact_text issue.summary, 200}"
|
|
34
|
-
|
|
35
|
-
assignee = issue.raw['fields']['assignee']
|
|
36
|
-
puts " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>" unless assignee.nil?
|
|
37
|
-
|
|
38
|
-
issue.raw['fields']['issuelinks'].each do |link|
|
|
39
|
-
puts " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}" if link['outwardIssue']
|
|
40
|
-
puts " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}" if link['inwardIssue']
|
|
41
|
-
end
|
|
42
|
-
issue.changes.each do |change|
|
|
43
|
-
value = change.value
|
|
44
|
-
old_value = change.old_value
|
|
45
|
-
|
|
46
|
-
# Description fields get pretty verbose so reduce the clutter
|
|
47
|
-
if change.field == 'description' || change.field == 'summary'
|
|
48
|
-
value = compact_text value
|
|
49
|
-
old_value = compact_text old_value
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
author = change.author
|
|
53
|
-
author = "(#{author})" if author
|
|
54
|
-
message = " [change] #{change.time} [#{change.field}] "
|
|
55
|
-
message << "#{compact_text(old_value).inspect} -> " unless old_value.nil? || old_value.empty?
|
|
56
|
-
message << compact_text(value).inspect
|
|
57
|
-
message << " #{author}" if author
|
|
58
|
-
message << ' <<artificial entry>>' if change.artificial?
|
|
59
|
-
puts message
|
|
60
|
-
end
|
|
61
|
-
puts ''
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def compact_text text, max = 60
|
|
65
|
-
return nil if text.nil?
|
|
66
|
-
|
|
67
|
-
text = text.gsub(/\s+/, ' ').strip
|
|
68
|
-
text = "#{text[0..max]}..." if text.length > max
|
|
69
|
-
text
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
if __FILE__ == $PROGRAM_NAME
|
|
74
|
-
ARGV.each do |key|
|
|
75
|
-
InfoDumper.new.run key
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
problems = problems_for :discarded_changes
|
|
3
|
-
unless problems.empty?
|
|
4
|
-
%>
|
|
5
|
-
<p>
|
|
6
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> have had information discarded. This configuration is set
|
|
7
|
-
to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
|
|
8
|
-
information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b>
|
|
9
|
-
<%= collapsible_issues_panel problems %>
|
|
10
|
-
</p>
|
|
11
|
-
<%
|
|
12
|
-
end
|
|
13
|
-
%>
|
|
14
|
-
|
|
15
|
-
<%
|
|
16
|
-
problems = problems_for :completed_but_not_started
|
|
17
|
-
unless problems.empty?
|
|
18
|
-
percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
|
|
19
|
-
%>
|
|
20
|
-
<p>
|
|
21
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were discarded from all charts using cycletime (scatterplot, histogram, etc) as we couldn't determine when they started.
|
|
22
|
-
<% if percentage_work_included < 85 %>
|
|
23
|
-
Consider whether looking at only <%= percentage_work_included %>% of the total data points is enough to come to any reasonable conclusions. See <a href="https://en.wikipedia.org/wiki/Survivorship_bias">Survivorship Bias</a>.
|
|
24
|
-
<% end %>
|
|
25
|
-
<%= collapsible_issues_panel problems %>
|
|
26
|
-
</p>
|
|
27
|
-
<%
|
|
28
|
-
end
|
|
29
|
-
%>
|
|
30
|
-
|
|
31
|
-
<%
|
|
32
|
-
problems = problems_for :status_changes_after_done
|
|
33
|
-
unless problems.empty?
|
|
34
|
-
%>
|
|
35
|
-
<p>
|
|
36
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> had a status change after being identified as done. We should question whether they were really done at that point or if we stopped the clock too early.
|
|
37
|
-
<%= collapsible_issues_panel problems %>
|
|
38
|
-
</p>
|
|
39
|
-
<%
|
|
40
|
-
end
|
|
41
|
-
%>
|
|
42
|
-
|
|
43
|
-
<%
|
|
44
|
-
problems = problems_for :backwards_through_status_categories
|
|
45
|
-
unless problems.empty?
|
|
46
|
-
%>
|
|
47
|
-
<p>
|
|
48
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board, <b>crossing status categories</b>. This will almost certainly have impacted timings as the end times are often taken at status category boundaries. You should assume that any timing measurements for this item are wrong.
|
|
49
|
-
<%= collapsible_issues_panel problems %>
|
|
50
|
-
</p>
|
|
51
|
-
<%
|
|
52
|
-
end
|
|
53
|
-
%>
|
|
54
|
-
|
|
55
|
-
<%
|
|
56
|
-
problems = problems_for :backwords_through_statuses
|
|
57
|
-
unless problems.empty?
|
|
58
|
-
%>
|
|
59
|
-
<p>
|
|
60
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board. Depending where we have set the start and end points, this may give us incorrect timing data. Note that these items did not cross a status category and may not have affected metrics.
|
|
61
|
-
<%= collapsible_issues_panel problems %>
|
|
62
|
-
</p>
|
|
63
|
-
<%
|
|
64
|
-
end
|
|
65
|
-
%>
|
|
66
|
-
|
|
67
|
-
<%
|
|
68
|
-
problems = problems_for :status_not_on_board
|
|
69
|
-
unless problems.empty?
|
|
70
|
-
%>
|
|
71
|
-
<p>
|
|
72
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were not visible on the board for some period of time. This may impact timings as the work was likely to have been forgotten if it wasn't visible.
|
|
73
|
-
<%= collapsible_issues_panel problems %>
|
|
74
|
-
</p>
|
|
75
|
-
<%
|
|
76
|
-
end
|
|
77
|
-
%>
|
|
78
|
-
|
|
79
|
-
<%
|
|
80
|
-
problems = problems_for :created_in_wrong_status
|
|
81
|
-
unless problems.empty?
|
|
82
|
-
%>
|
|
83
|
-
<p>
|
|
84
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were created in a status not designated as Backlog. This will impact the measurement of start times and will therefore impact whether it's shown as in progress or not.
|
|
85
|
-
<%= collapsible_issues_panel problems %>
|
|
86
|
-
</p>
|
|
87
|
-
<%
|
|
88
|
-
end
|
|
89
|
-
%>
|
|
90
|
-
|
|
91
|
-
<%
|
|
92
|
-
problems = problems_for :stopped_before_started
|
|
93
|
-
unless problems.empty?
|
|
94
|
-
%>
|
|
95
|
-
<p>
|
|
96
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were stopped before they were started and this will play havoc with any cycletime or WIP calculations. The most common case for this is when an item gets closed and then moved back into an in-progress status.
|
|
97
|
-
<%= collapsible_issues_panel problems %>
|
|
98
|
-
</p>
|
|
99
|
-
<%
|
|
100
|
-
end
|
|
101
|
-
%>
|
|
102
|
-
|
|
103
|
-
<%
|
|
104
|
-
problems = problems_for :issue_not_started_but_subtasks_have
|
|
105
|
-
unless problems.empty?
|
|
106
|
-
%>
|
|
107
|
-
<p>
|
|
108
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> still showing 'not started' while sub-tasks underneath them have started. This is almost always a mistake; if we're working on subtasks, the top level
|
|
109
|
-
item should also have started.
|
|
110
|
-
<%= collapsible_issues_panel problems %>
|
|
111
|
-
</p>
|
|
112
|
-
<%
|
|
113
|
-
end
|
|
114
|
-
%>
|
|
115
|
-
|
|
116
|
-
<%
|
|
117
|
-
problems = problems_for :issue_on_multiple_boards
|
|
118
|
-
unless problems.empty?
|
|
119
|
-
%>
|
|
120
|
-
<p>
|
|
121
|
-
<span class="quality_note_bullet">⮕</span> For <%= label_issues problems.size %>, we have an issue that shows up on more than one board. This could result in more data points showing up on a chart then there really should be.
|
|
122
|
-
<%= collapsible_issues_panel problems, :hide_board_column %>
|
|
123
|
-
</p>
|
|
124
|
-
<%
|
|
125
|
-
end
|
|
126
|
-
%>
|