eco-helpers 2.0.13 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -2
  3. data/eco-helpers.gemspec +6 -4
  4. data/lib/eco-helpers.rb +2 -0
  5. data/lib/eco/api/common/base_loader.rb +14 -0
  6. data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
  7. data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
  8. data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
  9. data/lib/eco/api/common/people/person_entry.rb +9 -2
  10. data/lib/eco/api/common/people/supervisor_helpers.rb +27 -0
  11. data/lib/eco/api/common/session/file_manager.rb +2 -2
  12. data/lib/eco/api/common/session/mailer.rb +0 -1
  13. data/lib/eco/api/common/session/s3_uploader.rb +0 -1
  14. data/lib/eco/api/common/session/sftp.rb +0 -1
  15. data/lib/eco/api/common/version_patches/exception.rb +8 -4
  16. data/lib/eco/api/error.rb +5 -3
  17. data/lib/eco/api/microcases.rb +3 -1
  18. data/lib/eco/api/microcases/append_usergroups.rb +0 -1
  19. data/lib/eco/api/microcases/people_cache.rb +2 -2
  20. data/lib/eco/api/microcases/people_load.rb +2 -2
  21. data/lib/eco/api/microcases/people_refresh.rb +2 -2
  22. data/lib/eco/api/microcases/people_search.rb +6 -6
  23. data/lib/eco/api/microcases/preserve_default_tag.rb +23 -0
  24. data/lib/eco/api/microcases/preserve_filter_tags.rb +28 -0
  25. data/lib/eco/api/microcases/preserve_policy_groups.rb +30 -0
  26. data/lib/eco/api/microcases/set_account.rb +0 -1
  27. data/lib/eco/api/organization.rb +1 -0
  28. data/lib/eco/api/organization/people.rb +7 -0
  29. data/lib/eco/api/organization/people_analytics.rb +60 -0
  30. data/lib/eco/api/organization/presets_factory.rb +116 -93
  31. data/lib/eco/api/organization/presets_integrity.json +58 -0
  32. data/lib/eco/api/organization/presets_values.json +5 -4
  33. data/lib/eco/api/policies/default_policies/99_user_access_policy.rb +0 -30
  34. data/lib/eco/api/session.rb +1 -20
  35. data/lib/eco/api/session/batch.rb +23 -7
  36. data/lib/eco/api/session/batch/job.rb +3 -0
  37. data/lib/eco/api/session/config.rb +16 -15
  38. data/lib/eco/api/session/config/api.rb +4 -0
  39. data/lib/eco/api/session/config/apis.rb +80 -0
  40. data/lib/eco/api/session/config/files.rb +7 -0
  41. data/lib/eco/api/session/config/people.rb +3 -19
  42. data/lib/eco/api/usecases/default_cases.rb +4 -1
  43. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +161 -0
  44. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +76 -0
  45. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
  46. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
  47. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
  48. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +72 -0
  49. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +59 -0
  50. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +104 -26
  51. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +62 -36
  52. data/lib/eco/cli.rb +0 -10
  53. data/lib/eco/cli/config/default/options.rb +19 -17
  54. data/lib/eco/cli/config/default/people_filters.rb +3 -3
  55. data/lib/eco/cli/config/default/usecases.rb +77 -25
  56. data/lib/eco/cli/config/default/workflow.rb +12 -3
  57. data/lib/eco/cli/config/help.rb +1 -0
  58. data/lib/eco/cli/config/options_set.rb +106 -13
  59. data/lib/eco/cli/config/use_cases.rb +33 -33
  60. data/lib/eco/cli/scripting/args_helpers.rb +30 -3
  61. data/lib/eco/data.rb +1 -0
  62. data/lib/eco/data/crypto/encryption.rb +3 -3
  63. data/lib/eco/data/files/directory.rb +28 -20
  64. data/lib/eco/data/files/helpers.rb +6 -4
  65. data/lib/eco/data/fuzzy_match.rb +119 -0
  66. data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
  67. data/lib/eco/data/fuzzy_match/chars_position_score.rb +37 -0
  68. data/lib/eco/data/fuzzy_match/ngrams_score.rb +73 -0
  69. data/lib/eco/data/fuzzy_match/pairing.rb +102 -0
  70. data/lib/eco/data/fuzzy_match/result.rb +67 -0
  71. data/lib/eco/data/fuzzy_match/results.rb +53 -0
  72. data/lib/eco/data/fuzzy_match/score.rb +44 -0
  73. data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
  74. data/lib/eco/data/fuzzy_match/string_helpers.rb +69 -0
  75. data/lib/eco/version.rb +1 -1
  76. metadata +86 -10
  77. data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
  78. data/lib/eco/api/organization/presets_reference.json +0 -59
  79. data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
