jirametrics 2.7 → 2.8
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 +3 -3
- data/lib/jirametrics/aging_work_bar_chart.rb +1 -1
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/board.rb +1 -1
- data/lib/jirametrics/change_item.rb +11 -5
- data/lib/jirametrics/chart_base.rb +22 -14
- data/lib/jirametrics/cycletime_config.rb +28 -2
- data/lib/jirametrics/cycletime_histogram.rb +2 -0
- data/lib/jirametrics/data_quality_report.rb +126 -6
- data/lib/jirametrics/download_config.rb +2 -2
- data/lib/jirametrics/downloader.rb +44 -11
- data/lib/jirametrics/examples/aggregated_project.rb +2 -5
- data/lib/jirametrics/examples/standard_project.rb +2 -4
- data/lib/jirametrics/expedited_chart.rb +7 -7
- data/lib/jirametrics/exporter.rb +2 -2
- data/lib/jirametrics/file_config.rb +2 -2
- data/lib/jirametrics/file_system.rb +18 -2
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -3
- data/lib/jirametrics/html/index.css +17 -3
- data/lib/jirametrics/html/index.erb +0 -3
- data/lib/jirametrics/html_report_config.rb +14 -0
- data/lib/jirametrics/issue.rb +47 -32
- data/lib/jirametrics/project_config.rb +143 -97
- data/lib/jirametrics/sprint_burndown.rb +1 -1
- data/lib/jirametrics/status.rb +61 -25
- data/lib/jirametrics/status_collection.rb +38 -5
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +7 -0
- metadata +5 -6
- data/lib/jirametrics/html/data_quality_report.erb +0 -138
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5bec8084c47e2383d25676b969891c6fc8e427d06427a1caf195378a830ff4f
|
|
4
|
+
data.tar.gz: e9364c2b43f66d90a651f50e115751c144f8dfc6f7d247b47dfd17495bdf167e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82c740fa8d23565eb33edf1e244cde1b8b8ef2d88f753091fd0f3cfb7e20aa67c32fc22dca70d4076c25ad40898a0872c13f8af14173a26b33054faf8838df14
|
|
7
|
+
data.tar.gz: 66d5db7495165aa4ac0ea36acd27e1b77d2d4b4d4ebb7912fbbffee57c9c160b8cee3fc919884564370db51618438d24fc6fde5e47e2343560aca52b53917d4b
|
|
@@ -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
|
|
@@ -86,7 +86,7 @@ class AggregateConfig
|
|
|
86
86
|
|
|
87
87
|
private
|
|
88
88
|
|
|
89
|
-
def
|
|
90
|
-
@project_config.exporter.file_system
|
|
89
|
+
def file_system
|
|
90
|
+
@project_config.exporter.file_system
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -116,7 +116,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
116
116
|
issue.changes.each do |change|
|
|
117
117
|
next unless change.status?
|
|
118
118
|
|
|
119
|
-
status = issue.
|
|
119
|
+
status = issue.find_status_by_id change.value_id, name: change.value
|
|
120
120
|
|
|
121
121
|
unless previous_start.nil? || previous_start < issue_started_time
|
|
122
122
|
hash = {
|
|
@@ -113,7 +113,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
113
113
|
|
|
114
114
|
def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
|
|
115
115
|
issues.filter_map do |issue|
|
|
116
|
-
stop = issue.first_time_in_status(*status_ids)
|
|
116
|
+
stop = issue.first_time_in_status(*status_ids)&.to_time
|
|
117
117
|
start, = issue.board.cycletime.started_stopped_times(issue)
|
|
118
118
|
|
|
119
119
|
# Skip if either it hasn't crossed the boundary or we can't tell when it started.
|
data/lib/jirametrics/board.rb
CHANGED
|
@@ -4,7 +4,7 @@ class Board
|
|
|
4
4
|
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
|
|
5
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
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ChangeItem
|
|
4
|
-
attr_reader :field, :value_id, :old_value_id, :raw, :author
|
|
5
|
-
attr_accessor :value, :old_value
|
|
4
|
+
attr_reader :field, :value_id, :old_value_id, :raw, :author, :time
|
|
5
|
+
attr_accessor :value, :old_value
|
|
6
6
|
|
|
7
7
|
def initialize raw:, time:, author:, artificial: false
|
|
8
8
|
@raw = raw
|
|
9
9
|
@time = time
|
|
10
|
-
raise
|
|
10
|
+
raise 'ChangeItem.new() time cannot be nil' if time.nil?
|
|
11
|
+
raise "Time must be an object of type Time in the correct timezone: #{@time.inspect}" unless @time.is_a? Time
|
|
11
12
|
|
|
12
|
-
@field =
|
|
13
|
-
@value =
|
|
13
|
+
@field = @raw['field']
|
|
14
|
+
@value = @raw['toString']
|
|
14
15
|
@value_id = @raw['to'].to_i
|
|
15
16
|
@old_value = @raw['fromString']
|
|
16
17
|
@old_value_id = @raw['from']&.to_i
|
|
@@ -34,6 +35,11 @@ class ChangeItem
|
|
|
34
35
|
|
|
35
36
|
def link? = (field == 'Link')
|
|
36
37
|
|
|
38
|
+
def labels? = (field == 'labels')
|
|
39
|
+
|
|
40
|
+
# An alias for time so that logic accepting a Time, Date, or ChangeItem can all respond to :to_time
|
|
41
|
+
def to_time = @time
|
|
42
|
+
|
|
37
43
|
def to_s
|
|
38
44
|
message = +''
|
|
39
45
|
message << "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: #{time_to_s(@time).inspect}"
|
|
@@ -25,6 +25,14 @@ class ChartBase
|
|
|
25
25
|
@aggregated_project
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def html_directory
|
|
29
|
+
pathname = Pathname.new(File.realpath(__FILE__))
|
|
30
|
+
# basename = pathname.basename.to_s
|
|
31
|
+
# raise "Unexpected filename #{basename.inspect}" unless basename.match?(/^(.+)\.rb$/)
|
|
32
|
+
|
|
33
|
+
"#{pathname.dirname}/html"
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
def render caller_binding, file
|
|
29
37
|
pathname = Pathname.new(File.realpath(file))
|
|
30
38
|
basename = pathname.basename.to_s
|
|
@@ -33,8 +41,8 @@ class ChartBase
|
|
|
33
41
|
# Insert a incrementing chart_id so that all the chart names on the page are unique
|
|
34
42
|
caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
|
|
35
43
|
|
|
36
|
-
@html_directory = "#{pathname.dirname}/html"
|
|
37
|
-
erb = ERB.new file_system.load "#{
|
|
44
|
+
# @html_directory = "#{pathname.dirname}/html"
|
|
45
|
+
erb = ERB.new file_system.load "#{html_directory}/#{$1}.erb"
|
|
38
46
|
erb.result(caller_binding)
|
|
39
47
|
end
|
|
40
48
|
|
|
@@ -100,7 +108,7 @@ class ChartBase
|
|
|
100
108
|
issues_id = next_id
|
|
101
109
|
|
|
102
110
|
issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
|
|
103
|
-
erb = ERB.new file_system.load
|
|
111
|
+
erb = ERB.new file_system.load File.join(html_directory, 'collapsible_issues_panel.erb')
|
|
104
112
|
erb.result(binding)
|
|
105
113
|
end
|
|
106
114
|
|
|
@@ -161,13 +169,13 @@ class ChartBase
|
|
|
161
169
|
end
|
|
162
170
|
end
|
|
163
171
|
|
|
164
|
-
def header_text text =
|
|
165
|
-
@header_text = text
|
|
172
|
+
def header_text text = :none
|
|
173
|
+
@header_text = text unless text == :none
|
|
166
174
|
@header_text
|
|
167
175
|
end
|
|
168
176
|
|
|
169
|
-
def description_text text =
|
|
170
|
-
@description_text = text
|
|
177
|
+
def description_text text = :none
|
|
178
|
+
@description_text = text unless text == :none
|
|
171
179
|
@description_text
|
|
172
180
|
end
|
|
173
181
|
|
|
@@ -192,8 +200,8 @@ class ChartBase
|
|
|
192
200
|
icon: ' 👀'
|
|
193
201
|
)
|
|
194
202
|
end
|
|
195
|
-
text = is_category ? status.
|
|
196
|
-
"<span title='Category: #{status.
|
|
203
|
+
text = is_category ? status.category.name : status.name
|
|
204
|
+
"<span title='Category: #{status.category.name}'>#{color_block color.name} #{text}</span>#{visibility}"
|
|
197
205
|
end
|
|
198
206
|
|
|
199
207
|
def icon_span title:, icon:
|
|
@@ -201,11 +209,11 @@ class ChartBase
|
|
|
201
209
|
end
|
|
202
210
|
|
|
203
211
|
def status_category_color status
|
|
204
|
-
case status.
|
|
205
|
-
when '
|
|
206
|
-
when '
|
|
207
|
-
when '
|
|
208
|
-
else '
|
|
212
|
+
case status.category.key
|
|
213
|
+
when 'new' then CssVariable['--status-category-todo-color']
|
|
214
|
+
when 'indeterminate' then CssVariable['--status-category-inprogress-color']
|
|
215
|
+
when 'done' then CssVariable['--status-category-done-color']
|
|
216
|
+
else CssVariable['--status-category-unknown-color'] # Theoretically impossible but seen in prod.
|
|
209
217
|
end
|
|
210
218
|
end
|
|
211
219
|
|
|
@@ -44,18 +44,44 @@ class CycleTimeConfig
|
|
|
44
44
|
started_stopped_times(issue).last
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def fabricate_change_item time
|
|
48
|
+
deprecated date: '2024-12-16', message: 'This method should now return a ChangeItem not a Time', depth: 4
|
|
49
|
+
raw = {
|
|
50
|
+
'field' => 'Fabricated change',
|
|
51
|
+
'to' => '0',
|
|
52
|
+
'toString' => '',
|
|
53
|
+
'from' => '0',
|
|
54
|
+
'fromString' => ''
|
|
55
|
+
}
|
|
56
|
+
ChangeItem.new raw: raw, time: time, author: 'unknown', artificial: true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def started_stopped_changes issue
|
|
48
60
|
started = @start_at.call(issue)
|
|
49
61
|
stopped = @stop_at.call(issue)
|
|
50
62
|
|
|
63
|
+
# Obscure edge case where some of the start_at and stop_at blocks might return false in place of nil.
|
|
64
|
+
# If they are false then explicitly make them nil.
|
|
65
|
+
started ||= nil
|
|
66
|
+
stopped ||= nil
|
|
67
|
+
|
|
68
|
+
# These are only here for backwards compatibility. Hopefully nobody will ever need them.
|
|
69
|
+
started = fabricate_change_item(started) if !started.nil? && !started.is_a?(ChangeItem)
|
|
70
|
+
stopped = fabricate_change_item(stopped) if !stopped.nil? && !stopped.is_a?(ChangeItem)
|
|
71
|
+
|
|
51
72
|
# In the case where started and stopped are exactly the same time, we pretend that
|
|
52
73
|
# it just stopped and never started. This allows us to have logic like 'in or right of'
|
|
53
74
|
# for the start and not have it conflict.
|
|
54
|
-
started = nil if started == stopped
|
|
75
|
+
started = nil if started&.time == stopped&.time
|
|
55
76
|
|
|
56
77
|
[started, stopped]
|
|
57
78
|
end
|
|
58
79
|
|
|
80
|
+
def started_stopped_times issue
|
|
81
|
+
started, stopped = started_stopped_changes(issue)
|
|
82
|
+
[started&.time, stopped&.time]
|
|
83
|
+
end
|
|
84
|
+
|
|
59
85
|
def started_stopped_dates issue
|
|
60
86
|
started_time, stopped_time = started_stopped_times(issue)
|
|
61
87
|
[started_time&.to_date, stopped_time&.to_date]
|
|
@@ -57,7 +57,25 @@ class DataQualityReport < ChartBase
|
|
|
57
57
|
entries_with_problems = entries_with_problems()
|
|
58
58
|
return '' if entries_with_problems.empty?
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
caller_binding = binding
|
|
61
|
+
result = +''
|
|
62
|
+
result << render_top_text(caller_binding)
|
|
63
|
+
|
|
64
|
+
result << '<ul class="quality_report">'
|
|
65
|
+
result << render_problem_type(:discarded_changes)
|
|
66
|
+
result << render_problem_type(:completed_but_not_started)
|
|
67
|
+
result << render_problem_type(:status_changes_after_done)
|
|
68
|
+
result << render_problem_type(:backwards_through_status_categories)
|
|
69
|
+
result << render_problem_type(:backwords_through_statuses)
|
|
70
|
+
result << render_problem_type(:status_not_on_board)
|
|
71
|
+
result << render_problem_type(:created_in_wrong_status)
|
|
72
|
+
result << render_problem_type(:stopped_before_started)
|
|
73
|
+
result << render_problem_type(:issue_not_started_but_subtasks_have)
|
|
74
|
+
result << render_problem_type(:incomplete_subtasks_when_issue_done)
|
|
75
|
+
result << render_problem_type(:issue_on_multiple_boards)
|
|
76
|
+
result << '</ul>'
|
|
77
|
+
|
|
78
|
+
result
|
|
61
79
|
end
|
|
62
80
|
|
|
63
81
|
def problems_for key
|
|
@@ -70,6 +88,18 @@ class DataQualityReport < ChartBase
|
|
|
70
88
|
result
|
|
71
89
|
end
|
|
72
90
|
|
|
91
|
+
def render_problem_type problem_key
|
|
92
|
+
problems = problems_for problem_key
|
|
93
|
+
return '' if problems.empty?
|
|
94
|
+
|
|
95
|
+
<<-HTML
|
|
96
|
+
<li>
|
|
97
|
+
#{__send__ :"render_#{problem_key}", problems}
|
|
98
|
+
#{collapsible_issues_panel problems}
|
|
99
|
+
</li>
|
|
100
|
+
HTML
|
|
101
|
+
end
|
|
102
|
+
|
|
73
103
|
# Return a format that's easier to assert against
|
|
74
104
|
def testable_entries
|
|
75
105
|
format = '%Y-%m-%d %H:%M:%S %z'
|
|
@@ -82,8 +112,8 @@ class DataQualityReport < ChartBase
|
|
|
82
112
|
@entries.reject { |entry| entry.problems.empty? }
|
|
83
113
|
end
|
|
84
114
|
|
|
85
|
-
def category_name_for
|
|
86
|
-
board.possible_statuses.
|
|
115
|
+
def category_name_for status_id:, board:
|
|
116
|
+
board.possible_statuses.find_by_id(status_id)&.category&.name
|
|
87
117
|
end
|
|
88
118
|
|
|
89
119
|
def initialize_entries
|
|
@@ -154,7 +184,7 @@ class DataQualityReport < ChartBase
|
|
|
154
184
|
index = entry.issue.board.visible_columns.find_index { |column| column.status_ids.include? change.value_id }
|
|
155
185
|
if index.nil?
|
|
156
186
|
# If it's a backlog status then ignore it. Not supposed to be visible.
|
|
157
|
-
next if entry.issue.board.backlog_statuses.include?
|
|
187
|
+
next if entry.issue.board.backlog_statuses.include?(board.possible_statuses.find_by_id(change.value_id))
|
|
158
188
|
|
|
159
189
|
detail = "Status #{format_status change.value, board: board} is not on the board"
|
|
160
190
|
if issue.board.possible_statuses.expand_statuses(change.value).empty?
|
|
@@ -168,8 +198,8 @@ class DataQualityReport < ChartBase
|
|
|
168
198
|
elsif change.old_value.nil?
|
|
169
199
|
# Do nothing
|
|
170
200
|
elsif index < last_index
|
|
171
|
-
new_category = category_name_for(
|
|
172
|
-
old_category = category_name_for(
|
|
201
|
+
new_category = category_name_for(status_id: change.value_id, board: board)
|
|
202
|
+
old_category = category_name_for(status_id: change.old_value_id, board: board)
|
|
173
203
|
|
|
174
204
|
if new_category == old_category
|
|
175
205
|
entry.report(
|
|
@@ -317,4 +347,94 @@ class DataQualityReport < ChartBase
|
|
|
317
347
|
)
|
|
318
348
|
end
|
|
319
349
|
end
|
|
350
|
+
|
|
351
|
+
def render_discarded_changes problems
|
|
352
|
+
<<-HTML
|
|
353
|
+
#{label_issues problems.size} have had information discarded. This configuration is set
|
|
354
|
+
to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
|
|
355
|
+
information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b> HTML
|
|
356
|
+
HTML
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def render_completed_but_not_started problems
|
|
360
|
+
percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
|
|
361
|
+
html = <<-HTML
|
|
362
|
+
#{label_issues problems.size} were discarded from all charts using cycletime (scatterplot, histogram, etc)
|
|
363
|
+
as we couldn't determine when they started.
|
|
364
|
+
HTML
|
|
365
|
+
if percentage_work_included < 85
|
|
366
|
+
html << <<-HTML
|
|
367
|
+
Consider whether looking at only #{percentage_work_included}% of the total data points is enough
|
|
368
|
+
to come to any reasonable conclusions. See <a href="https://unconsciousagile.com/2024/11/19/survivor-bias.html">
|
|
369
|
+
Survivor Bias</a>.
|
|
370
|
+
HTML
|
|
371
|
+
end
|
|
372
|
+
html
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def render_status_changes_after_done problems
|
|
376
|
+
<<-HTML
|
|
377
|
+
#{label_issues problems.size} had a status change after being identified as done. We should question
|
|
378
|
+
whether they were really done at that point or if we stopped the clock too early.
|
|
379
|
+
HTML
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def render_backwards_through_status_categories problems
|
|
383
|
+
<<-HTML
|
|
384
|
+
#{label_issues problems.size} moved backwards across the board, <b>crossing status categories</b>.
|
|
385
|
+
This will almost certainly have impacted timings as the end times are often taken at status category
|
|
386
|
+
boundaries. You should assume that any timing measurements for this item are wrong.
|
|
387
|
+
HTML
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def render_backwords_through_statuses problems
|
|
391
|
+
<<-HTML
|
|
392
|
+
#{label_issues problems.size} moved backwards across the board. Depending where we have set the
|
|
393
|
+
start and end points, this may give us incorrect timing data. Note that these items did not cross
|
|
394
|
+
a status category and may not have affected metrics.
|
|
395
|
+
HTML
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def render_status_not_on_board problems
|
|
399
|
+
<<-HTML
|
|
400
|
+
#{label_issues problems.size} were not visible on the board for some period of time. This may impact
|
|
401
|
+
timings as the work was likely to have been forgotten if it wasn't visible.
|
|
402
|
+
HTML
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def render_created_in_wrong_status problems
|
|
406
|
+
<<-HTML
|
|
407
|
+
#{label_issues problems.size} were created in a status not designated as Backlog. This will impact
|
|
408
|
+
the measurement of start times and will therefore impact whether it's shown as in progress or not.
|
|
409
|
+
HTML
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def render_stopped_before_started problems
|
|
413
|
+
<<-HTML
|
|
414
|
+
#{label_issues problems.size} were stopped before they were started and this will play havoc with
|
|
415
|
+
any cycletime or WIP calculations. The most common case for this is when an item gets closed and
|
|
416
|
+
then moved back into an in-progress status.
|
|
417
|
+
HTML
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def render_issue_not_started_but_subtasks_have problems
|
|
421
|
+
<<-HTML
|
|
422
|
+
#{label_issues problems.size} still showing 'not started' while sub-tasks underneath them have
|
|
423
|
+
started. This is almost always a mistake; if we're working on subtasks, the top level item should
|
|
424
|
+
also have started.
|
|
425
|
+
HTML
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def render_incomplete_subtasks_when_issue_done problems
|
|
429
|
+
<<-HTML
|
|
430
|
+
#{label_issues problems.size} issues were marked as done while subtasks were still not done.
|
|
431
|
+
HTML
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def render_issue_on_multiple_boards problems
|
|
435
|
+
<<-HTML
|
|
436
|
+
For #{label_issues problems.size}, we have an issue that shows up on more than one board. This
|
|
437
|
+
could result in more data points showing up on a chart then there really should be.
|
|
438
|
+
HTML
|
|
439
|
+
end
|
|
320
440
|
end
|
|
@@ -20,8 +20,8 @@ class DownloadConfig
|
|
|
20
20
|
@rolling_date_count
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def no_earlier_than date =
|
|
24
|
-
@no_earlier_than = Date.parse(date) unless date
|
|
23
|
+
def no_earlier_than date = :not_set
|
|
24
|
+
@no_earlier_than = Date.parse(date) unless date == :not_set
|
|
25
25
|
@no_earlier_than
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -39,6 +39,7 @@ class Downloader
|
|
|
39
39
|
# board_ids = @download_config.board_ids
|
|
40
40
|
|
|
41
41
|
remove_old_files
|
|
42
|
+
update_status_history_file
|
|
42
43
|
download_statuses
|
|
43
44
|
find_board_ids.each do |id|
|
|
44
45
|
board = download_board_configuration board_id: id
|
|
@@ -66,7 +67,7 @@ class Downloader
|
|
|
66
67
|
|
|
67
68
|
def download_issues board:
|
|
68
69
|
log " Downloading primary issues for board #{board.id}", both: true
|
|
69
|
-
path =
|
|
70
|
+
path = File.join(@target_path, "#{file_prefix}_issues/")
|
|
70
71
|
unless Dir.exist?(path)
|
|
71
72
|
log " Creating path #{path}"
|
|
72
73
|
Dir.mkdir(path)
|
|
@@ -153,30 +154,58 @@ class Downloader
|
|
|
153
154
|
|
|
154
155
|
@file_system.save_json(
|
|
155
156
|
json: json,
|
|
156
|
-
filename:
|
|
157
|
+
filename: File.join(@target_path, "#{file_prefix}_statuses.json")
|
|
157
158
|
)
|
|
158
159
|
end
|
|
159
160
|
|
|
161
|
+
def update_status_history_file
|
|
162
|
+
status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
|
|
163
|
+
return unless file_system.file_exist? status_filename
|
|
164
|
+
|
|
165
|
+
status_json = file_system.load_json(status_filename)
|
|
166
|
+
|
|
167
|
+
history_filename = File.join(@target_path, "#{file_prefix}_status_history.json")
|
|
168
|
+
history_json = file_system.load_json(history_filename) if file_system.file_exist? history_filename
|
|
169
|
+
|
|
170
|
+
if history_json
|
|
171
|
+
file_system.log ' Updating status history file', also_write_to_stderr: true
|
|
172
|
+
else
|
|
173
|
+
file_system.log ' Creating status history file', also_write_to_stderr: true
|
|
174
|
+
history_json = []
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
status_json.each do |status_item|
|
|
178
|
+
id = status_item['id']
|
|
179
|
+
history_item = history_json.find { |s| s['id'] == id }
|
|
180
|
+
history_json.delete(history_item) if history_item
|
|
181
|
+
history_json << status_item
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
file_system.save_json(filename: history_filename, json: history_json)
|
|
185
|
+
end
|
|
186
|
+
|
|
160
187
|
def download_board_configuration board_id:
|
|
161
188
|
log " Downloading board configuration for board #{board_id}", both: true
|
|
162
189
|
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
|
|
163
190
|
|
|
164
191
|
exit_if_call_failed json
|
|
165
192
|
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
@file_system.save_json(
|
|
194
|
+
json: json,
|
|
195
|
+
filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_configuration.json")
|
|
196
|
+
)
|
|
168
197
|
|
|
169
198
|
# We have a reported bug that blew up on this line. Moved it after the save so we can
|
|
170
199
|
# actually look at the returned json.
|
|
171
200
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
|
172
201
|
|
|
173
202
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
|
174
|
-
|
|
203
|
+
# TODO: Should be passing actual statuses, not empty list
|
|
204
|
+
Board.new raw: json, possible_statuses: StatusCollection.new
|
|
175
205
|
end
|
|
176
206
|
|
|
177
207
|
def download_sprints board_id:
|
|
178
208
|
log " Downloading sprints for board #{board_id}", both: true
|
|
179
|
-
file_prefix = @download_config.project_config.file_prefix
|
|
180
209
|
max_results = 100
|
|
181
210
|
start_at = 0
|
|
182
211
|
is_last = false
|
|
@@ -188,7 +217,7 @@ class Downloader
|
|
|
188
217
|
|
|
189
218
|
@file_system.save_json(
|
|
190
219
|
json: json,
|
|
191
|
-
filename:
|
|
220
|
+
filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json")
|
|
192
221
|
)
|
|
193
222
|
is_last = json['isLast']
|
|
194
223
|
max_results = json['maxResults']
|
|
@@ -201,7 +230,7 @@ class Downloader
|
|
|
201
230
|
end
|
|
202
231
|
|
|
203
232
|
def metadata_pathname
|
|
204
|
-
|
|
233
|
+
File.join(@target_path, "#{file_prefix}_meta.json")
|
|
205
234
|
end
|
|
206
235
|
|
|
207
236
|
def load_metadata
|
|
@@ -244,17 +273,17 @@ class Downloader
|
|
|
244
273
|
end
|
|
245
274
|
|
|
246
275
|
def remove_old_files
|
|
247
|
-
file_prefix = @download_config.project_config.file_prefix
|
|
248
276
|
Dir.foreach @target_path do |file|
|
|
249
277
|
next unless file.match?(/^#{file_prefix}_\d+\.json$/)
|
|
278
|
+
next if file == "#{file_prefix}_status_history.json"
|
|
250
279
|
|
|
251
|
-
File.unlink
|
|
280
|
+
File.unlink File.join(@target_path, file)
|
|
252
281
|
end
|
|
253
282
|
|
|
254
283
|
return if @cached_data_format_is_current
|
|
255
284
|
|
|
256
285
|
# Also throw away all the previously downloaded issues.
|
|
257
|
-
path = File.join
|
|
286
|
+
path = File.join(@target_path, "#{file_prefix}_issues")
|
|
258
287
|
return unless File.exist? path
|
|
259
288
|
|
|
260
289
|
Dir.foreach path do |file|
|
|
@@ -292,4 +321,8 @@ class Downloader
|
|
|
292
321
|
|
|
293
322
|
segments.join ' AND '
|
|
294
323
|
end
|
|
324
|
+
|
|
325
|
+
def file_prefix
|
|
326
|
+
@download_config.project_config.get_file_prefix
|
|
327
|
+
end
|
|
295
328
|
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
|
4
4
|
# as a complete setup that will work in every case.
|
|
5
5
|
#
|
|
6
|
-
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more details
|
|
7
|
-
#
|
|
8
6
|
# The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a
|
|
9
7
|
# S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a
|
|
10
8
|
# single team. For that reason, we look at slightly different things that we would on a single team board.
|
|
@@ -13,6 +11,7 @@ class Exporter
|
|
|
13
11
|
def aggregated_project name:, project_names:, settings: {}
|
|
14
12
|
project name: name do
|
|
15
13
|
puts name
|
|
14
|
+
file_prefix name
|
|
16
15
|
self.settings.merge! settings
|
|
17
16
|
|
|
18
17
|
aggregate do
|
|
@@ -21,8 +20,6 @@ class Exporter
|
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
file_prefix name
|
|
25
|
-
|
|
26
23
|
file do
|
|
27
24
|
file_suffix '.html'
|
|
28
25
|
issues.reject! do |issue|
|
|
@@ -34,7 +31,7 @@ class Exporter
|
|
|
34
31
|
board_lines = []
|
|
35
32
|
included_projects.each do |project|
|
|
36
33
|
project.all_boards.each_value do |board|
|
|
37
|
-
board_lines << "<a href='#{project.
|
|
34
|
+
board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}"
|
|
38
35
|
end
|
|
39
36
|
end
|
|
40
37
|
board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
|
4
4
|
# as a complete setup that will work in every case.
|
|
5
|
-
#
|
|
6
|
-
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
|
|
7
5
|
class Exporter
|
|
8
6
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
|
9
7
|
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
|
|
@@ -12,15 +10,15 @@ class Exporter
|
|
|
12
10
|
|
|
13
11
|
project name: name do
|
|
14
12
|
puts name
|
|
15
|
-
|
|
13
|
+
file_prefix file_prefix
|
|
16
14
|
|
|
15
|
+
self.anonymize if anonymize
|
|
17
16
|
self.settings.merge! settings
|
|
18
17
|
|
|
19
18
|
status_category_mappings.each do |status, category|
|
|
20
19
|
status_category_mapping status: status, category: category
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
file_prefix file_prefix
|
|
24
22
|
download do
|
|
25
23
|
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
|
26
24
|
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
|
@@ -63,7 +63,7 @@ class ExpeditedChart < ChartBase
|
|
|
63
63
|
next unless change.priority?
|
|
64
64
|
|
|
65
65
|
if expedited_priority_names.include? change.value
|
|
66
|
-
expedite_start = change.time
|
|
66
|
+
expedite_start = change.time.to_date
|
|
67
67
|
elsif expedite_start
|
|
68
68
|
start_date = expedite_start.to_date
|
|
69
69
|
stop_date = change.time.to_date
|
|
@@ -72,7 +72,7 @@ class ExpeditedChart < ChartBase
|
|
|
72
72
|
(start_date < date_range.begin && stop_date > date_range.end)
|
|
73
73
|
|
|
74
74
|
result << [expedite_start, :expedite_start]
|
|
75
|
-
result << [change.time, :expedite_stop]
|
|
75
|
+
result << [change.time.to_date, :expedite_stop]
|
|
76
76
|
end
|
|
77
77
|
expedite_start = nil
|
|
78
78
|
end
|
|
@@ -109,11 +109,11 @@ class ExpeditedChart < ChartBase
|
|
|
109
109
|
|
|
110
110
|
def make_expedite_lines_data_set issue:, expedite_data:
|
|
111
111
|
cycletime = issue.board.cycletime
|
|
112
|
-
|
|
112
|
+
started_date, stopped_date = cycletime.started_stopped_dates(issue)
|
|
113
113
|
|
|
114
|
-
expedite_data << [
|
|
115
|
-
expedite_data << [
|
|
116
|
-
expedite_data.sort_by!
|
|
114
|
+
expedite_data << [started_date, :issue_started] if started_date
|
|
115
|
+
expedite_data << [stopped_date, :issue_stopped] if stopped_date
|
|
116
|
+
expedite_data.sort_by!(&:first)
|
|
117
117
|
|
|
118
118
|
# If none of the data would be visible on the chart then skip it.
|
|
119
119
|
return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
|
|
@@ -150,7 +150,7 @@ class ExpeditedChart < ChartBase
|
|
|
150
150
|
|
|
151
151
|
unless expedite_data.empty?
|
|
152
152
|
last_change_time = expedite_data[-1][0].to_date
|
|
153
|
-
if last_change_time && last_change_time <= date_range.end &&
|
|
153
|
+
if last_change_time && last_change_time <= date_range.end && stopped_date.nil?
|
|
154
154
|
data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
|
|
155
155
|
dot_colors << '' # It won't be visible so it doesn't matter
|
|
156
156
|
point_styles << 'dash'
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
|
|
5
5
|
class Object
|
|
6
|
-
def deprecated message:, date:
|
|
6
|
+
def deprecated message:, date:, depth: 2
|
|
7
7
|
text = +''
|
|
8
8
|
text << "Deprecated(#{date}): "
|
|
9
9
|
text << message
|
|
10
|
-
caller(1..
|
|
10
|
+
caller(1..depth).each do |line|
|
|
11
11
|
text << "\n-> Called from #{line}"
|
|
12
12
|
end
|
|
13
13
|
warn text
|