eco-helpers 2.5.8 → 2.5.9
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +40 -1
- data/eco-helpers.gemspec +1 -1
- data/lib/eco/api/common/loaders/base.rb +1 -4
- data/lib/eco/api/common/loaders/parser.rb +5 -5
- data/lib/eco/api/organization/people.rb +9 -0
- data/lib/eco/api/usecases/base_io.rb +1 -1
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case.rb +14 -10
- data/lib/eco/api/usecases/default_cases/samples/sftp_case.rb +29 -14
- data/lib/eco/cli/config/default/workflow.rb +9 -7
- data/lib/eco/data/locations/node_base/tag_validations.rb +2 -1
- data/lib/eco/data/locations/node_base.rb +4 -0
- data/lib/eco/data/locations/node_level/cleaner.rb +35 -19
- data/lib/eco/data/locations/node_level/parsing.rb +5 -12
- data/lib/eco/data/locations/node_level.rb +71 -18
- data/lib/eco/language/models/parser_serializer.rb +1 -1
- data/lib/eco/version.rb +1 -1
- metadata +3 -7
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case/helper.rb +0 -99
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case/node.rb +0 -221
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case/nodes_cleaner.rb +0 -73
- data/lib/eco/api/usecases/default_cases/csv_to_tree_case/treeify.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f78310ad18a148ef707c1735932215754c047041fd3a1539a1434a9d213427d
|
4
|
+
data.tar.gz: 8d795944674e2cb2148a796ca1c1aa0817f342c5ffb9228ef22a99fe3cc39179
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88cbb36569c4683decb9210d9db4ed2da2e205c8d06f170707b15cb9085fa111d39407eefd495211022129c4532172fd69bebe266123ab9a10c4663f2c83a940
|
7
|
+
data.tar.gz: 49795a3e5c994e1d0a74f46257865062501ed0b400fba66a9de1e81c6b3826a3a7f71ebd2e46df48a0d13577cafc677d23e7b60ff9768a42229f18bec4cce703
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,12 +1,51 @@
|
|
1
1
|
# Change Log
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
-
## [2.5.
|
4
|
+
## [2.5.10] - 2023-09-xx
|
5
5
|
|
6
6
|
### Added
|
7
7
|
### Changed
|
8
8
|
### Fixed
|
9
9
|
|
10
|
+
## [2.5.9] - 2023-09-01
|
11
|
+
|
12
|
+
The input **csv** of trees could come very compacted, which had unintended consequences.
|
13
|
+
- This is a fix to this scenario.
|
14
|
+
|
15
|
+
### Added
|
16
|
+
- `Eco::Data::Locations::NodeLevel#raw_latest_consecutive_top_empty_level`
|
17
|
+
- To scope the missing ancestors (up to what level)
|
18
|
+
- `Eco::API::UseCases::DefaultCases::Samples::Sftp`
|
19
|
+
- Options for `remote_subfolder`, `remote_target_folder` and `remote_folder`
|
20
|
+
- `Eco::API::Organization::People#updated_or_created` **method**
|
21
|
+
- Scoping all people that have been updated as part of the current session.
|
22
|
+
- Note that it includes also those that are hris excluded.
|
23
|
+
- **Improvement** on `Eco::API::Common::Loaders::Base`
|
24
|
+
- Should be able to call `log(:level) { "Some message" }`
|
25
|
+
|
26
|
+
### Changed
|
27
|
+
- `Eco::Data::Locations::NodeLevel`
|
28
|
+
- **removed** `#merge!` and `#override_upper_levels`
|
29
|
+
- `#override_lower_levels` **renamed** to `#update_lower_levels`
|
30
|
+
- `Eco::API::UseCases::DefaultCases::CsvToTree` use case
|
31
|
+
- Moved to work with `Eco::Data::Locations::DSL`
|
32
|
+
- **Removed** helpers double-ups.
|
33
|
+
- Default workflow `on(:report)`: implementation of `-processed-people-to-csv`
|
34
|
+
- It now only includes people created or updated as part of the current session.
|
35
|
+
- **Improvement**: `Eco::API::Common::Loaders::Parser` made `serializer` method **not required**
|
36
|
+
- This aims to use the default serializer definition
|
37
|
+
- Made **inheritable** `parsing_phase` and `serializing_phase`
|
38
|
+
|
39
|
+
### Fixed
|
40
|
+
- `Eco::Data::Locations::NodeLevel#update_lower_levels`
|
41
|
+
- To use `#raw_latest_consecutive_top_empty_level`
|
42
|
+
- Compact from first filled in `tags_array` onwards (preserve empty tags at the beginning for clean validation)
|
43
|
+
- `Eco::Data::Locations::NodeLevel::Cleaner`
|
44
|
+
- `#tidy_nodes` **gap** calculation is scoped against previous node (common ancestors)
|
45
|
+
- `#fill_in_parents` to give feedback on unexpected parental relationship
|
46
|
+
- `Eco::API::UseCases::DefaultCases::Samples::Sftp`
|
47
|
+
- Prevent double slashed paths
|
48
|
+
|
10
49
|
## [2.5.8] - 2023-08-28
|
11
50
|
|
12
51
|
### Changed
|
data/eco-helpers.gemspec
CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency "yard", ">= 0.9.26", "< 1"
|
31
31
|
spec.add_development_dependency "redcarpet", ">= 3.5.1", "< 4"
|
32
32
|
|
33
|
-
spec.add_dependency 'ecoportal-api', '>= 0.9.
|
33
|
+
spec.add_dependency 'ecoportal-api', '>= 0.9.5', '< 0.10'
|
34
34
|
spec.add_dependency 'ecoportal-api-v2', '>= 1.1.3', '< 1.2'
|
35
35
|
spec.add_dependency 'ecoportal-api-graphql', '>= 0.3.10', '< 0.4'
|
36
36
|
spec.add_dependency 'aws-sdk-s3', '>= 1.83.0', '< 2'
|
@@ -4,6 +4,7 @@ module Eco
|
|
4
4
|
module Loaders
|
5
5
|
class Base
|
6
6
|
extend Eco::API::Common::ClassHelpers
|
7
|
+
include Eco::Language::AuxiliarLogger
|
7
8
|
|
8
9
|
class << self
|
9
10
|
# Sort order
|
@@ -47,10 +48,6 @@ module Eco
|
|
47
48
|
session.config
|
48
49
|
end
|
49
50
|
|
50
|
-
def logger
|
51
|
-
session.logger
|
52
|
-
end
|
53
|
-
|
54
51
|
def micro
|
55
52
|
session.micro
|
56
53
|
end
|
@@ -101,10 +101,9 @@ module Eco
|
|
101
101
|
keys = []
|
102
102
|
end
|
103
103
|
end
|
104
|
-
|
105
104
|
end
|
106
105
|
|
107
|
-
inheritable_class_vars :attribute
|
106
|
+
inheritable_class_vars :attribute, :parsing_phase, :serializing_phase
|
108
107
|
|
109
108
|
def initialize(person_parser)
|
110
109
|
raise "Expected Eco::API::Common::People::PersonParser. Given #{policies.class}" unless person_parser.is_a?(Eco::API::Common::People::PersonParser)
|
@@ -127,9 +126,9 @@ module Eco
|
|
127
126
|
# - when `:final`: it will receive a `Hash` with the **internal values** and **types**.
|
128
127
|
# - when `:person`: it will receive the `person` object.
|
129
128
|
# @param deps [Hash] the merged dependencies (default to the class object and when calling the parser).
|
130
|
-
def
|
131
|
-
|
132
|
-
end
|
129
|
+
# def serializer(data, deps)
|
130
|
+
# raise "You should implement this method"
|
131
|
+
# end
|
133
132
|
|
134
133
|
# @return [String, Symbol] the field/attribute or type this parser is linked to.
|
135
134
|
def attribute
|
@@ -147,6 +146,7 @@ module Eco
|
|
147
146
|
end
|
148
147
|
|
149
148
|
def _define_serializer(attr_parser)
|
149
|
+
return unless respond_to?(:serializer, true)
|
150
150
|
attr_parser.def_serializer(self.class.serializing_phase, &self.method(:serializer))
|
151
151
|
end
|
152
152
|
end
|
@@ -80,6 +80,15 @@ module Eco
|
|
80
80
|
account_present(false)
|
81
81
|
end
|
82
82
|
|
83
|
+
# Returns the people that are being or have been updated and/or created.
|
84
|
+
def updated_or_created
|
85
|
+
select do |person|
|
86
|
+
!person.as_update(:total).empty?
|
87
|
+
end.yield_self do |persons|
|
88
|
+
newFrom persons
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
83
92
|
def supervisors
|
84
93
|
sup_ids = self.ids & self.supervisor_ids
|
85
94
|
sup_ids.map do |id|
|
@@ -2,27 +2,36 @@ class Eco::API::UseCases::DefaultCases::CsvToTree < Eco::API::Common::Loaders::U
|
|
2
2
|
name "csv-to-tree"
|
3
3
|
type :other
|
4
4
|
|
5
|
+
include Eco::Data::Locations::DSL
|
6
|
+
|
5
7
|
TIME_FORMAT = '%Y%m%dT%H%M%S'
|
6
8
|
|
7
9
|
def main(session, options, usecase)
|
8
10
|
options[:end_get] = false
|
9
|
-
tree_struct =
|
11
|
+
tree_struct = org_tree(input_csv)
|
10
12
|
|
11
13
|
File.open(output_file, "w") do |fd|
|
12
|
-
|
13
|
-
fd << json
|
14
|
+
fd << tree_struct.as_json.to_json
|
14
15
|
end
|
15
16
|
logger.info("Saved structure in '#{output_file}'")
|
16
17
|
end
|
17
18
|
|
18
19
|
private
|
19
20
|
|
21
|
+
def output_file
|
22
|
+
@output_file ||= "#{active_enviro}_tree_#{timestamp}.json"
|
23
|
+
end
|
24
|
+
|
25
|
+
def input_csv
|
26
|
+
@input_csv ||= Eco::CSV.read(input_file, encoding: input_encoding)
|
27
|
+
end
|
28
|
+
|
20
29
|
def input_file
|
21
30
|
@input_file ||= options.dig(:source, :file)
|
22
31
|
end
|
23
32
|
|
24
|
-
def
|
25
|
-
|
33
|
+
def input_encoding
|
34
|
+
options.dig(:input, :file, :encoding) || 'utf-8'
|
26
35
|
end
|
27
36
|
|
28
37
|
def timestamp(date = Time.now)
|
@@ -33,8 +42,3 @@ class Eco::API::UseCases::DefaultCases::CsvToTree < Eco::API::Common::Loaders::U
|
|
33
42
|
config.active_enviro
|
34
43
|
end
|
35
44
|
end
|
36
|
-
|
37
|
-
require_relative 'csv_to_tree_case/node'
|
38
|
-
require_relative 'csv_to_tree_case/nodes_cleaner'
|
39
|
-
require_relative 'csv_to_tree_case/treeify'
|
40
|
-
require_relative 'csv_to_tree_case/helper'
|
@@ -2,10 +2,9 @@ class Eco::API::UseCases::DefaultCases::Samples::Sftp < Eco::API::Common::Loader
|
|
2
2
|
name "sftp-sample"
|
3
3
|
type :other
|
4
4
|
|
5
|
-
|
5
|
+
CONST_REFERRAL = /^(?:::)?(?:[A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*)$/
|
6
6
|
|
7
7
|
def main(session, options, usecase)
|
8
|
-
@session = session; @options = options
|
9
8
|
options[:end_get] = false
|
10
9
|
raise "The SFTP is not configured" unless session.sftp?
|
11
10
|
case options.dig(:sftp, :command)
|
@@ -22,17 +21,41 @@ class Eco::API::UseCases::DefaultCases::Samples::Sftp < Eco::API::Common::Loader
|
|
22
21
|
|
23
22
|
private
|
24
23
|
|
24
|
+
# Can't pass this via CLI option, as it breaks the regular expression
|
25
25
|
def file_pattern
|
26
|
+
fpc = file_pattern_const
|
27
|
+
return fpc if fpc
|
26
28
|
raise "You should redefine the file_pattern function as a RegEx expression that matches the target remote file"
|
27
29
|
end
|
28
30
|
|
31
|
+
def file_pattern_const
|
32
|
+
if fpc = options.dig(:sftp, :file_pattern_const)
|
33
|
+
raise "Invalid file pattern const referral: #{fpc}" unless fpc.match(CONST_REFERRAL)
|
34
|
+
self.eval(fpc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
29
38
|
# Ex: "/IN/Personnel"
|
30
39
|
def remote_subfolder
|
40
|
+
rm_sf = options.dig(:sftp, :remote_subfolder)
|
41
|
+
return rm_sf if rm_sf
|
31
42
|
raise "You should redefine remote_subfolder as the folder where the target file sits. Ex: /IN/Personnel"
|
32
43
|
end
|
33
44
|
|
34
|
-
|
35
|
-
|
45
|
+
# `remote_target_folder` overrides `sftp_config.remote_folder` as well as `remote_subfolder`
|
46
|
+
# `remote_folder` overrides `sftp_config.remote_folder` but NOT `remote_subfolder`
|
47
|
+
def remote_folder
|
48
|
+
rm_tf = options.dig(:sftp, :remote_target_folder)
|
49
|
+
rm_fd = options.dig(:sftp, :remote_folder) || sftp_config.remote_folder
|
50
|
+
rm_tf || File.join(rm_fd, remote_subfolder)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_remote_path(file)
|
54
|
+
File.join(remote_folder, file)
|
55
|
+
end
|
56
|
+
|
57
|
+
def local_folder
|
58
|
+
options.dig(:sftp, :local_folder) || "."
|
36
59
|
end
|
37
60
|
|
38
61
|
def with_remote_files
|
@@ -92,16 +115,8 @@ class Eco::API::UseCases::DefaultCases::Samples::Sftp < Eco::API::Common::Loader
|
|
92
115
|
end
|
93
116
|
end
|
94
117
|
|
95
|
-
def
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
def local_folder
|
100
|
-
options.dig(:sftp, :local_folder) || "."
|
101
|
-
end
|
102
|
-
|
103
|
-
def remote_folder
|
104
|
-
@remote_folder ||= sftp_config.remote_folder + remote_subfolder
|
118
|
+
def archive_subfolder
|
119
|
+
"Archive"
|
105
120
|
end
|
106
121
|
|
107
122
|
def sftp_config
|
@@ -153,14 +153,16 @@ ASSETS.cli.config do |config|
|
|
153
153
|
end
|
154
154
|
|
155
155
|
wf.on(:report) do |wf_report, io|
|
156
|
-
|
157
|
-
io.options.
|
158
|
-
options: {
|
159
|
-
|
160
|
-
|
161
|
-
|
156
|
+
io.tap do |_io|
|
157
|
+
if file = io.options.dig(:report, :people, :csv)
|
158
|
+
io.options.deep_merge!(export: {
|
159
|
+
options: {internal_names: true, nice_header: true, split_schemas: true},
|
160
|
+
file: {name: file, format: :csv}
|
161
|
+
})
|
162
|
+
aux_io = io.new(people: io.people.updated_or_created)
|
163
|
+
io.session.process_case("to-csv", io: aux_io, type: :export)
|
164
|
+
end
|
162
165
|
end
|
163
|
-
io
|
164
166
|
end
|
165
167
|
|
166
168
|
wf.on(:end) do |wf_end, io|
|
@@ -6,10 +6,11 @@ module Eco::Data::Locations::NodeBase
|
|
6
6
|
VALID_TAG_CHARS = /[#{ALLOWED_CHARACTERS}]+/
|
7
7
|
DOUBLE_BLANKS = /\s\s+/
|
8
8
|
|
9
|
-
def clean_id(str)
|
9
|
+
def clean_id(str, notify: true)
|
10
10
|
blanks_x2 = has_double_blanks?(str)
|
11
11
|
partial = replace_not_allowed(str)
|
12
12
|
remove_double_blanks(partial).tap do |result|
|
13
|
+
next unless notify
|
13
14
|
next if invalid_warned?
|
14
15
|
if partial != str
|
15
16
|
invalid_chars = identify_invalid_characters(str)
|
@@ -11,36 +11,41 @@ class Eco::Data::Locations::NodeLevel
|
|
11
11
|
# 3. It covers the gap if present by decoupling merged parent(s) from the same node (see node.decouple)
|
12
12
|
# 4. Then, it delegates the filling in of parents to `fill_in_parents` function.
|
13
13
|
# @return [Array<NodeLevel>] child to parent relationships solved and no double-ups.
|
14
|
-
def tidy_nodes(nodes,
|
14
|
+
def tidy_nodes(nodes, prev_node: nil, main: true)
|
15
15
|
reset_trackers! if main
|
16
|
+
|
17
|
+
prev_level = prev_node&.actual_level || 0
|
18
|
+
|
16
19
|
nodes.each_with_object([]) do |node, out|
|
17
|
-
|
18
|
-
|
19
|
-
repeated_ids << "#{
|
20
|
+
if done_ids.include?(node.id)
|
21
|
+
row_str = node.row_num ? " - (row: #{node.row_num})" : ''
|
22
|
+
repeated_ids << "#{node.id} (level: #{node.level})#{row_str}"
|
20
23
|
else
|
21
24
|
level = node.actual_level
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
common_level = node.common_level_with(prev_node)
|
26
|
+
common_level ||= prev_level
|
27
|
+
gap = level - (common_level + 1)
|
28
|
+
|
29
|
+
unless gap < 1
|
30
|
+
msg = "(Row: #{node.row_num}) ID/Tag '#{node.id}' (lev #{level}) jumps #{gap} level(s) (expected #{prev_level + 1})."
|
25
31
|
#puts " " + node.tags_array.pretty_inspect
|
26
32
|
missing_nodes = node.decouple(gap)
|
27
|
-
|
28
|
-
|
29
|
-
log(:info) { msg }
|
30
|
-
|
33
|
+
msg << "\n Adding missing upper node(s): " + missing_nodes.map(&:raw_tag).pretty_inspect
|
34
|
+
log(:debug) { msg }
|
31
35
|
# The very top missing node (first in list) should be checked against prev_level
|
32
36
|
# alongside any descendants in missing_nodes (when gap 2+)
|
33
|
-
tidied_nodes = tidy_nodes(missing_nodes,
|
37
|
+
tidied_nodes = tidy_nodes(missing_nodes, prev_node: prev_node, main: false)
|
34
38
|
out.push(*tidied_nodes)
|
35
|
-
#level = prev_level + 1 # <= we are actually on level and filled in the gaps
|
36
39
|
end
|
37
40
|
out << node
|
38
|
-
done_ids <<
|
39
|
-
|
41
|
+
done_ids << node.id
|
42
|
+
prev_node = node
|
43
|
+
end
|
44
|
+
end.tap do |out|
|
45
|
+
if main
|
46
|
+
report_repeated_node_ids(repeated_ids)
|
47
|
+
fill_in_parents(out)
|
40
48
|
end
|
41
|
-
end.yield_self do |out|
|
42
|
-
report_repeated_node_ids(repeated_ids) if main
|
43
|
-
fill_in_parents(out)
|
44
49
|
end
|
45
50
|
end
|
46
51
|
|
@@ -51,10 +56,21 @@ class Eco::Data::Locations::NodeLevel
|
|
51
56
|
nodes.tap do |nodes|
|
52
57
|
prev_nodes = empty_level_tracker_hash(11)
|
53
58
|
nodes.each do |node|
|
59
|
+
expected_parent_id = node.clean_parent_id&.upcase
|
60
|
+
msg = "Expecting node '#{node.id}' to have parent: '#{expected_parent_id}'\n"
|
54
61
|
if parent_node = prev_nodes[node.actual_level - 1]
|
55
62
|
node.parentId = parent_node.id
|
63
|
+
log(:warn) {
|
64
|
+
msg + " • We got '#{parent_node.id}' instead"
|
65
|
+
} unless expected_parent_id == node.parentId
|
66
|
+
elsif node.actual_level == 1
|
67
|
+
# expected to not have parent
|
68
|
+
else
|
69
|
+
log(:warn) {
|
70
|
+
msg + "but we did not get parent."
|
71
|
+
}
|
56
72
|
end
|
57
|
-
prev_nodes[node.
|
73
|
+
prev_nodes[node.actual_level] = node
|
58
74
|
end
|
59
75
|
end
|
60
76
|
end
|
@@ -30,17 +30,11 @@ class Eco::Data::Locations::NodeLevel
|
|
30
30
|
|
31
31
|
prev_level = nil
|
32
32
|
prev_node = nil
|
33
|
-
|
34
|
-
prev_node_get = proc do |raw_level|
|
35
|
-
prev = nil
|
36
|
-
(1..raw_level).to_a.reverse.each do |lev|
|
37
|
-
prev ||= prev_nodes[lev]
|
38
|
-
end
|
39
|
-
prev
|
40
|
-
end
|
33
|
+
|
41
34
|
# Convert to Eco::CSV::Table for a fresh start
|
42
35
|
csv = Eco::CSV.parse(csv.to_csv).nil_blank_cells.add_index_column(:row_num)
|
43
36
|
|
37
|
+
first = true
|
44
38
|
nodes = csv.each_with_object([]) do |row, out|
|
45
39
|
row_num, *values = row.fields
|
46
40
|
node = node_class.new(row_num, *values)
|
@@ -53,16 +47,15 @@ class Eco::Data::Locations::NodeLevel
|
|
53
47
|
# which allows to node#actual_level to work
|
54
48
|
node.set_high_levels(prev_node)
|
55
49
|
else
|
56
|
-
if
|
57
|
-
node.set_high_levels(parent_node)
|
58
|
-
elsif node.raw_level == 1
|
50
|
+
if node.raw_level == 1
|
59
51
|
# It is expected not to have parent (as it's top level tag)
|
52
|
+
elsif prev_node
|
53
|
+
node.set_high_levels(prev_node)
|
60
54
|
else
|
61
55
|
raise "Node '#{node.raw_tag}' (#{node.row_num} row) doesn't have parent"
|
62
56
|
end
|
63
57
|
end
|
64
58
|
out << node
|
65
|
-
prev_nodes[node.raw_level] = node
|
66
59
|
prev_node = node
|
67
60
|
end
|
68
61
|
tidy_nodes(nodes)
|
@@ -54,6 +54,42 @@ module Eco::Data::Locations
|
|
54
54
|
nil
|
55
55
|
end
|
56
56
|
|
57
|
+
def raw_prev_empty_level?
|
58
|
+
lev = raw_prev_empty_level
|
59
|
+
lev && lev > 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def raw_latest_consecutive_top_empty_level
|
63
|
+
tags_array[0..raw_level-1].each_with_index do |value, idx|
|
64
|
+
return idx if value
|
65
|
+
end
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Requires that all upper levels (lower positions) are filled-in
|
70
|
+
def common_level_with(other)
|
71
|
+
return nil unless other
|
72
|
+
otags_array = other.tags_array.compact
|
73
|
+
stags_array = tags_array.compact
|
74
|
+
raise "Missing lower levels for #{other.id}: #{other.tags_array.pretty_inspect}" unless other.highest_levels_set?
|
75
|
+
raise "Missing lower levels for #{self.id}: #{tags_array.pretty_inspect}" unless highest_levels_set?
|
76
|
+
otags_array.zip(stags_array).each_with_index do |(otag, stag), idx|
|
77
|
+
next if otag&.upcase&.strip == stag&.upcase&.strip
|
78
|
+
return nil if idx == 0
|
79
|
+
return idx # previous idx, which means prev_idx + 1 (so idx itself)
|
80
|
+
end
|
81
|
+
actual_level
|
82
|
+
end
|
83
|
+
|
84
|
+
# Second last id in tags_array
|
85
|
+
def raw_parent_id
|
86
|
+
tags_array.compact[-2]
|
87
|
+
end
|
88
|
+
|
89
|
+
def clean_parent_id
|
90
|
+
clean_tags_array.compact[-2]
|
91
|
+
end
|
92
|
+
|
57
93
|
def tag_idx
|
58
94
|
tags_array.index(raw_tag)
|
59
95
|
end
|
@@ -68,33 +104,50 @@ module Eco::Data::Locations
|
|
68
104
|
tary.index(nil) || tary.length + 1
|
69
105
|
end
|
70
106
|
|
107
|
+
def copy
|
108
|
+
super.tap do |dup|
|
109
|
+
dup.highest_levels_set!
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
71
113
|
# We got a missing level that is compacted in one row
|
72
114
|
# Here we get the missing intermediate levels
|
73
115
|
# This is done from upper to lower level to ensure processing order
|
74
116
|
# It skips last one, as that is this object already
|
117
|
+
# @note for each one in the gap, creates a copy and clears deepest levels thereof
|
75
118
|
def decouple(num = 1)
|
76
119
|
with_info = filled_idxs
|
77
120
|
# must be the last among filled_idxs, so let's use it to verify
|
78
121
|
unless with_info.last == tag_idx
|
122
|
+
# This can only happen when there are repeated nodes
|
79
123
|
raise "Review this (row #{row_num}; '#{raw_tag}'): tag_idx is #{tag_idx}, while last filled idx is #{with_info.last}"
|
80
124
|
end
|
81
|
-
len
|
125
|
+
len = with_info.length
|
82
126
|
target_idxs = with_info[len-(num+1)..-2]
|
83
127
|
target_idxs.map do |idx|
|
84
|
-
|
128
|
+
copy.tap do |dup|
|
85
129
|
dup.clear_level(idx_to_level(idx + 1))
|
86
130
|
end
|
87
131
|
end
|
88
132
|
end
|
89
133
|
|
90
|
-
def
|
91
|
-
|
134
|
+
def highest_levels_set?
|
135
|
+
return true if raw_level == 1
|
136
|
+
return true unless raw_prev_empty_level?
|
137
|
+
!!@highest_levels_set
|
92
138
|
end
|
93
139
|
|
94
|
-
def
|
95
|
-
|
140
|
+
def highest_levels_set!
|
141
|
+
@highest_levels_set = true
|
96
142
|
end
|
97
143
|
|
144
|
+
# Sets ancestors
|
145
|
+
def set_high_levels(node, override: false, compact: true)
|
146
|
+
update_lower_levels(node.tags_array, override: override)
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
# Clears the deepest levels, from level `i` onwards
|
98
151
|
def clear_level(i)
|
99
152
|
case i
|
100
153
|
when Enumerable
|
@@ -113,23 +166,17 @@ module Eco::Data::Locations
|
|
113
166
|
true
|
114
167
|
end
|
115
168
|
|
116
|
-
# Cleanses deepest tags
|
117
|
-
def override_upper_levels(src_tags_array, from_level: self.raw_level + 1)
|
118
|
-
target_lev = Array(from_level..tag_attrs_count)
|
119
|
-
target_tags = src_tags_array[level_to_idx(from_level)..level_to_idx(tag_attrs_count)]
|
120
|
-
target_lev.zip(target_tags).each do |(n, tag)|
|
121
|
-
set_attr("l#{n}", tag)
|
122
|
-
end
|
123
|
-
self
|
124
|
-
end
|
125
|
-
|
126
169
|
# Ensures parent is among the upper level tags
|
127
|
-
|
170
|
+
# It actually ensures all ancestors are there
|
171
|
+
# @param override [Boolean] `false` will only override upmost top consecutive empty levels.
|
172
|
+
def update_lower_levels(src_tags_array, to_level: self.raw_latest_consecutive_top_empty_level, override: false)
|
173
|
+
highest_levels_set!
|
128
174
|
return self unless to_level
|
129
175
|
target_lev = Array(1..to_level)
|
130
176
|
target_tags = src_tags_array[level_to_idx(1)..level_to_idx(to_level)]
|
131
177
|
target_lev.zip(target_tags).each do |(n, tag)|
|
132
|
-
|
178
|
+
attr_lev = "l#{n}"
|
179
|
+
set_attr(attr_lev, tag) # unless attr?(attr_lev) && !override
|
133
180
|
end
|
134
181
|
self
|
135
182
|
end
|
@@ -152,6 +199,12 @@ module Eco::Data::Locations
|
|
152
199
|
actual_level > empty_idx
|
153
200
|
end
|
154
201
|
|
202
|
+
def clean_tags_array
|
203
|
+
tags_array.map do |tg|
|
204
|
+
clean_id(tg, notify: false)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
155
208
|
def tags_array
|
156
209
|
values_at(*TAGS_ATTRS)
|
157
210
|
end
|
@@ -6,7 +6,7 @@ module Eco
|
|
6
6
|
class ParserSerializer
|
7
7
|
attr_reader :attr
|
8
8
|
|
9
|
-
# Parser/
|
9
|
+
# Parser/serializer.
|
10
10
|
# @param attr [String, Symbol] name of the parsed/serialized.
|
11
11
|
# @param dependencies [Hash] provisioning of _**default dependencies**_ that will be required when calling back to the
|
12
12
|
# parsing or serializing functions.
|
data/lib/eco/version.rb
CHANGED
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.5.
|
4
|
+
version: 2.5.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oscar Segura
|
@@ -116,7 +116,7 @@ dependencies:
|
|
116
116
|
requirements:
|
117
117
|
- - ">="
|
118
118
|
- !ruby/object:Gem::Version
|
119
|
-
version: 0.9.
|
119
|
+
version: 0.9.5
|
120
120
|
- - "<"
|
121
121
|
- !ruby/object:Gem::Version
|
122
122
|
version: '0.10'
|
@@ -126,7 +126,7 @@ dependencies:
|
|
126
126
|
requirements:
|
127
127
|
- - ">="
|
128
128
|
- !ruby/object:Gem::Version
|
129
|
-
version: 0.9.
|
129
|
+
version: 0.9.5
|
130
130
|
- - "<"
|
131
131
|
- !ruby/object:Gem::Version
|
132
132
|
version: '0.10'
|
@@ -555,10 +555,6 @@ files:
|
|
555
555
|
- lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb
|
556
556
|
- lib/eco/api/usecases/default_cases/create_tag_paths_case.rb
|
557
557
|
- lib/eco/api/usecases/default_cases/csv_to_tree_case.rb
|
558
|
-
- lib/eco/api/usecases/default_cases/csv_to_tree_case/helper.rb
|
559
|
-
- lib/eco/api/usecases/default_cases/csv_to_tree_case/node.rb
|
560
|
-
- lib/eco/api/usecases/default_cases/csv_to_tree_case/nodes_cleaner.rb
|
561
|
-
- lib/eco/api/usecases/default_cases/csv_to_tree_case/treeify.rb
|
562
558
|
- lib/eco/api/usecases/default_cases/delete_sync_case.rb
|
563
559
|
- lib/eco/api/usecases/default_cases/delete_trans_case.rb
|
564
560
|
- lib/eco/api/usecases/default_cases/email_as_id_case.rb
|
@@ -1,99 +0,0 @@
|
|
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 nodes_from_csv(csv)
|
18
|
-
i = 1; prev_level = nil; prev_node = nil; prev_nodes = Array(1..11).zip(Array.new(11, nil)).to_h
|
19
|
-
nodes = csv.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 node is nested in prev_node or is a sibling thereof
|
29
|
-
if prev_node.raw_level <= node.raw_level
|
30
|
-
# Make sure parent is among upper level tags
|
31
|
-
node.set_high_levels(prev_node)
|
32
|
-
else
|
33
|
-
if parent_node = prev_nodes[node.raw_level - 1]
|
34
|
-
node.set_high_levels(parent_node)
|
35
|
-
elsif node.raw_level == 1
|
36
|
-
# It is expected not to have parent
|
37
|
-
#puts "Node '#{node.raw_tag}' doesn't have parent, but it's top level tag"
|
38
|
-
else
|
39
|
-
raise "Node '#{node.raw_tag}' (#{node.row_num} row) doesn't have parent"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
out << node
|
43
|
-
prev_nodes[node.raw_level] = node
|
44
|
-
prev_node = node
|
45
|
-
end
|
46
|
-
tidy_nodes(nodes)
|
47
|
-
end
|
48
|
-
|
49
|
-
def csv_nodes(filename)
|
50
|
-
nodes_from_csv(csv_from(filename))
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def csv_from_content(filename)
|
56
|
-
CSV.parse(file_content(filename), headers: true)
|
57
|
-
end
|
58
|
-
|
59
|
-
def file_content(filename)
|
60
|
-
coding = encoding(filename)
|
61
|
-
coding = (coding != "utf-8")? "#{coding}|utf-8": coding
|
62
|
-
if content = File.read(filename, encoding: coding)
|
63
|
-
content.scrub do |bytes|
|
64
|
-
'<' + bytes.unpack('H*')[0] + '>'
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def csv_from_file(filename)
|
70
|
-
coding = encoding(filename)
|
71
|
-
coding = (coding != "utf-8")? "#{coding}|utf-8": coding
|
72
|
-
CSV.read(filename, headers: true, encoding: coding)
|
73
|
-
rescue CSV::MalformedCSVError => e
|
74
|
-
if line = e.message.match(/line (?<line>\d+)/i)[:line]
|
75
|
-
return line.to_i
|
76
|
-
else
|
77
|
-
raise
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def has_bom?(path)
|
82
|
-
return false if !path || file_empty?(path)
|
83
|
-
File.open(path, "rb") do |f|
|
84
|
-
bytes = f.read(3)
|
85
|
-
return bytes.unpack("C*") == [239, 187, 191]
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def encoding(path)
|
90
|
-
has_bom?(path) ? "bom" : "utf-8"
|
91
|
-
end
|
92
|
-
|
93
|
-
def file_empty?(path)
|
94
|
-
return true if !File.file?(path)
|
95
|
-
File.zero?(path)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
@@ -1,221 +0,0 @@
|
|
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
|
-
blanks_x2 = has_double_blanks?(str)
|
29
|
-
partial = replace_not_allowed(str)
|
30
|
-
remove_double_blanks(partial).tap do |result|
|
31
|
-
next if invalid_warned?
|
32
|
-
if partial != str
|
33
|
-
invalid_chars = identify_invalid_characters(str)
|
34
|
-
puts "• (Row: #{self.row_num}) Invalid characters _#{invalid_chars}_ (removed): '#{str}' (converted to '#{result}')"
|
35
|
-
elsif blanks_x2
|
36
|
-
puts "• (Row: #{self.row_num}) Double blanks (removed): '#{str}' (converted to '#{result}')"
|
37
|
-
end
|
38
|
-
invalid_warned!
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def invalid_warned?
|
44
|
-
@invalid_warned ||= false
|
45
|
-
end
|
46
|
-
|
47
|
-
def invalid_warned!
|
48
|
-
@invalid_warned = true
|
49
|
-
end
|
50
|
-
|
51
|
-
def raw_tag
|
52
|
-
values_at(*TAGS_ATTRS.reverse).compact.first
|
53
|
-
end
|
54
|
-
|
55
|
-
def level
|
56
|
-
actual_level
|
57
|
-
end
|
58
|
-
|
59
|
-
def actual_level
|
60
|
-
tags_array.compact.length
|
61
|
-
end
|
62
|
-
|
63
|
-
def raw_level
|
64
|
-
tags_array.index(raw_tag) + 1
|
65
|
-
end
|
66
|
-
|
67
|
-
def tag_idx
|
68
|
-
tags_array.index(raw_tag)
|
69
|
-
end
|
70
|
-
|
71
|
-
def previous_idx
|
72
|
-
idx = tag_idx - 1
|
73
|
-
idx < 0 ? nil : idx
|
74
|
-
end
|
75
|
-
|
76
|
-
def empty_idx
|
77
|
-
tary = tags_array
|
78
|
-
tary.index(nil) || tary.length + 1
|
79
|
-
end
|
80
|
-
|
81
|
-
def copy
|
82
|
-
self.class.new.set_attrs(**self.to_h)
|
83
|
-
end
|
84
|
-
|
85
|
-
# We got a missing level that is compacted in one row
|
86
|
-
# Here we get the missing intermediate levels
|
87
|
-
# This is done from upper to lower level to ensure processing order
|
88
|
-
# It skips last one, as that is this object already
|
89
|
-
def decouple(num = 1)
|
90
|
-
with_info = filled_idxs
|
91
|
-
# must be the last among filled_idxs, so let's use it to verify
|
92
|
-
unless with_info.last == tag_idx
|
93
|
-
raise "Review this (row #{row_num}; '#{raw_tag}'): tag_idx is #{tag_idx}, while last filled idx is #{with_info.last}"
|
94
|
-
end
|
95
|
-
len = with_info.length
|
96
|
-
target_idxs = with_info[len-(num+1)..-2]
|
97
|
-
target_idxs.map do |idx|
|
98
|
-
self.copy.tap do |dup|
|
99
|
-
dup.clear_level(idx_to_level(idx + 1))
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def merge!(node)
|
105
|
-
override_upper_levels(node.tags_array)
|
106
|
-
end
|
107
|
-
|
108
|
-
def set_high_levels(node)
|
109
|
-
override_lower_levels(node.tags_array)
|
110
|
-
end
|
111
|
-
|
112
|
-
def clear_level(i)
|
113
|
-
case i
|
114
|
-
when Enumerable
|
115
|
-
target = i.to_a
|
116
|
-
when Integer
|
117
|
-
return false unless i >= 1 && i <= tag_attrs_count
|
118
|
-
target = Array(i..tag_attrs_count)
|
119
|
-
else
|
120
|
-
return false
|
121
|
-
end
|
122
|
-
return false if target.empty?
|
123
|
-
target.each do |n|
|
124
|
-
#puts "clearing 'l#{n}': #{attr("l#{n}")}"
|
125
|
-
set_attr("l#{n}", nil)
|
126
|
-
end
|
127
|
-
true
|
128
|
-
end
|
129
|
-
|
130
|
-
def override_upper_levels(src_tags_array, from_level: self.raw_level + 1)
|
131
|
-
target_lev = Array(from_level..tag_attrs_count)
|
132
|
-
target_tags = src_tags_array[level_to_idx(from_level)..level_to_idx(tag_attrs_count)]
|
133
|
-
target_lev.zip(target_tags).each do |(n, tag)|
|
134
|
-
set_attr("l#{n}", tag)
|
135
|
-
end
|
136
|
-
self
|
137
|
-
end
|
138
|
-
|
139
|
-
def override_lower_levels(src_tags_array, to_level: self.raw_level - 1)
|
140
|
-
target_lev = Array(1..to_level)
|
141
|
-
target_tags = src_tags_array[level_to_idx(1)..level_to_idx(to_level)]
|
142
|
-
target_lev.zip(target_tags).each do |(n, tag)|
|
143
|
-
set_attr("l#{n}", tag)
|
144
|
-
end
|
145
|
-
self
|
146
|
-
end
|
147
|
-
|
148
|
-
def idx_to_level(x)
|
149
|
-
x + 1
|
150
|
-
end
|
151
|
-
|
152
|
-
def level_to_idx(x)
|
153
|
-
x - 1
|
154
|
-
end
|
155
|
-
|
156
|
-
def filled_idxs
|
157
|
-
tags_array.each_with_index.with_object([]) do |(t, i), o|
|
158
|
-
o << i if t
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def blanks_between?
|
163
|
-
actual_level > empty_idx
|
164
|
-
end
|
165
|
-
|
166
|
-
def tags_array
|
167
|
-
values_at(*TAGS_ATTRS)
|
168
|
-
end
|
169
|
-
|
170
|
-
def values_at(*attrs)
|
171
|
-
attrs.map {|a| attr(a)}
|
172
|
-
end
|
173
|
-
|
174
|
-
def to_h(*attrs)
|
175
|
-
attrs = ALL_ATTRS if attrs.empty?
|
176
|
-
attrs.zip(values_at(*attrs)).to_h
|
177
|
-
end
|
178
|
-
|
179
|
-
def slice(*attrs)
|
180
|
-
return {} if attrs.empty?
|
181
|
-
to_h(*attrs)
|
182
|
-
end
|
183
|
-
|
184
|
-
def set_attrs(**kargs)
|
185
|
-
kargs.each {|attr, value| set_attr(attr, value)}
|
186
|
-
self
|
187
|
-
end
|
188
|
-
|
189
|
-
def set_attr(attr, value)
|
190
|
-
self.send("#{attr}=", value)
|
191
|
-
end
|
192
|
-
|
193
|
-
def attr(sym)
|
194
|
-
self.send(sym.to_sym)
|
195
|
-
end
|
196
|
-
|
197
|
-
def tag_attrs_count
|
198
|
-
TAGS_ATTRS.length
|
199
|
-
end
|
200
|
-
|
201
|
-
def has_double_blanks?(str)
|
202
|
-
return false if str.nil?
|
203
|
-
str.match(DOUBLE_BLANKS)
|
204
|
-
end
|
205
|
-
|
206
|
-
def remove_double_blanks(str)
|
207
|
-
return nil if str.nil?
|
208
|
-
str.gsub(DOUBLE_BLANKS, ' ').strip
|
209
|
-
end
|
210
|
-
|
211
|
-
def replace_not_allowed(str)
|
212
|
-
return nil if str.nil?
|
213
|
-
return str if str.match(VALID_TAG_REGEX)
|
214
|
-
str.gsub(INVALID_TAG_REGEX, ' ')
|
215
|
-
end
|
216
|
-
|
217
|
-
def identify_invalid_characters(str)
|
218
|
-
str.gsub(VALID_TAG_CHARS, '')
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
@@ -1,73 +0,0 @@
|
|
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.actual_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
|
@@ -1,33 +0,0 @@
|
|
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
|