hlsv 1.0.0 → 2.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -3
  3. data/README.md +36 -29
  4. data/lib/hlsv/analysis_registry.rb +47 -0
  5. data/lib/hlsv/analysis_runner.rb +193 -0
  6. data/lib/hlsv/config_manager.rb +125 -0
  7. data/lib/hlsv/html2word.rb +18 -18
  8. data/lib/hlsv/path_guard.rb +32 -0
  9. data/lib/hlsv/sdtm_validation/dataset.rb +169 -0
  10. data/lib/hlsv/sdtm_validation/define.rb +142 -0
  11. data/lib/hlsv/sdtm_validation/report.rb +357 -0
  12. data/lib/hlsv/sdtm_validation.rb +390 -0
  13. data/lib/hlsv/url_helper.rb +28 -0
  14. data/lib/hlsv/version.rb +1 -1
  15. data/lib/hlsv/web_app.rb +264 -418
  16. data/lib/hlsv.rb +12 -5
  17. data/public/css/accessibility/accessibility.css +33 -0
  18. data/public/css/base/layout.css +33 -0
  19. data/public/css/base/reset.css +23 -0
  20. data/public/css/base/typography.css +31 -0
  21. data/public/css/components/buttons.css +142 -0
  22. data/public/css/components/file-tree.css +107 -0
  23. data/public/css/components/footer.css +43 -0
  24. data/public/css/components/forms.css +56 -0
  25. data/public/css/components/header.css +52 -0
  26. data/public/css/components/status.css +56 -0
  27. data/public/css/features/csv-table.css +204 -0
  28. data/public/css/features/file-browser.css +208 -0
  29. data/public/css/responsive/responsive.css +133 -0
  30. data/public/css/styles.css +25 -0
  31. data/public/css/styles_csv.css +23 -0
  32. data/public/favicon.ico +0 -0
  33. data/public/js/analysis.js +201 -0
  34. data/public/js/app.js +63 -0
  35. data/public/js/browser.js +172 -0
  36. data/public/js/config.js +214 -0
  37. data/public/js/results.js +240 -0
  38. data/public/js/utils.js +57 -0
  39. data/views/csv_view.erb +11 -12
  40. data/views/index.erb +70 -19
  41. data/views/{report_template.erb → report.erb} +203 -188
  42. metadata +39 -41
  43. data/lib/hlsv/find_keys.rb +0 -979
  44. data/lib/hlsv/mon_script.rb +0 -169
  45. data/public/app.js +0 -569
  46. data/public/styles.css +0 -586
  47. data/public/styles_csv.css +0 -448
