Class: DataQualityReport
Defined Under Namespace
Classes: Entry
Instance Attribute Summary collapse
Attributes inherited from ChartBase
#aggregated_project, #all_boards, #canvas_height, #canvas_width, #data_quality, #date_range, #holiday_dates, #issues, #settings, #time_range, #timezone_offset
Instance Method Summary
collapse
Methods inherited from ChartBase
#aggregated_project?, #canvas, #canvas_responsive?, #chart_format, #collapsible_issues_panel, #color_for, #completed_issues_in_range, #current_board, #daily_chart_dataset, #description_text, #filter_issues, #format_integer, #format_status, #header_text, #holidays, #label_days, #link_to_issue, #next_id, #random_color, #render, #sprints_in_time_range, #status_category_color, #wrap_and_render
Constructor Details
#initialize(original_issue_times) ⇒ DataQualityReport
Returns a new instance of DataQualityReport.
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# File 'lib/jirametrics/data_quality_report.rb', line 22
def initialize original_issue_times
super()
@original_issue_times = original_issue_times
'Data Quality Report'
description_text <<-HTML
<p>
We have a tendency to assume that anything we see in a chart is 100% accurate, although that's
not always true. To understand the accuracy of the chart, we have to understand how accurate the
initial data was and also how much of the original data set was used in the chart. This section
will hopefully give you enough information to make that decision.
</p>
HTML
end
|
Instance Attribute Details
Returns the value of attribute board_id.
5
6
7
|
# File 'lib/jirametrics/data_quality_report.rb', line 5
def board_id
@board_id
end
|
#original_issue_times ⇒ Object
For testing purposes only
4
5
6
|
# File 'lib/jirametrics/data_quality_report.rb', line 4
def original_issue_times
@original_issue_times
end
|
Instance Method Details
#category_name_for(status_name:, board:) ⇒ Object
81
82
83
|
# File 'lib/jirametrics/data_quality_report.rb', line 81
def category_name_for status_name:, board:
board.possible_statuses.find { |status| status.name == status_name }&.category_name
end
|
#entries_with_problems ⇒ Object
77
78
79
|
# File 'lib/jirametrics/data_quality_report.rb', line 77
def entries_with_problems
@entries.reject { |entry| entry.problems.empty? }
end
|
#initialize_entries ⇒ Object
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
# File 'lib/jirametrics/data_quality_report.rb', line 85
def initialize_entries
@entries = @issues.collect do |issue|
cycletime = issue.board.cycletime
started = cycletime.started_time(issue)
stopped = cycletime.stopped_time(issue)
next if stopped && stopped < time_range.begin
next if started && started > time_range.end
Entry.new started: started, stopped: stopped, issue: issue
end.compact
@entries.sort! do |a, b|
a.issue.key =~ /.+-(\d+)$/
a_id = $1.to_i
b.issue.key =~ /.+-(\d+)$/
b_id = $1.to_i
a_id <=> b_id
end
end
|
#label_issues(number) ⇒ Object
238
239
240
241
242
|
# File 'lib/jirametrics/data_quality_report.rb', line 238
def label_issues number
return '1 item' if number == 1
"#{number} items"
end
|
#problems_for(key) ⇒ Object
62
63
64
65
66
67
68
69
70
|
# File 'lib/jirametrics/data_quality_report.rb', line 62
def problems_for key
result = []
@entries.each do |entry|
entry.problems.each do |problem_key, detail|
result << [entry.issue, detail, key] if problem_key == key
end
end
result
end
|
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
# File 'lib/jirametrics/data_quality_report.rb', line 38
def run
initialize_entries
@entries.each do |entry|
board = entry.issue.board
backlog_statuses = board.backlog_statuses
scan_for_completed_issues_without_a_start_time entry: entry
scan_for_status_change_after_done entry: entry
scan_for_backwards_movement entry: entry, backlog_statuses: backlog_statuses
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
scan_for_stopped_before_started entry: entry
scan_for_issues_not_started_with_subtasks_that_have entry: entry
scan_for_discarded_data entry: entry
end
scan_for_issues_on_multiple_boards entries: @entries
entries_with_problems = entries_with_problems()
return '' if entries_with_problems.empty?
wrap_and_render(binding, __FILE__)
end
|
#scan_for_backwards_movement(entry:, backlog_statuses:) ⇒ Object
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
# File 'lib/jirametrics/data_quality_report.rb', line 143
def scan_for_backwards_movement entry:, backlog_statuses:
issue = entry.issue
last_index = -1
issue.changes.each do |change|
next unless change.status?
board = entry.issue.board
index = entry.issue.board.visible_columns.find_index { |column| column.status_ids.include? change.value_id }
if index.nil?
next if entry.issue.board.backlog_statuses.include? change.value_id
detail = "Status #{format_status change.value, board: board} is not on the board"
if issue.board.possible_statuses.expand_statuses(change.value).empty?
detail = "Status #{format_status change.value, board: board} cannot be found at all. Was it deleted?"
end
detail = nil if backlog_statuses.any? { |s| s.name == change.value }
entry.report(problem_key: :status_not_on_board, detail: detail) unless detail.nil?
elsif change.old_value.nil?
elsif index < last_index
new_category = category_name_for(status_name: change.value, board: board)
old_category = category_name_for(status_name: change.old_value, board: board)
if new_category == old_category
entry.report(
problem_key: :backwords_through_statuses,
detail: "Moved from #{format_status change.old_value, board: board}" \
" to #{format_status change.value, board: board}" \
" on #{change.time.to_date}"
)
else
entry.report(
problem_key: :backwards_through_status_categories,
detail: "Moved from #{format_status change.old_value, board: board}" \
" to #{format_status change.value, board: board}" \
" on #{change.time.to_date}, " \
" crossing from category #{format_status old_category, board: board, is_category: true}" \
" to #{format_status new_category, board: board, is_category: true}."
)
end
end
last_index = index || -1
end
end
|
#scan_for_completed_issues_without_a_start_time(entry:) ⇒ Object
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
# File 'lib/jirametrics/data_quality_report.rb', line 107
def scan_for_completed_issues_without_a_start_time entry:
return unless entry.stopped && entry.started.nil?
status_names = entry.issue.changes.collect do |change|
next unless change.status?
format_status change.value, board: entry.issue.board
end.compact
entry.report(
problem_key: :completed_but_not_started,
detail: "Status changes: #{status_names.join ' → '}"
)
end
|
#scan_for_discarded_data(entry:) ⇒ Object
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
# File 'lib/jirametrics/data_quality_report.rb', line 244
def scan_for_discarded_data entry:
hash = @original_issue_times[entry.issue]
return if hash.nil?
old_start_time = hash[:started_time]
cutoff_time = hash[:cutoff_time]
old_start_date = old_start_time.to_date
cutoff_date = cutoff_time.to_date
days_ignored = (cutoff_date - old_start_date).to_i + 1
message = "Started: #{old_start_date}, Discarded: #{cutoff_date}, Ignored: #{label_days days_ignored}"
return if days_ignored == 1
entry.report(
problem_key: :discarded_changes,
detail: message
)
end
|
#scan_for_issues_not_created_in_a_backlog_status(entry:, backlog_statuses:) ⇒ Object
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
# File 'lib/jirametrics/data_quality_report.rb', line 194
def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
return if backlog_statuses.empty?
creation_change = entry.issue.changes.find { |issue| issue.status? }
return if backlog_statuses.any? { |status| status.id == creation_change.value_id }
status_string = backlog_statuses.collect { |s| format_status s.name, board: entry.issue.board }.join(', ')
entry.report(
problem_key: :created_in_wrong_status,
detail: "Created in #{format_status creation_change.value, board: entry.issue.board}, " \
"which is not one of the backlog statuses for this board: #{status_string}"
)
end
|
#scan_for_issues_not_started_with_subtasks_that_have(entry:) ⇒ Object
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
|
# File 'lib/jirametrics/data_quality_report.rb', line 218
def scan_for_issues_not_started_with_subtasks_that_have entry:
return if entry.started
started_subtasks = []
entry.issue.subtasks.each do |subtask|
started_subtasks << subtask if subtask.board.cycletime.started_time(subtask)
end
return if started_subtasks.empty?
subtask_labels = started_subtasks.collect do |subtask|
"Started subtask: #{link_to_issue(subtask)} (#{format_status subtask.status.name, board: entry.issue.board}) " \
"#{subtask.summary[..50].inspect}"
end
entry.report(
problem_key: :issue_not_started_but_subtasks_have,
detail: subtask_labels.join('<br />')
)
end
|
#scan_for_issues_on_multiple_boards(entries:) ⇒ Object
266
267
268
269
270
271
272
273
274
275
276
277
|
# File 'lib/jirametrics/data_quality_report.rb', line 266
def scan_for_issues_on_multiple_boards entries:
grouped_entries = entries.group_by { |entry| entry.issue.key }
grouped_entries.each_value do |entry_list|
next if entry_list.size == 1
board_names = entry_list.collect { |entry| entry.issue.board.name.inspect }
entry_list.first.report(
problem_key: :issue_on_multiple_boards,
detail: "Found on boards: #{board_names.join(', ')}"
)
end
end
|
#scan_for_status_change_after_done(entry:) ⇒ Object
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
# File 'lib/jirametrics/data_quality_report.rb', line 122
def scan_for_status_change_after_done entry:
return unless entry.stopped
changes_after_done = entry.issue.changes.select do |change|
change.status? && change.time >= entry.stopped
end
done_status = changes_after_done.shift.value
return if changes_after_done.empty?
board = entry.issue.board
problem = "Completed on #{entry.stopped.to_date} with status #{format_status done_status, board: board}."
changes_after_done.each do |change|
problem << " Changed to #{format_status change.value, board: board} on #{change.time.to_date}."
end
entry.report(
problem_key: :status_changes_after_done,
detail: problem
)
end
|
#scan_for_stopped_before_started(entry:) ⇒ Object
209
210
211
212
213
214
215
216
|
# File 'lib/jirametrics/data_quality_report.rb', line 209
def scan_for_stopped_before_started entry:
return unless entry.stopped && entry.started && entry.stopped < entry.started
entry.report(
problem_key: :stopped_before_started,
detail: "The stopped time '#{entry.stopped}' is before the started time '#{entry.started}'"
)
end
|
#testable_entries ⇒ Object
Return a format that’s easier to assert against
73
74
75
|
# File 'lib/jirametrics/data_quality_report.rb', line 73
def testable_entries
@entries.collect { |entry| [entry.started.to_s, entry.stopped.to_s, entry.issue] }
end
|