jirametrics 2.6 → 2.7
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 +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
|