eco-helpers 2.1.12 → 2.2.1

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