jirametrics 2.12.1 → 2.20.1
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/anonymizer.rb +8 -6
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/board_config.rb +3 -1
- data/lib/jirametrics/change_item.rb +3 -2
- data/lib/jirametrics/chart_base.rb +5 -2
- data/lib/jirametrics/cycletime_config.rb +22 -3
- data/lib/jirametrics/cycletime_histogram.rb +3 -1
- data/lib/jirametrics/daily_view.rb +57 -53
- data/lib/jirametrics/data_quality_report.rb +6 -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 +10 -5
- data/lib/jirametrics/file_system.rb +24 -1
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +1 -1
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cycletime_histogram.erb +2 -2
- data/lib/jirametrics/html/index.css +5 -10
- data/lib/jirametrics/html/index.erb +4 -36
- data/lib/jirametrics/html/index.js +90 -0
- data/lib/jirametrics/html/sprint_burndown.erb +5 -3
- data/lib/jirametrics/html_report_config.rb +5 -3
- data/lib/jirametrics/issue.rb +32 -20
- data/lib/jirametrics/jira_gateway.rb +59 -17
- data/lib/jirametrics/project_config.rb +30 -3
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/status_collection.rb +1 -0
- data/lib/jirametrics.rb +19 -69
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbe1101b082615d38939850c0adc688aedefe02a74537503bcd390cdf11d0d4e
|
|
4
|
+
data.tar.gz: eeffbda7c7ba8280273e0d749ede1ed3c1caa33d06a1f50a2c47b2331035d5af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b73533c90e457c2c5f7a7f2d1759ccd44d218ebe984052fcb581d8ab88225abf43b5612923217b4fa7467ed11e82ba5b4fe2cc656a324f269b71c2497bf67659
|
|
7
|
+
data.tar.gz: 57cbc54fe6c739d0c85f68cc0efcfbc6005975af0b40174ed9ee35790a83dac9b5e524601b770dffc6a3fefbd10f0d577d19b840560b3acfb63b0b542728d5fc
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'random-word'
|
|
4
4
|
|
|
5
|
-
class Anonymizer
|
|
5
|
+
class Anonymizer < ChartBase
|
|
6
6
|
# needed for testing
|
|
7
7
|
attr_reader :project_config, :issues
|
|
8
8
|
|
|
9
9
|
def initialize project_config:, date_adjustment: -200
|
|
10
|
+
super()
|
|
10
11
|
@project_config = project_config
|
|
11
12
|
@issues = @project_config.issues
|
|
12
13
|
@all_boards = @project_config.all_boards
|
|
@@ -130,18 +131,19 @@ class Anonymizer
|
|
|
130
131
|
end
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
def shift_all_dates
|
|
134
|
-
|
|
134
|
+
def shift_all_dates date_adjustment: @date_adjustment
|
|
135
|
+
adjustment_in_seconds = 60 * 60 * 24 * date_adjustment
|
|
136
|
+
@file_system.log "Shifting all dates by #{label_days date_adjustment}"
|
|
135
137
|
@issues.each do |issue|
|
|
136
138
|
issue.changes.each do |change|
|
|
137
|
-
change.time = change.time +
|
|
139
|
+
change.time = change.time + adjustment_in_seconds
|
|
138
140
|
end
|
|
139
141
|
|
|
140
|
-
issue.raw['fields']['updated'] = (issue.updated +
|
|
142
|
+
issue.raw['fields']['updated'] = (issue.updated + adjustment_in_seconds).to_s
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
range = @project_config.time_range
|
|
144
|
-
@project_config.time_range = (range.begin +
|
|
146
|
+
@project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
def random_name
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AtlassianDocumentFormat
|
|
4
|
+
attr_reader :users
|
|
5
|
+
|
|
6
|
+
def initialize users:, timezone_offset:
|
|
7
|
+
@users = users
|
|
8
|
+
@timezone_offset = timezone_offset
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_html input
|
|
12
|
+
if input.is_a? String
|
|
13
|
+
input
|
|
14
|
+
.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
|
|
15
|
+
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
16
|
+
.gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
17
|
+
.gsub("\n", '<br />')
|
|
18
|
+
elsif input&.[]('content')
|
|
19
|
+
input['content'].collect { |element| adf_node_to_html element }.join("\n")
|
|
20
|
+
else
|
|
21
|
+
# We have an actual ADF document with no content.
|
|
22
|
+
''
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ADF is Atlassian Document Format
|
|
27
|
+
# https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
|
28
|
+
def adf_node_to_html node # rubocop:disable Metrics/CyclomaticComplexity
|
|
29
|
+
closing_tag = nil
|
|
30
|
+
node_attrs = node['attrs']
|
|
31
|
+
|
|
32
|
+
result = +''
|
|
33
|
+
case node['type']
|
|
34
|
+
when 'blockquote'
|
|
35
|
+
result << '<blockquote>'
|
|
36
|
+
closing_tag = '</blockquote>'
|
|
37
|
+
when 'bulletList'
|
|
38
|
+
result << '<ul>'
|
|
39
|
+
closing_tag = '</ul>'
|
|
40
|
+
when 'codeBlock'
|
|
41
|
+
result << '<code>'
|
|
42
|
+
closing_tag = '</code>'
|
|
43
|
+
when 'date'
|
|
44
|
+
result << Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s
|
|
45
|
+
when 'decisionItem'
|
|
46
|
+
result << '<li>'
|
|
47
|
+
closing_tag = '</li>'
|
|
48
|
+
when 'decisionList'
|
|
49
|
+
result << '<div>Decisions<ul>'
|
|
50
|
+
closing_tag = '</ul></div>'
|
|
51
|
+
when 'emoji'
|
|
52
|
+
result << node_attrs['text']
|
|
53
|
+
when 'expand'
|
|
54
|
+
# TODO: Maybe, someday, make this actually expandable. For now it's always open
|
|
55
|
+
result << "<div>#{node_attrs['title']}</div>"
|
|
56
|
+
when 'hardBreak'
|
|
57
|
+
result << '<br />'
|
|
58
|
+
when 'heading'
|
|
59
|
+
level = node_attrs['level']
|
|
60
|
+
result << "<h#{level}>"
|
|
61
|
+
closing_tag = "</h#{level}>"
|
|
62
|
+
when 'inlineCard'
|
|
63
|
+
url = node_attrs['url']
|
|
64
|
+
result << "[Inline card]: <a href='#{url}'>#{url}</a>"
|
|
65
|
+
when 'listItem'
|
|
66
|
+
result << '<li>'
|
|
67
|
+
closing_tag = '</li>'
|
|
68
|
+
when 'media'
|
|
69
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
70
|
+
result << "Media: #{text}"
|
|
71
|
+
when 'mediaSingle', 'mediaGroup'
|
|
72
|
+
result << '<div>'
|
|
73
|
+
closing_tag = '</div>'
|
|
74
|
+
when 'mention'
|
|
75
|
+
user = node_attrs['text']
|
|
76
|
+
result << "<b>#{user}</b>"
|
|
77
|
+
when 'orderedList'
|
|
78
|
+
result << '<ol>'
|
|
79
|
+
closing_tag = '</ol>'
|
|
80
|
+
when 'panel'
|
|
81
|
+
type = node_attrs['panelType']
|
|
82
|
+
result << "<div>#{type.upcase}</div>"
|
|
83
|
+
when 'paragraph'
|
|
84
|
+
result << '<p>'
|
|
85
|
+
closing_tag = '</p>'
|
|
86
|
+
when 'rule'
|
|
87
|
+
result << '<hr />'
|
|
88
|
+
when 'status'
|
|
89
|
+
text = node_attrs['text']
|
|
90
|
+
result << text
|
|
91
|
+
when 'table'
|
|
92
|
+
result << '<table>'
|
|
93
|
+
closing_tag = '</table>'
|
|
94
|
+
when 'tableCell'
|
|
95
|
+
result << '<td>'
|
|
96
|
+
closing_tag = '</td>'
|
|
97
|
+
when 'tableHeader'
|
|
98
|
+
result << '<th>'
|
|
99
|
+
closing_tag = '</th>'
|
|
100
|
+
when 'tableRow'
|
|
101
|
+
result << '<tr>'
|
|
102
|
+
closing_tag = '</tr>'
|
|
103
|
+
when 'text'
|
|
104
|
+
marks = adf_marks_to_html node['marks']
|
|
105
|
+
result << marks.collect(&:first).join
|
|
106
|
+
result << node['text']
|
|
107
|
+
result << marks.collect(&:last).join
|
|
108
|
+
when 'taskItem'
|
|
109
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
110
|
+
result << "<li>#{state} "
|
|
111
|
+
closing_tag = '</li>'
|
|
112
|
+
when 'taskList'
|
|
113
|
+
result << "<ul class='taskList'>"
|
|
114
|
+
closing_tag = '</ul>'
|
|
115
|
+
else
|
|
116
|
+
result << "<p>Unparseable section: #{node['type']}</p>"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
node['content']&.each do |child|
|
|
120
|
+
result << adf_node_to_html(child)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result << closing_tag if closing_tag
|
|
124
|
+
result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def adf_marks_to_html list
|
|
128
|
+
return [] if list.nil?
|
|
129
|
+
|
|
130
|
+
mappings = [
|
|
131
|
+
['strong', '<b>', '</b>'],
|
|
132
|
+
['code', '<code>', '</code>'],
|
|
133
|
+
['em', '<em>', '</em>'],
|
|
134
|
+
['strike', '<s>', '</s>'],
|
|
135
|
+
['underline', '<u>', '</u>']
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
list.filter_map do |mark|
|
|
139
|
+
type = mark['type']
|
|
140
|
+
if type == 'textColor'
|
|
141
|
+
color = mark['attrs']['color']
|
|
142
|
+
["<span style='color: #{color}'>", '</span>']
|
|
143
|
+
elsif type == 'link'
|
|
144
|
+
href = mark['attrs']['href']
|
|
145
|
+
title = mark['attrs']['title']
|
|
146
|
+
["<a href='#{href}' title='#{title}'>", '</a>']
|
|
147
|
+
else
|
|
148
|
+
line = mappings.find { |key, _open, _close| key == type }
|
|
149
|
+
[line[1], line[2]] if line
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def expand_account_id account_id
|
|
155
|
+
user = @users.find { |u| u.account_id == account_id }
|
|
156
|
+
text = account_id
|
|
157
|
+
text = "@#{user.display_name}" if user
|
|
158
|
+
"<span class='account_id'>#{text}</span>"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -11,6 +11,7 @@ class BoardConfig
|
|
|
11
11
|
|
|
12
12
|
def run
|
|
13
13
|
@board = @project_config.all_boards[id]
|
|
14
|
+
raise "Can't find board #{id.inspect} in #{@project_config.all_boards.keys.inspect}" unless @board
|
|
14
15
|
|
|
15
16
|
instance_eval(&@block)
|
|
16
17
|
raise "Must specify a cycletime for board #{@id}" if @board.cycletime.nil?
|
|
@@ -23,7 +24,8 @@ class BoardConfig
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
@board.cycletime = CycleTimeConfig.new(
|
|
26
|
-
parent_config: self, label: label, block: block, file_system: project_config.file_system
|
|
27
|
+
parent_config: self, label: label, block: block, file_system: project_config.file_system,
|
|
28
|
+
settings: project_config.settings
|
|
27
29
|
)
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -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
|
|
5
|
+
attr_accessor :value, :old_value, :time
|
|
6
6
|
|
|
7
7
|
def initialize raw:, author_raw:, time:, artificial: false
|
|
8
8
|
@raw = raw
|
|
@@ -30,6 +30,7 @@ class ChangeItem
|
|
|
30
30
|
def artificial? = @artificial
|
|
31
31
|
def assignee? = (field == 'assignee')
|
|
32
32
|
def comment? = (field == 'comment')
|
|
33
|
+
def description? = (field == 'description')
|
|
33
34
|
def due_date? = (field == 'duedate')
|
|
34
35
|
def flagged? = (field == 'Flagged')
|
|
35
36
|
def issue_type? = field == 'issuetype'
|
|
@@ -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
|
|
|
@@ -44,7 +45,7 @@ class ChartBase
|
|
|
44
45
|
|
|
45
46
|
def render_top_text caller_binding
|
|
46
47
|
result = +''
|
|
47
|
-
result << "<h1>#{@header_text}</h1>" if @header_text
|
|
48
|
+
result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
|
|
48
49
|
result << ERB.new(@description_text).result(caller_binding) if @description_text
|
|
49
50
|
result
|
|
50
51
|
end
|
|
@@ -66,6 +67,8 @@ class ChartBase
|
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def label_days days
|
|
70
|
+
return 'unknown' if days.nil?
|
|
71
|
+
|
|
69
72
|
"#{days} day#{'s' unless days == 1}"
|
|
70
73
|
end
|
|
71
74
|
|
|
@@ -6,12 +6,15 @@ require 'date'
|
|
|
6
6
|
class CycleTimeConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
|
-
attr_reader :label, :parent_config
|
|
9
|
+
attr_reader :label, :parent_config, :settings, :file_system
|
|
10
|
+
|
|
11
|
+
def initialize parent_config:, label:, block:, settings:, file_system: nil, today: Date.today
|
|
10
12
|
|
|
11
|
-
def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
|
|
12
13
|
@parent_config = parent_config
|
|
13
14
|
@label = label
|
|
14
15
|
@today = today
|
|
16
|
+
@settings = settings
|
|
17
|
+
@cache_cycletime_calculations = settings['cache_cycletime_calculations']
|
|
15
18
|
|
|
16
19
|
# If we hit something deprecated and this is nil then we'll blow up. Although it's ugly, this
|
|
17
20
|
# may make it easier to find problems in the test code ;-)
|
|
@@ -63,6 +66,10 @@ class CycleTimeConfig
|
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def started_stopped_changes issue
|
|
69
|
+
cache_key = "#{issue.key}:#{issue.board.id}"
|
|
70
|
+
last_result = (@cache ||= {})[cache_key]
|
|
71
|
+
return *last_result if last_result && @cache_cycletime_calculations
|
|
72
|
+
|
|
66
73
|
started = @start_at.call(issue)
|
|
67
74
|
stopped = @stop_at.call(issue)
|
|
68
75
|
|
|
@@ -80,7 +87,15 @@ class CycleTimeConfig
|
|
|
80
87
|
# for the start and not have it conflict.
|
|
81
88
|
started = nil if started&.time == stopped&.time
|
|
82
89
|
|
|
83
|
-
[started, stopped]
|
|
90
|
+
result = [started, stopped]
|
|
91
|
+
if last_result && result != last_result
|
|
92
|
+
@file_system.error(
|
|
93
|
+
"Calculation mismatch; this could break caching. #{issue.inspect} new=#{result.inspect}, " \
|
|
94
|
+
"previous=#{last_result.inspect}"
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
@cache[cache_key] = result
|
|
98
|
+
result
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
def started_stopped_times issue
|
|
@@ -88,6 +103,10 @@ class CycleTimeConfig
|
|
|
88
103
|
[started&.time, stopped&.time]
|
|
89
104
|
end
|
|
90
105
|
|
|
106
|
+
def flush_cache
|
|
107
|
+
@cache = nil
|
|
108
|
+
end
|
|
109
|
+
|
|
91
110
|
def started_stopped_dates issue
|
|
92
111
|
started_time, stopped_time = started_stopped_times(issue)
|
|
93
112
|
[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
|
|
@@ -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|
|
|
@@ -410,14 +410,17 @@ class DataQualityReport < ChartBase
|
|
|
410
410
|
def render_status_not_on_board problems
|
|
411
411
|
<<-HTML
|
|
412
412
|
#{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.
|
|
413
|
+
timings as the work was likely to have been forgotten if it wasn't visible. What does "not visible"
|
|
414
|
+
mean in this context? The issue was in a status that is not mapped to any visible column on the board.
|
|
415
|
+
Look in "unmapped statuses" on your board.
|
|
414
416
|
HTML
|
|
415
417
|
end
|
|
416
418
|
|
|
417
419
|
def render_created_in_wrong_status problems
|
|
418
420
|
<<-HTML
|
|
419
|
-
#{label_issues problems.size} were created in a status not
|
|
420
|
-
|
|
421
|
+
#{label_issues problems.size} were created in a status that is not considered to be some varient
|
|
422
|
+
of To Do. Most likely this means that the issue was created from one of the columns on the board,
|
|
423
|
+
rather than in the backlog. Why Jira allows this is still a mystery.
|
|
421
424
|
HTML
|
|
422
425
|
end
|
|
423
426
|
|
|
@@ -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}"
|