jirametrics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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