csa-ccm 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,32 +3,19 @@ module Csa::Ccm
3
3
  class Answer
4
4
 
5
5
  ATTRIBS = %i(
6
- id content
7
- control-id question-id
8
- notes
6
+ control_id
7
+ question_id
8
+ answer
9
+ comment
9
10
  )
10
11
 
11
12
  attr_accessor *ATTRIBS
12
13
 
13
14
  def initialize(options={})
14
- @examples = []
15
- @notes = []
16
-
17
- # puts "options #{options.inspect}"
18
-
19
15
  options.each_pair do |k, v|
20
- next unless v
21
- case k
22
- when /^example/
23
- @examples << v
24
- when /^note/
25
- @notes << v
26
- else
27
- # puts"Key #{k}"
28
- key = k.gsub("-", "_")
29
- self.send("#{key}=", v)
30
- end
16
+ self.send("#{k}=", v)
31
17
  end
18
+
32
19
  self
33
20
  end
34
21
 
@@ -42,16 +29,6 @@ class Answer
42
29
  end
43
30
  end
44
31
  end
45
-
46
- # # entry-status
47
- # ## Must be one of notValid valid superseded retired
48
- # def entry_status=(value)
49
- # unless %w(notValid valid superseded retired).include?(value)
50
- # value = "notValid"
51
- # end
52
- # @entry_status = value
53
- # end
54
-
55
32
  end
56
33
 
57
34
  end
@@ -11,101 +11,106 @@ module Csa
11
11
  desc 'ccm-yaml VERSION', 'Generating a machine-readable CCM/CAIQ'
12
12
  option :output_file, aliases: :o, type: :string, desc: 'Optional output YAML file. If missed, the input file’s name will be used'
13
13
 
14
- def ccm_yaml(version)
15
- input_files = Resource.lookup_version(version)
14
+ def ccm_yaml(caiq_version)
15
+ input_files = Resource.lookup_version(caiq_version)
16
16
 
17
- unless input_files || !input_files.empty?
18
- UI.say("No file found for #{version} version")
17
+ unless input_files && !input_files.empty?
18
+ UI.say("No file found for #{caiq_version} version")
19
19
  return
20
20
  end
21
21
 
22
22
  input_file = input_files.first
23
23
 
24
24
  unless File.exist? input_file
25
- UI.say('No file found for version')
25
+ UI.say("#{input_file} doesn't exists for #{caiq_version} version")
26
26
  return
27
27
  end
28
28
 
29
- matrix = Matrix.from_xlsx(version, input_file)
29
+ matrix = Matrix.from_xlsx(input_file)
30
30
 
31
- output_file = options[:output_file] || "caiq-#{version}.yaml"
32
- matrix.to_file(output_file)
31
+ output_file = options[:output_file] || "caiq-#{matrix.version}.yaml"
32
+ matrix.to_control_file(output_file)
33
33
  end
34
34
 
35
- desc "xlsx2yaml XSLT_PATH", "Converting CCM XSLX to YAML"
35
+ desc "xlsx2yaml XLSX_PATH", "Converting CCM XSLX to YAML"
36
36
  option :output_file, aliases: :o, type: :string, desc: "Optional output YAML file. If missed, the input file’s name will be used"
37
37
 
38
- def xlsx2yaml(xslt_path)
39
- raise 'Not implemented yet'
38
+ def xlsx2yaml(input_xlsx_file)
39
+ unless input_xlsx_file
40
+ UI.say("#{input_xlsx_file} file doesn't exists")
41
+ return
42
+ end
43
+
44
+ matrix = Matrix.from_xlsx(input_xlsx_file)
45
+
46
+ output_file = options[:output_file] || input_xlsx_file.gsub('.xlsx', '.yaml')
47
+ matrix.to_control_file(output_file)
40
48
  end
41
49
 
42
- desc "caiq2yaml XSLT_PATH", "Converting a filled CAIQ to YAML"
50
+ desc "caiq2yaml XLSX_PATH", "Converting a filled CAIQ to YAML"
43
51
  option :output_name, aliases: :n, type: :string, desc: "Optional output CAIQ YAML file. If missed, the input file’s name will be used"
44
52
  option :output_path, aliases: :p, type: :string, desc: "Optional output directory for result file. If missed pwd will be used"
45
53
 