@@ -21,9 +21,9 @@ module Eco
21
21
 
22
22
  start = Time.now
23
23
  entries = session.batch.get_people(people, silent: true)
24
- secs = Time.now - start
24
+ secs = (Time.now - start).round(3)
25
25
  cnt = entries.count
26
- per_sec = (cnt.to_f / secs).floor
26
+ per_sec = (cnt.to_f / secs).round(2)
27
27
  logger.info("Re-loaded #{cnt} people (out of #{people.length}) in #{secs} seconds (#{per_sec} people/sec)")
28
28
 
29
29
  missing = people.length - entries.length
@@ -14,10 +14,10 @@ module Eco
14
14
 
15
15
  start = Time.now
16
16
  people = session.batch.search(data, silent: silent).yield_self do |status|
17
- secs = Time.now - start
17
+ secs = (Time.now - start).round(3)
18
18
  Eco::API::Organization::People.new(status.people).tap do |people|
19
19
  cnt = people.count
20
- per_sec = (cnt.to_f / secs).floor
20
+ per_sec = (cnt.to_f / secs).round(2)
21
21
  msg = "... could get #{cnt} people (out of #{data.length} entries) in #{secs} seconds (#{per_sec} people/sec)"
22
22
  session.logger.info(msg)
23
23
  end
@@ -29,10 +29,10 @@ module Eco
29
29
  session.logger.info(" Going to api get #{supers.length} current supervisors...")
30
30
  start = Time.now
31
31
  people = session.batch.search(supers, silent: silent).yield_self do |status|
32
- secs = Time.now - start
32
+ secs = (Time.now - start).round(3)
33
33
  found = status.people
34
34
  cnt = found.count
35
- per_sec = (cnt.to_f / secs).floor
35
+ per_sec = (cnt.to_f / secs).round(2)
36
36
  msg = "... could find #{cnt} current supers (out of #{supers.length}) in #{secs} seconds (#{per_sec} people/sec)"
37
37
  session.logger.info(msg)
38
38
  people.merge(found, strict: micro.strict_search?(options))
@@ -46,10 +46,10 @@ module Eco
46
46
  start = Time.now
47
47
 
48
48
  people = session.batch.search(supers, silent: silent).yield_self do |status|
49
- secs = Time.now - start
49
+ secs = (Time.now - start).round(3)
50
50
  found = status.people
51
51
  cnt = found.count
52
- per_sec = (cnt.to_f / secs).floor
52
+ per_sec = (cnt.to_f / secs).round(2)
53
53
  msg = "... could find #{cnt} input supers (out of #{supers.length}) in #{secs} seconds (#{per_sec} people/sec)"
54
54
  session.logger.info(msg)
55
55
  people.merge(found, strict: micro.strict_search?(options))
@@ -0,0 +1,23 @@
1
+ module Eco
2
+ module API
3
+ class MicroCases
4
+ # Helper to preserve the original `default_tag`.
5
+ # @note
6
+ # 1. It only works if the original value of `default_tag` was **not** empty
7
+ # @param person [Ecoportal::API::V1::Person] the person we want to update, carrying the changes to be done.
8
+ # @param options [Hash] the options.
9
+ # @return [String] the final value of `default_tag`.
10
+ def preserve_default_tag(person, options)
11
+ if account = person.account
12
+ if account.as_update.key?("default_tag")
13
+ if original = person.original_doc.dig("account", "default_tag")
14
+ person.account.default_tag = original
15
+ end
16
+ end
17
+ end
18
+ person.account&.default_tag
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Eco
2
+ module API
3
+ class MicroCases
4
+ # Helper to preserve the original filter tags.
5
+ # @note
6
+ # 1. It only works if the original value of `filter_tags` was **not** empty
7
+ # @param person [Ecoportal::API::V1::Person] the person we want to update, carrying the changes to be done.
8
+ # @param options [Hash] the options.
9
+ # @param keep_new [Boolean] tells if it should keep the new tags or get rid of them.
10
+ # @return [Array<String>] the final value of `filter_tags`.
11
+ def preserve_filter_tags(person, options, keep_new: false)
12
+ if person.as_update.key?("filter_tags")
13
+ if original = person.original_doc["filter_tags"]
14
+ unless original.empty?
15
+ if keep_new
16
+ person.filter_tags += original
17
+ else
18
+ person.filter_tags = original
19
+ end
20
+ end
21
+ end
22
+ end
23
+ person.filter_tags
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Eco
2
+ module API
3
+ class MicroCases
4
+ # Helper to preserve the original `policy_group_ids`.
5
+ # @note
6
+ # 1. It only works if the original value of `policy_group_ids` was **not** empty
7
+ # @param person [Ecoportal::API::V1::Person] the person we want to update, carrying the changes to be done.
8
+ # @param options [Hash] the options.
9
+ # @param keep_new [Boolean] tells if it should keep the new policy groups or get rid of them.
10
+ # @return [String] the final value of `policy_group_ids`.
11
+ def preserve_policy_groups(person, options, keep_new: false)
12
+ if account = person.account
13
+ if account.as_update.key?("policy_group_ids")
14
+ if original = person.original_doc.dig("account", "policy_group_ids")
15
+ unless original.empty?
16
+ if keep_new
17
+ person.account.policy_group_ids += original
18
+ else
19
+ person.account.policy_group_ids = original
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ person.account&.policy_group_ids
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -11,7 +11,6 @@ module Eco
11
11
  person.account.send_invites = options[:send_invites] if options.key?(:send_invites)
