jirametrics 2.13 → 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 +8 -4
- data/lib/jirametrics/board_config.rb +3 -1
- data/lib/jirametrics/change_item.rb +2 -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 +49 -42
- data/lib/jirametrics/data_quality_report.rb +6 -3
- data/lib/jirametrics/dependency_chart.rb +4 -1
- data/lib/jirametrics/downloader.rb +34 -99
- 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 +2 -34
- 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 +31 -19
- data/lib/jirametrics/jira_gateway.rb +55 -17
- data/lib/jirametrics/project_config.rb +30 -3
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics.rb +19 -70
- metadata +6 -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
|
|
@@ -5,6 +5,7 @@ class AtlassianDocumentFormat
|
|
|
5
5
|
|
|
6
6
|
def initialize users:, timezone_offset:
|
|
7
7
|
@users = users
|
|
8
|
+
@timezone_offset = timezone_offset
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def to_html input
|
|
@@ -12,16 +13,19 @@ class AtlassianDocumentFormat
|
|
|
12
13
|
input
|
|
13
14
|
.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
|
|
14
15
|
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
15
|
-
.gsub(/\[([
|
|
16
|
+
.gsub(/\[([^|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
16
17
|
.gsub("\n", '<br />')
|
|
17
|
-
|
|
18
|
+
elsif input&.[]('content')
|
|
18
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
|
+
''
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
# ADF is Atlassian Document Format
|
|
23
27
|
# https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
|
24
|
-
def adf_node_to_html node
|
|
28
|
+
def adf_node_to_html node # rubocop:disable Metrics/CyclomaticComplexity
|
|
25
29
|
closing_tag = nil
|
|
26
30
|
node_attrs = node['attrs']
|
|
27
31
|
|
|
@@ -153,4 +157,4 @@ class AtlassianDocumentFormat
|
|
|
153
157
|
text = "@#{user.display_name}" if user
|
|
154
158
|
"<span class='account_id'>#{text}</span>"
|
|
155
159
|
end
|
|
156
|
-
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
|
|
@@ -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)
|
|
@@ -33,10 +33,6 @@ class DailyView < ChartBase
|
|
|
33
33
|
result
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def atlassian_document_format
|
|
37
|
-
@atlassian_document_format ||= AtlassianDocumentFormat.new(users: users, timezone_offset: timezone_offset)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
36
|
def select_aging_issues
|
|
41
37
|
aging_issues = issues.select do |issue|
|
|
42
38
|
started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
|
|
@@ -82,7 +78,7 @@ class DailyView < ChartBase
|
|
|
82
78
|
blocked_stalled = issue.blocked_stalled_by_date(
|
|
83
79
|
date_range: today..today, chart_end_time: time_range.end, settings: settings
|
|
84
80
|
)[today]
|
|
85
|
-
return []
|
|
81
|
+
return [] if blocked_stalled.active?
|
|
86
82
|
|
|
87
83
|
lines = []
|
|
88
84
|
if blocked_stalled.blocked?
|
|
@@ -102,15 +98,18 @@ class DailyView < ChartBase
|
|
|
102
98
|
lines
|
|
103
99
|
end
|
|
104
100
|
|
|
105
|
-
def make_issue_label issue
|
|
106
|
-
"<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
|
107
|
-
|
|
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
|
|
108
107
|
end
|
|
109
108
|
|
|
110
|
-
def make_title_line issue
|
|
109
|
+
def make_title_line issue:, done:
|
|
111
110
|
title_line = +''
|
|
112
111
|
title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
|
|
113
|
-
title_line << make_issue_label(issue)
|
|
112
|
+
title_line << make_issue_label(issue: issue, done: done)
|
|
114
113
|
title_line
|
|
115
114
|
end
|
|
116
115
|
|
|
@@ -119,20 +118,25 @@ class DailyView < ChartBase
|
|
|
119
118
|
parent_key = issue.parent_key
|
|
120
119
|
if parent_key
|
|
121
120
|
parent = issues.find_by_key key: parent_key, include_hidden: true
|
|
122
|
-
text = parent ? make_issue_label(parent) : parent_key
|
|
121
|
+
text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
|
|
123
122
|
lines << ["Parent: #{text}"]
|
|
124
123
|
end
|
|
125
124
|
lines
|
|
126
125
|
end
|
|
127
126
|
|
|
128
|
-
def make_stats_lines issue
|
|
127
|
+
def make_stats_lines issue:, done:
|
|
129
128
|
line = []
|
|
130
129
|
|
|
131
130
|
line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
if done
|
|
133
|
+
cycletime = issue.board.cycletime.cycletime(issue)
|
|
135
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
|
|
136
140
|
line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
|
|
137
141
|
|
|
138
142
|
column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
|
|
@@ -158,13 +162,14 @@ class DailyView < ChartBase
|
|
|
158
162
|
|
|
159
163
|
def make_child_lines issue
|
|
160
164
|
lines = []
|
|
161
|
-
subtasks = issue.subtasks
|
|
165
|
+
subtasks = issue.subtasks
|
|
166
|
+
|
|
167
|
+
return lines if subtasks.empty?
|
|
168
|
+
|
|
169
|
+
lines << '<section><div class="foldable">Child issues</div>'
|
|
170
|
+
lines += subtasks
|
|
171
|
+
lines << '</section>'
|
|
162
172
|
|
|
163
|
-
unless subtasks.empty?
|
|
164
|
-
icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
|
165
|
-
lines << (icon_urls << 'Incomplete child issues')
|
|
166
|
-
lines += subtasks
|
|
167
|
-
end
|
|
168
173
|
lines
|
|
169
174
|
end
|
|
170
175
|
|
|
@@ -172,16 +177,11 @@ class DailyView < ChartBase
|
|
|
172
177
|
history = issue.changes.reverse
|
|
173
178
|
lines = []
|
|
174
179
|
|
|
175
|
-
|
|
176
|
-
lines << [
|
|
177
|
-
"<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
|
|
178
|
-
"<span id='open#{id}'>▶ Issue History</span>" \
|
|
179
|
-
"<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
|
|
180
|
-
]
|
|
180
|
+
lines << '<section><div class="foldable startFolded">Issue history</div>'
|
|
181
181
|
table = +''
|
|
182
|
-
table <<
|
|
182
|
+
table << '<table>'
|
|
183
183
|
history.each do |c|
|
|
184
|
-
time = c.time.strftime '%b %d, %I:%M%P'
|
|
184
|
+
time = c.time.strftime '%b %d, %Y @ %I:%M%P'
|
|
185
185
|
|
|
186
186
|
table << '<tr>'
|
|
187
187
|
table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
|
|
@@ -192,23 +192,24 @@ class DailyView < ChartBase
|
|
|
192
192
|
end
|
|
193
193
|
table << '</table>'
|
|
194
194
|
lines << [table]
|
|
195
|
+
lines << '</section>'
|
|
195
196
|
lines
|
|
196
197
|
end
|
|
197
198
|
|
|
198
199
|
def history_text change:, board:
|
|
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
|
+
|
|
199
203
|
if change.comment? || change.description?
|
|
200
204
|
atlassian_document_format.to_html(change.value)
|
|
201
|
-
elsif change.
|
|
202
|
-
|
|
203
|
-
to = convertor.call(change.value_id)
|
|
205
|
+
elsif %w[status priority assignee duedate issuetype].include?(change.field)
|
|
206
|
+
to = convertor.call(change.value, change.value_id)
|
|
204
207
|
if change.old_value
|
|
205
|
-
from = convertor.call(change.old_value_id)
|
|
208
|
+
from = convertor.call(change.old_value, change.old_value_id)
|
|
206
209
|
"Changed from #{from} to #{to}"
|
|
207
210
|
else
|
|
208
211
|
"Set to #{to}"
|
|
209
212
|
end
|
|
210
|
-
elsif %w[priority assignee duedate issuetype].include?(change.field)
|
|
211
|
-
"Changed from \"#{change.old_value}\" to \"#{change.value}\""
|
|
212
213
|
elsif change.flagged?
|
|
213
214
|
change.value == '' ? 'Off' : 'On'
|
|
214
215
|
else
|
|
@@ -242,15 +243,19 @@ class DailyView < ChartBase
|
|
|
242
243
|
end
|
|
243
244
|
|
|
244
245
|
def assemble_issue_lines issue, child:
|
|
246
|
+
done = issue.done?
|
|
247
|
+
|
|
245
248
|
lines = []
|
|
246
|
-
lines << [make_title_line(issue)]
|
|
249
|
+
lines << [make_title_line(issue: issue, done: done)]
|
|
247
250
|
lines += make_parent_lines(issue) unless child
|
|
248
|
-
lines += make_stats_lines(issue)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
254
259
|
lines
|
|
255
260
|
end
|
|
256
261
|
|
|
@@ -261,6 +266,8 @@ class DailyView < ChartBase
|
|
|
261
266
|
assemble_issue_lines(issue, child: child).each do |row|
|
|
262
267
|
if row.is_a? Issue
|
|
263
268
|
result << render_issue(row, child: true)
|
|
269
|
+
elsif row.is_a?(String)
|
|
270
|
+
result << row
|
|
264
271
|
else
|
|
265
272
|
result << '<div class="heading">'
|
|
266
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}"
|
|
@@ -3,8 +3,29 @@
|
|
|
3
3
|
require 'cgi'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
+
class DownloadIssueData
|
|
7
|
+
attr_accessor :key, :found_in_primary_query, :last_modified,
|
|
8
|
+
:up_to_date, :cache_path, :issue
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
key:,
|
|
12
|
+
found_in_primary_query: true,
|
|
13
|
+
last_modified: nil,
|
|
14
|
+
up_to_date: true,
|
|
15
|
+
cache_path: nil,
|
|
16
|
+
issue: nil
|
|
17
|
+
)
|
|
18
|
+
@key = key
|
|
19
|
+
@found_in_primary_query = found_in_primary_query
|
|
20
|
+
@last_modified = last_modified
|
|
21
|
+
@up_to_date = up_to_date
|
|
22
|
+
@cache_path = cache_path
|
|
23
|
+
@issue = issue
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
class Downloader
|
|
7
|
-
CURRENT_METADATA_VERSION =
|
|
28
|
+
CURRENT_METADATA_VERSION = 5
|
|
8
29
|
|
|
9
30
|
attr_accessor :metadata
|
|
10
31
|
attr_reader :file_system
|
|
@@ -12,6 +33,15 @@ class Downloader
|
|
|
12
33
|
# For testing only
|
|
13
34
|
attr_reader :start_date_in_query, :board_id_to_filter_id
|
|
14
35
|
|
|
36
|
+
def self.create download_config:, file_system:, jira_gateway:
|
|
37
|
+
is_cloud = jira_gateway.settings['jira_cloud'] || jira_gateway.cloud?
|
|
38
|
+
(is_cloud ? DownloaderForCloud : DownloaderForDataCenter).new(
|
|
39
|
+
download_config: download_config,
|
|
40
|
+
file_system: file_system,
|
|
41
|
+
jira_gateway: jira_gateway
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
15
45
|
def initialize download_config:, file_system:, jira_gateway:
|
|
16
46
|
@metadata = {}
|
|
17
47
|
@download_config = download_config
|
|
@@ -28,7 +58,6 @@ class Downloader
|
|
|
28
58
|
log '', both: true
|
|
29
59
|
log @download_config.project_config.name, both: true
|
|
30
60
|
|
|
31
|
-
init_gateway
|
|
32
61
|
load_metadata
|
|
33
62
|
|
|
34
63
|
if @metadata['no-download']
|
|
@@ -50,11 +79,6 @@ class Downloader
|
|
|
50
79
|
save_metadata
|
|
51
80
|
end
|
|
52
81
|
|
|
53
|
-
def init_gateway
|
|
54
|
-
@jira_gateway.load_jira_config(@download_config.project_config.jira_config)
|
|
55
|
-
@jira_gateway.ignore_ssl_errors = @download_config.project_config.settings['ignore_ssl_errors']
|
|
56
|
-
end
|
|
57
|
-
|
|
58
82
|
def log text, both: false
|
|
59
83
|
@file_system.log text, also_write_to_stderr: both
|
|
60
84
|
end
|
|
@@ -66,93 +90,6 @@ class Downloader
|
|
|
66
90
|
ids
|
|
67
91
|
end
|
|
68
92
|
|
|
69
|
-
def download_issues board:
|
|
70
|
-
log " Downloading primary issues for board #{board.id}", both: true
|
|
71
|
-
path = File.join(@target_path, "#{file_prefix}_issues/")
|
|
72
|
-
unless Dir.exist?(path)
|
|
73
|
-
log " Creating path #{path}"
|
|
74
|
-
Dir.mkdir(path)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
filter_id = @board_id_to_filter_id[board.id]
|
|
78
|
-
jql = make_jql(filter_id: filter_id)
|
|
79
|
-
jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
|
|
80
|
-
|
|
81
|
-
log " Downloading linked issues for board #{board.id}", both: true
|
|
82
|
-
loop do
|
|
83
|
-
@issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
|
|
84
|
-
break if @issue_keys_pending_download.empty?
|
|
85
|
-
|
|
86
|
-
keys_to_request = @issue_keys_pending_download[0..99]
|
|
87
|
-
@issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
|
|
88
|
-
jql = "key in (#{keys_to_request.join(', ')})"
|
|
89
|
-
jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def jira_search_by_jql jql:, initial_query:, board:, path:
|
|
94
|
-
intercept_jql = @download_config.project_config.settings['intercept_jql']
|
|
95
|
-
jql = intercept_jql.call jql if intercept_jql
|
|
96
|
-
|
|
97
|
-
log " JQL: #{jql}"
|
|
98
|
-
escaped_jql = CGI.escape jql
|
|
99
|
-
|
|
100
|
-
if @jira_gateway.cloud?
|
|
101
|
-
max_results = 5_000 # The maximum allowed by Jira
|
|
102
|
-
next_page_token = nil
|
|
103
|
-
issue_count = 0
|
|
104
|
-
|
|
105
|
-
loop do
|
|
106
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/3/search/jql' \
|
|
107
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&" \
|
|
108
|
-
"nextPageToken=#{next_page_token}&expand=changelog&fields=*all"
|
|
109
|
-
next_page_token = json['nextPageToken']
|
|
110
|
-
|
|
111
|
-
json['issues'].each do |issue_json|
|
|
112
|
-
issue_json['exporter'] = {
|
|
113
|
-
'in_initial_query' => initial_query
|
|
114
|
-
}
|
|
115
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
116
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
117
|
-
|
|
118
|
-
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
119
|
-
issue_count += 1
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
message = " Downloaded #{issue_count} issues"
|
|
123
|
-
log message, both: true
|
|
124
|
-
|
|
125
|
-
break unless next_page_token
|
|
126
|
-
end
|
|
127
|
-
else
|
|
128
|
-
max_results = 100
|
|
129
|
-
start_at = 0
|
|
130
|
-
total = 1
|
|
131
|
-
while start_at < total
|
|
132
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
133
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
134
|
-
|
|
135
|
-
json['issues'].each do |issue_json|
|
|
136
|
-
issue_json['exporter'] = {
|
|
137
|
-
'in_initial_query' => initial_query
|
|
138
|
-
}
|
|
139
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
140
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
141
|
-
|
|
142
|
-
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
total = json['total'].to_i
|
|
146
|
-
max_results = json['maxResults']
|
|
147
|
-
|
|
148
|
-
message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
|
|
149
|
-
log message, both: true
|
|
150
|
-
|
|
151
|
-
start_at += json['issues'].size
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
93
|
def identify_other_issues_to_be_downloaded raw_issue:, board:
|
|
157
94
|
issue = Issue.new raw: raw_issue, board: board
|
|
158
95
|
@issue_keys_downloaded_in_current_run << issue.key
|
|
@@ -178,6 +115,8 @@ class Downloader
|
|
|
178
115
|
end
|
|
179
116
|
|
|
180
117
|
def download_users
|
|
118
|
+
return unless @jira_gateway.cloud?
|
|
119
|
+
|
|
181
120
|
log ' Downloading all users', both: true
|
|
182
121
|
json = @jira_gateway.call_url relative_url: '/rest/api/2/users'
|
|
183
122
|
|
|
@@ -327,11 +266,7 @@ class Downloader
|
|
|
327
266
|
|
|
328
267
|
if start_date
|
|
329
268
|
@download_date_range = start_date..today.to_date
|
|
330
|
-
|
|
331
|
-
# For an incremental download, we want to query from the end of the previous one, not from the
|
|
332
|
-
# beginning of the full range.
|
|
333
|
-
@start_date_in_query = metadata['date_end'] || @download_date_range.begin
|
|
334
|
-
log " Incremental download only. Pulling from #{@start_date_in_query}", both: true if metadata['date_end']
|
|
269
|
+
@start_date_in_query = @download_date_range.begin
|
|
335
270
|
|
|
336
271
|
# Catch-all to pick up anything that's been around since before the range started but hasn't
|
|
337
272
|
# had an update during the range.
|