jirametrics 2.6 → 2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +6 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +6 -6
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +4 -5
- data/lib/jirametrics/blocked_stalled_change.rb +1 -1
- data/lib/jirametrics/board.rb +14 -12
- data/lib/jirametrics/chart_base.rb +5 -7
- data/lib/jirametrics/cycletime_config.rb +26 -7
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- 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 +45 -6
- data/lib/jirametrics/dependency_chart.rb +3 -4
- data/lib/jirametrics/discard_changes_before.rb +1 -1
- data/lib/jirametrics/downloader.rb +14 -13
- data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
- data/lib/jirametrics/examples/aggregated_project.rb +1 -1
- data/lib/jirametrics/examples/standard_project.rb +10 -7
- data/lib/jirametrics/expedited_chart.rb +1 -2
- data/lib/jirametrics/exporter.rb +25 -0
- data/lib/jirametrics/file_system.rb +1 -1
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +113 -0
- data/lib/jirametrics/html/data_quality_report.erb +12 -0
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html_report_config.rb +2 -1
- data/lib/jirametrics/issue.rb +62 -10
- data/lib/jirametrics/jira_gateway.rb +1 -1
- data/lib/jirametrics/project_config.rb +27 -19
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/sprint_burndown.rb +2 -2
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/status_collection.rb +4 -0
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +15 -5
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73e2f2e8408a55ccecb221ba0c6d241c77f19de18d2100a9b05213a6470ed65f
|
4
|
+
data.tar.gz: d0f9056615c44e783c824ca1c6566c95bf68f16a26f81a596475c1d458fee9f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e58594175d798269e0e4b8ec1fd4c0f8b693767670222bc6144be19646b04f83e5edb508e4f933f8466865018e8802f789ecb348db368c3f626318df7774c0e0
|
7
|
+
data.tar.gz: 4555628c33a398cd3aaedd6e36a074b54d89f5a431f47d8617344e4af45e389e529ba54dd9cbd7d9e7d8ad7f673a68f664d00b951451b433d47f90c6feb439a5
|
@@ -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
|
+
log "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:
|
@@ -53,8 +53,8 @@ class AgingWorkBarChart < ChartBase
|
|
53
53
|
percentage_line_x = date_range.end - calculate_percent_line if percentage
|
54
54
|
|
55
55
|
if aging_issues.empty?
|
56
|
-
@description_text =
|
57
|
-
return render_top_text(binding)
|
56
|
+
@description_text = '<p>There is no aging work</p>'
|
57
|
+
return render_top_text(binding)
|
58
58
|
end
|
59
59
|
|
60
60
|
wrap_and_render(binding, __FILE__)
|
@@ -62,7 +62,7 @@ class AgingWorkBarChart < ChartBase
|
|
62
62
|
|
63
63
|
def data_sets_for_one_issue issue:, today:
|
64
64
|
cycletime = issue.board.cycletime
|
65
|
-
issue_start_time = cycletime.
|
65
|
+
issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
|
66
66
|
issue_start_date = issue_start_time.to_date
|
67
67
|
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
68
68
|
[
|
@@ -92,8 +92,8 @@ class AgingWorkBarChart < ChartBase
|
|
92
92
|
|
93
93
|
def select_aging_issues issues:
|
94
94
|
issues.select do |issue|
|
95
|
-
|
96
|
-
|
95
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
96
|
+
started_time && stopped_time.nil?
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
@@ -107,7 +107,7 @@ class AgingWorkBarChart < ChartBase
|
|
107
107
|
def status_data_sets issue:, label:, today:
|
108
108
|
cycletime = issue.board.cycletime
|
109
109
|
|
110
|
-
issue_started_time = cycletime.
|
110
|
+
issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
|
111
111
|
|
112
112
|
previous_start = nil
|
113
113
|
previous_status = nil
|
@@ -114,7 +114,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
114
114
|
def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
|
115
115
|
issues.filter_map do |issue|
|
116
116
|
stop = issue.first_time_in_status(*status_ids)
|
117
|
-
start = issue.board.cycletime.
|
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.
|
120
120
|
next if stop.nil? || start.nil?
|
@@ -36,16 +36,15 @@ class AgingWorkTable < ChartBase
|
|
36
36
|
|
37
37
|
def expedited_but_not_started
|
38
38
|
@issues.select do |issue|
|
39
|
-
|
40
|
-
|
39
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
40
|
+
started_time.nil? && stopped_time.nil? && issue.expedited?
|
41
41
|
end.sort_by(&:created)
|
42
42
|
end
|
43
43
|
|
44
44
|
def select_aging_issues
|
45
45
|
aging_issues = @issues.select do |issue|
|
46
46
|
cycletime = issue.board.cycletime
|
47
|
-
started = cycletime.
|
48
|
-
stopped = cycletime.stopped_time(issue)
|
47
|
+
started, stopped = cycletime.started_stopped_times(issue)
|
49
48
|
next false if started.nil? || stopped
|
50
49
|
next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
|
51
50
|
|
@@ -64,7 +63,7 @@ class AgingWorkTable < ChartBase
|
|
64
63
|
end
|
65
64
|
|
66
65
|
def blocked_text issue
|
67
|
-
started_time = issue.board.cycletime.
|
66
|
+
started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
68
67
|
return nil if started_time.nil?
|
69
68
|
|
70
69
|
current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
|
data/lib/jirametrics/board.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Board
|
4
|
-
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :
|
4
|
+
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
|
5
5
|
attr_accessor :cycletime, :project_config
|
6
6
|
|
7
7
|
def initialize raw:, possible_statuses: StatusCollection.new
|
@@ -15,24 +15,26 @@ class Board
|
|
15
15
|
# For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
|
16
16
|
# visible on the board. If the board is configured to have a kanban backlog then it will have
|
17
17
|
# statuses matched to it and otherwise, there will be no statuses.
|
18
|
-
if kanban?
|
19
|
-
@backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
|
20
|
-
# There is a status defined as being 'backlog' that is no longer being returned in statuses.
|
21
|
-
# We used to display a warning for this but honestly, there is nothing that anyone can do about it
|
22
|
-
# so now we just quietly ignore it.
|
23
|
-
end
|
24
|
-
columns = columns[1..]
|
25
|
-
else
|
26
|
-
# We currently don't know how to get the backlog status for a Scrum board
|
27
|
-
@backlog_statuses = []
|
28
|
-
end
|
18
|
+
columns = columns[1..] if kanban?
|
29
19
|
|
20
|
+
@backlog_statuses = []
|
30
21
|
@visible_columns = columns.filter_map do |column|
|
31
22
|
# It's possible for a column to be defined without any statuses and in this case, it won't be visible.
|
32
23
|
BoardColumn.new column unless status_ids_from_column(column).empty?
|
33
24
|
end
|
34
25
|
end
|
35
26
|
|
27
|
+
def backlog_statuses
|
28
|
+
if @backlog_statuses.empty? && kanban?
|
29
|
+
status_ids = status_ids_from_column raw['columnConfig']['columns'].first
|
30
|
+
@backlog_statuses = @possible_statuses.expand_statuses(status_ids) do |unknown_status|
|
31
|
+
# If a status is returned here that is no longer in the system then there's nothing useful
|
32
|
+
# we can do about it. Ignore it.
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@backlog_statuses
|
36
|
+
end
|
37
|
+
|
36
38
|
def server_url_prefix
|
37
39
|
raise "Cannot parse self: #{@raw['self'].inspect}" unless @raw['self'] =~ /^(https?:\/\/.+)\/rest\//
|
38
40
|
|
@@ -144,8 +144,7 @@ class ChartBase
|
|
144
144
|
def completed_issues_in_range include_unstarted: false
|
145
145
|
issues.select do |issue|
|
146
146
|
cycletime = issue.board.cycletime
|
147
|
-
stopped_time = cycletime.
|
148
|
-
started_time = cycletime.started_time(issue)
|
147
|
+
started_time, stopped_time = cycletime.started_stopped_times(issue)
|
149
148
|
|
150
149
|
stopped_time &&
|
151
150
|
date_range.include?(stopped_time.to_date) && # Remove outside range
|
@@ -179,7 +178,7 @@ class ChartBase
|
|
179
178
|
def format_status name_or_id, board:, is_category: false
|
180
179
|
begin
|
181
180
|
statuses = board.possible_statuses.expand_statuses([name_or_id])
|
182
|
-
rescue StatusNotFoundError
|
181
|
+
rescue StatusNotFoundError
|
183
182
|
return "<span style='color: red'>#{name_or_id}</span>"
|
184
183
|
end
|
185
184
|
|
@@ -189,10 +188,9 @@ class ChartBase
|
|
189
188
|
visibility = ''
|
190
189
|
if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
|
191
190
|
visibility = icon_span(
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
191
|
+
title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
|
192
|
+
icon: ' 👀'
|
193
|
+
)
|
196
194
|
end
|
197
195
|
text = is_category ? status.category_name : status.name
|
198
196
|
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
|
@@ -26,31 +26,50 @@ class CycleTimeConfig
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def in_progress? issue
|
29
|
-
started_time
|
29
|
+
started_time, stopped_time = started_stopped_times(issue)
|
30
|
+
started_time && stopped_time.nil?
|
30
31
|
end
|
31
32
|
|
32
33
|
def done? issue
|
33
|
-
|
34
|
+
started_stopped_times(issue).last
|
34
35
|
end
|
35
36
|
|
36
37
|
def started_time issue
|
37
|
-
|
38
|
+
deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
|
39
|
+
started_stopped_times(issue).first
|
38
40
|
end
|
39
41
|
|
40
42
|
def stopped_time issue
|
41
|
-
|
43
|
+
deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
|
44
|
+
started_stopped_times(issue).last
|
45
|
+
end
|
46
|
+
|
47
|
+
def started_stopped_times issue
|
48
|
+
started = @start_at.call(issue)
|
49
|
+
stopped = @stop_at.call(issue)
|
50
|
+
|
51
|
+
# In the case where started and stopped are exactly the same time, we pretend that
|
52
|
+
# it just stopped and never started. This allows us to have logic like 'in or right of'
|
53
|
+
# for the start and not have it conflict.
|
54
|
+
started = nil if started == stopped
|
55
|
+
|
56
|
+
[started, stopped]
|
57
|
+
end
|
58
|
+
|
59
|
+
def started_stopped_dates issue
|
60
|
+
started_time, stopped_time = started_stopped_times(issue)
|
61
|
+
[started_time&.to_date, stopped_time&.to_date]
|
42
62
|
end
|
43
63
|
|
44
64
|
def cycletime issue
|
45
|
-
start =
|
46
|
-
stop = stopped_time(issue)
|
65
|
+
start, stop = started_stopped_times(issue)
|
47
66
|
return nil if start.nil? || stop.nil?
|
48
67
|
|
49
68
|
(stop.to_date - start.to_date).to_i + 1
|
50
69
|
end
|
51
70
|
|
52
71
|
def age issue, today: nil
|
53
|
-
start =
|
72
|
+
start = started_stopped_times(issue).first
|
54
73
|
stop = today || @today || Date.today
|
55
74
|
return nil if start.nil? || stop.nil?
|
56
75
|
|
@@ -30,7 +30,7 @@ class CycletimeHistogram < ChartBase
|
|
30
30
|
stopped_issues = completed_issues_in_range include_unstarted: true
|
31
31
|
|
32
32
|
# For the histogram, we only want to consider items that have both a start and a stop time.
|
33
|
-
histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.
|
33
|
+
histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_stopped_times(issue).first }
|
34
34
|
rules_to_issues = group_issues histogram_issues
|
35
35
|
|
36
36
|
data_sets = rules_to_issues.keys.collect do |rules|
|
@@ -24,7 +24,7 @@ class CycletimeScatterplot < ChartBase
|
|
24
24
|
predict that most work of this type will complete in <%= overall_percent_line %> days or
|
25
25
|
less. The other lines reflect the 85% line for that respective type of work.
|
26
26
|
</div>
|
27
|
-
#{
|
27
|
+
#{describe_non_working_days}
|
28
28
|
HTML
|
29
29
|
|
30
30
|
init_configuration_block block do
|
@@ -53,10 +53,7 @@ class CycletimeScatterplot < ChartBase
|
|
53
53
|
def create_datasets completed_issues
|
54
54
|
data_sets = []
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
groups.each_key do |rules|
|
59
|
-
completed_issues_by_type = groups[rules]
|
56
|
+
group_issues(completed_issues).each do |rules, completed_issues_by_type|
|
60
57
|
label = rules.label
|
61
58
|
color = rules.color
|
62
59
|
percent_line = calculate_percent_line completed_issues_by_type
|
@@ -117,7 +114,7 @@ class CycletimeScatterplot < ChartBase
|
|
117
114
|
|
118
115
|
{
|
119
116
|
y: cycle_time,
|
120
|
-
x: chart_format(issue.board.cycletime.
|
117
|
+
x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
|
121
118
|
title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
|
122
119
|
}
|
123
120
|
end
|
@@ -4,7 +4,7 @@ require 'jirametrics/daily_wip_chart'
|
|
4
4
|
|
5
5
|
class DailyWipByAgeChart < DailyWipChart
|
6
6
|
def initialize block
|
7
|
-
super
|
7
|
+
super
|
8
8
|
|
9
9
|
add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
|
10
10
|
'Less than a day',
|
@@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def default_grouping_rules issue:, rules:
|
52
|
-
|
53
|
-
started = cycletime.started_time(issue)&.to_date
|
54
|
-
stopped = cycletime.stopped_time(issue)&.to_date
|
52
|
+
started, stopped = issue.board.cycletime.started_stopped_dates(issue)
|
55
53
|
|
56
54
|
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
57
55
|
|
@@ -39,8 +39,8 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def default_grouping_rules issue:, rules:
|
42
|
-
started = issue.board.cycletime.
|
43
|
-
stopped_date =
|
42
|
+
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
43
|
+
stopped_date = stopped&.to_date
|
44
44
|
|
45
45
|
date = rules.current_date
|
46
46
|
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
@@ -6,7 +6,7 @@ class DailyGroupingRules < GroupingRules
|
|
6
6
|
attr_accessor :current_date, :group_priority, :issue_hint
|
7
7
|
|
8
8
|
def initialize
|
9
|
-
super
|
9
|
+
super
|
10
10
|
@group_priority = 0
|
11
11
|
end
|
12
12
|
end
|
@@ -22,10 +22,10 @@ class DailyWipChart < ChartBase
|
|
22
22
|
|
23
23
|
instance_eval(&block) if block
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
return if @group_by_block
|
26
|
+
|
27
|
+
grouping_rules do |issue, rules|
|
28
|
+
default_grouping_rules issue: issue, rules: rules
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -66,9 +66,7 @@ class DailyWipChart < ChartBase
|
|
66
66
|
hash = {}
|
67
67
|
|
68
68
|
@issues.each do |issue|
|
69
|
-
|
70
|
-
start = cycletime.started_time(issue)&.to_date
|
71
|
-
stop = cycletime.stopped_time(issue)&.to_date
|
69
|
+
start, stop = issue.board.cycletime.started_stopped_dates(issue)
|
72
70
|
next if start.nil? && stop.nil?
|
73
71
|
|
74
72
|
# If it stopped but never started then assume it started at creation so the data points
|
@@ -158,7 +156,7 @@ class DailyWipChart < ChartBase
|
|
158
156
|
|
159
157
|
{
|
160
158
|
type: 'line',
|
161
|
-
label:
|
159
|
+
label: 'Trendline',
|
162
160
|
data: data_points,
|
163
161
|
fill: false,
|
164
162
|
borderWidth: 1,
|
@@ -48,6 +48,7 @@ class DataQualityReport < ChartBase
|
|
48
48
|
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
|
49
49
|
scan_for_stopped_before_started entry: entry
|
50
50
|
scan_for_issues_not_started_with_subtasks_that_have entry: entry
|
51
|
+
scan_for_incomplete_subtasks_when_issue_done entry: entry
|
51
52
|
scan_for_discarded_data entry: entry
|
52
53
|
end
|
53
54
|
|
@@ -87,9 +88,7 @@ class DataQualityReport < ChartBase
|
|
87
88
|
|
88
89
|
def initialize_entries
|
89
90
|
@entries = @issues.filter_map do |issue|
|
90
|
-
|
91
|
-
started = cycletime.started_time(issue)
|
92
|
-
stopped = cycletime.stopped_time(issue)
|
91
|
+
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
93
92
|
next if stopped && stopped < time_range.begin
|
94
93
|
next if started && started > time_range.end
|
95
94
|
|
@@ -223,14 +222,13 @@ class DataQualityReport < ChartBase
|
|
223
222
|
|
224
223
|
started_subtasks = []
|
225
224
|
entry.issue.subtasks.each do |subtask|
|
226
|
-
started_subtasks << subtask if subtask.board.cycletime.
|
225
|
+
started_subtasks << subtask if subtask.board.cycletime.started_stopped_times(subtask).first
|
227
226
|
end
|
228
227
|
|
229
228
|
return if started_subtasks.empty?
|
230
229
|
|
231
230
|
subtask_labels = started_subtasks.collect do |subtask|
|
232
|
-
|
233
|
-
"#{subtask.summary[..50].inspect}"
|
231
|
+
subtask_label(subtask)
|
234
232
|
end
|
235
233
|
entry.report(
|
236
234
|
problem_key: :issue_not_started_but_subtasks_have,
|
@@ -238,6 +236,47 @@ class DataQualityReport < ChartBase
|
|
238
236
|
)
|
239
237
|
end
|
240
238
|
|
239
|
+
def subtask_label subtask
|
240
|
+
"<img src='#{subtask.type_icon_url}' /> #{link_to_issue(subtask)} #{subtask.summary[..50].inspect}"
|
241
|
+
end
|
242
|
+
|
243
|
+
def time_as_english(from_time, to_time)
|
244
|
+
delta = (to_time - from_time).to_i
|
245
|
+
return "#{delta} seconds" if delta < 60
|
246
|
+
|
247
|
+
delta /= 60
|
248
|
+
return "#{delta} minutes" if delta < 60
|
249
|
+
|
250
|
+
delta /= 60
|
251
|
+
return "#{delta} hours" if delta < 24
|
252
|
+
|
253
|
+
delta /= 24
|
254
|
+
"#{delta} days"
|
255
|
+
end
|
256
|
+
|
257
|
+
def scan_for_incomplete_subtasks_when_issue_done entry:
|
258
|
+
return unless entry.stopped
|
259
|
+
|
260
|
+
subtask_labels = entry.issue.subtasks.filter_map do |subtask|
|
261
|
+
subtask_started, subtask_stopped = subtask.board.cycletime.started_stopped_times(subtask)
|
262
|
+
|
263
|
+
if !subtask_started && !subtask_stopped
|
264
|
+
"#{subtask_label subtask} (Not even started)"
|
265
|
+
elsif !subtask_stopped
|
266
|
+
"#{subtask_label subtask} (Still not done)"
|
267
|
+
elsif subtask_stopped > entry.stopped
|
268
|
+
"#{subtask_label subtask} (Closed #{time_as_english entry.stopped, subtask_stopped} later)"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
return if subtask_labels.empty?
|
273
|
+
|
274
|
+
entry.report(
|
275
|
+
problem_key: :incomplete_subtasks_when_issue_done,
|
276
|
+
detail: subtask_labels.join('<br />')
|
277
|
+
)
|
278
|
+
end
|
279
|
+
|
241
280
|
def label_issues number
|
242
281
|
return '1 item' if number == 1
|
243
282
|
|
@@ -183,9 +183,8 @@ class DependencyChart < ChartBase
|
|
183
183
|
return stdout.read
|
184
184
|
end
|
185
185
|
rescue # rubocop:disable Style/RescueStandardError
|
186
|
-
message =
|
187
|
-
|
188
|
-
puts message
|
186
|
+
message = 'Unable to generate the dependency chart because graphviz could not be found in the path.'
|
187
|
+
file_system.log message, also_write_to_stderr: true
|
189
188
|
message
|
190
189
|
end
|
191
190
|
|
@@ -229,7 +228,7 @@ class DependencyChart < ChartBase
|
|
229
228
|
elsif is_done
|
230
229
|
line2 << 'Done'
|
231
230
|
else
|
232
|
-
started_at = issue.board.cycletime.
|
231
|
+
started_at = issue.board.cycletime.started_stopped_times(issue).first
|
233
232
|
if started_at.nil?
|
234
233
|
line2 << 'Not started'
|
235
234
|
else
|
@@ -31,7 +31,7 @@ module DiscardChangesBefore
|
|
31
31
|
discard_changes_before_hook issues_cutoff_times
|
32
32
|
|
33
33
|
issues_cutoff_times.each do |issue, cutoff_time|
|
34
|
-
issue.
|
34
|
+
issue.discard_changes_before cutoff_time
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -41,8 +41,8 @@ class Downloader
|
|
41
41
|
remove_old_files
|
42
42
|
download_statuses
|
43
43
|
find_board_ids.each do |id|
|
44
|
-
download_board_configuration board_id: id
|
45
|
-
download_issues
|
44
|
+
board = download_board_configuration board_id: id
|
45
|
+
download_issues board: board
|
46
46
|
end
|
47
47
|
|
48
48
|
save_metadata
|
@@ -64,19 +64,19 @@ class Downloader
|
|
64
64
|
ids
|
65
65
|
end
|
66
66
|
|
67
|
-
def download_issues
|
68
|
-
log " Downloading primary issues for board #{
|
67
|
+
def download_issues board:
|
68
|
+
log " Downloading primary issues for board #{board.id}", both: true
|
69
69
|
path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
|
70
70
|
unless Dir.exist?(path)
|
71
71
|
log " Creating path #{path}"
|
72
72
|
Dir.mkdir(path)
|
73
73
|
end
|
74
74
|
|
75
|
-
filter_id = @board_id_to_filter_id[
|
75
|
+
filter_id = @board_id_to_filter_id[board.id]
|
76
76
|
jql = make_jql(filter_id: filter_id)
|
77
|
-
jira_search_by_jql(jql: jql, initial_query: true,
|
77
|
+
jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
|
78
78
|
|
79
|
-
log " Downloading linked issues for board #{
|
79
|
+
log " Downloading linked issues for board #{board.id}", both: true
|
80
80
|
loop do
|
81
81
|
@issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
|
82
82
|
break if @issue_keys_pending_download.empty?
|
@@ -84,11 +84,11 @@ class Downloader
|
|
84
84
|
keys_to_request = @issue_keys_pending_download[0..99]
|
85
85
|
@issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
|
86
86
|
jql = "key in (#{keys_to_request.join(', ')})"
|
87
|
-
jira_search_by_jql(jql: jql, initial_query: false,
|
87
|
+
jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
-
def jira_search_by_jql jql:, initial_query:,
|
91
|
+
def jira_search_by_jql jql:, initial_query:, board:, path:
|
92
92
|
intercept_jql = @download_config.project_config.settings['intercept_jql']
|
93
93
|
jql = intercept_jql.call jql if intercept_jql
|
94
94
|
|
@@ -108,8 +108,8 @@ class Downloader
|
|
108
108
|
issue_json['exporter'] = {
|
109
109
|
'in_initial_query' => initial_query
|
110
110
|
}
|
111
|
-
identify_other_issues_to_be_downloaded issue_json
|
112
|
-
file = "#{issue_json['key']}-#{
|
111
|
+
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
112
|
+
file = "#{issue_json['key']}-#{board.id}.json"
|
113
113
|
|
114
114
|
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
115
115
|
end
|
@@ -124,8 +124,8 @@ class Downloader
|
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
|
-
def identify_other_issues_to_be_downloaded raw_issue
|
128
|
-
issue = Issue.new raw: raw_issue, board:
|
127
|
+
def identify_other_issues_to_be_downloaded raw_issue:, board:
|
128
|
+
issue = Issue.new raw: raw_issue, board: board
|
129
129
|
@issue_keys_downloaded_in_current_run << issue.key
|
130
130
|
|
131
131
|
# Parent
|
@@ -171,6 +171,7 @@ class Downloader
|
|
171
171
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
172
172
|
|
173
173
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
174
|
+
Board.new raw: json
|
174
175
|
end
|
175
176
|
|
176
177
|
def download_sprints board_id:
|
@@ -83,8 +83,7 @@ class EstimateAccuracyChart < ChartBase
|
|
83
83
|
|
84
84
|
issues.each do |issue|
|
85
85
|
cycletime = issue.board.cycletime
|
86
|
-
start_time = cycletime.
|
87
|
-
stop_time = cycletime.stopped_time(issue)
|
86
|
+
start_time, stop_time = cycletime.started_stopped_times(issue)
|
88
87
|
|
89
88
|
next unless start_time
|
90
89
|
|
@@ -33,7 +33,7 @@ class Exporter
|
|
33
33
|
html '<h1>Boards included in this report</h1><ul>', type: :header
|
34
34
|
board_lines = []
|
35
35
|
included_projects.each do |project|
|
36
|
-
project.all_boards.
|
36
|
+
project.all_boards.each_value do |board|
|
37
37
|
board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
|
38
38
|
end
|
39
39
|
end
|
@@ -7,7 +7,8 @@
|
|
7
7
|
class Exporter
|
8
8
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
9
9
|
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
|
10
|
-
rolling_date_count: 90, no_earlier_than: nil
|
10
|
+
rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],
|
11
|
+
show_experimental_charts: false
|
11
12
|
|
12
13
|
project name: name do
|
13
14
|
puts name
|
@@ -38,11 +39,14 @@ class Exporter
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
42
|
+
issues.reject! do |issue|
|
43
|
+
ignore_types.include? issue.type
|
44
|
+
end
|
45
|
+
|
46
|
+
discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
|
47
|
+
|
41
48
|
file do
|
42
49
|
file_suffix '.html'
|
43
|
-
issues.reject! do |issue|
|
44
|
-
%w[Sub-task Epic].include? issue.type
|
45
|
-
end
|
46
50
|
|
47
51
|
issues.reject! { |issue| ignore_issues.include? issue.key } if ignore_issues
|
48
52
|
|
@@ -52,12 +56,10 @@ class Exporter
|
|
52
56
|
html "<H1>#{name}</H1>", type: :header
|
53
57
|
boards.each_key do |id|
|
54
58
|
board = find_board id
|
55
|
-
html "<div><a href='#{board.url}'>#{id} #{board.name}</a
|
59
|
+
html "<div><a href='#{board.url}'>#{id} #{board.name}</a> (#{board.board_type})</div>",
|
56
60
|
type: :header
|
57
61
|
end
|
58
62
|
|
59
|
-
discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
|
60
|
-
|
61
63
|
cycletime_scatterplot do
|
62
64
|
show_trend_lines
|
63
65
|
end
|
@@ -84,6 +86,7 @@ class Exporter
|
|
84
86
|
daily_wip_by_age_chart
|
85
87
|
daily_wip_by_blocked_stalled_chart
|
86
88
|
daily_wip_by_parent_chart
|
89
|
+
flow_efficiency_scatterplot if show_experimental_charts
|
87
90
|
expedited_chart
|
88
91
|
sprint_burndown
|
89
92
|
estimate_accuracy_chart
|
@@ -109,8 +109,7 @@ class ExpeditedChart < ChartBase
|
|
109
109
|
|
110
110
|
def make_expedite_lines_data_set issue:, expedite_data:
|
111
111
|
cycletime = issue.board.cycletime
|
112
|
-
started_time = cycletime.
|
113
|
-
stopped_time = cycletime.stopped_time(issue)
|
112
|
+
started_time, stopped_time = cycletime.started_stopped_times(issue)
|
114
113
|
|
115
114
|
expedite_data << [started_time, :issue_started] if started_time
|
116
115
|
expedite_data << [stopped_time, :issue_stopped] if stopped_time
|