12
12
  micro.refresh_default_tag(entry, person, options)
13
13
  micro.fix_default_group(entry, person, options)
14
- micro.refresh_abilities(person, options)
15
14
  end
16
15
  end
17
16
 
@@ -9,6 +9,7 @@ require_relative 'organization/tag_tree'
9
9
  require_relative 'organization/presets_factory'
10
10
  require_relative 'organization/preferences'
11
11
  require_relative 'organization/people'
12
+ require_relative 'organization/people_analytics'
12
13
  require_relative 'organization/person_schemas'
13
14
  require_relative 'organization/policy_groups'
14
15
  require_relative 'organization/login_providers'
@@ -176,6 +176,12 @@ module Eco
176
176
  end
177
177
  # @!endgroup
178
178
 
179
+ # @!group Helper methods
180
+ def analytics
181
+ Eco::API::Organization::PeopleAnalytics.new(self.to_a)
182
+ end
183
+ # @!endgroup
184
+
179
185
  protected
180
186
 
181
187
  def on_change
@@ -190,6 +196,7 @@ module Eco
190
196
  @by_external_id = to_h('external_id')
191
197
  @by_users_email = users.to_h('email')
192
198
  @by_non_users_email = non_users.to_h('email')
199
+ @by_email = to_h('email')
193
200
  @caches_init = true
194
201
  end
195
202
 
@@ -0,0 +1,60 @@
1
+ module Eco
2
+ module API
3
+ module Organization
4
+ class PeopleAnalytics < Eco::API::Organization::People
5
+ include Eco::Data::FuzzyMatch
6
+
7
+ # @!group Helpers
8
+
9
+ # @!endgroup
10
+
11
+ # @!group Searchers
12
+
13
+ # It gathers those that have the same `email`
14
+ # @return [Hash] where `keys` are `email`s and `values` an `Array<Person>`
15
+ def repeated_emails
16
+ init_caches
17
+ @by_email.select do |email, people|
18
+ people.count > 1
19
+ end
20
+ end
21
+
22
+ # @!endgroup
23
+
24
+ # @!group Analysers
25
+
26
+ # TODO: Sort results by `results.first.methods`
27
+ def similarity(**options)
28
+ each_with_object({}) do |person, results|
29
+ results[person.id] = find_all_with_score(person, **options)
30
+ end
31
+ end
32
+
33
+
34
+ def print_analysis(threshold)
35
+ similarity.each do |id, results|
36
+ msg = results.results.select do |result|
37
+ result.threshold?(threshold)
38
+ end.map do |result|
39
+ result.print
40
+ end.join("\n ")
41
+
42
+ puts "'#{self[id].identify}':\n " + msg
43
+ end
44
+ end
45
+ # @!endgroup
46
+
47
+ protected
48
+
49
+ def on_change
50
+ remove_instance_variable(@fuzzy_match)
51
+ super
52
+ end
53
+
54
+ private
55
+
56
+
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,112 +4,58 @@ module Eco
4
4
 
5
5
  class PresetsFactory
6
6
  ABILITIES = File.join(__dir__, 'presets_values.json')
7
- DEFAULT_CUSTOM = 'presets_custom.json'
8
- DEFAULT_MAP = 'presets_map.json'
7
+ INTEGRITY = File.join(__dir__, 'presets_integrity.json')
9
8
 
10
- def initialize(presets_custom: DEFAULT_CUSTOM, presets_map: DEFAULT_MAP, enviro: nil, policy_groups: nil)
11
- @abilities = JSON.load(File.open(ABILITIES))
12
- @habilities = @abilities.map do |key, values|
13
- h_values = values.map { |v| [v, true] }.to_h
14
- [key, h_values]
15
- end.to_h
9
+ class << self
16
10
 