46
- def caiq2yaml(xslt_path)
47
- # if xslt_path.nil?
48
- # puts 'Error: no filepath given as first argument.'
49
- # exit 1
50
- # end
51
-
52
- # if Pathname.new(xslt_path).extname != ".xlsx"
53
- # puts 'Error: filepath given must have extension .xlsx.'
54
- # exit 1
55
- # end
56
-
57
- # workbook = Csa::Ccm::MatrixWorkbook.new(xslt_path)
58
- # workbook.glossary_info.metadata_section.structure
59
- # workbook.glossary_info.metadata_section.attributes
60
-
61
- # languages = {}
62
-
63
- # workbook.languages_supported.map do |lang|
64
- # puts "************** WORKING ON LANGUAGE (#{lang})"
65
- # sheet = workbook.language_sheet(lang)
66
- # termsec = sheet.terms_section
67
- # languages[sheet.language_code] = termsec.terms
68
- # end
54
+ def caiq2yaml(input_xlsx_file)
55
+ unless input_xlsx_file
56
+ UI.say("#{input_xlsx_file} file doesn't exists")
57
+ return
58
+ end
69
59
 
70
- # collection = Csa::Ccm::ControlDomain.new
60
+ matrix = Matrix.from_xlsx(input_xlsx_file)
71
61
 
72
- # languages.each_pair do |lang, terms|
73
- # terms.each do |term|
74
- # collection.add_term(term)
75
- # end
76
- # end
62
+ base_output_file = options[:output_name] || File.basename(input_xlsx_file.gsub('.xlsx', ''))
63
+ if options[:output_path]
64
+ base_output_file = File.join(options[:output_path], base_output_file)
65
+ end
77
66
 
78
- # # collection[1206].inspect
67
+ control_output_file = "#{base_output_file}.control.yaml"
68
+ answers_output_file = "#{base_output_file}.answers.yaml"
79
69
 
80
- # # FIXME use instea output dir
81
- # output_dir = options[:output_path] || Dir.pwd
82
- # output_file = options[:output_name] || File.join(output_dir, Pathname.new(filepath).basename.sub_ext(".yaml"))
70
+ matrix.to_control_file(control_output_file)
71
+ matrix.to_answers_file(answers_output_file)
72
+ end
83
73
 
84
- # collection.to_file(output_file)
74
+ desc "generate-with-answers ANSWERS_YAML", "Writing to the CAIQ XSLX template using YAML"
75
+ option :template_path, aliases: :t, type: :string, desc: "Optional input template CAIQ XSLT file. If missed -r will be checked"
76
+ option :caiq_version, aliases: :r, type: :string, default: "3.0.1", desc: "Optional input template CAIQ XSLT version. If missed -t will be checked"
77
+ option :output_file, aliases: :o, type: :string, desc: 'Optional output XSLT file. If missed, the input file’s name will be used'
85
78
 
86
- # collection_output_dir = File.join(output_dir, "concepts")
79
+ def generate_with_answers(answers_yaml_path)
80
+ unless File.exist? answers_yaml_path
81
+ UI.say("#{answers_yaml_path} file doesn't exists")
82
+ return
83
+ end
87
84
 
88
- # FileUtils.mkdir_p(collection_output_dir)
85
+ unless options[:template_path] || options[:caiq_version]
86
+ UI.say("No input template specified by -r or -t")
87
+ return
88
+ end
89
89
 
90
- # collection.keys.each do |id|
91
- # collection[id].to_file(File.join(collection_output_dir, "concept-#{id}.yaml"))
92
- # end
90
+ template_xslt_path = options[:template_path]
91
+ unless template_xslt_path
92
+ caiq_version = options[:caiq_version]
93
+ input_files = Resource.lookup_version(caiq_version)
93
94
 
94
- # french = workbook.language_sheet("French")
95
- # french.sections[3].structure
96
- # french.sections[3].terms
95
+ unless input_files && !input_files.empty?
96
+ UI.say("No file found for #{caiq_version} version")
97
+ return
98
+ end
97
99
 
98
- # english = workbook.language_sheet("English")
99
- # english.terms_section
100
- # english.terms_section.terms
100
+ template_xslt_path = input_files.first
101
+ end
101
102
 
102
- #pry.binding
103
- end
103
+ unless File.exist? template_xslt_path
104
+ UI.say("#{template_xslt_path} file doesn't exists")
105
+ return
106
+ end
104
107
 
105
- desc "generate-with-answers ANSWERS_YAML", "Writing to the CAIQ XSLX template using YAML"
108
+ output_file = options[:output_file]
109
+ unless options[:output_file]
110
+ output_file = "#{answers_yaml_path.gsub('answers.yaml', '')}.xlsx"
111
+ end
106
112
 
107
- def generate_with_answers(answers)
108
- raise 'Not implemented yet'
113
+ Matrix.fill_answers(answers_yaml_path, template_xslt_path, output_file)
109
114
  end
