eco-helpers 2.0.18 → 2.0.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -1
- data/lib/eco/api/common/people/entry_factory.rb +26 -9
- data/lib/eco/api/common/people/person_entry.rb +1 -0
- data/lib/eco/api/common/session.rb +1 -0
- data/lib/eco/api/common/session/base_session.rb +2 -0
- data/lib/eco/api/common/session/helpers.rb +30 -0
- data/lib/eco/api/common/session/helpers/prompt_user.rb +34 -0
- data/lib/eco/api/common/version_patches/ecoportal_api/external_person.rb +1 -1
- data/lib/eco/api/common/version_patches/ecoportal_api/internal_person.rb +7 -4
- data/lib/eco/api/microcases/with_each.rb +67 -6
- data/lib/eco/api/microcases/with_each_present.rb +4 -2
- data/lib/eco/api/microcases/with_each_starter.rb +4 -2
- data/lib/eco/api/organization.rb +1 -1
- data/lib/eco/api/organization/people.rb +92 -23
- data/lib/eco/api/organization/people_similarity.rb +112 -0
- data/lib/eco/api/organization/person_schemas.rb +5 -1
- data/lib/eco/api/organization/policy_groups.rb +5 -1
- data/lib/eco/api/session.rb +5 -2
- data/lib/eco/api/session/batch.rb +7 -5
- data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +12 -35
- data/lib/eco/api/usecases/default_cases/to_csv_case.rb +81 -36
- data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +3 -4
- data/lib/eco/api/usecases/ooze_samples/ooze_update_case.rb +3 -2
- data/lib/eco/cli/config/default/options.rb +2 -1
- data/lib/eco/cli/config/default/usecases.rb +2 -0
- data/lib/eco/cli/config/default/workflow.rb +4 -1
- data/lib/eco/csv.rb +4 -2
- data/lib/eco/data/fuzzy_match.rb +63 -21
- data/lib/eco/data/fuzzy_match/ngrams_score.rb +7 -2
- data/lib/eco/data/fuzzy_match/pairing.rb +0 -1
- data/lib/eco/data/fuzzy_match/result.rb +7 -1
- data/lib/eco/data/fuzzy_match/results.rb +12 -6
- data/lib/eco/version.rb +1 -1
- metadata +4 -2
- data/lib/eco/api/organization/people_analytics.rb +0 -60
@@ -0,0 +1,112 @@
|
|
1
|
+
module Eco
|
2
|
+
module API
|
3
|
+
module Organization
|
4
|
+
|
5
|
+
# Class to find out duplicates in the People Manager
|
6
|
+
#
|
7
|
+
# @attr_writer attribute [String, Proc, nil] the target attribute to be read.
|
8
|
+
class PeopleSimilarity < Eco::API::Organization::People
|
9
|
+
include Eco::Data::FuzzyMatch
|
10
|
+
|
11
|
+
attr_accessor :attribute
|
12
|
+
|
13
|
+
# @!group Config
|
14
|
+
# @return [String, Proc, nil] the target attribute to be read.
|
15
|
+
def attribute=(attr)
|
16
|
+
@attribute ||= "name"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Define the order or relevant of per user matches
|
20
|
+
# @param values[Array<Symbol>] the algorithms' results it should be ordered by
|
21
|
+
# * Possible values: `:dice`, `:levenshtein`, `:jaro_winkler`, `:ngrams`, `:words_ngrams`, `:chars_position`
|
22
|
+
def order=(values)
|
23
|
+
@order = values
|
24
|
+
end
|
25
|
+
|
26
|
+
def order
|
27
|
+
@order ||= [:words_ngrams, :dice]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Define the order or relevant of per user matches
|
31
|
+
# @param value [Float] the threshold that all of the algorithms should comply with
|
32
|
+
def threshold=(value)
|
33
|
+
@threshold = value
|
34
|
+
end
|
35
|
+
|
36
|
+
def threshold
|
37
|
+
@threshold ||= 0.15
|
38
|
+
end
|
39
|
+
|
40
|
+
# @!endgroup
|
41
|
+
|
42
|
+
# @!group Searchers
|
43
|
+
|
44
|
+
# It gathers those that have the same `email`
|
45
|
+
# @return [Hash] where `keys` are `email`s and `values` an `Array<Person>`
|
46
|
+
def repeated_emails
|
47
|
+
init_caches
|
48
|
+
@by_email.select do |email, people|
|
49
|
+
people.count > 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!endgroup
|
54
|
+
|
55
|
+
# @!group Analysers
|
56
|
+
|
57
|
+
# Analyses People bases on `options`
|
58
|
+
# @return [Hash] where the _keys_ are the people `id`s and the _values_ the `Eco::Data::FuzzyMatch::Results`
|
59
|
+
def analyse(**options)
|
60
|
+
options = { read: self.attribute }.merge(options)
|
61
|
+
each_with_object({}) do |person, results|
|
62
|
+
results[person.id] = find_all_with_score(person, **options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Launches a reanalyis on `analysed` based on `options`
|
67
|
+
# @param analysed [Hash] where the _keys_ are the people `id`s and the _values_ the `Eco::Data::FuzzyMatch::Results`
|
68
|
+
def re_analyse(analysed, **options)
|
69
|
+
analysed.each_with_object({}) do |(id, results), out|
|
70
|
+
out[id] = results.relevant_results(**options)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!group Helpers
|
75
|
+
|
76
|
+
# @return [String] well structured text
|
77
|
+
def analysis(analysed, format: :txt)
|
78
|
+
case
|
79
|
+
when format == :txt
|
80
|
+
analysed.each_with_object("") do |(id, results), out|
|
81
|
+
msg = results.results.map {|r| r.print}.join("\n ")
|
82
|
+
"'#{self[id].identify}':\n " + msg
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @note
|
88
|
+
# 1. Unless `:analysed` is provided, it launches an analysis cutting with Jaro Winker min 0.5
|
89
|
+
# 2. It then re-sorts and cuts based on `options`
|
90
|
+
# @return [Hash] where the _keys_ are the people `id`s and the _values_ the `Eco::Data::FuzzyMatch::Results`
|
91
|
+
def print_analysis(**options)
|
92
|
+
analysed = options[:analysed] || results_with_false_positives.analyse(**options)
|
93
|
+
analysed.each_with_object({}) do |(id, results), out|
|
94
|
+
puts analysis(analysed)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
# @!endgroup
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def on_change
|
102
|
+
remove_instance_variable(@fuzzy_match)
|
103
|
+
super
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -44,7 +44,11 @@ module Eco
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def policy_group(id_name)
|
47
|
-
|
47
|
+
self[id_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](id_name)
|
51
|
+
@by_id[policy_group_id(id_name)]
|
48
52
|
end
|
49
53
|
|
50
54
|
def user_pg_ids(initial: [], final: [], non_custom: (non_custom_not_used = true; []), preserve_custom: true)
|
data/lib/eco/api/session.rb
CHANGED
@@ -74,12 +74,15 @@ module Eco
|
|
74
74
|
def entry_factory(schema: nil)
|
75
75
|
schema = to_schema(schema) || self.schema
|
76
76
|
return @entry_factories[schema&.id] if @entry_factories.key?(schema&.id)
|
77
|
+
unless @entry_factories.empty?
|
78
|
+
@entry_factories[schema&.id] = @entry_factories.values.first.newFactory(schema: schema)
|
79
|
+
return @entry_factories[schema&.id]
|
80
|
+
end
|
77
81
|
|
78
82
|
mappings = []
|
79
83
|
if map_file = config.people.fields_mapper
|
80
84
|
mappings = map_file ? file_manager.load_json(map_file) : []
|
81
85
|
end
|
82
|
-
|
83
86
|
@entry_factories[schema&.id] = Eco::API::Common::People::EntryFactory.new(
|
84
87
|
enviro,
|
85
88
|
schema: schema,
|
@@ -127,7 +130,7 @@ module Eco
|
|
127
130
|
# @see Eco::API::Common::People::EntryFactory#new
|
128
131
|
# @return [Eco::API::Common::People::PersonEntry] parsed entry.
|
129
132
|
def new_entry(data, dependencies: {})
|
130
|
-
entry_factory.new(data, dependencies: dependencies)
|
133
|
+
entry_factory(schema: data&.details&.schema_id).new(data, dependencies: dependencies)
|
131
134
|
end
|
132
135
|
|
133
136
|
# @see Eco::API::Common::People::EntryFactory#entries
|
@@ -136,11 +136,13 @@ module Eco
|
|
136
136
|
block.call
|
137
137
|
rescue error_type => e
|
138
138
|
raise unless retries_left > 0
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
139
|
+
explanation = "Batch TimeOut. You have #{retries_left} retries left."
|
140
|
+
prompt_user("Do you want to retry (y/N)?", explanation, default: "Y", timeout: 10) do |response|
|
141
|
+
if response.upcase.start_with?("Y")
|
142
|
+
offer_retry_on(error_type, retries_left - 1, &block)
|
143
|
+
else
|
144
|
+
raise
|
145
|
+
end
|
144
146
|
end
|
145
147
|
end
|
146
148
|
end
|
@@ -7,14 +7,18 @@ class Eco::API::UseCases::DefaultCases::AnalysePeople < Eco::API::Common::Loader
|
|
7
7
|
def main(people, session, options, usecase)
|
8
8
|
@session = session; @options = options; @people = people
|
9
9
|
|
10
|
-
save!(
|
10
|
+
save!(analysis)
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
14
14
|
|
15
|
-
def
|
16
|
-
analytics.
|
17
|
-
|
15
|
+
def analysis
|
16
|
+
analytics.attribute = :name
|
17
|
+
analysed = analytics.analyse({
|
18
|
+
threshold: 0.20,
|
19
|
+
order: [:average, :dice]
|
20
|
+
})
|
21
|
+
analytics.re_analyse(analysed, threshold: 0.5, order: [:average])
|
18
22
|
end
|
19
23
|
|
20
24
|
def analytics
|
@@ -25,9 +29,9 @@ class Eco::API::UseCases::DefaultCases::AnalysePeople < Eco::API::Common::Loader
|
|
25
29
|
@file ||= options.dig(:output, :file) || "analytics.txt"
|
26
30
|
end
|
27
31
|
|
28
|
-
def save!(
|
29
|
-
if
|
30
|
-
session.logger.info("There were
|
32
|
+
def save!(analysed)
|
33
|
+
if analysed.empty?
|
34
|
+
session.logger.info("There were results identified!!")
|
31
35
|
return
|
32
36
|
end
|
33
37
|
|
@@ -35,42 +39,15 @@ class Eco::API::UseCases::DefaultCases::AnalysePeople < Eco::API::Common::Loader
|
|
35
39
|
|
36
40
|
File.open(file, "w") do |fd|
|
37
41
|
if ext == "txt"
|
38
|
-
|
42
|
+
fd << analytics.analysis(analysed, format: :txt)
|
39
43
|
elsif ext == "html"
|
40
44
|
puts "html is still not supported"
|
41
45
|
exit(1)
|
42
|
-
create_file(data, file: file, format: :html)
|
43
46
|
elsif ext == "json"
|
44
47
|
puts "json is still not supported"
|
45
48
|
exit(1)
|
46
|
-
create_file(data, file: file, format: :json)
|
47
49
|
end
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
def create_file(sets, file:, format: :txt)
|
52
|
-
File.open(file, "w") do |fd|
|
53
|
-
fd << sets_to_str(sets, format: format)
|
54
|
-
end
|
55
|
-
puts "Generated file #{file}"
|
56
|
-
end
|
57
|
-
|
58
|
-
def sets_to_str(sets, format: :txt)
|
59
|
-
raise "Required Array. Given: #{sets.class}" unless sets.is_a?(Array)
|
60
|
-
"".tap do |str|
|
61
|
-
sets.each do |set|
|
62
|
-
str << set_to_str(set, format: format)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def set_to_str(set, lev: 0, format: :txt)
|
68
|
-
raise "Required Array. Given: #{set.class}" unless set.is_a?(Array)
|
69
|
-
"".tap do |str|
|
70
|
-
entry = set.shift
|
71
|
-
str << "#{" " * lev}#{(lev > 0)? "+-#{lev}- " : ""}#{entry.name} (#{entry.external_id}|#{entry.email}|#{entry.id})\n"
|
72
|
-
str << set_to_str(set, lev: lev + 1, format: format) unless !set || set.empty?
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
53
|
end
|
@@ -13,28 +13,42 @@ class Eco::API::UseCases::DefaultCases::ToCsvCase < Eco::API::Common::Loaders::U
|
|
13
13
|
return false
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
people.each do |person|
|
22
|
-
csv << to_row(person)
|
16
|
+
if options.dig(:export, :options, :split_schemas)
|
17
|
+
by_schema.each do |id, people|
|
18
|
+
sch_name = schemas.to_name(id)
|
19
|
+
prefix = sch_name ? sch_name.gsub(" ", "_") : "No_Schema"
|
20
|
+
create_file!("#{prefix}_#{file}", people)
|
23
21
|
end
|
22
|
+
else
|
23
|
+
create_file!(file, people)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
+
def create_file!(filename = file, data = people)
|
30
|
+
session.logger.info("going to create file: #{filename}")
|
31
|
+
|
32
|
+
CSV.open(filename, "w") do |csv|
|
33
|
+
csv << spot_header(data.first)
|
34
|
+
data.each do |person|
|
35
|
+
csv << to_row(person)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
29
40
|
def to_row(person)
|
30
41
|
entry = to_entry_type(person)
|
31
|
-
entry.values_at(*keys(entry))
|
42
|
+
entry.values_at(*keys(entry)).tap do |row|
|
43
|
+
row << schemas.to_name(person.details&.schema_id) || "No Schema"
|
44
|
+
end
|
32
45
|
end
|
33
46
|
|
34
|
-
def spot_header
|
35
|
-
header = keys(to_entry_type(
|
47
|
+
def spot_header(person = people.first)
|
48
|
+
header = keys(to_entry_type(person))
|
49
|
+
header << "Schema"
|
36
50
|
header = yield(header) if block_given?
|
37
|
-
header = nice_header_names(header) if nice_header_names?
|
51
|
+
header = nice_header_names(header, schema: schema(person.details)) if nice_header_names?
|
38
52
|
header
|
39
53
|
end
|
40
54
|
|
@@ -46,10 +60,62 @@ class Eco::API::UseCases::DefaultCases::ToCsvCase < Eco::API::Common::Loaders::U
|
|
46
60
|
options.dig(:nice_header) || options.dig(:export, :options, :nice_header)
|
47
61
|
end
|
48
62
|
|
49
|
-
def nice_header_names(header)
|
50
|
-
|
63
|
+
def nice_header_names(header, schema: nil)
|
64
|
+
schema ||= session.schema
|
65
|
+
name_maps = schema.fields_by_alt_id.each_with_object({}) do |(alt_id, fld), mappings|
|
51
66
|
mappings[alt_id] = fld.name
|
52
|
-
end.merge(
|
67
|
+
end.merge(nice_header_maps)
|
68
|
+
header.map {|name| name_maps[name] ? name_maps[name] : name}
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_entry_type(person)
|
72
|
+
session.new_entry(person, dependencies: deps).yield_self do |person_entry|
|
73
|
+
options.dig(:export, :options, :internal_names) ? person_entry.mapped_entry : person_entry.external_entry
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def deps
|
78
|
+
@deps ||= {"supervisor_id" => {people: people}}
|
79
|
+
end
|
80
|
+
|
81
|
+
def file
|
82
|
+
@file ||= (options[:file] || options.dig(:export, :file, :name)).tap do |filename|
|
83
|
+
unless filename
|
84
|
+
session.logger.error("Destination file not specified")
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def by_schema
|
91
|
+
people.group_by do |person|
|
92
|
+
if details = person.details
|
93
|
+
details.schema_id
|
94
|
+
end
|
95
|
+
end.transform_values do |persons|
|
96
|
+
people.newFrom persons
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def schema(value)
|
101
|
+
case value
|
102
|
+
when Ecoportal::API::V1::Person
|
103
|
+
schema(value.details&.schema_id)
|
104
|
+
when String
|
105
|
+
schemas[value]
|
106
|
+
when Ecoportal::API::V1::PersonDetails
|
107
|
+
schema(value.schema_id)
|
108
|
+
when Ecoportal::API::V1::PersonSchema
|
109
|
+
value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def schemas
|
114
|
+
session.schemas
|
115
|
+
end
|
116
|
+
|
117
|
+
def nice_header_maps
|
118
|
+
@nice_header_maps ||= {
|
53
119
|
"policy_group_ids" => "User Group(s)",
|
54
120
|
"email" => "Email",
|
55
121
|
"name" => "Name",
|
@@ -96,28 +162,7 @@ class Eco::API::UseCases::DefaultCases::ToCsvCase < Eco::API::Common::Loaders::U
|
|
96
162
|
"custom_person_details" => "(min) on People Schema Details",
|
97
163
|
"custom_person_account" => "(min) on Users",
|
98
164
|
"custom_person_abilities" => "(min) on Users' Abilities"
|
99
|
-
}
|
100
|
-
header.map {|name| name_maps[name] ? name_maps[name] : name}
|
101
|
-
end
|
102
|
-
|
103
|
-
def to_entry_type(person)
|
104
|
-
session.new_entry(person, dependencies: deps).yield_self do |person_entry|
|
105
|
-
options.dig(:export, :options, :internal_names) ? person_entry.mapped_entry : person_entry.external_entry
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def deps
|
110
|
-
@deps ||= {"supervisor_id" => {people: people}}
|
165
|
+
}
|
111
166
|
end
|
112
167
|
|
113
|
-
def file
|
114
|
-
@file ||= (options[:file] || options.dig(:export, :file, :name)).tap do |filename|
|
115
|
-
unless filename
|
116
|
-
session.logger.error("Destination file not specified")
|
117
|
-
return false
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
|
123
168
|
end
|
@@ -5,8 +5,7 @@ class Eco::API::UseCases::DefaultCases::ToCsvDetailedCase < Eco::API::UseCases::
|
|
5
5
|
private
|
6
6
|
|
7
7
|
def to_row(person)
|
8
|
-
|
9
|
-
data = entry.values_at(*keys(entry))
|
8
|
+
data = super(person)
|
10
9
|
data << person.subordinates
|
11
10
|
data << person_supervisor(person)
|
12
11
|
data += user_abilities(person)
|
@@ -38,8 +37,8 @@ class Eco::API::UseCases::DefaultCases::ToCsvDetailedCase < Eco::API::UseCases::
|
|
38
37
|
preferences.map {|key| user_preferences[key] || false}
|
39
38
|
end
|
40
39
|
|
41
|
-
def spot_header
|
42
|
-
super do |header|
|
40
|
+
def spot_header(person = people.first)
|
41
|
+
super(person) do |header|
|
43
42
|
header << "Subordinates"
|
44
43
|
header << "Supervisor Name"
|
45
44
|
header += abilities_header
|
@@ -124,8 +124,9 @@ class Eco::API::UseCases::OozeSamples::OozeUpdateCase < Eco::API::Common::Loader
|
|
124
124
|
end
|
125
125
|
|
126
126
|
def prompt_to_confirm!
|
127
|
-
|
128
|
-
|
127
|
+
prompt_user("Do you want to proceed (y/N)?", default: "Y") do |response|
|
128
|
+
exit(1) unless response.upcase.start_with?("Y")
|
129
|
+
end
|
129
130
|
end
|
130
131
|
|
131
132
|
end
|