jirametrics 2.12.1 → 2.22
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/aging_work_bar_chart.rb +176 -134
- data/lib/jirametrics/anonymizer.rb +8 -6
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +4 -1
- data/lib/jirametrics/change_item.rb +12 -4
- data/lib/jirametrics/chart_base.rb +36 -2
- data/lib/jirametrics/cycletime_config.rb +22 -4
- data/lib/jirametrics/cycletime_histogram.rb +3 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
- data/lib/jirametrics/daily_view.rb +57 -53
- data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +8 -3
- data/lib/jirametrics/dependency_chart.rb +4 -1
- data/lib/jirametrics/downloader.rb +34 -70
- data/lib/jirametrics/downloader_for_cloud.rb +202 -0
- data/lib/jirametrics/downloader_for_data_center.rb +94 -0
- data/lib/jirametrics/examples/standard_project.rb +9 -9
- data/lib/jirametrics/expedited_chart.rb +1 -1
- data/lib/jirametrics/exporter.rb +12 -5
- data/lib/jirametrics/file_system.rb +24 -1
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
- data/lib/jirametrics/groupable_issue_chart.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
- data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
- data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
- data/lib/jirametrics/html/expedited_chart.erb +3 -1
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
- data/lib/jirametrics/html/index.css +21 -9
- data/lib/jirametrics/html/index.erb +5 -37
- data/lib/jirametrics/html/index.js +114 -0
- data/lib/jirametrics/html/sprint_burndown.erb +11 -3
- data/lib/jirametrics/html/throughput_chart.erb +2 -2
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +8 -25
- data/lib/jirametrics/issue.rb +128 -23
- data/lib/jirametrics/jira_gateway.rb +59 -17
- data/lib/jirametrics/project_config.rb +42 -5
- data/lib/jirametrics/raw_javascript.rb +13 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +6 -2
- data/lib/jirametrics/status_collection.rb +1 -0
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +26 -69
- metadata +11 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ChangeItem
|
|
4
|
-
attr_reader :field, :value_id, :old_value_id, :raw, :
|
|
5
|
-
attr_accessor :value, :old_value
|
|
4
|
+
attr_reader :field, :value_id, :old_value_id, :raw, :author_raw, :field_id
|
|
5
|
+
attr_accessor :value, :old_value, :time
|
|
6
6
|
|
|
7
7
|
def initialize raw:, author_raw:, time:, artificial: false
|
|
8
8
|
@raw = raw
|
|
@@ -13,9 +13,15 @@ class ChangeItem
|
|
|
13
13
|
|
|
14
14
|
@field = @raw['field']
|
|
15
15
|
@value = @raw['toString']
|
|
16
|
-
@value_id = @raw['to'].to_i
|
|
17
16
|
@old_value = @raw['fromString']
|
|
18
|
-
|
|
17
|
+
if sprint?
|
|
18
|
+
@value_id = @raw['to'].split(', ').collect(&:to_i)
|
|
19
|
+
@old_value_id = (@raw['from'] || '').split(', ').collect(&:to_i)
|
|
20
|
+
else
|
|
21
|
+
@value_id = @raw['to'].to_i
|
|
22
|
+
@old_value_id = @raw['from']&.to_i
|
|
23
|
+
end
|
|
24
|
+
@field_id = @raw['fieldId']
|
|
19
25
|
@artificial = artificial
|
|
20
26
|
end
|
|
21
27
|
|
|
@@ -30,6 +36,7 @@ class ChangeItem
|
|
|
30
36
|
def artificial? = @artificial
|
|
31
37
|
def assignee? = (field == 'assignee')
|
|
32
38
|
def comment? = (field == 'comment')
|
|
39
|
+
def description? = (field == 'description')
|
|
33
40
|
def due_date? = (field == 'duedate')
|
|
34
41
|
def flagged? = (field == 'Flagged')
|
|
35
42
|
def issue_type? = field == 'issuetype'
|
|
@@ -53,6 +60,7 @@ class ChangeItem
|
|
|
53
60
|
message << ':' << old_value_id.inspect if status?
|
|
54
61
|
end
|
|
55
62
|
message << ", time: #{time_to_s(@time).inspect}"
|
|
63
|
+
message << ", field_id: #{@field_id.inspect}" if @field_id
|
|
56
64
|
message << ', artificial' if artificial?
|
|
57
65
|
message << ')'
|
|
58
66
|
message
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class ChartBase
|
|
4
4
|
attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
|
|
5
|
-
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
|
|
5
|
+
:time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
|
|
6
|
+
:atlassian_document_format
|
|
6
7
|
attr_writer :aggregated_project
|
|
7
8
|
attr_reader :canvas_width, :canvas_height
|
|
8
9
|
|
|
@@ -21,6 +22,14 @@ class ChartBase
|
|
|
21
22
|
@canvas_responsive = true
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def call_before_run &proc
|
|
26
|
+
(@call_before_run_procs ||= []) << proc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def before_run
|
|
30
|
+
@call_before_run_procs&.each { |proc| proc.call }
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
def aggregated_project?
|
|
25
34
|
@aggregated_project
|
|
26
35
|
end
|
|
@@ -44,7 +53,7 @@ class ChartBase
|
|
|
44
53
|
|
|
45
54
|
def render_top_text caller_binding
|
|
46
55
|
result = +''
|
|
47
|
-
result << "<h1>#{@header_text}</h1>" if @header_text
|
|
56
|
+
result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
|
|
48
57
|
result << ERB.new(@description_text).result(caller_binding) if @description_text
|
|
49
58
|
result
|
|
50
59
|
end
|
|
@@ -66,6 +75,8 @@ class ChartBase
|
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def label_days days
|
|
78
|
+
return 'unknown' if days.nil?
|
|
79
|
+
|
|
69
80
|
"#{days} day#{'s' unless days == 1}"
|
|
70
81
|
end
|
|
71
82
|
|
|
@@ -276,4 +287,27 @@ class ChartBase
|
|
|
276
287
|
</div>
|
|
277
288
|
TEXT
|
|
278
289
|
end
|
|
290
|
+
|
|
291
|
+
# Set a cycletime for just this one chart, overriding the one for the report.
|
|
292
|
+
def cycletime &block
|
|
293
|
+
call_before_run do
|
|
294
|
+
@cycletime = CycleTimeConfig.new(
|
|
295
|
+
possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system,
|
|
296
|
+
settings: settings
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Returns the cycletime in use right now, which may be specific to the chart or across the report.
|
|
302
|
+
def cycletime_for_issue issue
|
|
303
|
+
@cycletime || issue.board.cycletime
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def seam_start type = 'chart'
|
|
307
|
+
"\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def seam_end type = 'chart'
|
|
311
|
+
"\n<!-- seam-end | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
|
|
312
|
+
end
|
|
279
313
|
end
|
|
@@ -6,12 +6,14 @@ require 'date'
|
|
|
6
6
|
class CycleTimeConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
|
-
attr_reader :label, :
|
|
9
|
+
attr_reader :label, :possible_statuses, :settings, :file_system
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
12
|
-
|
|
11
|
+
def initialize possible_statuses:, label:, block:, settings:, file_system: nil, today: Date.today
|
|
12
|
+
|
|
13
|
+
@possible_statuses = possible_statuses
|
|
13
14
|
@label = label
|
|
14
15
|
@today = today
|
|
16
|
+
@settings = settings
|
|
15
17
|
|
|
16
18
|
# If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
|
|
17
19
|
# may make it easier to find problems in the test code ;-)
|
|
@@ -63,6 +65,10 @@ class CycleTimeConfig
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def started_stopped_changes issue
|
|
68
|
+
cache_key = "#{issue.key}:#{issue.board.id}"
|
|
69
|
+
last_result = (@cache ||= {})[cache_key]
|
|
70
|
+
return *last_result if last_result && settings['cache_cycletime_calculations']
|
|
71
|
+
|
|
66
72
|
started = @start_at.call(issue)
|
|
67
73
|
stopped = @stop_at.call(issue)
|
|
68
74
|
|
|
@@ -80,7 +86,15 @@ class CycleTimeConfig
|
|
|
80
86
|
# for the start and not have it conflict.
|
|
81
87
|
started = nil if started&.time == stopped&.time
|
|
82
88
|
|
|
83
|
-
[started, stopped]
|
|
89
|
+
result = [started, stopped]
|
|
90
|
+
if last_result && result != last_result
|
|
91
|
+
@file_system.error(
|
|
92
|
+
"Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
|
|
93
|
+
"previous=#{last_result.inspect}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
@cache[cache_key] = result
|
|
97
|
+
result
|
|
84
98
|
end
|
|
85
99
|
|
|
86
100
|
def started_stopped_times issue
|
|
@@ -88,6 +102,10 @@ class CycleTimeConfig
|
|
|
88
102
|
[started&.time, stopped&.time]
|
|
89
103
|
end
|
|
90
104
|
|
|
105
|
+
def flush_cache
|
|
106
|
+
@cache = nil
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
def started_stopped_dates issue
|
|
92
110
|
started_time, stopped_time = started_stopped_times(issue)
|
|
93
111
|
[started_time&.to_date, stopped_time&.to_date]
|
|
@@ -62,7 +62,9 @@ class CycletimeHistogram < ChartBase
|
|
|
62
62
|
)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
if data_sets.empty?
|
|
66
|
+
return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>"
|
|
67
|
+
end
|
|
66
68
|
|
|
67
69
|
wrap_and_render(binding, __FILE__)
|
|
68
70
|
end
|
|
@@ -35,14 +35,33 @@ class CycletimeScatterplot < ChartBase
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
@percentage_lines = []
|
|
38
|
-
@
|
|
38
|
+
@highest_y_value = 0
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
42
|
-
|
|
41
|
+
def all_items
|
|
42
|
+
completed_issues_in_range include_unstarted: false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def x_value item
|
|
46
|
+
item.board.cycletime.started_stopped_times(item).last
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def y_value item
|
|
50
|
+
item.board.cycletime.cycletime(item)
|
|
51
|
+
end
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
def title_value item
|
|
54
|
+
"#{item.key} : #{item.summary} (#{label_days(y_value(item))})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def y_axis_heading
|
|
58
|
+
'Cycle time in days'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run
|
|
62
|
+
items = all_items
|
|
63
|
+
data_sets = create_datasets items
|
|
64
|
+
overall_percent_line = calculate_percent_line(items)
|
|
46
65
|
@percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
|
|
47
66
|
|
|
48
67
|
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
@@ -50,14 +69,14 @@ class CycletimeScatterplot < ChartBase
|
|
|
50
69
|
wrap_and_render(binding, __FILE__)
|
|
51
70
|
end
|
|
52
71
|
|
|
53
|
-
def create_datasets
|
|
72
|
+
def create_datasets items
|
|
54
73
|
data_sets = []
|
|
55
74
|
|
|
56
|
-
group_issues(
|
|
75
|
+
group_issues(items).each do |rules, completed_items_by_type|
|
|
57
76
|
label = rules.label
|
|
58
77
|
color = rules.color
|
|
59
|
-
percent_line = calculate_percent_line
|
|
60
|
-
data =
|
|
78
|
+
percent_line = calculate_percent_line completed_items_by_type
|
|
79
|
+
data = completed_items_by_type.filter_map { |issue| data_for_issue(issue) }
|
|
61
80
|
data_sets << {
|
|
62
81
|
label: "#{label} (85% at #{label_days(percent_line)})",
|
|
63
82
|
data: data,
|
|
@@ -86,7 +105,7 @@ class CycletimeScatterplot < ChartBase
|
|
|
86
105
|
calculator = TrendLineCalculator.new(points)
|
|
87
106
|
data_points = calculator.chart_datapoints(
|
|
88
107
|
range: time_range.begin.to_i..time_range.end.to_i,
|
|
89
|
-
max_y: @
|
|
108
|
+
max_y: @highest_y_value
|
|
90
109
|
)
|
|
91
110
|
data_points.each do |point_hash|
|
|
92
111
|
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
@@ -106,21 +125,21 @@ class CycletimeScatterplot < ChartBase
|
|
|
106
125
|
}
|
|
107
126
|
end
|
|
108
127
|
|
|
109
|
-
def data_for_issue
|
|
110
|
-
cycle_time =
|
|
128
|
+
def data_for_issue item
|
|
129
|
+
cycle_time = y_value(item)
|
|
111
130
|
return nil if cycle_time < 1 # These will get called out on the quality report
|
|
112
131
|
|
|
113
|
-
@
|
|
132
|
+
@highest_y_value = cycle_time if @highest_y_value < cycle_time
|
|
114
133
|
|
|
115
134
|
{
|
|
116
135
|
y: cycle_time,
|
|
117
|
-
x: chart_format(
|
|
118
|
-
title: [
|
|
136
|
+
x: chart_format(x_value(item)),
|
|
137
|
+
title: [title_value(item)]
|
|
119
138
|
}
|
|
120
139
|
end
|
|
121
140
|
|
|
122
|
-
def calculate_percent_line
|
|
123
|
-
times =
|
|
141
|
+
def calculate_percent_line items
|
|
142
|
+
times = items.collect { |item| y_value(item) }
|
|
124
143
|
index = times.size * 85 / 100
|
|
125
144
|
times.sort[index]
|
|
126
145
|
end
|
|
@@ -23,7 +23,7 @@ class DailyView < ChartBase
|
|
|
23
23
|
def run
|
|
24
24
|
aging_issues = select_aging_issues
|
|
25
25
|
|
|
26
|
-
return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
|
|
26
|
+
return "<h1 class='foldable'>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
|
|
27
27
|
|
|
28
28
|
result = +''
|
|
29
29
|
result << render_top_text(binding)
|
|
@@ -78,7 +78,7 @@ class DailyView < ChartBase
|
|
|
78
78
|
blocked_stalled = issue.blocked_stalled_by_date(
|
|
79
79
|
date_range: today..today, chart_end_time: time_range.end, settings: settings
|
|
80
80
|
)[today]
|
|
81
|
-
return []
|
|
81
|
+
return [] if blocked_stalled.active?
|
|
82
82
|
|
|
83
83
|
lines = []
|
|
84
84
|
if blocked_stalled.blocked?
|
|
@@ -98,15 +98,18 @@ class DailyView < ChartBase
|
|
|
98
98
|
lines
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
def make_issue_label issue
|
|
102
|
-
"<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
|
103
|
-
|
|
101
|
+
def make_issue_label issue:, done:
|
|
102
|
+
label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
|
103
|
+
label << '<s>' if done
|
|
104
|
+
label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> <i>#{issue.summary}</i>"
|
|
105
|
+
label << '</s>' if done
|
|
106
|
+
label
|
|
104
107
|
end
|
|
105
108
|
|
|
106
|
-
def make_title_line issue
|
|
109
|
+
def make_title_line issue:, done:
|
|
107
110
|
title_line = +''
|
|
108
111
|
title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
|
|
109
|
-
title_line << make_issue_label(issue)
|
|
112
|
+
title_line << make_issue_label(issue: issue, done: done)
|
|
110
113
|
title_line
|
|
111
114
|
end
|
|
112
115
|
|
|
@@ -115,20 +118,25 @@ class DailyView < ChartBase
|
|
|
115
118
|
parent_key = issue.parent_key
|
|
116
119
|
if parent_key
|
|
117
120
|
parent = issues.find_by_key key: parent_key, include_hidden: true
|
|
118
|
-
text = parent ? make_issue_label(parent) : parent_key
|
|
121
|
+
text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
|
|
119
122
|
lines << ["Parent: #{text}"]
|
|
120
123
|
end
|
|
121
124
|
lines
|
|
122
125
|
end
|
|
123
126
|
|
|
124
|
-
def make_stats_lines issue
|
|
127
|
+
def make_stats_lines issue:, done:
|
|
125
128
|
line = []
|
|
126
129
|
|
|
127
130
|
line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
if done
|
|
133
|
+
cycletime = issue.board.cycletime.cycletime(issue)
|
|
131
134
|
|
|
135
|
+
line << "Cycletime: <b>#{label_days cycletime}</b>"
|
|
136
|
+
else
|
|
137
|
+
age = issue.board.cycletime.age(issue, today: date_range.end)
|
|
138
|
+
line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
|
|
139
|
+
end
|
|
132
140
|
line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
|
|
133
141
|
|
|
134
142
|
column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
|
|
@@ -154,45 +162,26 @@ class DailyView < ChartBase
|
|
|
154
162
|
|
|
155
163
|
def make_child_lines issue
|
|
156
164
|
lines = []
|
|
157
|
-
subtasks = issue.subtasks
|
|
165
|
+
subtasks = issue.subtasks
|
|
158
166
|
|
|
159
|
-
|
|
160
|
-
icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
|
161
|
-
lines << (icon_urls << 'Incomplete child issues')
|
|
162
|
-
lines += subtasks
|
|
163
|
-
end
|
|
164
|
-
lines
|
|
165
|
-
end
|
|
167
|
+
return lines if subtasks.empty?
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
171
|
-
.gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
172
|
-
.gsub("\n", '<br />')
|
|
173
|
-
end
|
|
169
|
+
lines << '<section><div class="foldable">Child issues</div>'
|
|
170
|
+
lines += subtasks
|
|
171
|
+
lines << '</section>'
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
user = @users.find { |u| u.account_id == account_id }
|
|
177
|
-
text = account_id
|
|
178
|
-
text = "@#{user.display_name}" if user
|
|
179
|
-
"<span class='account_id'>#{text}</span>"
|
|
173
|
+
lines
|
|
180
174
|
end
|
|
181
175
|
|
|
182
176
|
def make_history_lines issue
|
|
183
177
|
history = issue.changes.reverse
|
|
184
178
|
lines = []
|
|
185
179
|
|
|
186
|
-
|
|
187
|
-
lines << [
|
|
188
|
-
"<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
|
|
189
|
-
"<span id='open#{id}'>▶ Issue History</span>" \
|
|
190
|
-
"<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
|
|
191
|
-
]
|
|
180
|
+
lines << '<section><div class="foldable startFolded">Issue history</div>'
|
|
192
181
|
table = +''
|
|
193
|
-
table <<
|
|
182
|
+
table << '<table>'
|
|
194
183
|
history.each do |c|
|
|
195
|
-
time = c.time.strftime '%b %d, %I:%M%P'
|
|
184
|
+
time = c.time.strftime '%b %d, %Y @ %I:%M%P'
|
|
196
185
|
|
|
197
186
|
table << '<tr>'
|
|
198
187
|
table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
|
|
@@ -203,23 +192,24 @@ class DailyView < ChartBase
|
|
|
203
192
|
end
|
|
204
193
|
table << '</table>'
|
|
205
194
|
lines << [table]
|
|
195
|
+
lines << '</section>'
|
|
206
196
|
lines
|
|
207
197
|
end
|
|
208
198
|
|
|
209
199
|
def history_text change:, board:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
200
|
+
convertor = ->(value, _id) { value.inspect }
|
|
201
|
+
convertor = ->(_value, id) { format_status(board.possible_statuses.find_by_id(id), board: board) } if change.status?
|
|
202
|
+
|
|
203
|
+
if change.comment? || change.description?
|
|
204
|
+
atlassian_document_format.to_html(change.value)
|
|
205
|
+
elsif %w[status priority assignee duedate issuetype].include?(change.field)
|
|
206
|
+
to = convertor.call(change.value, change.value_id)
|
|
215
207
|
if change.old_value
|
|
216
|
-
from = convertor.call(change.old_value_id)
|
|
208
|
+
from = convertor.call(change.old_value, change.old_value_id)
|
|
217
209
|
"Changed from #{from} to #{to}"
|
|
218
210
|
else
|
|
219
211
|
"Set to #{to}"
|
|
220
212
|
end
|
|
221
|
-
elsif %w[priority assignee duedate issuetype].include?(change.field)
|
|
222
|
-
"Changed from \"#{change.old_value}\" to \"#{change.value}\""
|
|
223
213
|
elsif change.flagged?
|
|
224
214
|
change.value == '' ? 'Off' : 'On'
|
|
225
215
|
else
|
|
@@ -245,15 +235,27 @@ class DailyView < ChartBase
|
|
|
245
235
|
.join(' ')]]
|
|
246
236
|
end
|
|
247
237
|
|
|
238
|
+
def make_description_lines issue
|
|
239
|
+
description = issue.raw['fields']['description']
|
|
240
|
+
result = []
|
|
241
|
+
result << [atlassian_document_format.to_html(description)] if description
|
|
242
|
+
result
|
|
243
|
+
end
|
|
244
|
+
|
|
248
245
|
def assemble_issue_lines issue, child:
|
|
246
|
+
done = issue.done?
|
|
247
|
+
|
|
249
248
|
lines = []
|
|
250
|
-
lines << [make_title_line(issue)]
|
|
249
|
+
lines << [make_title_line(issue: issue, done: done)]
|
|
251
250
|
lines += make_parent_lines(issue) unless child
|
|
252
|
-
lines += make_stats_lines(issue)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
lines += make_stats_lines(issue: issue, done: done)
|
|
252
|
+
unless done
|
|
253
|
+
lines += make_description_lines(issue)
|
|
254
|
+
lines += make_sprints_lines(issue)
|
|
255
|
+
lines += make_blocked_stalled_lines(issue)
|
|
256
|
+
lines += make_child_lines(issue)
|
|
257
|
+
lines += make_history_lines(issue)
|
|
258
|
+
end
|
|
257
259
|
lines
|
|
258
260
|
end
|
|
259
261
|
|
|
@@ -264,6 +266,8 @@ class DailyView < ChartBase
|
|
|
264
266
|
assemble_issue_lines(issue, child: child).each do |row|
|
|
265
267
|
if row.is_a? Issue
|
|
266
268
|
result << render_issue(row, child: true)
|
|
269
|
+
elsif row.is_a?(String)
|
|
270
|
+
result << row
|
|
267
271
|
else
|
|
268
272
|
result << '<div class="heading">'
|
|
269
273
|
row.each do |chunk|
|
|
@@ -51,8 +51,6 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
51
51
|
def default_grouping_rules issue:, rules:
|
|
52
52
|
started, stopped = issue.board.cycletime.started_stopped_dates(issue)
|
|
53
53
|
|
|
54
|
-
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
|
55
|
-
|
|
56
54
|
if stopped && started.nil? # We can't tell when it started
|
|
57
55
|
@has_completed_but_not_started = true
|
|
58
56
|
not_started stopped: stopped, rules: rules, created: issue.created.to_date
|
|
@@ -72,7 +70,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
72
70
|
rules.label = 'Start date unknown'
|
|
73
71
|
rules.color = '--body-background'
|
|
74
72
|
rules.group_priority = 11
|
|
75
|
-
created_days = rules.current_date - created
|
|
73
|
+
created_days = rules.current_date - created
|
|
76
74
|
rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
|
|
77
75
|
end
|
|
78
76
|
end
|
|
@@ -84,7 +82,8 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
def group_by_age started:, rules:
|
|
87
|
-
age = rules.current_date - started + 1
|
|
85
|
+
age = (rules.current_date - started).to_i + 1
|
|
86
|
+
rules.issue_hint = "(age: #{label_days age})"
|
|
88
87
|
|
|
89
88
|
case age
|
|
90
89
|
when 1
|
|
@@ -41,21 +41,30 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
41
41
|
def default_grouping_rules issue:, rules:
|
|
42
42
|
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
|
43
43
|
stopped_date = stopped&.to_date
|
|
44
|
+
started_date = started&.to_date
|
|
44
45
|
|
|
45
46
|
date = rules.current_date
|
|
46
47
|
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
|
47
|
-
|
|
48
48
|
stopped_today = stopped_date == rules.current_date
|
|
49
49
|
|
|
50
|
+
days = nil
|
|
51
|
+
if started_date && stopped_date
|
|
52
|
+
days = (stopped_date - started_date).to_i + 1 # cycletime
|
|
53
|
+
elsif started_date
|
|
54
|
+
days = (time_range.end.to_date - started_date).to_i + 1 # age
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
if stopped_today && started.nil?
|
|
51
58
|
@has_completed_but_not_started = true
|
|
52
59
|
rules.label = 'Completed but not started'
|
|
53
60
|
rules.color = '--wip-chart-completed-but-not-started-color'
|
|
54
61
|
rules.group_priority = -1
|
|
62
|
+
rules.issue_hint = '(Cycle time: Unknown)'
|
|
55
63
|
elsif stopped_today
|
|
56
64
|
rules.label = 'Completed'
|
|
57
65
|
rules.color = '--wip-chart-completed-color'
|
|
58
66
|
rules.group_priority = -2
|
|
67
|
+
rules.issue_hint = "(Cycle time: #{label_days days})"
|
|
59
68
|
elsif started.nil?
|
|
60
69
|
rules.label = 'Start date unknown'
|
|
61
70
|
rules.color = '--body-background'
|
|
@@ -64,16 +73,17 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
64
73
|
rules.label = 'Blocked'
|
|
65
74
|
rules.color = '--blocked-color'
|
|
66
75
|
rules.group_priority = 1
|
|
67
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
76
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
68
77
|
elsif change&.stalled?
|
|
69
78
|
rules.label = 'Stalled'
|
|
70
79
|
rules.color = '--stalled-color'
|
|
71
80
|
rules.group_priority = 2
|
|
72
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
81
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
73
82
|
else
|
|
74
83
|
rules.label = 'Active'
|
|
75
84
|
rules.color = '--wip-chart-active-color'
|
|
76
85
|
rules.group_priority = 3
|
|
86
|
+
rules.issue_hint = "(Age: #{label_days days})"
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
end
|
|
@@ -66,7 +66,7 @@ class DailyWipChart < ChartBase
|
|
|
66
66
|
hash = {}
|
|
67
67
|
|
|
68
68
|
@issues.each do |issue|
|
|
69
|
-
start, stop = issue.
|
|
69
|
+
start, stop = cycletime_for_issue(issue).started_stopped_dates(issue)
|
|
70
70
|
next if start.nil? && stop.nil?
|
|
71
71
|
|
|
72
72
|
# If it stopped but never started then assume it started at creation so the data points
|
|
@@ -266,6 +266,8 @@ class DataQualityReport < ChartBase
|
|
|
266
266
|
|
|
267
267
|
def scan_for_items_blocked_on_closed_tickets entry:
|
|
268
268
|
entry.issue.issue_links.each do |link|
|
|
269
|
+
next unless settings['blocked_link_text'].include?(link.label)
|
|
270
|
+
|
|
269
271
|
this_active = !entry.stopped
|
|
270
272
|
other_active = !link.other_issue.board.cycletime.started_stopped_times(link.other_issue).last
|
|
271
273
|
next unless this_active && !other_active
|
|
@@ -410,14 +412,17 @@ class DataQualityReport < ChartBase
|
|
|
410
412
|
def render_status_not_on_board problems
|
|
411
413
|
<<-HTML
|
|
412
414
|
#{label_issues problems.size} were not visible on the board for some period of time. This may impact
|
|
413
|
-
timings as the work was likely to have been forgotten if it wasn't visible.
|
|
415
|
+
timings as the work was likely to have been forgotten if it wasn't visible. What does "not visible"
|
|
416
|
+
mean in this context? The issue was in a status that is not mapped to any visible column on the board.
|
|
417
|
+
Look in "unmapped statuses" on your board.
|
|
414
418
|
HTML
|
|
415
419
|
end
|
|
416
420
|
|
|
417
421
|
def render_created_in_wrong_status problems
|
|
418
422
|
<<-HTML
|
|
419
|
-
#{label_issues problems.size} were created in a status not
|
|
420
|
-
|
|
423
|
+
#{label_issues problems.size} were created in a status that is not considered to be some varient
|
|
424
|
+
of To Do. Most likely this means that the issue was created from one of the columns on the board,
|
|
425
|
+
rather than in the backlog. Why Jira allows this is still a mystery.
|
|
421
426
|
HTML
|
|
422
427
|
end
|
|
423
428
|
|
|
@@ -51,7 +51,10 @@ class DependencyChart < ChartBase
|
|
|
51
51
|
instance_eval(&@rules_block) if @rules_block
|
|
52
52
|
|
|
53
53
|
dot_graph = build_dot_graph
|
|
54
|
-
|
|
54
|
+
if dot_graph.nil?
|
|
55
|
+
return "<h1 class='foldable'>#{@header_text}</h1>" \
|
|
56
|
+
'<div>No data matched the selected criteria. Nothing to show.</div>'
|
|
57
|
+
end
|
|
55
58
|
|
|
56
59
|
svg = execute_graphviz(dot_graph.join("\n"))
|
|
57
60
|
"<h1>#{@header_text}</h1><div>#{@description_text}</div>#{shrink_svg svg}"
|