csa-ccm 0.1.2 → 0.1.3

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.
@@ -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