eco-helpers 2.1.12 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 335e1112db6baee9a3ddfa8ebe5bf66d6bd257c91c5c09808182683b07b7f408
4
- data.tar.gz: e0b69526d52606752bb27c80f16534a8eb2cd3ae8e334cfe2576d71b37be596f
3
+ metadata.gz: c71f3bf20fae0dff4ec0ccd0ee8205b8af1dee3c9fabb02791f6102a84f7f9ab
4
+ data.tar.gz: 81ba2c67c4653941529051d433189101e6480313447fdbe6c64da617887371b7
5
5
  SHA512:
6
- metadata.gz: c07bd1e0009ce543d7f479a2bf5e21eada3b59e8e2a8e79c65b265b97fbd5c2a7a196f7ef6cd8ed8a8a979ea9ac7b59c29f986d16edac25b113369ece6bedcf9
7
- data.tar.gz: c8e042cd62c67ad78db3f7cb2771c394142e471ca49b8dcf43351df4fa9b60f01e4f49df956775e0d842ddff7fea7cf6bc8d5fb309ac2f3c002ca06306dec361
6
+ metadata.gz: b67b415da8341ab6d20a4165ef9f761777bcdb871635a33b0681a56228dae6138c779c31852ce2e748156fa41832ab1ea457ad1aa7ff9dc66c3e1c16dc016be4
7
+ data.tar.gz: 53e6154974cc3db39e56a127710c1a676e34af8786012b0f3c41051fe14d0b27cc55945875843cdfa1f5fb98d64ed8afcae7eebcd2cfb0cb30da43f7aee2c16f
data/CHANGELOG.md CHANGED
@@ -1,14 +1,24 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [2.1.13] - 2022-11-xx
4
+ ## [2.2.2] - 2023-02-xx
5
5
 
6
6
  ### Added
7
7
  ### Changed
8
8
  ### Fixed
9
- - `Eco::API::Session::Batch::Errors#str` remove double up on error message
10
- - wrong require
11
9
 
10
+ ## [2.2.1] - 2023-02-24
11
+
12
+ ### Added
13
+ - `Ecoportal::API::V1::Person#contractor_organization_id`
14
+ - **Support** for **Reporting Structures** (breaking change)
15
+ - `Eco::API::UseCases::DefaultCases::CsvToTree` use case to generate tree json out of a csv
16
+ - The output file can be fed to `Eco::API::Organization::TagTree`
17
+
18
+ ### Changed
19
+ - **Patch** `Ecoportal::API::V1::Person::VALID_TAG_REGEX` it now allows for dot `.`
20
+ - update gem dependencies
21
+
12
22
  ## [2.1.12] - 2022-11-30
13
23
 
14
24
  ### Fixed
data/eco-helpers.gemspec CHANGED
@@ -31,8 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "redcarpet", ">= 3.5.1", "< 3.6"
32
32
 
33
33
  spec.add_dependency 'ecoportal-api', '>= 0.8.5', '< 0.9'
34
- spec.add_dependency 'ecoportal-api-v2', '>= 0.9.7', '< 0.10'
35
- spec.add_dependency 'ecoportal-api-graphql', '>= 0.1.11', '< 0.2'
34
+ spec.add_dependency 'ecoportal-api-v2', '>= 1.0.1', '< 1.1'
35
+ spec.add_dependency 'ecoportal-api-graphql', '>= 0.2.2', '< 0.3'
36
36
  spec.add_dependency 'aws-sdk-s3', '>= 1.83.0', '< 2'
37
37
  spec.add_dependency 'aws-sdk-ses', '>= 1.36.0', '< 2'
38
38
  spec.add_dependency 'dotenv', '>= 2.7.6', '< 2.8'
@@ -13,7 +13,7 @@ module Eco
13
13
  autoloads_children_of "Eco::API::Common::Loaders::Parser"
14
14
  autoload_namespace_ignore "Eco::API"
15
15
 
16
- CORE_ATTRS = ["id", "external_id", "email", "name", "supervisor_id", "filter_tags", "freemium"]
16
+ CORE_ATTRS = ["id", "external_id", "email", "name", "supervisor_id", "filter_tags", "contractor_organization_id", "freemium"]
17
17
  ACCOUNT_ATTRS = ["policy_group_ids", "default_tag", "send_invites", "landing_page_id", "login_provider_ids"]
18
18
  TYPE = [:select, :text, :date, :number, :phone_number, :boolean, :multiple]
19
19
  FORMAT = [:csv, :xml, :json, :xls]
@@ -3,6 +3,17 @@ module Ecoportal
3
3
  class V1
4
4
  # @attr entry [Eco::API::Common::People::PersonEntry, Hash] the input entry plain hash data used to update/create this person.
5
5
  class Person
