jirametrics 2.2.1 → 2.4pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +13 -25
- data/lib/jirametrics/aging_work_bar_chart.rb +57 -39
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +9 -26
- data/lib/jirametrics/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/board_config.rb +2 -2
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/chart_base.rb +27 -39
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +1 -1
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +3 -16
- data/lib/jirametrics/daily_wip_chart.rb +1 -13
- data/lib/jirametrics/data_quality_report.rb +4 -1
- data/lib/jirametrics/dependency_chart.rb +1 -1
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +31 -25
- data/lib/jirametrics/examples/standard_project.rb +1 -1
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +3 -3
- data/lib/jirametrics/file_config.rb +12 -8
- data/lib/jirametrics/file_system.rb +11 -2
- data/lib/jirametrics/groupable_issue_chart.rb +2 -4
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -3
- data/lib/jirametrics/html/index.erb +1 -0
- data/lib/jirametrics/html_report_config.rb +61 -74
- data/lib/jirametrics/issue.rb +129 -57
- data/lib/jirametrics/project_config.rb +13 -7
- data/lib/jirametrics/sprint_burndown.rb +11 -0
- data/lib/jirametrics/status_collection.rb +4 -1
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +1 -1
- metadata +5 -7
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- /data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class
|
4
|
-
def initialize configuration_block
|
3
|
+
class EstimateAccuracyChart < ChartBase
|
4
|
+
def initialize configuration_block
|
5
5
|
super()
|
6
6
|
|
7
7
|
header_text 'Estimate Accuracy'
|
@@ -12,7 +12,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
12
12
|
</div>
|
13
13
|
<div class="p">
|
14
14
|
The #{color_block '--estimate-accuracy-chart-completed-fill-color'} completed dots indicate
|
15
|
-
cycletimes.
|
15
|
+
cycletimes.
|
16
16
|
<% if @has_aging_data %>
|
17
17
|
The #{color_block '--estimate-accuracy-chart-active-fill-color'} aging dots
|
18
18
|
(click on the legend to turn them on) show the current
|
@@ -27,7 +27,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
27
27
|
@y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
|
28
28
|
@y_axis_sort_order = nil
|
29
29
|
|
30
|
-
instance_eval(&configuration_block)
|
30
|
+
instance_eval(&configuration_block)
|
31
31
|
end
|
32
32
|
|
33
33
|
def run
|
@@ -39,26 +39,7 @@ class StoryPointAccuracyChart < ChartBase
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def scan_issues
|
42
|
-
aging_hash =
|
43
|
-
completed_hash = {}
|
44
|
-
|
45
|
-
issues.each do |issue|
|
46
|
-
cycletime = issue.board.cycletime
|
47
|
-
start_time = cycletime.started_time(issue)
|
48
|
-
stop_time = cycletime.stopped_time(issue)
|
49
|
-
|
50
|
-
next unless start_time
|
51
|
-
|
52
|
-
hash = stop_time ? completed_hash : aging_hash
|
53
|
-
|
54
|
-
estimate = @y_axis_block.call issue, start_time
|
55
|
-
cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
|
56
|
-
|
57
|
-
next if estimate.nil?
|
58
|
-
|
59
|
-
key = [estimate, cycle_time]
|
60
|
-
(hash[key] ||= []) << issue
|
61
|
-
end
|
42
|
+
completed_hash, aging_hash = split_into_completed_and_aging issues: issues
|
62
43
|
|
63
44
|
@has_aging_data = !aging_hash.empty?
|
64
45
|
|
@@ -93,7 +74,32 @@ class StoryPointAccuracyChart < ChartBase
|
|
93
74
|
'borderColor' => border_color,
|
94
75
|
'hidden' => starts_hidden
|
95
76
|
}
|
96
|
-
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def split_into_completed_and_aging issues:
|
81
|
+
aging_hash = {}
|
82
|
+
completed_hash = {}
|
83
|
+
|
84
|
+
issues.each do |issue|
|
85
|
+
cycletime = issue.board.cycletime
|
86
|
+
start_time = cycletime.started_time(issue)
|
87
|
+
stop_time = cycletime.stopped_time(issue)
|
88
|
+
|
89
|
+
next unless start_time
|
90
|
+
|
91
|
+
hash = stop_time ? completed_hash : aging_hash
|
92
|
+
|
93
|
+
estimate = @y_axis_block.call issue, start_time
|
94
|
+
cycle_time = ((stop_time&.to_date || date_range.end) - start_time.to_date).to_i + 1
|
95
|
+
|
96
|
+
next if estimate.nil?
|
97
|
+
|
98
|
+
key = [estimate, cycle_time]
|
99
|
+
(hash[key] ||= []) << issue
|
100
|
+
end
|
101
|
+
|
102
|
+
[completed_hash, aging_hash]
|
97
103
|
end
|
98
104
|
|
99
105
|
def hash_sorter
|
@@ -20,7 +20,7 @@ class ExpeditedChart < ChartBase
|
|
20
20
|
attr_accessor :issues, :cycletime, :possible_statuses, :date_range
|
21
21
|
attr_reader :expedited_label
|
22
22
|
|
23
|
-
def initialize
|
23
|
+
def initialize block
|
24
24
|
super()
|
25
25
|
|
26
26
|
header_text 'Expedited work'
|
@@ -38,6 +38,8 @@ class ExpeditedChart < ChartBase
|
|
38
38
|
</div>
|
39
39
|
#{describe_non_working_days}
|
40
40
|
HTML
|
41
|
+
|
42
|
+
instance_eval(&block)
|
41
43
|
end
|
42
44
|
|
43
45
|
def run
|
data/lib/jirametrics/exporter.rb
CHANGED
@@ -5,7 +5,7 @@ require 'fileutils'
|
|
5
5
|
class Object
|
6
6
|
def deprecated message:, date:
|
7
7
|
text = +''
|
8
|
-
text << "Deprecated(#{date}):"
|
8
|
+
text << "Deprecated(#{date}): "
|
9
9
|
text << message
|
10
10
|
text << "\n-> Called from #{caller(1..1).first}"
|
11
11
|
warn text
|
@@ -32,11 +32,12 @@ class Exporter
|
|
32
32
|
|
33
33
|
def initialize file_system: FileSystem.new
|
34
34
|
@project_configs = []
|
35
|
-
@timezone_offset = '+00:00'
|
36
35
|
@target_path = '.'
|
37
36
|
@holiday_dates = []
|
38
37
|
@downloading = false
|
39
38
|
@file_system = file_system
|
39
|
+
|
40
|
+
timezone_offset '+00:00'
|
40
41
|
end
|
41
42
|
|
42
43
|
def export name_filter:
|
@@ -79,7 +80,6 @@ class Exporter
|
|
79
80
|
end
|
80
81
|
|
81
82
|
def project name: nil, &block
|
82
|
-
raise 'target_path was never set!' if @target_path.nil?
|
83
83
|
raise 'jira_config not set' if @jira_config.nil?
|
84
84
|
|
85
85
|
@project_configs << ProjectConfig.new(
|
@@ -5,10 +5,11 @@ require 'csv'
|
|
5
5
|
class FileConfig
|
6
6
|
attr_reader :project_config, :issues
|
7
7
|
|
8
|
-
def initialize project_config:, block:
|
8
|
+
def initialize project_config:, block:, today: Date.today
|
9
9
|
@project_config = project_config
|
10
10
|
@block = block
|
11
11
|
@columns = nil
|
12
|
+
@today = today
|
12
13
|
end
|
13
14
|
|
14
15
|
def run
|
@@ -18,11 +19,8 @@ class FileConfig
|
|
18
19
|
if @columns
|
19
20
|
all_lines = prepare_grid
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
file.puts CSV.generate_line(output_line)
|
24
|
-
end
|
25
|
-
end
|
22
|
+
content = all_lines.collect { |line| CSV.generate_line line }.join
|
23
|
+
project_config.exporter.file_system.save_file content: content, filename: output_filename
|
26
24
|
elsif @html_report
|
27
25
|
@html_report.run
|
28
26
|
else
|
@@ -59,7 +57,7 @@ class FileConfig
|
|
59
57
|
segments = []
|
60
58
|
segments << project_config.target_path
|
61
59
|
segments << project_config.file_prefix
|
62
|
-
segments << (@file_suffix || "-#{
|
60
|
+
segments << (@file_suffix || "-#{@today}.csv")
|
63
61
|
segments.join
|
64
62
|
end
|
65
63
|
|
@@ -68,7 +66,9 @@ class FileConfig
|
|
68
66
|
# is that all empty values in the first column should be at the bottom.
|
69
67
|
def sort_output all_lines
|
70
68
|
all_lines.sort do |a, b|
|
71
|
-
if a[0]
|
69
|
+
if a[0] == b[0]
|
70
|
+
a[1..] <=> b[1..]
|
71
|
+
elsif a[0].nil?
|
72
72
|
1
|
73
73
|
elsif b[0].nil?
|
74
74
|
-1
|
@@ -112,6 +112,10 @@ class FileConfig
|
|
112
112
|
object.to_s
|
113
113
|
end
|
114
114
|
|
115
|
+
def to_integer object
|
116
|
+
object.to_i
|
117
|
+
end
|
118
|
+
|
115
119
|
def file_suffix suffix = nil
|
116
120
|
@file_suffix = suffix unless suffix.nil?
|
117
121
|
@file_suffix
|
@@ -5,17 +5,26 @@ require 'json'
|
|
5
5
|
class FileSystem
|
6
6
|
attr_accessor :logfile, :logfile_name
|
7
7
|
|
8
|
+
# Effectively the same as File.read except it forces the encoding to UTF-8
|
9
|
+
def load filename
|
10
|
+
File.read filename, encoding: 'UTF-8'
|
11
|
+
end
|
12
|
+
|
8
13
|
def load_json filename, fail_on_error: true
|
9
14
|
return nil if fail_on_error == false && File.exist?(filename) == false
|
10
15
|
|
11
|
-
JSON.parse
|
16
|
+
JSON.parse load(filename)
|
12
17
|
end
|
13
18
|
|
14
19
|
def save_json json:, filename:
|
20
|
+
save_file content: JSON.pretty_generate(compress json), filename: filename
|
21
|
+
end
|
22
|
+
|
23
|
+
def save_file content:, filename:
|
15
24
|
file_path = File.dirname(filename)
|
16
25
|
FileUtils.mkdir_p file_path unless File.exist?(file_path)
|
17
26
|
|
18
|
-
File.write(filename,
|
27
|
+
File.write(filename, content)
|
19
28
|
end
|
20
29
|
|
21
30
|
def log message
|
@@ -5,10 +5,8 @@ require 'jirametrics/grouping_rules'
|
|
5
5
|
|
6
6
|
module GroupableIssueChart
|
7
7
|
def init_configuration_block user_provided_block, &default_block
|
8
|
-
|
9
|
-
|
10
|
-
return if @group_by_block
|
11
|
-
end
|
8
|
+
instance_eval(&user_provided_block)
|
9
|
+
return if @group_by_block
|
12
10
|
|
13
11
|
instance_eval(&default_block)
|
14
12
|
end
|
@@ -3,15 +3,15 @@
|
|
3
3
|
require 'jirametrics/chart_base'
|
4
4
|
|
5
5
|
class HierarchyTable < ChartBase
|
6
|
-
def initialize block
|
6
|
+
def initialize block
|
7
7
|
super()
|
8
8
|
|
9
9
|
header_text 'Hierarchy Table'
|
10
|
-
description_text
|
11
|
-
<p>
|
10
|
+
description_text <<~HTML
|
11
|
+
<p>Shows all issues through this time period and the full hierarchy of their parents.</p>
|
12
12
|
HTML
|
13
13
|
|
14
|
-
instance_eval(&block)
|
14
|
+
instance_eval(&block)
|
15
15
|
end
|
16
16
|
|
17
17
|
def run
|
@@ -7,7 +7,7 @@
|
|
7
7
|
<th>Issue</th>
|
8
8
|
<th>Status</th>
|
9
9
|
<th>Fix versions</th>
|
10
|
-
<% if any_scrum_boards
|
10
|
+
<% if any_scrum_boards %>
|
11
11
|
<th>Sprints</th>
|
12
12
|
<% end %>
|
13
13
|
<th><%= aggregated_project? ? 'Board' : 'Who' %></th>
|
@@ -40,9 +40,9 @@
|
|
40
40
|
</div>
|
41
41
|
<% end %>
|
42
42
|
</td>
|
43
|
-
<td><%= format_status issue.status.name, board: issue.board
|
43
|
+
<td><%= format_status issue.status.name, board: issue.board %></td>
|
44
44
|
<td><%= fix_versions_text(issue) %></td>
|
45
|
-
<% if any_scrum_boards
|
45
|
+
<% if any_scrum_boards %>
|
46
46
|
<td><%= sprints_text(issue) %></td>
|
47
47
|
<% end %>
|
48
48
|
<td><%= aggregated_project? ? issue.board.name : issue.assigned_to %></td>
|
@@ -1,6 +1,7 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
3
|
<meta charset="UTF-8">
|
4
|
+
<link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
|
4
5
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
|
5
6
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
6
7
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
|
@@ -9,18 +9,45 @@ class HtmlReportConfig
|
|
9
9
|
|
10
10
|
attr_reader :file_config, :sections
|
11
11
|
|
12
|
+
def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
|
13
|
+
lines = []
|
14
|
+
lines << "def #{name} &block"
|
15
|
+
lines << ' block = ->(_) {} unless block'
|
16
|
+
if deprecated_warning
|
17
|
+
lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
|
18
|
+
end
|
19
|
+
lines << " execute_chart #{classname}.new(block)"
|
20
|
+
lines << 'end'
|
21
|
+
module_eval lines.join("\n"), __FILE__, __LINE__
|
22
|
+
end
|
23
|
+
|
24
|
+
define_chart name: 'aging_work_bar_chart', classname: 'AgingWorkBarChart'
|
25
|
+
define_chart name: 'aging_work_table', classname: 'AgingWorkTable'
|
26
|
+
define_chart name: 'cycletime_scatterplot', classname: 'CycletimeScatterplot'
|
27
|
+
define_chart name: 'daily_wip_chart', classname: 'DailyWipChart'
|
28
|
+
define_chart name: 'daily_wip_by_age_chart', classname: 'DailyWipByAgeChart'
|
29
|
+
define_chart name: 'daily_wip_by_blocked_stalled_chart', classname: 'DailyWipByBlockedStalledChart'
|
30
|
+
define_chart name: 'daily_wip_by_parent_chart', classname: 'DailyWipByParentChart'
|
31
|
+
define_chart name: 'throughput_chart', classname: 'ThroughputChart'
|
32
|
+
define_chart name: 'expedited_chart', classname: 'ExpeditedChart'
|
33
|
+
define_chart name: 'cycletime_histogram', classname: 'CycletimeHistogram'
|
34
|
+
define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
|
35
|
+
define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
|
36
|
+
|
37
|
+
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
38
|
+
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
39
|
+
define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
|
40
|
+
deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
|
41
|
+
|
12
42
|
def initialize file_config:, block:
|
13
43
|
@file_config = file_config
|
14
44
|
@block = block
|
15
|
-
# @cycletimes = []
|
16
45
|
@sections = []
|
17
46
|
end
|
18
47
|
|
19
48
|
def cycletime label = nil, &block
|
20
|
-
# TODO: This is about to become deprecated
|
21
|
-
|
22
49
|
@file_config.project_config.all_boards.each_value do |board|
|
23
|
-
raise 'Multiple cycletimes not supported
|
50
|
+
raise 'Multiple cycletimes not supported' if board.cycletime
|
24
51
|
|
25
52
|
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
|
26
53
|
end
|
@@ -39,21 +66,36 @@ class HtmlReportConfig
|
|
39
66
|
execute_chart DataQualityReport.new(@original_issue_times || {})
|
40
67
|
@sections.rotate!(-1)
|
41
68
|
|
42
|
-
File.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
69
|
+
html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
|
70
|
+
css = load_css html_directory: html_directory
|
71
|
+
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
72
|
+
file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
|
73
|
+
end
|
74
|
+
|
75
|
+
def file_system
|
76
|
+
@file_config.project_config.exporter.file_system
|
77
|
+
end
|
78
|
+
|
79
|
+
def log message
|
80
|
+
file_system.log message
|
48
81
|
end
|
49
82
|
|
50
83
|
def load_css html_directory:
|
51
|
-
|
84
|
+
base_css_filename = File.join(html_directory, 'index.css')
|
85
|
+
base_css = file_system.load(base_css_filename)
|
86
|
+
log("Loaded CSS: #{base_css_filename}")
|
87
|
+
|
52
88
|
extra_css_filename = settings['include_css']
|
53
|
-
|
89
|
+
if extra_css_filename
|
90
|
+
if File.exist?(extra_css_filename)
|
91
|
+
base_css << "\n\n" << file_system.load(extra_css_filename)
|
92
|
+
log("Loaded CSS: #{extra_css_filename}")
|
93
|
+
else
|
94
|
+
log("Unable to find specified CSS file: #{extra_css_filename}")
|
95
|
+
end
|
96
|
+
end
|
54
97
|
|
55
|
-
|
56
|
-
base_css << "\n\n" << File.read(extra_css_filename)
|
98
|
+
base_css
|
57
99
|
end
|
58
100
|
|
59
101
|
def board_id id = nil
|
@@ -66,6 +108,8 @@ class HtmlReportConfig
|
|
66
108
|
end
|
67
109
|
|
68
110
|
def aging_work_in_progress_chart board_id: nil, &block
|
111
|
+
block ||= ->(_) {}
|
112
|
+
|
69
113
|
if board_id.nil?
|
70
114
|
ids = issues.collect { |i| i.board.id }.uniq.sort
|
71
115
|
else
|
@@ -79,50 +123,6 @@ class HtmlReportConfig
|
|
79
123
|
end
|
80
124
|
end
|
81
125
|
|
82
|
-
def aging_work_bar_chart &block
|
83
|
-
execute_chart AgingWorkBarChart.new(block)
|
84
|
-
end
|
85
|
-
|
86
|
-
def aging_work_table &block
|
87
|
-
execute_chart AgingWorkTable.new(block)
|
88
|
-
end
|
89
|
-
|
90
|
-
def cycletime_scatterplot &block
|
91
|
-
execute_chart CycletimeScatterplot.new block
|
92
|
-
end
|
93
|
-
|
94
|
-
def daily_wip_chart &block
|
95
|
-
execute_chart DailyWipChart.new(block)
|
96
|
-
end
|
97
|
-
|
98
|
-
def daily_wip_by_age_chart &block
|
99
|
-
execute_chart DailyWipByAgeChart.new block
|
100
|
-
end
|
101
|
-
|
102
|
-
def daily_wip_by_type &block
|
103
|
-
execute_chart DailyWipChart.new block
|
104
|
-
end
|
105
|
-
|
106
|
-
def daily_wip_by_blocked_stalled_chart
|
107
|
-
execute_chart DailyWipByBlockedStalledChart.new
|
108
|
-
end
|
109
|
-
|
110
|
-
def daily_wip_by_parent_chart &block
|
111
|
-
execute_chart DailyWipByParentChart.new block
|
112
|
-
end
|
113
|
-
|
114
|
-
def throughput_chart &block
|
115
|
-
execute_chart ThroughputChart.new(block)
|
116
|
-
end
|
117
|
-
|
118
|
-
def expedited_chart
|
119
|
-
execute_chart ExpeditedChart.new
|
120
|
-
end
|
121
|
-
|
122
|
-
def cycletime_histogram &block
|
123
|
-
execute_chart CycletimeHistogram.new block
|
124
|
-
end
|
125
|
-
|
126
126
|
def random_color
|
127
127
|
"##{Random.bytes(3).unpack1('H*')}"
|
128
128
|
end
|
@@ -140,14 +140,6 @@ class HtmlReportConfig
|
|
140
140
|
end
|
141
141
|
end
|
142
142
|
|
143
|
-
def story_point_accuracy_chart &block
|
144
|
-
execute_chart StoryPointAccuracyChart.new block
|
145
|
-
end
|
146
|
-
|
147
|
-
def hierarchy_table &block
|
148
|
-
execute_chart HierarchyTable.new block
|
149
|
-
end
|
150
|
-
|
151
143
|
def discard_changes_before_hook issues_cutoff_times
|
152
144
|
# raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
|
153
145
|
|
@@ -173,6 +165,7 @@ class HtmlReportConfig
|
|
173
165
|
def execute_chart chart, &after_init_block
|
174
166
|
project_config = @file_config.project_config
|
175
167
|
|
168
|
+
chart.file_system = file_system
|
176
169
|
chart.issues = issues
|
177
170
|
chart.time_range = project_config.time_range
|
178
171
|
chart.timezone_offset = timezone_offset
|
@@ -199,19 +192,13 @@ class HtmlReportConfig
|
|
199
192
|
@file_config.issues
|
200
193
|
end
|
201
194
|
|
195
|
+
# For use by the user config
|
202
196
|
def find_board id
|
203
197
|
@file_config.project_config.all_boards[id]
|
204
198
|
end
|
205
199
|
|
206
|
-
|
207
|
-
@file_config.project_config.name
|
208
|
-
end
|
209
|
-
|
200
|
+
# For use by the user config
|
210
201
|
def boards
|
211
202
|
@file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
|
212
203
|
end
|
213
|
-
|
214
|
-
def find_project_by_name name
|
215
|
-
@file_config.project_config.exporter.project_configs.find { |p| p.name == name }
|
216
|
-
end
|
217
204
|
end
|