17
- fatal("Expecting Environment object. Given: #{enviro}") if enviro && !enviro.is_a?(Eco::API::Common::Session::Environment)
18
- @enviro = enviro
11
+ def all_abilities(hash = {})
12
+ Hash[abilities.each_with_object(nil).to_a].merge(hash)
13
+ end
19
14
 
20
- policy_groups = policy_groups || @enviro&.api&.policy_groups.to_a
21
- if policy_groups.is_a?(Eco::API::Organization::PolicyGroups)
22
- @policy_groups = policy_groups
23
- else
24
- @policy_groups = Eco::API::Organization::PolicyGroups.new(policy_groups)
15
+ def abilities_model
16
+ @abilities_model ||= JSON.load(File.open(ABILITIES))
25
17
  end
26
18
 
27
- init_custom(presets_custom)
28
- init_map(presets_map)
29
- end
19
+ def integrity_model
20
+ @integrity_model ||= JSON.load(File.open(INTEGRITY))
21
+ end
30
22
 
31
- def new(*policy_group_ids_or_names)
23
+ def abilities
24
+ @abilities ||= abilities_model.keys
25
+ end
32
26
 
33
- names = policy_group_ids_or_names.map do |id_name|
34
- @policy_groups.to_name(id_name)&.downcase
35
- end.compact
27
+ end
36
28
 
37
- if @presets_map
38
- preset_names = names.map { |name| @presets_map.fetch(name, nil) }
39
- else # option to do not use preset mapping (so just the policy group name)
40
- preset_names = names
41
- end
42
- compile(*preset_names)
29
+ def initialize(enviro: nil, policy_groups: nil)
30
+ fatal("Expecting Environment object. Given: #{enviro}") if enviro && !enviro.is_a?(Eco::API::Common::Session::Environment)
31
+ @enviro = enviro
32
+ @policy_groups = policy_groups
43
33
  end
44
34
 
45
35
  # @return [Array<String>] all the abilities
46
36
  def keys
47
- @abilities.keys
37
+ self.class.abilities
48
38
  end
49
39
 
50
- private
51
-
52
- def init_custom(file = DEFAULT_CUSTOM)
53
- @presets_custom = nil
54
-
55
- return if !file
56
- file = File.expand_path(file)
57
-
58
- if File.exists?(file)
59
- @presets_custom = JSON.load(File.open(file))
60
-
61
- errors = @presets_custom.map do |key, preset|
62
- (err = preset_errors(preset)) ? "{ '#{key}' preset -> #{err}}": nil
63
- end.compact
64
-
65
- fatal("File '#{file}' contains invalid presets:\n #{errors.join("\n ")}") if errors.length > 0
66
- end
67
-
40
+ def valid?(preset)
41
+ validate(perset).length == 0
68
42
  end
69
43
 
70
- def init_map(file = DEFAULT_MAP)
71
- @presets_map = nil
72
-
73
- return if !file
74
- file = File.expand_path(file)
75
-
76
- if File.exists?(file)
77
- fatal("Maps file specified without custom presets file. Aborting!") if !@presets_custom
78
- @presets_map = JSON.load(File.open(file))
79
-
80
- errors = []
81
- if @policy_groups.length > 0
82
- errors = @policy_groups.map do |pg|
83
- exists = @presets_map[pg.name.downcase] || @presets_custom[pg.name.downcase]
84
- exists ? nil : "'#{pg.name}'"
85
- end.compact
86
-
87
- warn("No maps or no preset for policy group(s): #{errors.join(", ")}") if errors.length > 0
44
+ def validate(preset)
45
+ [].tap do |errors|
46
+ if err = preset_errors(preset)
47
+ errors << "{ '#{key}' preset -> #{err}}"
48
+ end
49
+ if err = preset_integrity(preset)
50
+ errors << "{ '#{key}' preset -> #{err}}"
88
51
  end
89
-
90
- errors = @presets_map.map do |source, dest|
91
- @presets_custom[dest] ? nil : "'#{dest}'"
92
- end.compact
93
-
94
- warn("Unexisting mapped preset(s): #{errors.uniq.join(", ")}") if errors.length > 0
95
52
  end
96
-
97
- end
98
-
99
- def fatal(msg)
100
- raise msg if !@enviro
101
- @enviro.logger.fatal(msg)
102
- raise msg
103
53
  end
104
54
 
105
- def warn(msg)
106
- raise msg if !@enviro
107
- @enviro.logger.warn(msg)
108
- end
55
+ private
109
56
 