110
115
  end
111
116
  end
@@ -1,4 +1,3 @@
1
- require 'rubyXL'
2
1
  require 'yaml'
3
2
 
4
3
  require_relative '../../../ext/string'
@@ -15,14 +14,6 @@ module Csa
15
14
  Dir["#{root_gem}/resources/**/*v#{version}*.xlsx"]
16
15
  end
17
16
 
18
- def self.from_xlsx(xslt_path)
19
- raise 'Not implemented yet'
20
- end
21
-
22
- def from_caiq(xslt_path, output_name, output_path)
23
- raise 'Not implemented yet'
24
- end
25
-
26
17
  def self.to_file(hash, output_file)
27
18
  File.open(output_file, 'w') { |f| f.write hash.to_yaml(line_width: 9999) }
28
19
  rescue Errno::ENOENT => e
@@ -1,7 +1,7 @@
1
1
  module Csa
2
2
  module Ccm
3
3
  module Cli
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
6
6
  end
7
7
  end
@@ -2,7 +2,7 @@ module Csa::Ccm
2
2
 
3
3
  class Control
4
4
  ATTRIBS = %i(
5
- id name specification questions
5
+ id title specification questions
6
6
  )
7
7
 
8
8
  attr_accessor *ATTRIBS
@@ -22,11 +22,9 @@ class Control
22
22
  value = self.send(attrib)
23
23
 
24
24
  unless value.nil?
25
+
25
26
  if attrib == :questions
26
- value = value.inject([]) do |acc, (k, v)|
27
- acc << v.to_hash
28
- acc
29
- end
27
+ value = value.values.map(&:to_hash)
30
28
  end
31
29
 
32
30
  acc.merge(attrib.to_s => value)
@@ -4,7 +4,7 @@ module Csa::Ccm
4
4
 
5
5
  class ControlDomain
6
6
  ATTRIBS = %i(
7
- id name controls
7
+ id title controls
8
8
  )
9
9
 
10
10
  attr_accessor *ATTRIBS
@@ -25,10 +25,7 @@ class ControlDomain
25
25
 
26
26
  unless value.nil?
27
27
  if attrib == :controls
28
- value = value.inject([]) do |acc, (k, v)|
29
- acc << v.to_hash
30
- acc
31
- end
28
+ value = value.values.map(&:to_hash)
32
29
  end
33
30
 
34
31
  acc.merge(attrib.to_s => value)
@@ -1,26 +1,36 @@
1
- require_relative "control_domain"
2
- require_relative "control"
3
- require_relative "question"
1
+ require_relative 'control_domain'
2
+ require_relative 'control'
3
+ require_relative 'question'
4
+ require_relative 'answer'
5
+
6
+ require 'rubyXL'
7
+ require 'rubyXL/convenience_methods/cell'
8
+ require 'rubyXL/convenience_methods/workbook'
9
+ require 'rubyXL/convenience_methods/worksheet'
4
10
 
5
11
  module Csa::Ccm
6
12
 
7
13
  class Matrix
8
- ATTRIBS = %i(
9
- version title source_file workbook source_path control_domains
10
- )
14
+
15
+ ATTRIBS = %i[
16
+ version title source_file workbook source_path control_domains answers
17
+ ].freeze
11
18
 
12
19
  attr_accessor *ATTRIBS
13
20
 
14
- def initialize(options={})
15
- options.each_pair do |k,v|
16
- self.send("#{k}=", v)
21
+ def initialize(options = {})
22
+ options.each_pair do |k, v|
23
+ send("#{k}=", v)
17
24
  end
18
25
 
19
26
  if source_path
20
27
  @workbook = RubyXL::Parser.parse(source_path)
28
+
29
+ parse_version if version.nil?
21
30
  end
22
31
 
23
32
  @control_domains ||= {}
33
+ @answers ||= []
24
34
 
25
35
  self
26
36
  end
@@ -29,10 +39,25 @@ class Matrix
29
39
  @source_file || File.basename(source_path)
30
40
  end
31
41
 
