jirametrics 2.13 → 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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aging_work_bar_chart.rb +176 -134
  3. data/lib/jirametrics/anonymizer.rb +8 -6
  4. data/lib/jirametrics/atlassian_document_format.rb +8 -4
  5. data/lib/jirametrics/bar_chart_range.rb +17 -0
  6. data/lib/jirametrics/board.rb +4 -0
  7. data/lib/jirametrics/board_config.rb +4 -1
  8. data/lib/jirametrics/change_item.rb +11 -4
  9. data/lib/jirametrics/chart_base.rb +36 -2
  10. data/lib/jirametrics/cycletime_config.rb +22 -4
  11. data/lib/jirametrics/cycletime_histogram.rb +3 -1
  12. data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
  13. data/lib/jirametrics/daily_view.rb +49 -42
  14. data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
  15. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
  16. data/lib/jirametrics/daily_wip_chart.rb +1 -1
  17. data/lib/jirametrics/data_quality_report.rb +8 -3
  18. data/lib/jirametrics/dependency_chart.rb +4 -1
  19. data/lib/jirametrics/downloader.rb +34 -99
  20. data/lib/jirametrics/downloader_for_cloud.rb +202 -0
  21. data/lib/jirametrics/downloader_for_data_center.rb +94 -0
  22. data/lib/jirametrics/examples/standard_project.rb +9 -9
  23. data/lib/jirametrics/expedited_chart.rb +1 -1
  24. data/lib/jirametrics/exporter.rb +12 -5
  25. data/lib/jirametrics/file_system.rb +24 -1
  26. data/lib/jirametrics/fix_version.rb +13 -0
  27. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  28. data/lib/jirametrics/groupable_issue_chart.rb +7 -1
  29. data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
  30. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  31. data/lib/jirametrics/html/aging_work_table.erb +2 -0
  32. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  33. data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
  34. data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
  35. data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
  36. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
  37. data/lib/jirametrics/html/expedited_chart.erb +3 -1
  38. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
  39. data/lib/jirametrics/html/index.css +21 -9
  40. data/lib/jirametrics/html/index.erb +3 -35
  41. data/lib/jirametrics/html/index.js +114 -0
  42. data/lib/jirametrics/html/sprint_burndown.erb +11 -3
  43. data/lib/jirametrics/html/throughput_chart.erb +2 -2
  44. data/lib/jirametrics/html_generator.rb +31 -0
  45. data/lib/jirametrics/html_report_config.rb +8 -25
  46. data/lib/jirametrics/issue.rb +127 -22
  47. data/lib/jirametrics/jira_gateway.rb +55 -17
  48. data/lib/jirametrics/project_config.rb +42 -5
  49. data/lib/jirametrics/raw_javascript.rb +13 -0
  50. data/lib/jirametrics/settings.json +3 -1
  51. data/lib/jirametrics/sprint.rb +12 -0
  52. data/lib/jirametrics/sprint_burndown.rb +6 -2
  53. data/lib/jirametrics/stitcher.rb +75 -0
  54. data/lib/jirametrics.rb +26 -70
  55. metadata +10 -3
@@ -6,12 +6,14 @@ require 'date'
6
6
  class CycleTimeConfig
7
7
  include SelfOrIssueDispatcher
8
8
 
9
- attr_reader :label, :parent_config
9
+ attr_reader :label, :possible_statuses, :settings, :file_system
10
10
 
11
- def initialize parent_config:, label:, block:, file_system: nil, today: Date.today
12
- @parent_config = parent_config
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
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
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
- @highest_cycletime = 0
38
+ @highest_y_value = 0
39
39
  end
40
40
 
41
- def run
42
- completed_issues = completed_issues_in_range include_unstarted: false
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
- data_sets = create_datasets completed_issues
45
- overall_percent_line = calculate_percent_line(completed_issues)
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 completed_issues
72
+ def create_datasets items
54
73
  data_sets = []
55
74
 
56
- group_issues(completed_issues).each do |rules, completed_issues_by_type|
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 completed_issues_by_type
60
- data = completed_issues_by_type.filter_map { |issue| data_for_issue(issue) }
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: @highest_cycletime
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 issue
110
- cycle_time = issue.board.cycletime.cycletime(issue)
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
- @highest_cycletime = cycle_time if @highest_cycletime < cycle_time
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(issue.board.cycletime.started_stopped_times(issue).last),
118
- title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
136
+ x: chart_format(x_value(item)),
137
+ title: [title_value(item)]
119
138
  }
120
139
  end
121
140
 
122
- def calculate_percent_line completed_issues
123
- times = completed_issues.collect { |issue| issue.board.cycletime.cycletime(issue) }
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)
@@ -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 [] unless blocked_stalled
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
- "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
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> &nbsp;<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
- age = issue.board.cycletime.age(issue, today: date_range.end)
134
- line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
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.reject { |i| i.done? }
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
- id = next_id
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 << "<table id='table#{id}' style='display: none'>"
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.status?
202
- convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
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
- lines += make_description_lines(issue)
250
- lines += make_sprints_lines(issue)
251
- lines += make_blocked_stalled_lines(issue)
252
- lines += make_child_lines(issue)
253
- lines += make_history_lines(issue)
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|
@@ -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 + 1
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.board.cycletime.started_stopped_dates(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 designated as Backlog. This will impact
420
- the measurement of start times and will therefore impact whether it's shown as in progress or not.
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
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
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 = 4
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.