110
- def compile(*preset_names)
111
- fatal("You need to specify an existing file for the custom presets.") if !@presets_custom
112
- @presets_custom.values_at(*preset_names).compact.reduce({}) do |p1, p2|
57
+ def compile(*presets)
58
+ presets.compact.reduce({}) do |p1, p2|
113
59
  merge(p1, p2)
114
60
  end
115
61
  end
@@ -117,7 +63,7 @@ module Eco
117
63
  def merge(preset1, preset2)
118
64
  keys = preset1.keys | preset2.keys
119
65
 
120
- @abilities.each_with_object({}) do |(key, values), result|
66
+ abilities_model.each_with_object({}) do |(key, values), result|
121
67
  next unless keys.include?(key)
122
68
  idx = [
123
69
  values.index(preset1[key]),
@@ -127,19 +73,96 @@ module Eco
127
73
  end
128
74
  end
129
75
 
130
- # unsused: only play with the given abilities
131
- def empty_model
132
- JSON.parse(@abilities.to_json).transform_values {|v| nil }
133
- end
134
-
135
76
  def preset_errors(preset)
136
77
  return "No preset given" if !preset
137
78
  errors = preset.map do |k, v|
138
- @habilities.dig(k, v) ? nil : "#{k}:#{v}"
79
+ value_exists?(k, v) ? nil : "#{k}:#{v}"
139
80
  end.compact
140
- return " unknown: {#{errors.join(", ")}}" if errors.length > 0
81
+ return " Unknown: {#{errors.join(", ")}}" if errors.length > 0
141
82
  nil
142
83
  end
84
+
85
+ def preset_integrity(preset)
86
+ preset.each_with_object([]) do |(ability, value), errors|
87
+ next unless checks = integrity_model[ability]
88
+
89
+ suberrors = []
90
+
91
+ checks.each do |check|
92
+ next unless check["value"] == value
93
+ check["conditions"].each do |cond, targets|
94
+ case cond
95
+ when "at_least"
96
+ targets.each do |other, minimum|
97
+ unless (ability_value_idx(other, minimum) <= ability_value_idx(other, preset[other]))
98
+ suberrors << "'#{other}' should be at least '#{minimum}'"
99
+ end
100
+ end
101
+ when "one_of"
102
+ unless targets.any? {|other, expected| preset[other] == expected}
103
+ suberrors << targets.each_with_object([]) do |(other, expected), out|
104
+ out << "'#{other}': '#{expected}'"
105
+ end.join(", ").yield_self do |msg|
106
+ "there should be at least one of: {#{msg}}"
107
+ end
108
+ end
109
+ else
110
+ warn("Unsuported integrity condition statement '#{cond}' in '#{ability}' with level '#{value}'")
111
+ end
112
+ end
113
+ end
114
+
115
+ if suberrors.length > 0
116
+ errors << "Incorrect value '#{value}' for '#{ability}' - reasons: {#{suberrors.join(", ")}}"
117
+ end
118
+ end.yield_self do |errors|
119
+ " Integrity errors: { #{errors.join(", ")} }" if errors.length > 0
120
+ end
121
+ end
122
+
123
+ def integrity_model
124
+ self.class.integrity_model
125
+ end
126
+
127
+ def value_exists?(ability, value)
128
+ abilities_model_inverted.dig(ability, value)
129
+ end
130
+
131
+ def abilities_model_inverted
132
+ @abilities_model_inverted ||= abilities_model.each_with_object({}) do |(key, values), out|
133
+ out[key] = values.each_with_object({}) {|v, h| h[v] = true }
134
+ end
135
+ end
136
+
137
+ def ability_value_idx(ability, value)
138
+ abilities_model[ability].index(value) || -1
139
+ end
140
+
141
+ def abilities_model
142
+ self.class.abilities_model
143
+ end
144
+
145
+ def policy_groups
146
+ return @policy_groups if @policy_groups.is_a?(Eco::API::Organization::PolicyGroups)
147
+ @policy_groups ||= @enviro&.api&.policy_groups.to_a
148
+
149
+ unless @policy_groups.is_a?(Eco::API::Organization::PolicyGroups)
150
+ @policy_groups = Eco::API::Organization::PolicyGroups.new(@policy_groups)
151
+ end
152
+ @policy_groups
153
+ end
154
+
155
+ def fatal(msg)
156
+ raise msg if !@enviro
157
+ @enviro.logger.fatal(msg)
158
+ raise msg
159
+ end
160
+
161
+ def warn(msg)
162
+ raise msg if !@enviro
163
+ @enviro.logger.warn(msg)
164
+ end
165
+
143
166
  end
144
167
 
145
168
  end