42
+ def parse_version
43
+ title_prefix = 'consensus assessments initiative questionnaire v'
44
+ first_row = workbook[0][0]
45
+
46
+ if first_row[2].value.downcase.start_with? title_prefix # version v3.0.1
47
+ self.version = first_row[2].value.downcase[title_prefix.length..-1]
48
+ elsif first_row[0].value.downcase.start_with? title_prefix # version v1.1
49
+ self.version = first_row[0].value.downcase[title_prefix.length..-1]
50
+ else # version 1.0
51
+ self.version = workbook[1][0][0].value[/(?<=Version )(\d+\.?)+(?= \()/]
52
+ end
53
+ end
54
+
32
55
  def worksheet
33
56
  workbook.worksheets.first
34
57
  end
35
58
 
59
+ attr_reader :workbook
60
+
36
61
  def title
37
62
  worksheet = workbook.worksheets.first
38
63
  worksheet[0][2].value
@@ -47,11 +72,11 @@ class Matrix
47
72
  end
48
73
 
49
74
  class Row
50
- ATTRIBS = %i(
75
+ ATTRIBS = %i[
51
76
  control_domain_id control_id question_id control_spec
52
- question_content answer_yes answer_no answer_na notes
77
+ question_content answer_yes answer_no answer_na comment
53
78
  control_domain_description
54
- )
79
+ ].freeze
55
80
 
56
81
  attr_accessor *ATTRIBS
57
82
 
@@ -64,10 +89,21 @@ class Matrix
64
89
  @answer_yes = ruby_xl_row[5].value
65
90
  @answer_no = ruby_xl_row[6].value
66
91
  @answer_na = ruby_xl_row[7].value
92
+ @comment = ruby_xl_row[8].value
93
+
94
+ # In 3.0.1 2017-09-01, question_id for "AIS-02.2" is listed as "AIS- 02.2"
95
+ %w[control_id question_id].each do |field|
96
+ if val = send(field)
97
+ send("#{field}=", val.gsub(/\s/, ''))
98
+ end
99
+ end
67
100
 
68
101
  # In 3.0.1 2017-09-01, Rows 276 and 277's control ID says "LG-02" but it should be "STA-05" instead.
69
- @control_id = question_id.split(".").first if question_id
70
- @control_domain_id = control_id.split("-").first if control_id
102
+ if @control_id.nil? && @question_id
103
+ @control_id = @question_id.split('.').first
104
+ end
105
+
106
+ @control_domain_id = control_id.split('-').first if @control_id
71
107
 
72
108
  # puts "HERE IN ROW! #{ruby_xl_row.cells.map(&:value)}"
73
109
 
@@ -78,16 +114,18 @@ class Matrix
78
114
  self
79
115
  end
80
116
 
81
- def control_domain_name
117
+ def control_domain_title
82
118
  return nil if control_domain_description.nil?
83
- name, _, control_name = control_domain_description.split(/(\n)/)
119
+
120
+ name, = control_domain_description.split(/(\n)/)
84
121
  name
85
122
  end
86
123
 
87
- def control_name
124
+ def control_title
88
125
  return nil if control_domain_description.nil?
89
- name, _, control_name = control_domain_description.split(/(\n)/)
90
- control_name
126
+
127
+ _, _, control_title = control_domain_description.split(/(\n)/)
128
+ control_title
91
129
  end
92
130
  end
93
131
 
@@ -95,27 +133,34 @@ class Matrix
95
133
  Row.new(worksheet[i])
96
134
  end
97
135
 
98
- def self.from_xlsx(version, input_file)
136
+ def self.get_start_row(version)
137
+ case version
138
+ when '1.0'
139
+ 2
140
+ when '1.1'
141
+ 3
142
+ else
143
+ # '3.0.1' and assume beyond
144
+ 4
145
+ end
146
+ end
147
+
148
+ def self.version_from_filepath(input_file)
149
+ input_file[/(?<=v)[0-9\.]*(?=-)/] || 'unknown'
150
+ end
151
+
152
+ def self.from_xlsx(input_file)
99
153
  matrix = Matrix.new(
100
- version: version,
101
154
  source_path: input_file
102
155
  )
103
156
 
104
157
  all_rows = matrix.worksheet.sheet_data.rows
105
- start_row = 4 # FIXME add some basic logic to calculate it
106
-
107
- last_control_domain = nil
108
- last_control_id = nil
109
- last_control_specification = nil
110
158
 
111
- worksheet = matrix.worksheet
112
-
113
- row_number = start_row
159
+ start_row = get_start_row(matrix.version)
114
160
  max_row_number = all_rows.length - 1
115
161
 
116
162
  # We loop over all Questions
117
163
  (start_row..max_row_number).each do |row_number|
118
-
119
164
  # puts "looping row #{row_number}"
120
165
  row = matrix.row(row_number)
121
166
  # Skip row if there is no question-id
@@ -130,10 +175,10 @@ class Matrix
130
175
  unless domain_id.nil?
131
176
 
132
177
  control_domain = matrix.control_domains[domain_id] ||
133
- ControlDomain.new(
134
- id: row.control_domain_id,
135
- name: row.control_domain_name
136
- )
178
+ ControlDomain.new(
179
+ id: row.control_domain_id,
180
+ title: row.control_domain_title
181
+ )
137
182
 
138
183
  # puts"control_domain #{control_domain.to_hash}"
139
184
 
@@ -147,7 +192,7 @@ class Matrix
147
192
  control_domain = matrix.control_domains[domain_id]
148
193
  control = control_domain.controls[control_id] || Control.new(
149
194
  id: row.control_id,
150
- name: row.control_name,
195
+ title: row.control_title,
151
196
  specification: row.control_spec
152
197
  )
153
198
 
@@ -160,28 +205,104 @@ class Matrix
160
205
  # Store the Question
161
206
  # putsquestion.to_hash
162
207
  control.questions[row.question_id] = Question.new(id: row.question_id, content: row.question_content)
208
+
209
+ answer = if row.answer_na
210
+ 'NA'
211
+ elsif row.answer_no
212
+ 'no'
213
+ elsif row.answer_yes
214
+ 'yes'
215
+ end
216
+
217
+ matrix.answers << Answer.new(
218
+ question_id: row.question_id,
219
+ control_id: control_id,
220
+ answer: answer,
221
+ comment: row.comment
222
+ )
163
223
  end
164
224
 
165
225
  matrix
166
226
  end
167
227
 
168
- def to_hash
228
+ def self.fill_answers(answers_yaml_path, template_xslt_path, output_xslt_path)
229
+ ccm = YAML.safe_load(File.read(answers_yaml_path, encoding: 'UTF-8'))['ccm']
230
+ answers = ccm['answers']
231
+ answers_hash = Hash[*answers.collect { |a| [a['question-id'], a] }.flatten]
232
+ answers_version = ccm['metadata']['version']
233
+ template_version = version_from_filepath(template_xslt_path)
234
+
235
+ unless template_version == answers_version
236
+ raise "Template XLSX & answers YAML version missmatch #{template_version} vs. #{answers_version}"
237
+ end
238
+
239
+ matrix = Matrix.new(
240
+ version: template_version,
241
+ source_path: template_xslt_path
242
+ )
243
+
244
+ worksheet = matrix.worksheet
245
+ all_rows = worksheet.sheet_data.rows
246
+
247
+ start_row = get_start_row(matrix.version)
248
+ max_row_number = all_rows.length - 1
249
+
250
+ (start_row..max_row_number).each do |row_number|
251
+ question_id = worksheet[row_number][2].value
252
+
253
+ next unless answers_hash.key?(question_id)
254
+
255
+ answer = answers_hash[question_id]
256
+ answer_value = answer['answer']
257
+
258
+ answer_col = case answer_value
259
+ when 'yes', true
260
+ 5
261
+ when 'no', false
262
+ 6
263
+ when 'NA'
264
+ 7
265
+ end
266
+
267
+ worksheet[row_number][answer_col].change_contents(answer['notes'])
268
+ end
269
+
270
+ matrix.workbook.write(output_xslt_path)
271
+ worksheet
272
+ end
273
+
274
+ def to_control_hash
169
275
  {
170
- "ccm" => {
171
- "metadata" => metadata.to_hash,
172
- "control_domains" => control_domains.inject([]) do |acc, (k, v)|
173
- acc << v.to_hash
174
- acc
175
- end
276
+ 'ccm' => {
277
+ 'metadata' => metadata.to_hash,
278
+ 'control_domains' => control_domains.each_with_object([]) do |(_k, v), acc|
279
+ acc << v.to_hash
280
+ end
176
281
  }
177
282
  }
178
283
  end
179
284
 
180
- def to_file(filename)
181
- File.open(filename,"w") do |file|
182
- file.write(to_hash.to_yaml)
285
+ def to_answers_hash
286
+ {
287
+ 'ccm' => {
288
+ 'metadata' => metadata.to_hash,
289
+ 'answers' => answers.each_with_object([]) do |v, acc|
290
+ acc << v.to_hash
291
+ end
292
+ }
293
+ }
294
+ end
295
+
296
+ def to_control_file(filename)
297
+ File.open(filename, 'w') do |file|
298
+ file.write(to_control_hash.to_yaml)
183
299
  end
184
300
  end
185
301
 
302
+ def to_answers_file(filename)
303
+ File.open(filename, 'w') do |file|
304
+ file.write(to_answers_hash.to_yaml)
305
+ end
306
+ end
307
+ end
186
308
  end
187
- end