jirametrics 1.0.0

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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/bin/jirametrics +4 -0
  3. data/lib/jirametrics/aggregate_config.rb +89 -0
  4. data/lib/jirametrics/aging_work_bar_chart.rb +235 -0
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +148 -0
  6. data/lib/jirametrics/aging_work_table.rb +149 -0
  7. data/lib/jirametrics/anonymizer.rb +186 -0
  8. data/lib/jirametrics/blocked_stalled_change.rb +43 -0
  9. data/lib/jirametrics/board.rb +85 -0
  10. data/lib/jirametrics/board_column.rb +14 -0
  11. data/lib/jirametrics/board_config.rb +31 -0
  12. data/lib/jirametrics/change_item.rb +80 -0
  13. data/lib/jirametrics/chart_base.rb +239 -0
  14. data/lib/jirametrics/columns_config.rb +42 -0
  15. data/lib/jirametrics/cycletime_config.rb +69 -0
  16. data/lib/jirametrics/cycletime_histogram.rb +74 -0
  17. data/lib/jirametrics/cycletime_scatterplot.rb +128 -0
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +88 -0
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +77 -0
  20. data/lib/jirametrics/daily_wip_chart.rb +123 -0
  21. data/lib/jirametrics/data_quality_report.rb +278 -0
  22. data/lib/jirametrics/dependency_chart.rb +217 -0
  23. data/lib/jirametrics/discard_changes_before.rb +37 -0
  24. data/lib/jirametrics/download_config.rb +41 -0
  25. data/lib/jirametrics/downloader.rb +337 -0
  26. data/lib/jirametrics/examples/aggregated_project.rb +36 -0
  27. data/lib/jirametrics/examples/standard_project.rb +111 -0
  28. data/lib/jirametrics/expedited_chart.rb +169 -0
  29. data/lib/jirametrics/experimental/generator.rb +209 -0
  30. data/lib/jirametrics/experimental/info.rb +77 -0
  31. data/lib/jirametrics/exporter.rb +127 -0
  32. data/lib/jirametrics/file_config.rb +119 -0
  33. data/lib/jirametrics/fix_version.rb +21 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +44 -0
  35. data/lib/jirametrics/grouping_rules.rb +13 -0
  36. data/lib/jirametrics/hierarchy_table.rb +31 -0
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +72 -0
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +52 -0
  39. data/lib/jirametrics/html/aging_work_table.erb +60 -0
  40. data/lib/jirametrics/html/collapsible_issues_panel.erb +32 -0
  41. data/lib/jirametrics/html/cycletime_histogram.erb +41 -0
  42. data/lib/jirametrics/html/cycletime_scatterplot.erb +103 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +63 -0
  44. data/lib/jirametrics/html/data_quality_report.erb +126 -0
  45. data/lib/jirametrics/html/expedited_chart.erb +67 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +29 -0
  47. data/lib/jirametrics/html/index.erb +66 -0
  48. data/lib/jirametrics/html/sprint_burndown.erb +116 -0
  49. data/lib/jirametrics/html/story_point_accuracy_chart.erb +57 -0
  50. data/lib/jirametrics/html/throughput_chart.erb +65 -0
  51. data/lib/jirametrics/html_report_config.rb +217 -0
  52. data/lib/jirametrics/issue.rb +521 -0
  53. data/lib/jirametrics/issue_link.rb +60 -0
  54. data/lib/jirametrics/json_file_loader.rb +9 -0
  55. data/lib/jirametrics/project_config.rb +442 -0
  56. data/lib/jirametrics/rules.rb +34 -0
  57. data/lib/jirametrics/self_or_issue_dispatcher.rb +15 -0
  58. data/lib/jirametrics/sprint.rb +43 -0
  59. data/lib/jirametrics/sprint_burndown.rb +335 -0
  60. data/lib/jirametrics/sprint_issue_change_data.rb +31 -0
  61. data/lib/jirametrics/status.rb +26 -0
  62. data/lib/jirametrics/status_collection.rb +67 -0
  63. data/lib/jirametrics/story_point_accuracy_chart.rb +139 -0
  64. data/lib/jirametrics/throughput_chart.rb +91 -0
  65. data/lib/jirametrics/tree_organizer.rb +96 -0
  66. data/lib/jirametrics/trend_line_calculator.rb +74 -0
  67. data/lib/jirametrics.rb +85 -0
  68. metadata +167 -0
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DataQualityReport < ChartBase
4
+ attr_reader :original_issue_times # For testing purposes only
5
+ attr_accessor :board_id
6
+
7
+ class Entry
8
+ attr_reader :started, :stopped, :issue, :problems
9
+
10
+ def initialize started:, stopped:, issue:
11
+ @started = started
12
+ @stopped = stopped
13
+ @issue = issue
14
+ @problems = []
15
+ end
16
+
17
+ def report problem_key: nil, detail: nil
18
+ @problems << [problem_key, detail]
19
+ end
20
+ end
21
+
22
+ def initialize original_issue_times
23
+ super()
24
+
25
+ @original_issue_times = original_issue_times
26
+
27
+ header_text 'Data Quality Report'
28
+ description_text <<-HTML
29
+ <p>
30
+ We have a tendency to assume that anything we see in a chart is 100% accurate, although that's
31
+ not always true. To understand the accuracy of the chart, we have to understand how accurate the
32
+ initial data was and also how much of the original data set was used in the chart. This section
33
+ will hopefully give you enough information to make that decision.
34
+ </p>
35
+ HTML
36
+ end
37
+
38
+ def run
39
+ initialize_entries
40
+
41
+ @entries.each do |entry|
42
+ board = entry.issue.board
43
+ backlog_statuses = board.backlog_statuses
44
+
45
+ scan_for_completed_issues_without_a_start_time entry: entry
46
+ scan_for_status_change_after_done entry: entry
47
+ scan_for_backwards_movement entry: entry, backlog_statuses: backlog_statuses
48
+ scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
49
+ scan_for_stopped_before_started entry: entry
50
+ scan_for_issues_not_started_with_subtasks_that_have entry: entry
51
+ scan_for_discarded_data entry: entry
52
+ end
53
+
54
+ scan_for_issues_on_multiple_boards entries: @entries
55
+
56
+ entries_with_problems = entries_with_problems()
57
+ return '' if entries_with_problems.empty?
58
+
59
+ wrap_and_render(binding, __FILE__)
60
+ end
61
+
62
+ def problems_for key
63
+ result = []
64
+ @entries.each do |entry|
65
+ entry.problems.each do |problem_key, detail|
66
+ result << [entry.issue, detail, key] if problem_key == key
67
+ end
68
+ end
69
+ result
70
+ end
71
+
72
+ # Return a format that's easier to assert against
73
+ def testable_entries
74
+ @entries.collect { |entry| [entry.started.to_s, entry.stopped.to_s, entry.issue] }
75
+ end
76
+
77
+ def entries_with_problems
78
+ @entries.reject { |entry| entry.problems.empty? }
79
+ end
80
+
81
+ def category_name_for status_name:, board:
82
+ board.possible_statuses.find { |status| status.name == status_name }&.category_name
83
+ end
84
+
85
+ def initialize_entries
86
+ @entries = @issues.collect do |issue|
87
+ cycletime = issue.board.cycletime
88
+ started = cycletime.started_time(issue)
89
+ stopped = cycletime.stopped_time(issue)
90
+ next if stopped && stopped < time_range.begin
91
+ next if started && started > time_range.end
92
+
93
+ Entry.new started: started, stopped: stopped, issue: issue
94
+ end.compact
95
+
96
+ @entries.sort! do |a, b|
97
+ a.issue.key =~ /.+-(\d+)$/
98
+ a_id = $1.to_i
99
+
100
+ b.issue.key =~ /.+-(\d+)$/
101
+ b_id = $1.to_i
102
+
103
+ a_id <=> b_id
104
+ end
105
+ end
106
+
107
+ def scan_for_completed_issues_without_a_start_time entry:
108
+ return unless entry.stopped && entry.started.nil?
109
+
110
+ status_names = entry.issue.changes.collect do |change|
111
+ next unless change.status?
112
+
113
+ format_status change.value, board: entry.issue.board
114
+ end.compact
115
+
116
+ entry.report(
117
+ problem_key: :completed_but_not_started,
118
+ detail: "Status changes: #{status_names.join ' → '}"
119
+ )
120
+ end
121
+
122
+ def scan_for_status_change_after_done entry:
123
+ return unless entry.stopped
124
+
125
+ changes_after_done = entry.issue.changes.select do |change|
126
+ change.status? && change.time >= entry.stopped
127
+ end
128
+ done_status = changes_after_done.shift.value
129
+
130
+ return if changes_after_done.empty?
131
+
132
+ board = entry.issue.board
133
+ problem = "Completed on #{entry.stopped.to_date} with status #{format_status done_status, board: board}."
134
+ changes_after_done.each do |change|
135
+ problem << " Changed to #{format_status change.value, board: board} on #{change.time.to_date}."
136
+ end
137
+ entry.report(
138
+ problem_key: :status_changes_after_done,
139
+ detail: problem
140
+ )
141
+ end
142
+
143
+ def scan_for_backwards_movement entry:, backlog_statuses:
144
+ issue = entry.issue
145
+
146
+ # Moving backwards through statuses is bad. Moving backwards through status categories is almost always worse.
147
+ last_index = -1
148
+ issue.changes.each do |change|
149
+ next unless change.status?
150
+
151
+ board = entry.issue.board
152
+ index = entry.issue.board.visible_columns.find_index { |column| column.status_ids.include? change.value_id }
153
+ if index.nil?
154
+ # If it's a backlog status then ignore it. Not supposed to be visible.
155
+ next if entry.issue.board.backlog_statuses.include? change.value_id
156
+
157
+ detail = "Status #{format_status change.value, board: board} is not on the board"
158
+ if issue.board.possible_statuses.expand_statuses(change.value).empty?
159
+ detail = "Status #{format_status change.value, board: board} cannot be found at all. Was it deleted?"
160
+ end
161
+
162
+ # If it's been moved back to backlog then it's on a different report. Ignore it here.
163
+ detail = nil if backlog_statuses.any? { |s| s.name == change.value }
164
+
165
+ entry.report(problem_key: :status_not_on_board, detail: detail) unless detail.nil?
166
+ elsif change.old_value.nil?
167
+ # Do nothing
168
+ elsif index < last_index
169
+ new_category = category_name_for(status_name: change.value, board: board)
170
+ old_category = category_name_for(status_name: change.old_value, board: board)
171
+
172
+ if new_category == old_category
173
+ entry.report(
174
+ problem_key: :backwords_through_statuses,
175
+ detail: "Moved from #{format_status change.old_value, board: board}" \
176
+ " to #{format_status change.value, board: board}" \
177
+ " on #{change.time.to_date}"
178
+ )
179
+ else
180
+ entry.report(
181
+ problem_key: :backwards_through_status_categories,
182
+ detail: "Moved from #{format_status change.old_value, board: board}" \
183
+ " to #{format_status change.value, board: board}" \
184
+ " on #{change.time.to_date}, " \
185
+ " crossing from category #{format_status old_category, board: board, is_category: true}" \
186
+ " to #{format_status new_category, board: board, is_category: true}."
187
+ )
188
+ end
189
+ end
190
+ last_index = index || -1
191
+ end
192
+ end
193
+
194
+ def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
195
+ return if backlog_statuses.empty?
196
+
197
+ creation_change = entry.issue.changes.find { |issue| issue.status? }
198
+
199
+ return if backlog_statuses.any? { |status| status.id == creation_change.value_id }
200
+
201
+ status_string = backlog_statuses.collect { |s| format_status s.name, board: entry.issue.board }.join(', ')
202
+ entry.report(
203
+ problem_key: :created_in_wrong_status,
204
+ detail: "Created in #{format_status creation_change.value, board: entry.issue.board}, " \
205
+ "which is not one of the backlog statuses for this board: #{status_string}"
206
+ )
207
+ end
208
+
209
+ def scan_for_stopped_before_started entry:
210
+ return unless entry.stopped && entry.started && entry.stopped < entry.started
211
+
212
+ entry.report(
213
+ problem_key: :stopped_before_started,
214
+ detail: "The stopped time '#{entry.stopped}' is before the started time '#{entry.started}'"
215
+ )
216
+ end
217
+
218
+ def scan_for_issues_not_started_with_subtasks_that_have entry:
219
+ return if entry.started
220
+
221
+ started_subtasks = []
222
+ entry.issue.subtasks.each do |subtask|
223
+ started_subtasks << subtask if subtask.board.cycletime.started_time(subtask)
224
+ end
225
+
226
+ return if started_subtasks.empty?
227
+
228
+ subtask_labels = started_subtasks.collect do |subtask|
229
+ "Started subtask: #{link_to_issue(subtask)} (#{format_status subtask.status.name, board: entry.issue.board}) " \
230
+ "#{subtask.summary[..50].inspect}"
231
+ end
232
+ entry.report(
233
+ problem_key: :issue_not_started_but_subtasks_have,
234
+ detail: subtask_labels.join('<br />')
235
+ )
236
+ end
237
+
238
+ def label_issues number
239
+ return '1 item' if number == 1
240
+
241
+ "#{number} items"
242
+ end
243
+
244
+ def scan_for_discarded_data entry:
245
+ hash = @original_issue_times[entry.issue]
246
+ return if hash.nil?
247
+
248
+ old_start_time = hash[:started_time]
249
+ cutoff_time = hash[:cutoff_time]
250
+
251
+ old_start_date = old_start_time.to_date
252
+ cutoff_date = cutoff_time.to_date
253
+
254
+ days_ignored = (cutoff_date - old_start_date).to_i + 1
255
+ message = "Started: #{old_start_date}, Discarded: #{cutoff_date}, Ignored: #{label_days days_ignored}"
256
+
257
+ # If days_ignored is zero then we don't really care as it won't affect any of the calculations.
258
+ return if days_ignored == 1
259
+
260
+ entry.report(
261
+ problem_key: :discarded_changes,
262
+ detail: message
263
+ )
264
+ end
265
+
266
+ def scan_for_issues_on_multiple_boards entries:
267
+ grouped_entries = entries.group_by { |entry| entry.issue.key }
268
+ grouped_entries.each_value do |entry_list|
269
+ next if entry_list.size == 1
270
+
271
+ board_names = entry_list.collect { |entry| entry.issue.board.name.inspect }
272
+ entry_list.first.report(
273
+ problem_key: :issue_on_multiple_boards,
274
+ detail: "Found on boards: #{board_names.join(', ')}"
275
+ )
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/chart_base'
4
+ require 'open3'
5
+ require 'jirametrics/rules'
6
+
7
+ class DependencyChart < ChartBase
8
+ class LinkRules < Rules
9
+ attr_accessor :line_color, :label
10
+
11
+ def merge_bidirectional keep: 'inward'
12
+ raise "Keep must be either inward or outward: #{keep}" unless %i[inward outward].include? keep.to_sym
13
+
14
+ @merge_bidirectional = keep.to_sym
15
+ end
16
+
17
+ def get_merge_bidirectional # rubocop:disable Naming/AccessorMethodName
18
+ @merge_bidirectional
19
+ end
20
+
21
+ def use_bidirectional_arrows
22
+ @use_bidirectional_arrows = true
23
+ end
24
+
25
+ def bidirectional_arrows?
26
+ @use_bidirectional_arrows
27
+ end
28
+ end
29
+
30
+ class IssueRules < Rules
31
+ attr_accessor :color, :label
32
+ end
33
+
34
+ def initialize rules_block
35
+ super()
36
+
37
+ header_text 'Dependencies'
38
+ description_text <<-HTML
39
+ <p>
40
+ These are all the "linked issues" as defined in Jira
41
+ </p>
42
+ HTML
43
+
44
+ @rules_block = rules_block
45
+ @link_rules_block = ->(link_name, link_rules) {}
46
+
47
+ issue_rules do |issue, rules|
48
+ key = issue.key
49
+ key = "<S>#{key} </S> " if issue.status.category_name == 'Done'
50
+ rules.label = "<#{key} [#{issue.type}]<BR/>#{word_wrap issue.summary}>"
51
+ end
52
+ end
53
+
54
+ def run
55
+ instance_eval(&@rules_block) if @rules_block
56
+
57
+ dot_graph = build_dot_graph
58
+ return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
59
+
60
+ svg = execute_graphviz(dot_graph.join("\n"))
61
+ "<h1>#{@header_text}</h1><div>#{@description_text}</div>#{shrink_svg svg}"
62
+ end
63
+
64
+ def link_rules &block
65
+ @link_rules_block = block
66
+ end
67
+
68
+ def issue_rules &block
69
+ @issue_rules_block = block
70
+ end
71
+
72
+ def find_links
73
+ result = []
74
+ issues.each do |issue|
75
+ result += issue.issue_links
76
+ end
77
+ result
78
+ end
79
+
80
+ def make_dot_link issue_link:, link_rules:
81
+ result = String.new
82
+ result << issue_link.origin.key.inspect
83
+ result << ' -> '
84
+ result << issue_link.other_issue.key.inspect
85
+ result << '['
86
+ result << 'label=' << (link_rules.label || issue_link.label).inspect
87
+ result << ',color=' << (link_rules.line_color || 'black').inspect
88
+ result << ',dir=both' if link_rules.bidirectional_arrows?
89
+ result << '];'
90
+ result
91
+ end
92
+
93
+ def make_dot_issue issue:, issue_rules:
94
+ result = String.new
95
+ result << issue.key.inspect
96
+ result << '['
97
+ label = issue_rules.label || "#{issue.key}|#{issue.type}"
98
+ label = label.inspect unless label =~ /^<.+>$/
99
+ result << "label=#{label}"
100
+ result << ',shape=Mrecord'
101
+ tooltip = "#{issue.key}: #{issue.summary}"
102
+ result << ",tooltip=#{tooltip[0..80].inspect}"
103
+ unless issue_rules.color == :none
104
+ result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type, shade: :light)}")
105
+ end
106
+ result << ']'
107
+ result
108
+ end
109
+
110
+ def build_dot_graph
111
+ issue_links = find_links
112
+
113
+ visible_issues = {}
114
+ link_graph = []
115
+ links_to_ignore = []
116
+
117
+ issue_links.each do |link|
118
+ next if links_to_ignore.include? link
119
+
120
+ link_rules = LinkRules.new
121
+ @link_rules_block.call link, link_rules
122
+
123
+ next if link_rules.ignored?
124
+
125
+ if link_rules.get_merge_bidirectional
126
+ opposite = issue_links.find do |l|
127
+ l.name == link.name && l.origin.key == link.other_issue.key && l.other_issue.key == link.origin.key
128
+ end
129
+ if opposite
130
+ # rubocop:disable Style/GuardClause
131
+ if link_rules.get_merge_bidirectional.to_sym == link.direction
132
+ # We keep this one and discard the opposite
133
+ links_to_ignore << opposite
134
+ else
135
+ # We keep the opposite and discard this one
136
+ next
137
+ end
138
+ # rubocop:enable Style/GuardClause
139
+ end
140
+ end
141
+
142
+ link_graph << make_dot_link(issue_link: link, link_rules: link_rules)
143
+
144
+ visible_issues[link.origin.key] = link.origin
145
+ visible_issues[link.other_issue.key] = link.other_issue
146
+ end
147
+
148
+ dot_graph = []
149
+ dot_graph << 'digraph mygraph {'
150
+ dot_graph << 'rankdir=LR'
151
+
152
+ # Sort the keys so they are proccessed in a deterministic order.
153
+ visible_issues.values.sort_by(&:key_as_i).each do |issue|
154
+ rules = IssueRules.new
155
+ @issue_rules_block.call(issue, rules)
156
+ dot_graph << make_dot_issue(issue: issue, issue_rules: rules)
157
+ end
158
+
159
+ dot_graph += link_graph
160
+ dot_graph << '}'
161
+
162
+ return nil if visible_issues.empty?
163
+ dot_graph
164
+ end
165
+
166
+ def execute_graphviz dot_graph
167
+ Open3.popen3('dot -Tsvg') do |stdin, stdout, _stderr, _wait_thread|
168
+ stdin.write dot_graph
169
+ stdin.close
170
+ return stdout.read
171
+ end
172
+ rescue # rubocop:disable Style/RescueStandardError
173
+ message = "Unable to execute the command 'dot' which is part of graphviz. " \
174
+ 'Ensure that graphviz is installed and that dot is in your path.'
175
+ puts message
176
+ message
177
+ end
178
+
179
+ def default_color_for_issue issue
180
+ {
181
+ 'Story' => '#90EE90',
182
+ 'Task' => '#87CEFA',
183
+ 'Bug' => '#f08080',
184
+ 'Defect' => '#f08080',
185
+ 'Epic' => '#fafad2',
186
+ 'Spike' => '#7fffd4',
187
+ 'Sub-task' => '#dcdcdc'
188
+ }[issue.type]
189
+ end
190
+
191
+ def shrink_svg svg
192
+ scale = 0.8
193
+ svg.sub(/width="([\d.]+)pt" height="([\d.]+)pt"/) do
194
+ width = $1.to_i * scale
195
+ height = $2.to_i * scale
196
+ "width=\"#{width.to_i}pt\" height=\"#{height.to_i}pt\""
197
+ end
198
+ end
199
+
200
+ def word_wrap text, max_width: 50, separator: '<BR/>'
201
+ text.chomp.lines.collect do |line|
202
+ line.chomp!
203
+
204
+ # The following characters all cause problems when passed to graphviz
205
+ line.gsub!(/[{<]/, '[')
206
+ line.gsub!(/[}>]/, ']')
207
+ line.gsub!(/\s*&\s*/, ' and ')
208
+ line.gsub!('|', '')
209
+
210
+ if line.length > max_width
211
+ line.gsub(/(.{1,#{max_width}})(\s+|$)/, "\\1#{separator}").strip
212
+ else
213
+ line
214
+ end
215
+ end.join(separator)
216
+ end
217
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscardChangesBefore
4
+ def discard_changes_before status_becomes: nil, &block
5
+ if status_becomes
6
+ status_becomes = [status_becomes] unless status_becomes.is_a? Array
7
+
8
+ block = lambda do |issue|
9
+ trigger_statuses = status_becomes.collect do |status_name|
10
+ if status_name == :backlog
11
+ issue.board.backlog_statuses.collect(&:name)
12
+ else
13
+ status_name
14
+ end
15
+ end.flatten
16
+
17
+ time = nil
18
+ issue.changes.each do |change|
19
+ time = change.time if change.status? && trigger_statuses.include?(change.value) && change.artificial? == false
20
+ end
21
+ time
22
+ end
23
+ end
24
+
25
+ issues_cutoff_times = []
26
+ issues.each do |issue|
27
+ cutoff_time = block.call(issue)
28
+ issues_cutoff_times << [issue, cutoff_time] if cutoff_time
29
+ end
30
+
31
+ discard_changes_before_hook issues_cutoff_times
32
+
33
+ issues_cutoff_times.each do |issue, cutoff_time|
34
+ issue.changes.reject! { |change| change.status? && change.time <= cutoff_time && change.artificial? == false }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ class DownloadConfig
6
+ attr_reader :project_config
7
+
8
+ def initialize project_config:, block:
9
+ @project_config = project_config
10
+ @block = block
11
+ @board_ids = []
12
+ end
13
+
14
+ def run
15
+ instance_eval(&@block)
16
+ end
17
+
18
+ def project_key _key = nil
19
+ raise 'project, filter, and jql directives are no longer supported. See ' \
20
+ 'https://github.com/mikebowler/jira-export/wiki/Deprecated#project-filter-and-jql-are-no-longer-supported-in-the-download-section'
21
+ end
22
+
23
+ def board_ids *ids
24
+ deprecated message: 'board_ids in the download block are deprecated. See https://github.com/mikebowler/jira-export/wiki/Deprecated'
25
+ @board_ids = ids unless ids.empty?
26
+ @board_ids
27
+ end
28
+
29
+ def filter_name _filter = nil
30
+ project_key
31
+ end
32
+
33
+ def jql _query = nil
34
+ project_key
35
+ end
36
+
37
+ def rolling_date_count count = nil
38
+ @rolling_date_count = count unless count.nil?
39
+ @rolling_date_count
40
+ end
41
+ end