6
+ class << self
7
+ def redef_without_warning(const, value)
8
+ self.class.send(:remove_const, const) if self.class.const_defined?(const)
9
+ self.class.const_set(const, value)
10
+ end
11
+ end
12
+
13
+ redef_without_warning('VALID_TAG_REGEX', /^[A-Za-z0-9 &_'\/.-]+$/)
14
+
15
+ passthrough :contractor_organization_id
16
+
6
17
  attr_accessor :entry
7
18
 
8
19
  def identify(section = :person)
@@ -13,7 +24,6 @@ module Ecoportal
13
24
  "'#{name}' (#{str_id}ext_id: '#{external_id}'; email: '#{email}')"
14
25
  end
15
26
  end
16
-
17
27
  end
18
28
  end
19
29
  end
@@ -1,10 +1,13 @@
1
1
  module Eco
2
2
  module API
3
3
  module Organization
4
-
5
4
  # Provides helpers to deal with tagtrees.
6
5
  class TagTree
7
- attr_reader :tag, :nodes, :children_count
6
+ attr_accessor :id
7
+ alias_method :tag, :id
8
+ attr_accessor :name
9
+
10
+ attr_reader :nodes, :children_count
8
11
  attr_reader :depth, :path
9
12
  attr_reader :enviro
10
13
 
@@ -18,7 +21,7 @@ module Eco
18
21
  # ]}]
19
22
  # tree = TagTree.new(tree.to_json)
20
23
  # @param tagtree [String] representation of the tagtree in json.
21
- def initialize(tagtree = [], depth: -1, path: [], enviro: nil)
24
+ def initialize(tagtree = [], name: nil, id: nil, depth: -1, path: [], enviro: nil)
22
25
  case tagtree
23
26
  when String
24
27
  @source = JSON.parse(tagtree)
@@ -30,23 +33,27 @@ module Eco
30
33
  @enviro = enviro
31
34
 
32
35
  @depth = depth
33
- @tag = @source.is_a?(Array) ? nil : @source.dig('tag')&.upcase
36
+ if @source.is_a?(Array)
37
+ @id = id
38
+ @name = name
39
+ @nodes = @source
40
+ else
41
+ @id = @source.values_at('tag', 'id').compact.first&.upcase
42
+ @name = @source['name']
43
+ @nodes = @source['nodes'] || []
44
+ end
34
45
 
35
46
  @path = path || []
36
- @path.push(@tag) unless !@tag
47
+ @path.push(@id) unless top?
37
48
 
38
- nodes = @source.is_a?(Array) ? @source : @source.dig('nodes') || []
39
- @nodes = nodes.map {|cnode| TagTree.new(cnode, depth: @depth + 1, path: @path.dup, enviro: @enviro)}
49
+ @nodes = nodes.map do |cnode|
50
+ TagTree.new(cnode, depth: @depth + 1, path: @path.dup, enviro: @enviro)
51
+ end
40
52
  @children_count = @nodes.count
41
53
 
42
54
  init_hashes
43
55
  end
44
56
 
45
- # Updates the tag of the current tree
46
- def tag=(value)
47
- @tag = value
48
- end
49
-
50
57
  # @return [Eco::API::Organization::TagTree]
51
58
  def dup
52
59
  self.class.new(as_json)
@@ -69,7 +76,7 @@ module Eco
69
76
  nodes_json
70
77
  else
71
78
  {
72
- "tag" => tag,
79
+ "id" => tag,
73
80
  "nodes" => nodes_json
74
81
  }
75
82
  end
@@ -80,6 +87,11 @@ module Eco
80
87
  @has_tags.empty?
81
88
  end
82
89
 
90
+ # @return [Integer] the number of locations
91
+ def count
92
+ @hash_tags.keys.count
93
+ end
94
+
83
95
  # @return [Integer] the highest `depth` of all the children.
84
96
  def total_depth
85
97
  @total_depth ||= if children_count > 0
@@ -245,12 +257,12 @@ module Eco
245
257
 
246
258
  def init_hashes
247
259
  @hash_tags = {}
248
- @hash_tags[@tag] = self unless !@tag
260
+ @hash_tags[@id] = self unless top?
249
261
  @hash_tags = @nodes.reduce(@hash_tags) do |h,n|
250
262
  h.merge(n.hash)
251
263
  end
252
264
  @hash_paths = {}
253
- @hash_paths[@tag] = @path
265
+ @hash_paths[@id] = @path unless top?
254
266
  @hash_paths = @nodes.reduce(@hash_paths) do |h,n|
255
267
  h.merge(n.hash_paths)
256
268
  end
@@ -266,9 +278,7 @@ module Eco
266
278
  raise msg if !@enviro
267
279
  @enviro.logger.warn(msg)
268
280
  end
269
-
270
281
  end
271
-
272
282
  end
273
283
  end
274
284
  end
@@ -3,14 +3,41 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class BaseConfig < Hash
6
-
7
6
  attr_reader :config
8
7
 
8
+ class << self
9
+ def attr_key(*attrs)
10
+ attrs.each do |attr|
11
+ method = "#{attr}".freeze
12
+ if self.instance_methods.include?(method.to_sym)
13
+ puts "WARNING (#{self}): redefining method already defined '#{method}'."
14
+ end
15
+
16
+ define_method method do
17
+ self[method]
18
+ end
19
+
20
+ define_method "#{method}=" do |value|
21
+ self[method] = value
22
+ end
23
+ end
24
+ self
25
+ end
26
+ end
27
+
9
28
  def initialize(config:)
10
29
  super(nil)
11
30
  @config = config
12
31
  end
13
32
 
33
+ def file_manager
34
+ config.file_manager
35
+ end
36
+
37
+ def apis
38
+ config.apis
39
+ end
40
+
14
41
  def clone(config:)
15
42
  keys.each_with_object(self.class.new(config: config)) do |key, cnf|
16
43
  begin
@@ -24,7 +51,6 @@ module Eco
24
51
  end
25
52
  end
26
53
  end
27
-
28
54
  end
29
55
  end
30
56
  end
@@ -24,13 +24,7 @@ module Eco
24
24
  self["dir"]
25
25
  end
26
26
 
27
- def timestamp_pattern=(pattern)
28
- self["timestamp_pattern"] = pattern
29
- end
30
-
31
- def timestamp_pattern
32
- self["timestamp_pattern"]
33
- end
27
+ attr_key :timestamp_pattern
34
28
 
35
29
  def add_validation(format)
36
30
  raise "Block must be given" unless block_given?
@@ -3,47 +3,12 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class Logger < BaseConfig
6
-
7
- def console_level=(value)
8
- self["console_level"] = value
9
- end
10
-
11
- def console_level
12
- self["console_level"]
13
- end
14
-
15
- def file_level=(value)
16
- self["file_level"] = value
17
- end
18
-
19
- def file_level
20
- self["file_level"]
21
- end
22
-
23
- def file=(file)
24
- self["file"] = file
25
- end
26
-
27
- def file
28
- self["file"]
29
- end
30
-
31
- def timestamp_console=(value)
32
- self["timestamp_console"] = value
33
- end
34
-
35
- def timestamp_console
36
- self["timestamp_console"]
37
- end
38
-
39
- def log_connection=(value)
40
- self["log_connection"] = !!value
41
- end
6
+ attr_key :console_level, :file_level, :file
7
+ attr_key :timestamp_console, :log_connection
42
8
 
43
9
  def log_connection?
44
- self["log_connection"]
10
+ !!log_connection
45
11
  end
46
-
47
12
  end
48
13
  end
49
14
  end
@@ -3,71 +3,14 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class Mailer < BaseConfig
6
+ attr_key :access_key_id, :secret_access_key
7
+ attr_key :region, :server, :message_id_domain
8
+ attr_key :to, :from
6
9
 
7
10
  def configured?
8
11
  required = access_key_id && secret_access_key && region
9
12
  !!required
10
13
  end
11
-
12
- def to=(value)
13
- self["to"] = value
14
- end
15
-
16
- def to
17
- self["to"]
18
- end
19
-
20
- def from=(value)
21
- self["from"] = value
22
- end
23
-
24
- def from
25
- self["from"]
26
- end
27
-
28
- def access_key_id=(key)
29
- self["access_key_id"] = key
30
- end
31
-
32
- def access_key_id
33
- self["access_key_id"]
34
- end
35
-
36
- def secret_access_key=(key)
37
- self["secret_access_key"] = key
38
- end
39
-
40
- def secret_access_key
41
- self["secret_access_key"]
42
- end
43
-
44
- # AWS::SES::Client
45
- def region=(region)
46
- self["region"] = region
47
- end
48
-
49
- def region
50
- self["region"]
51
- end
52
-
53
- # AWS::SES::Base
54
- def server=(domain)
55
- self["server"] = domain
56
- end
57
-
58
- def server
59
- self["server"]
60
- end
61
-
62
- # AWS::SES::Base
63
- def message_id_domain=(domain)
64
- self["message_id_domain"] = domain
65
- end
66
-
67
- def message_id_domain
68
- self["message_id_domain"]
69
- end
70
-
71
14
  end
72
15
  end
73
16
  end
@@ -63,40 +63,19 @@ module Eco
63
63
  end
64
64
 
65
65
  # person model
66
- def default_usergroup=(value)
67
- self["default_usergroup"] = value
68
- end
66
+ attr_key :default_usergroup, :default_schema, :default_login_method
69
67
 
70
- def default_usergroup
71
- self["default_usergroup"]
72
- end
73
68
 
74
69
  def default_usergroup?
75
- !!self["default_usergroup"]
76
- end
77
-
78
- def default_schema=(name)
79
- self["default_schema"] = name
80
- end
81
-
82
- def default_schema
83
- self["default_schema"]
70
+ !!default_usergroup
84
71
  end
85
72
 
86
73
  def default_schema?
87
- !!self["default_schema"]
88
- end
89
-
90
- def default_login_method=(name)
91
- self["default_login_method"] = name
92
- end
93
-
94
- def default_login_method
95
- self["default_login_method"]
74
+ !!default_schema
96
75
  end
97
76
 
98
77
  def default_login_method?
99
- !!self["default_login_method"]
78
+ !!default_login_method
100
79
  end
101
80
 
102
81
  # @return [Hash] with defined pairs format `key` and Person parsers.
@@ -116,7 +95,6 @@ module Eco
116
95
  prs
117
96
  end
118
97
  end
119
-
120
98
  end
121
99
  end
122
100
  end
@@ -3,52 +3,14 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class S3Storage < BaseConfig
6
-
6
+ attr_key :bucket_name, :prefix, :region
7
+ attr_key :access_key_id, :secret_access_key
8
+
7
9
  def configured?
8
10
  required = bucket_name && prefix && access_key_id && secret_access_key && region
9
11
  !!required
10
12
  end
11
13
 
12
- def bucket_name=(value)
13
- self["bucket_name"] = value
14
- end
15
-
16
- def bucket_name
17
- self["bucket_name"]
18
- end
19
-
20
- def prefix=(value)
21
- self["prefix"] = value
22
- end
23
-
24
- def prefix
25
- self["prefix"]
26
- end
27
-
28
- def access_key_id=(key)
29
- self["access_key_id"] = key
30
- end
31
-
32
- def access_key_id
33
- self["access_key_id"]
34
- end
35
-
36
- def secret_access_key=(key)
37
- self["secret_access_key"] = key
38
- end
39
-
40
- def secret_access_key
41
- self["secret_access_key"]
42
- end
43
-
44
- def region=(region)
45
- self["region"] = region
46
- end
47
-
48
- def region
49
- self["region"]
50
- end
51
-
52
14
  def target_files=(value)
53
15
  self["target_files"] = [value].flatten
54
16
  end
@@ -72,7 +34,6 @@ module Eco
72
34
  def target_file_patterns
73
35
  self["target_file_patterns"]
74
36
  end
75
-
76
37
  end
77
38
  end
78
39
  end
@@ -3,60 +3,15 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class SFTP < BaseConfig
6
+ attr_key :host, :user
7
+ attr_key :password, :key_file
8
+ attr_key :base_path, :enviro_subpaths
6
9
 
7
10
  def configured?
8
11
  required = host && user && (key_file || password)
9
12
  !!required
10
13
  end
11
14
 
12
- def host=(value)
13
- self["host"] = value
14
- end
15
-
16
- def host
17
- self["host"]
18
- end
19
-
20
- def user=(value)
21
- self["user"] = value
22
- end
23
-
24
- def user
25
- self["user"]
26
- end
27
-
28
- def password=(var)
29
- self["password"] = var
30
- end
31
-
32
- def password
33
- self["password"]
34
- end
35
-
36
- def key_file=(key)
37
- self["key_file"] = key
38
- end
39
-
40
- def key_file
41
- self["key_file"]
42
- end
43
-
44
- def base_path=(path)
45
- self["base_path"] = path
46
- end
47
-
48
- def base_path
49
- self["base_path"]
50
- end
51
-
52
- def enviro_subpaths=(hash)
53
- self["enviro_subpaths"] = hash
54
- end
55
-
56
- def enviro_subpaths
57
- self["enviro_subpaths"]
58
- end
59
-
60
15
  def enviro_subpath
61
16
  enviro_subpaths[config.active_enviro]
62
17
  end
@@ -0,0 +1,48 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Config
5
+ class TagTree < BaseConfig
6
+ attr_key :file
7
+
8
+ def scope_tree(enviro: nil)
9
+ return @tagtree if instance_variable_defined?(:@tagtree) && @tagtree.enviro == enviro
10
+ if tree_file = self.file
11
+ if (tree = file_manager.load_json(tree_file)) && !tree.empty?
12
+ @tagtree = Eco::API::Organization::TagTree.new(tree, enviro: enviro)
13
+ end
14
+ end
15
+ @tagtree ||= live_tree(enviro: enviro)
16
+ end
17
+
18
+ # Among all the locations structures it selects the one with more location nodes
19
+ def live_tree(enviro: nil)
20
+ return @live_tree if instance_variable_defined?(:@live_tree) && @live_tree.enviro == enviro
21
+ trees = live_trees(enviro: enviro)
22
+ @live_tree = trees.reject do |tree|
23
+ tree.empty?
24
+ end.max {|a,b| a.count <=> b.count}
25
+ end
26
+
27
+ # Retrieves all the location structures of the organisation
28
+ def live_trees(enviro: nil)
29
+ [].tap do |trees|
30
+ next unless apis.active_api.version_available?(:graphql)
31
+ next unless graphql = apis.api(version: :graphql)
32
+ kargs = {
33
+ includeArchived: false,
34
+ includeUnpublished: false
35
+ }
36
+ next unless trees = graphql.currentOrganization.locationsStructures(**kargs)
37
+ trees.each do |tree|
38
+ args = { enviro: enviro, id: tree.id, name: tree.name}
39
+ eco_tree = Eco::API::Organization::TagTree.new(tree.treeify, **args)
40
+ trees.push(eco_tree)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -46,6 +46,12 @@ module Eco
46
46
  def mailer
47
47
  self["mailer"] ||= Eco::API::Session::Config::Mailer.new(config: self)
48
48
  end
49
+
50
+ # Helper scope reporting structures.
51
+ # @return [Eco::API::Session::Config::TagTree]
52
+ def tagtree_config
53
+ org["tagtree_config"] ||= Eco::API::Session::Config::TagTree.new(config: self)
54
+ end
49
55
  # @!endgroup
50
56
 
51
57
  # @!group Logger
@@ -225,35 +231,20 @@ module Eco
225
231
  end
226
232
 
227
233
  def tagtree=(file)
228
- org["tagtree"] = file
234
+ tagtree_config.file = file
229
235
  end
230
236
 
231
- # It uses the `tagtree.json` file and in its absence, if `graphql` enabled, the `life_tagtree`
237
+ # It uses the `tagtree.json` file and in its absence, if `graphql` enabled, the largest `life_tagtree`
232
238
  # @return [Eco::API::Organization::TagTree]
233
239
  def tagtree(enviro: nil)
234
- return @tagtree if instance_variable_defined?(:@tagtree) && @tagtree.enviro == enviro
235
- if tree_file = org["tagtree"]
236
- tree = []
237
- tree = file_manager.load_json(tree_file) unless !tree_file
238
- @tagtree = Eco::API::Organization::TagTree.new(tree, enviro: enviro)
239
- else
240
- @tagtree = live_tree(enviro: enviro)
241
- end
240
+ @tagtree ||= tagtree_config.scope_tree(enviro: enviro)
242
241
  end
243
242
 
244
243
  # It obtains the first of the live tagtree in the org
245
244
  # @note it requires graphql connection configuration parameters
246
245
  # @return [Eco::API::Organization::TagTree]
247
246
  def live_tree(enviro: nil)
248
- return @live_tree if instance_variable_defined?(:@live_tree) && @live_tree.enviro == enviro
249
- if apis.active_api.version_available?(:graphql)
250
- graphql = apis.api(version: :graphql)
251
- if tree = graphql.currentOrganization.tagTrees.to_a.first.treeify
252
- @live_tree = Eco::API::Organization::TagTree.new(tree, enviro: enviro)
253
- else
254
- @live_tree = nil
255
- end
256
- end
247
+ @live_tree ||= tagtree_config.live_tree(enviro: enviro)
257
248
  end
258
249
 
259
250
  # @return [Eco::API::Organization::PolicyGroups]
@@ -415,5 +406,6 @@ require_relative 'config/sftp'
415
406
  require_relative 'config/s3_storage'
416
407
  require_relative 'config/files'
417
408
  require_relative 'config/people'
409
+ require_relative 'config/tagtree'
418
410
  require_relative 'config/post_launch'
419
411
  require_relative 'config/workflow'
@@ -0,0 +1,90 @@
1
+ class Eco::API::UseCases::DefaultCases::CsvToTree
2
+ module Helper
3
+ extend NodesCleaner
4
+ extend Treeify
5
+
6
+ class << self
7
+ def csv_from(filename)
8
+ raise "Missing #{filename}" unless File.exists?(filename)
9
+ result = csv_from_file(filename)
10
+ if result.is_a?(Integer)
11
+ puts "An encoding problem was found on line #{result}"
12
+ result = csv_from_content(filename)
13
+ end
14
+ result
15
+ end
16
+
17
+ def csv_nodes(filename)
18
+ i = 1; prev_level = nil; prev_node = nil; prev_nodes = Array(1..11).zip(Array.new(11, nil)).to_h
19
+ nodes = csv_from(filename).each_with_object([]) do |row, out|
20
+ values = row.fields.map do |value|
21
+ value = value.to_s.strip
22
+ value.empty?? nil : value
23
+ end
24
+ i += 1
25
+ node = Node.new(i, *values)
26
+ prev_node ||= node
27
+
28
+ if prev_node.raw_level <= node.raw_level
29
+ node.set_high_levels(prev_node)
30
+ else
31
+ if parent_node = prev_nodes[node.raw_level - 1]
32
+ node.set_high_levels(parent_node)
33
+ else
34
+ raise "Node '#{node.raw_tag}' (#{node.row_num} row) doesn't have parent"
35
+ end
36
+ end
37
+ out << node
38
+ prev_nodes[node.raw_level] = node
39
+ prev_node = node
40
+ end
41
+ tidy_nodes(nodes)
42
+ end
43
+
44
+ private
45
+
46
+ def csv_from_content(filename)
47
+ CSV.parse(file_content(filename), headers: true)
48
+ end
49
+
50
+ def file_content(filename)
51
+ coding = encoding(filename)
52
+ coding = (coding != "utf-8")? "#{coding}|utf-8": coding
53
+ if content = File.read(filename, encoding: coding)
54
+ content.scrub do |bytes|
55
+ '<' + bytes.unpack('H*')[0] + '>'
56
+ end
57
+ end
58
+ end
59
+
60
+ def csv_from_file(filename)
61
+ coding = encoding(filename)
62
+ coding = (coding != "utf-8")? "#{coding}|utf-8": coding
63
+ CSV.read(filename, headers: true, encoding: coding)
64
+ rescue CSV::MalformedCSVError => e
65
+ if line = e.message.match(/line (?<line>\d+)/i)[:line]
66
+ return line.to_i
67
+ else
68
+ raise
69
+ end
70
+ end
71
+
72
+ def has_bom?(path)
73
+ return false if !path || file_empty?(path)
74
+ File.open(path, "rb") do |f|
75
+ bytes = f.read(3)
76
+ return bytes.unpack("C*") == [239, 187, 191]
77
+ end
78
+ end
79
+
80
+ def encoding(path)
81
+ has_bom?(path) ? "bom" : "utf-8"
82
+ end
83
+
84
+ def file_empty?(path)
85
+ return true if !File.file?(path)
86
+ File.zero?(path)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,206 @@
1
+ class Eco::API::UseCases::DefaultCases::CsvToTree
2
+ class Node < Struct.new(:row_num, :l1, :l2, :l3, :l4, :l5, :l6, :l7, :l8, :l9, :l10, :l11)
3
+ TAGS_ATTRS = [:l1, :l2, :l3, :l4, :l5, :l6, :l7, :l8, :l9, :l10, :l11]
4
+ ADDITIONAL_ATTRS = [:row_num]
5
+ ALL_ATTRS = ADDITIONAL_ATTRS + TAGS_ATTRS
6
+ ALLOWED_CHARACTERS = "A-Za-z0-9 &_'\/.-"
7
+ VALID_TAG_REGEX = /^[#{ALLOWED_CHARACTERS}]+$/
8
+ INVALID_TAG_REGEX = /[^#{ALLOWED_CHARACTERS}]+/
9
+ VALID_TAG_CHARS = /[#{ALLOWED_CHARACTERS}]+/
10
+ DOUBLE_BLANKS = /\s\s+/
11
+
12
+ attr_accessor :parentId
13
+
14
+ def nodeId
15
+ id
16
+ end
17
+
18
+ def id
19
+ tag.upcase
20
+ end
21
+
22
+ def name
23
+ tag
24
+ end
25
+
26
+ def tag
27
+ raw_tag.yield_self do |str|
28
+ partial = replace_not_allowed(str)
29
+ remove_double_blanks(partial).tap do |result|
30
+ if partial != str
31
+ invalid_chars = identify_invalid_characters(str)
32
+ puts "• (Row: #{self.row_num}) Invalid characters _#{invalid_chars}_ (removed): '#{str}' (converted to '#{result}')"
33
+ end
34
+ if result != partial
35
+ #puts "• There were DOUBLE BLANKS: tag '#{str}' (converted to '#{result}')"
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def raw_tag
42
+ values_at(*TAGS_ATTRS.reverse).compact.first
43
+ end
44
+
45
+ def level
46
+ actual_level
47
+ end
48
+
49
+ def actual_level
50
+ tags_array.compact.length
51
+ end
52
+
53
+ def raw_level
54
+ tags_array.index(raw_tag) + 1
55
+ end
56
+
57
+ def tag_idx
58
+ tags_array.index(raw_tag)
59
+ end
60
+
61
+ def previous_idx
62
+ idx = tag_idx - 1
63
+ idx < 0 ? nil : idx
64
+ end
65
+
66
+ def empty_idx
67
+ tary = tags_array
68
+ tary.index(nil) || tary.length + 1
69
+ end
70
+
71
+ def copy
72
+ self.class.new.set_attrs(**self.to_h)
73
+ end
74
+
75
+ # We got a missing level that is compacted in one row
76
+ # Here we get the missing intermediate levels
77
+ # This is done from upper to lower level to ensure processing order
78
+ # It skips last one, as that is this object already
79
+ def decouple(num = 1)
80
+ with_info = filled_idxs
81
+ # must be the last among filled_idxs, so let's use it to verify
82
+ unless with_info.last == tag_idx
83
+ raise "Review this (row #{row_num}; '#{raw_tag}'): tag_idx is #{tag_idx}, while last filled idx is #{with_info.last}"
84
+ end
85
+ len = with_info.length
86
+ target_idxs = with_info[len-(num+1)..-2]
87
+ target_idxs.map do |idx|
88
+ self.copy.tap do |dup|
89
+ dup.clear_level(idx_to_level(idx + 1))
90
+ end
91
+ end
92
+ end
93
+
94
+ def merge!(node)
95
+ override_upper_levels(node.tags_array)
96
+ end
97
+
98
+ def set_high_levels(node)
99
+ override_lower_levels(node.tags_array)
100
+ end
101
+
102
+ def clear_level(i)
103
+ case i
104
+ when Enumerable
105
+ target = i.to_a
106
+ when Integer
107
+ return false unless i >= 1 && i <= tag_attrs_count
108
+ target = Array(i..tag_attrs_count)
109
+ else
110
+ return false
111
+ end
112
+ return false if target.empty?
113
+ target.each do |n|
114
+ #puts "clearing 'l#{n}': #{attr("l#{n}")}"
115
+ set_attr("l#{n}", nil)
116
+ end
117
+ true
118
+ end
119
+
120
+ def override_upper_levels(src_tags_array, from_level: self.raw_level + 1)
121
+ target_lev = Array(from_level..tag_attrs_count)
122
+ target_tags = src_tags_array[level_to_idx(from_level)..level_to_idx(tag_attrs_count)]
123
+ target_lev.zip(target_tags).each do |(n, tag)|
124
+ set_attr("l#{n}", tag)
125
+ end
126
+ self
127
+ end
128
+
129
+ def override_lower_levels(src_tags_array, to_level: self.raw_level - 1)
130
+ target_lev = Array(1..to_level)
131
+ target_tags = src_tags_array[level_to_idx(1)..level_to_idx(to_level)]
132
+ target_lev.zip(target_tags).each do |(n, tag)|
133
+ set_attr("l#{n}", tag)
134
+ end
135
+ self
136
+ end
137
+
138
+ def idx_to_level(x)
139
+ x + 1
140
+ end
141
+
142
+ def level_to_idx(x)
143
+ x - 1
144
+ end
145
+
146
+ def filled_idxs
147
+ tags_array.each_with_index.with_object([]) do |(t, i), o|
148
+ o << i if t
149
+ end
150
+ end
151
+
152
+ def blanks_between?
153
+ actual_level > empty_idx
154
+ end
155
+
156
+ def tags_array
157
+ values_at(*TAGS_ATTRS)
158
+ end
159
+
160
+ def values_at(*attrs)
161
+ attrs.map {|a| attr(a)}
162
+ end
163
+
164
+ def to_h(*attrs)
165
+ attrs = ALL_ATTRS if attrs.empty?
166
+ ALL_ATTRS.zip(values_at(*ALL_ATTRS)).to_h
167
+ end
168
+
169
+ def slice(*attrs)
170
+ return {} if attrs.empty?
171
+ to_h(*attrs)
172
+ end
173
+
174
+ def set_attrs(**kargs)
175
+ kargs.each {|attr, value| set_attr(attr, value)}
176
+ self
177
+ end
178
+
179
+ def set_attr(attr, value)
180
+ self.send("#{attr}=", value)
181
+ end
182
+
183
+ def attr(sym)
184
+ self.send(sym.to_sym)
185
+ end
186
+
187
+ def tag_attrs_count
188
+ TAGS_ATTRS.length
189
+ end
190
+
191
+ def remove_double_blanks(str)
192
+ return nil if str.nil?
193
+ str.gsub(DOUBLE_BLANKS, ' ').strip
194
+ end
195
+
196
+ def replace_not_allowed(str)
197
+ return nil if str.nil?
198
+ return str if str.match(VALID_TAG_REGEX)
199
+ str.gsub(INVALID_TAG_REGEX, ' ')
200
+ end
201
+
202
+ def identify_invalid_characters(str)
203
+ str.gsub(VALID_TAG_CHARS, '')
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,73 @@
1
+ class Eco::API::UseCases::DefaultCases::CsvToTree
2
+ module NodesCleaner
3
+ def repeated_tags
4
+ @repeated_tags ||= []
5
+ end
6
+
7
+ def done_tags
8
+ @done_tags ||= []
9
+ end
10
+
11
+ def fill_in_parents(nodes)
12
+ nodes.tap do |nodes|
13
+ prev_nodes = Array(1..11).zip(Array.new(11, nil)).to_h
14
+ nodes.each do |node|
15
+ if parent_node = prev_nodes[node.raw_level - 1]
16
+ node.parentId = parent_node.id
17
+ end
18
+ prev_nodes[node.raw_level] = node
19
+ end
20
+ end
21
+ end
22
+
23
+ def tidy_nodes(nodes, prev_level: 0, main: true)
24
+ out = nodes.each_with_object([]) do |node, out|
25
+ if done_tags.include?(tag = node.tag)
26
+ repeated_tags << "#{tag} (level: #{node.level})"
27
+ else
28
+ level = node.actual_level
29
+ if level > prev_level + 1
30
+ gap = level - (prev_level + 1)
31
+ puts "(Row: #{node.row_num}) Tag '#{tag}' (lev #{level}) jumps #{gap} level(s) (expected #{prev_level + 1})."
32
+ #puts " " + node.tags_array.pretty_inspect
33
+ missing_nodes = node.decouple(gap)
34
+ puts " Adding missing upper level(s): " + missing_nodes.map(&:raw_tag).pretty_inspect
35
+ out.push(*tidy_nodes(missing_nodes, prev_level: prev_level, main: false))
36
+ # puts node.actual_level
37
+ # pp node.tags_array
38
+ level = prev_level + 1
39
+ end
40
+ out << node
41
+ done_tags << tag
42
+ prev_level = level
43
+ end
44
+ end
45
+ if main
46
+ unless repeated_tags.empty?
47
+ puts "There were #{repeated_tags.length} repeated tags. Only one included. These excluded:"
48
+ pp repeated_tags
49
+ end
50
+ end
51
+ fill_in_parents(out)
52
+ end
53
+
54
+ def to_rows(nodes, prev_level: 0, main: true)
55
+ out = tidy_nodes(nodes).each_with_object([]) do |node, out|
56
+ tag = node.tag
57
+ level = node.actual_level
58
+ out << (row = Array.new(level, nil))
59
+ row[-1..-1] = [tag.upcase]
60
+ prev_level = level
61
+ end
62
+ if main
63
+ # Normalize length
64
+ max_row = out.max {|a, b| a.length <=> b.length}
65
+ holder = Array.new(max_row.length, nil)
66
+ out = out.map do |row|
67
+ row.dup.concat(holder)[0..max_row.length-1]
68
+ end
69
+ end
70
+ out
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ class Eco::API::UseCases::DefaultCases::CsvToTree
2
+ module Treeify
3
+ def treeify(nodes, &block)
4
+ get_children(nil, parents_hash(nodes), &block)
5
+ end
6
+
7
+ private
8
+
9
+ def parents_hash(nodes)
10
+ nodes.each_with_object({}) do |node, parents|
11
+ (parents[node.parentId] ||= []).push(node)
12
+ end
13
+ end
14
+
15
+ def get_children(node_id, parents, &block)
16
+ (parents[node_id] ||= []).each_with_object([]) do |child, results|
17
+ node_hash = {
18
+ "id" => child.id,
19
+ "name" => child.name
20
+ }
21
+
22
+ if block_given?
23
+ yield_hash = yield(child)
24
+ node_hash.merge(yield_hash) if yield_hash.is_a?(Hash)
25
+ end
26
+
27
+ results << node_hash.merge({
28
+ "nodes" => get_children(child.id, parents, &block).compact
29
+ })
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ class Eco::API::UseCases::DefaultCases::CsvToTree < Eco::API::Common::Loaders::UseCase
2
+ name "csv-to-tree"
3
+ type :other
4
+
5
+ TIME_FORMAT = '%Y%m%dT%H%M%S'
6
+
7
+ attr_reader :session, :options
8
+
9
+ def main(session, options, usecase)
10
+ options[:end_get] = false
11
+ @session = session; @options = options
12
+
13
+ tree_struct = Helper.treeify(Helper.csv_nodes(input_file))
14
+
15
+ File.open(output_file, "w") do |fd|
16
+ json = tree_struct.to_json
17
+ fd << json
18
+ end
19
+ logger.info("Saved structure in '#{output_file}'")
20
+ end
21
+
22
+ private
23
+
24
+ def input_file
25
+ @input_file ||= options.dig(:source, :file)
26
+ end
27
+
28
+ def output_file
29
+ @output_file ||= "#{active_enviro}_tree_#{timestamp}.json"
30
+ end
31
+
32
+ def timestamp(date = Time.now)
33
+ date.strftime(TIME_FORMAT)
34
+ end
35
+
36
+ def active_enviro
37
+ config.active_enviro
38
+ end
39
+ end
40
+
41
+ require_relative 'csv_to_tree_case/node'
42
+ require_relative 'csv_to_tree_case/nodes_cleaner'
43
+ require_relative 'csv_to_tree_case/treeify'
44
+ require_relative 'csv_to_tree_case/helper'
@@ -22,6 +22,7 @@ require_relative 'default_cases/create_case'
22
22
  require_relative 'default_cases/create_details_case'
23
23
  require_relative 'default_cases/create_details_with_supervisor_case'
24
24
  require_relative 'default_cases/create_tag_paths_case'
25
+ require_relative 'default_cases/csv_to_tree_case'
25
26
  require_relative 'default_cases/delete_trans_case'
26
27
  require_relative 'default_cases/delete_sync_case'
27
28
  require_relative 'default_cases/email_as_id_case'
@@ -16,7 +16,7 @@ class Eco::API::UseCases::GraphQL::Base < Eco::API::Common::Loaders::UseCase
16
16
  end
17
17
 
18
18
  def graphql
19
- @graphql ||= session.api(version: :graphql)
19
+ @graphql ||= session.api(version: :graphql)
20
20
  end
21
21
 
22
22
  def exit_error(msg)
@@ -71,7 +71,7 @@ class Eco::API::UseCases::OozeSamples
71
71
  return src_fld, dst_fld if dst_flds.empty? || src_flds.empty?
72
72
 
73
73
  if dst_flds.count > 1
74
- logger.warn("There are #{dst_flds.count} destination '#{type}' fields named '#{label_dst}' (source: '#{src.id}'). Using first...")
74
+ logger.warn("There are #{dst_flds.count} destination '#{type}' fields named '#{label_dst}' (source: '#{dst.id}'). Using first...")
75
75
  end
76
76
 
77
77
  if src_flds.count > 1
@@ -98,6 +98,12 @@ ASSETS.cli.config do |cnf|
98
98
  desc = "Creates a CSV with the paths to each tag"
99
99
  cases.add("-create-tag-paths", :other, desc, case_name: "create-tag-paths")
100
100
 
101
+ desc = "Creates a JSON file with the tagtree from a CSV file"
102
+ cases.add("-csv-to-tree", :other, desc, case_name: "csv-to-tree") do |session, options, usecase|
103
+ file = SCR.get_file("-csv-to-tree", required: true, should_exist: true)
104
+ options.deep_merge!(source: {file: file})
105
+ end
106
+
101
107
  desc = "Cleans from filter_tags those tags that are not present in the tagtree (as per tagtree.json file)."
102
108
  desc += " It will preserve standard register tags of most common registers (i.e. EVENT, RISK)."
103
109
  cases.add("-clean-unknown-tags", :transform, desc, case_name: "clean-unknown-tags")
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = "2.1.12"
2
+ VERSION = "2.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.12
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
@@ -136,40 +136,40 @@ dependencies:
136
136
  requirements:
137
137
  - - ">="
138
138
  - !ruby/object:Gem::Version
139
- version: 0.9.7
139
+ version: 1.0.1
140
140
  - - "<"
141
141
  - !ruby/object:Gem::Version
142
- version: '0.10'
142
+ version: '1.1'
143
143
  type: :runtime
144
144
  prerelease: false
145
145
  version_requirements: !ruby/object:Gem::Requirement
146
146
  requirements:
147
147
  - - ">="
148
148
  - !ruby/object:Gem::Version
149
- version: 0.9.7
149
+ version: 1.0.1
150
150
  - - "<"
151
151
  - !ruby/object:Gem::Version
152
- version: '0.10'
152
+ version: '1.1'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: ecoportal-api-graphql
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: 0.1.11
159
+ version: 0.2.2
160
160
  - - "<"
161
161
  - !ruby/object:Gem::Version
162
- version: '0.2'
162
+ version: '0.3'
163
163
  type: :runtime
164
164
  prerelease: false
165
165
  version_requirements: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - ">="
168
168
  - !ruby/object:Gem::Version
169
- version: 0.1.11
169
+ version: 0.2.2
170
170
  - - "<"
171
171
  - !ruby/object:Gem::Version
172
- version: '0.2'
172
+ version: '0.3'
173
173
  - !ruby/object:Gem::Dependency
174
174
  name: aws-sdk-s3
175
175
  requirement: !ruby/object:Gem::Requirement
@@ -540,6 +540,7 @@ files:
540
540
  - lib/eco/api/session/config/post_launch.rb
541
541
  - lib/eco/api/session/config/s3_storage.rb
542
542
  - lib/eco/api/session/config/sftp.rb
543
+ - lib/eco/api/session/config/tagtree.rb
543
544
  - lib/eco/api/session/config/workflow.rb
544
545
  - lib/eco/api/usecases.rb
545
546
  - lib/eco/api/usecases/base_case.rb
@@ -556,6 +557,11 @@ files:
556
557
  - lib/eco/api/usecases/default_cases/create_details_case.rb
557
558
  - lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb
558
559
  - lib/eco/api/usecases/default_cases/create_tag_paths_case.rb
560
+ - lib/eco/api/usecases/default_cases/csv_to_tree_case.rb
561
+ - lib/eco/api/usecases/default_cases/csv_to_tree_case/helper.rb
562
+ - lib/eco/api/usecases/default_cases/csv_to_tree_case/node.rb
563
+ - lib/eco/api/usecases/default_cases/csv_to_tree_case/nodes_cleaner.rb
564
+ - lib/eco/api/usecases/default_cases/csv_to_tree_case/treeify.rb
559
565
  - lib/eco/api/usecases/default_cases/delete_sync_case.rb
560
566
  - lib/eco/api/usecases/default_cases/delete_trans_case.rb
561
567
  - lib/eco/api/usecases/default_cases/email_as_id_case.rb