@@ -0,0 +1,390 @@
1
+ require 'yaml'
2
+ require 'nokogiri'
3
+ require 'fileutils'
4
+ require 'csv'
5
+
6
+ require_relative 'xpt'
7
+
8
+ require_relative "html2word"
9
+
10
+ require_relative 'sdtm_validation/dataset'
11
+ require_relative 'sdtm_validation/define'
12
+ require_relative 'sdtm_validation/report'
13
+
14
+ module Hlsv
15
+ class SdtmValidation
16
+
17
+ attr_reader :web_mode
18
+ attr_reader :verbose
19
+
20
+ attr_reader :config
21
+
22
+ attr_reader :define
23
+ attr_reader :datasets
24
+ attr_reader :ds_path
25
+
26
+ def initialize(config_file, web_mode: false, verbose: false)
27
+ @web_mode = web_mode
28
+ @verbose = verbose
29
+
30
+ # load the config
31
+ @config = load_config(config_file)
32
+
33
+ # load define
34
+ @define = Define.new(@config['define_path'])
35
+
36
+ # load data
37
+ # get all xpt files in the directory present in the config file (data_directory parameter)
38
+ @ds_path = @config['data_directory'].gsub('\\', '/')
39
+
40
+ unless File.directory? @ds_path
41
+ raise Hlsv::Error, "⚠ dataset directory is not a directory, analysis not possible: #{@ds_path}"
42
+ end
43
+
44
+ # present of excluded datasets
45
+ puts "Excluded dataset: #{excluded_dataset.join(', ')}" unless excluded_dataset.empty?
46
+
47
+ @datasets = Dir["#{@ds_path}/*"]
48
+ .select { |f| File.extname(f) == '.xpt' }
49
+ .reject { |f| excluded_dataset.include? File.basename(f, '.xpt').upcase }
50
+ .to_h { |ds| [File.basename(ds, '.xpt').upcase, Dataset.from_xpt(ds)] }
51
+
52
+ if @datasets.empty?
53
+ raise Hlsv::Error, "⚠ no xpt files detected, analysis not possible"
54
+ end
55
+
56
+ # refresh the output directory
57
+ prepare_output_dir
58
+
59
+ end
60
+
61
+ def validate
62
+ ascii_search
63
+ check_define_key if @define.status
64
+ check_data_key
65
+ end
66
+
67
+ # check config key in the data
68
+ def check_data_key
69
+
70
+ puts "\n=== data check"
71
+
72
+ @datasets.each do |ds, dataset|
73
+ puts ds
74
+
75
+ # get candidate keys
76
+ candidates = dataset.candidates_keys(ds, @config)
77
+
78
+ # get the dataset records
79
+ records = @datasets[ds].records
80
+
81
+ # no records present
82
+ if records.empty?
83
+ puts '- no records present'
84
+ next
85
+ end
86
+
87
+ # search the minimal combination of variables to avoid duplicates
88
+ candidates.each_with_index do |candidate, i|
89
+
90
+ if candidates.size == 1
91
+ idx = i;
92
+ else
93
+ idx = i+1
94
+ end
95
+
96
+ duplicate_records = look_keys(ds, candidate, records)
97
+
98
+ unless duplicate_records.empty?
99
+ dup_file = write_csv(ds, duplicate_records, :data, idx)
100
+
101
+ @datasets[ds].data_duplicate_file << dup_file
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ private def look_keys(ds, keys_candidates, records)
108
+ # initialize the number of duplicate with the records of records present in the dataset
109
+ prev_records_max = records.size
110
+ prev_nb_sub = records.size
111
+ # initialize the useless variables as an empty array
112
+ useless_vars = []
113
+
114
+ # display the variables check during the search
115
+ puts "- the key search is based on the following variables in the given order: #{keys_candidates.join(', ')}"
116
+
117
+ # loop on all candidate keys variables
118
+ keys_candidates.size.times do |i|
119
+
120
+ # tested key
121
+ keys_vars = keys_candidates[..i]
122
+ # remove useless variables
123
+ useless_vars.each do |v|
124
+ keys_vars.delete(v)
125
+ end
126
+ puts " - The key tried is: #{keys_vars.join(', ')}" if @verbose
127
+
128
+ # array of values of the variables
129
+ keys_records = records.group_by do |p|
130
+ keys_vars.map { |var| p[var] }
131
+ end
132
+
133
+ # get the maximum of records by key
134
+ records_max = 0
135
+ nb_sub = 0
136
+ keys_records.each do |values, list|
137
+ records_max = list.size if list.size > records_max
138
+ if list.size > 1
139
+ nb_sub += 1
140
+ end
141
+ end
142
+
143
+ # break if no duplicate
144
+ if records_max == 1
145
+ puts "- the minimal valid key have been found: #{keys_vars.join(', ')}"
146
+ @datasets[ds].minimal_key << keys_vars
147
+ @datasets[ds].minimal_key_status << true
148
+ return []
149
+ end
150
+
151
+ # duplicate still present
152
+ puts " - failure: #{nb_sub} subjects have a maximum #{records_max} records for #{keys_vars.join(', ')}" if @verbose
153
+ # if the number of current duplicate is superior or equal to the previous
154
+ # number of duplicate then the variable is not useful for the key search and
155
+ # the previous number of maximum duplicate stay as is
156
+ # the current variable will be remove for the next search
157
+ if prev_records_max <= records_max && prev_nb_sub <= nb_sub
158
+ puts " - #{keys_vars.last} not useful: The number of subjects and the maximum number of duplicates are the same as for the previous tested key." if @verbose
159
+ useless_vars << keys_vars.last
160
+ end
161
+
162
+ # current duplicate number become the current number of duplicate
163
+ prev_records_max = records_max
164
+ prev_nb_sub = nb_sub
165
+
166
+ # duplicate if no more variables
167
+ if i == (keys_candidates.size-1)
168
+ # duplicates records with the last valid key
169
+ last_valid_keys = keys_candidates.reject { |kc| useless_vars.include? kc }
170
+ puts "- The minimal key has not been found. The last key that was tested is: #{last_valid_keys.join(', ')}"
171
+ @datasets[ds].minimal_key << last_valid_keys
172
+ @datasets[ds].minimal_key_status << false
173
+
174
+ # compile all duplicates with the last valid keys
175
+ duplicate_records = records.group_by do |p|
176
+ last_valid_keys.map { |var| p[var] }
177
+ end
178
+ @datasets[ds].data_duplicate_records << duplicate_records
179
+
180
+ return duplicate_records
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ # check define key validity
187
+ def check_define_key
188
+
189
+ puts "\n=== define check"
190
+
191
+ # get all datasets: present in data & in define
192
+ ds_list = (@datasets.values.map(&:name) + @define.datasets).uniq
193
+
194
+ ds_list.each do |ds|
195
+
196
+ # no check if excluded dataset
197
+ if excluded_dataset.include? ds
198
+ @define.keys.delete(ds)
199
+ next
200
+ end
201
+
202
+ # get the define keys
203
+ define_key = @define.define_key_for(ds)
204
+
205
+ # no define key
206
+ if define_key.nil?
207
+ puts "#{ds}: no define key"
208
+ next
209
+ end
210
+ # transform key in symbol
211
+ define_key.map!(&:to_sym)
212
+
213
+ # add into dataset
214
+ if @datasets.keys.include? ds
215
+ @datasets[ds].define_keys = define_key
216
+ else
217
+ puts "#{ds}: present in define.xml but not in dataset"
218
+ @datasets[ds] = Dataset.new(ds, nil, [])
219
+ end
220
+
221
+ # get the records
222
+ records = @datasets[ds].records
223
+
224
+ # no records
225
+ if records.empty?
226
+ puts "#{ds}: no records for define datasets"
227
+ next
228
+ end
229
+
230
+ # define key & records are present
231
+ keys_records = records.group_by do |record|
232
+ define_key.map { |var| record[var] }
233
+ end
234
+
235
+ records_max = keys_records.map { |_, list| list.size }.max || 0
236
+
237
+ if records_max == 1
238
+ puts "#{ds}: define key is valid"
239
+ @datasets[ds].define_status = true
240
+ else
241
+ puts "#{ds}: define key is invalid"
242
+ @datasets[ds].define_status = false
243
+ @datasets[ds].define_duplicate_records = keys_records
244
+
245
+ dup_file = write_csv(ds, keys_records, :define, 0)
246
+ @datasets[ds].define_duplicate_file = dup_file
247
+ end
248
+ end
249
+ end
250
+
251
+ # search non-ASCII character
252
+ def ascii_search
253
+
254
+ puts "\n=== Non-ASCII Characters Search"
255
+
256
+ @datasets.values.each do |ds|
257
+ records = ds.records
258
+ # no records no test
259
+ next if records.empty?
260
+ # initialize list of exception
261
+ non_ascii_list = []
262
+ # read all characters
263
+ records.each do |record|
264
+ record.each do |var, value|
265
+ next if value.nil? || value.is_a?(Integer) || value.is_a?(Float)
266
+ if value.match(/[^[:ascii:]]/)
267
+ non_ascii_list << "#{var}: #{value.inspect}"
268
+ end
269
+ end
270
+ end
271
+
272
+ ds.non_ascii_values = non_ascii_list
273
+
274
+ if non_ascii_list.empty?
275
+ puts "#{ds.name}: non-ASCII absent"
276
+ else
277
+ puts "#{ds.name}: non-ASCII present", non_ascii_list.map { |v| "- #{v}" }
278
+ end
279
+ end
280
+ end
281
+
282
+ # report creation
283
+ def report
284
+ @report ||= Report.new(study_name, @ds_path, @datasets, @define, excluded_dataset, web_mode: @web_mode)
285
+ end
286
+
287
+ ####
288
+ # load YAML file
289
+ # the validation on the config is done by the web app
290
+ # return hash: parameter => value
291
+ def load_config(file)
292
+ YAML.load_file(file)
293
+ rescue Psych::SyntaxError => e
294
+ raise Hlsv::Error, "❌ Invalid configuration\n\n> #{e.message}"
295
+ rescue Errno::ENOENT
296
+ raise Hlsv::Error, "❌ File not found\n\n> `#{file}` does not exist"
297
+ end
298
+
299
+ ###
300
+ # display
301
+ #
302
+ # create a CVS with all duplicates
303
+ def write_csv(ds, duplicate_records, type, index)
304
+
305
+ if index == 0 then
306
+ display_out = "#{dir_out}/#{type.to_s}_#{ds}.csv"
307
+ else
308
+ display_out = "#{dir_out}/#{type.to_s}_#{index}_#{ds}.csv"
309
+ end
310
+
311
+ # TODO: hum ça marche ouais mais non, faut pas laisser comme ça
312
+ header = duplicate_records.first.last.first.keys
313
+ header = header.insert(0, 'No')
314
+
315
+ duplicate = duplicate_records.reject { |key, list| list.size == 1 }
316
+
317
+ CSV.open(display_out, "w") do |csv|
318
+ # header
319
+ csv << header
320
+ # body
321
+ duplicate.each do |key, list|
322
+ idx = duplicate.keys.index(key) + 1
323
+ list.each do |l|
324
+ row = l.values.insert(0, idx)
325
+ csv << row
326
+ end
327
+ end
328
+ end
329
+
330
+ display_out
331
+ end
332
+
333
+ ###
334
+ # Helpers
335
+
336
+ # study name
337
+ def study_name
338
+ @config['study_name']
339
+ end
340
+
341
+ # excluded ds
342
+ def excluded_dataset
343
+ @excluded_dataset ||= begin
344
+ exclus = @config['excluded_ds']&.gsub(/[,;]/, ' ')
345
+ if exclus.nil?
346
+ []
347
+ else
348
+ exclus.split.map(&:upcase)
349
+ end
350
+ end
351
+ end
352
+
353
+ # output directory
354
+ def prepare_output_dir
355
+ dir = "#{study_name}/#{config['output_directory']}"
356
+
357
+ # no value, current repository
358
+ if dir.nil? || dir.strip.empty?
359
+ dir = '.'
360
+
361
+ # repository present, delete all csv file
362
+ elsif Dir.exist?(dir)
363
+ Dir.glob("#{dir}/**/*.csv").each { |f| File.delete(f) }
364
+
365
+ # no repository, creation
366
+ else
367
+ FileUtils.mkdir_p(dir)
368
+ end
369
+
370
+ dir
371
+ end
372
+
373
+ def dir_out
374
+ dir = "#{study_name}/#{config['output_directory']}"
375
+ dir = '.' if dir.nil? || dir.strip.empty?
376
+ dir
377
+ end
378
+
379
+
380
+ end
381
+
382
+ # sv = SdtmValidation.new('../../config.yaml')
383
+ # sv.validate
384
+ # sv.report.html
385
+ # sv.report.docx
386
+ # sv.report.excel_export('SV')
387
+ # sv.report.excel_export('DA')
388
+ # sv.report.excel_export('DM')
389
+ # sv.report.excel_export('DC')
390
+ end
@@ -0,0 +1,28 @@
1
+ # lib/hlsv/url_helper.rb
2
+ module Hlsv
3
+ module UrlHelper
4
+ # Builds an encoded query string from a parameter hash.
5
+ # nil values are ignored (Useful for the optional `last_valid_key` option, etc.)
6
+ def build_query(params)
7
+ params.compact.map { |k, v| "#{k}=#{::URI.encode_www_form_component(v.to_s)}" }.join('&')
8
+ end
9
+
10
+ def csv_view_url(file:, study:, dataset:, last_valid_key: nil)
11
+ "/csv_view?#{build_query(
12
+ file: file,
13
+ study: study,
14
+ dataset: dataset,
15
+ last_valid_key: last_valid_key&.join(',')
16
+ )}"
17
+ end
18
+
19
+ def export_csv_excel_url(file:, study:, dataset:, last_valid_key: nil)
20
+ "/export_csv_excel?#{build_query(
21
+ file: file,
22
+ study: study,
23
+ dataset: dataset,
24
+ last_valid_key: last_valid_key&.join(',')
25
+ )}"
26
+ end
27
+ end
28
+ end
data/lib/hlsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hlsv
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0"
5
5
  end