eco-helpers 2.0.12 → 2.0.17

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -73
  3. data/eco-helpers.gemspec +6 -4
  4. data/lib/eco-helpers.rb +1 -0
  5. data/lib/eco/api/common/base_loader.rb +14 -0
  6. data/lib/eco/api/common/loaders/use_case.rb +1 -1
  7. data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
  8. data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
  9. data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
  10. data/lib/eco/api/common/people/person_entry.rb +9 -2
  11. data/lib/eco/api/common/people/supervisor_helpers.rb +27 -0
  12. data/lib/eco/api/common/session/file_manager.rb +2 -2
  13. data/lib/eco/api/common/session/mailer.rb +0 -1
  14. data/lib/eco/api/common/session/s3_uploader.rb +0 -1
  15. data/lib/eco/api/common/session/sftp.rb +0 -1
  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 +42 -10
  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 +14 -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.rb +2 -0
  43. data/lib/eco/api/usecases/default_cases.rb +4 -1
  44. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +161 -0
  45. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +76 -0
  46. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
  47. data/lib/eco/api/usecases/default_cases/hris_case.rb +14 -8
  48. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
  49. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
  50. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +72 -0
  51. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +59 -0
  52. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +104 -26
  53. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +62 -36
  54. data/lib/eco/cli.rb +0 -10
  55. data/lib/eco/cli/config/default/options.rb +19 -17
  56. data/lib/eco/cli/config/default/people_filters.rb +3 -3
  57. data/lib/eco/cli/config/default/usecases.rb +77 -25
  58. data/lib/eco/cli/config/default/workflow.rb +6 -1
  59. data/lib/eco/cli/config/help.rb +1 -0
  60. data/lib/eco/cli/config/options_set.rb +106 -13
  61. data/lib/eco/cli/config/use_cases.rb +33 -33
  62. data/lib/eco/cli/scripting/args_helpers.rb +30 -3
  63. data/lib/eco/data.rb +1 -0
  64. data/lib/eco/data/crypto/encryption.rb +3 -3
  65. data/lib/eco/data/files/directory.rb +28 -20
  66. data/lib/eco/data/files/helpers.rb +6 -4
  67. data/lib/eco/data/fuzzy_match.rb +119 -0
  68. data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
  69. data/lib/eco/data/fuzzy_match/chars_position_score.rb +37 -0
  70. data/lib/eco/data/fuzzy_match/ngrams_score.rb +73 -0
  71. data/lib/eco/data/fuzzy_match/pairing.rb +102 -0
  72. data/lib/eco/data/fuzzy_match/result.rb +67 -0
  73. data/lib/eco/data/fuzzy_match/results.rb +53 -0
  74. data/lib/eco/data/fuzzy_match/score.rb +44 -0
  75. data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
  76. data/lib/eco/data/fuzzy_match/string_helpers.rb +69 -0
  77. data/lib/eco/version.rb +1 -1
  78. metadata +86 -10
  79. data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
  80. data/lib/eco/api/organization/presets_reference.json +0 -59
  81. data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
@@ -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
@@ -0,0 +1,58 @@
1
+ {
2
+ "person_core_create": [
3
+ { "value": "create", "conditions": {
4
+ "at_least": {"person_core": "view_people_manager"}
5
+ }
6
+ }
7
+ ],
8
+ "person_core_edit": [
9
+ { "value": "edit", "conditions": {
10
+ "at_least": {"person_core": "view_people_manager"}
11
+ }
12
+ }
13
+ ],
14
+ "person_details": [
15
+ { "value": "view", "conditions": {
16
+ "at_least": {"person_core": "attach"}
17
+ }
18
+ },
19
+ { "value": "edit_public", "conditions": {
20
+ "one_of": {
21
+ "person_core_edit": "edit",
22
+ "person_core_create": "create"
23
+ }
24
+ }
25
+ },
26
+ { "value": "view_private", "conditions": {
27
+ "at_least": {"person_core": "attach" }
28
+ }
29
+ },
30
+ { "value": "edit_private", "conditions": {
31
+ "one_of": {
32
+ "person_core_edit": "edit",
33
+ "person_core_create": "create"
34
+ }
35
+ }
36
+ }
37
+ ],
38
+ "person_account": [
39
+ { "value": "view", "conditions": {
40
+ "at_least": {"person_core": "attach" }
41
+ }
42
+ },
43
+ { "value": "create", "conditions": {
44
+ "at_least": {"person_core_create": "create"}
45
+ }
46
+ },
47
+ { "value": "edit", "conditions": {
48
+ "at_least": {"person_core_edit": "edit"}
49
+ }
50
+ }
51
+ ],
52
+ "person_abilities": [
53
+ { "value": "view", "conditions": {
54
+ "at_least": {"person_core_edit": "edit"}
55
+ }
56
+ }
57
+ ]
58
+ }
@@ -2,14 +2,15 @@
2
2
  "files": [null, "download", "upload", "browse", "administrate"],
3
3
  "data": [null, "view", "update", "administrate", "implement"],
4
4
  "reports": [null, "view", "edit", "administrate"],
5
+ "pages": [null, "view", "update", "create", "administrate"],
6
+ "page_editor": [null, "basic", "intermediate", "advanced", "implement"],
7
+ "registers": [null, "view", "dashboard", "administrate", "implement"],
5
8
  "tasks": [null, "reassign_self", "reassign", "administrate"],
6
9
  "organization": [null, "view", "administrate", "implement"],
7
10
  "person_core": [null, "attach", "view_people_manager", "dashboard"],
8
11
  "person_core_create": [null, "create"],
9
12
  "person_core_edit": [null, "edit"],
10
- "person_account": [null, "view", "create", "edit"],
11
13
  "person_details": [null, "view", "edit_public", "view_private", "edit_private"],
12
- "pages": [null, "view", "update", "create", "administrate"],
13
- "page_editor": [null, "basic", "intermediate", "advanced", "implement"],
14
- "registers": [null, "view", "dashboard", "administrate", "implement"]
14
+ "person_account": [null, "view", "create", "edit"],
15
+ "person_abilities": [null, "view", "edit"]
15
16
  }
@@ -12,7 +12,6 @@ class Eco::API::Policies::DefaultPolicies::UserAccess < Eco::API::Common::Loader
12
12
  people.each do |person|
13
13
  remove_account_when_no_email!(person) if person.email.to_s.empty?
14
14
  person.account.policy_group_ids = defid if no_policy_group_ids?(person)
15
- refresh_abilities!(person)
16
15
  end
17
16
 
18
17
  warn_account_removal!
@@ -40,39 +39,10 @@ class Eco::API::Policies::DefaultPolicies::UserAccess < Eco::API::Common::Loader
40
39
  return !!person.original_doc["account"]
41
40
  end
42
41
 
43
- def refresh_abilities!(person)
44
- return nil if options.dig(:exclude, :abilities)
45
- return nil unless account = person.account
46
- account.permissions_custom = session.new_preset(person)
47
- account.permissions_custom = min_abilities if no_abilities?(person)
48
- end
49
-
50
42
  def no_policy_group_ids?(person)
51
43
  (account = person.account) && account.policy_group_ids.empty?
52
44
  end
53
45
 
54
- def no_abilities?(person)
55
- return true unless account = person.account
56
- account.permissions_custom && account.permissions_custom.values.all?(&:nil?)
57
- end
58
-
59
- def min_abilities
60
- {
61
- "files" => "upload",
62
- "data" => nil,
63
- "reports" => nil,
64
- "pages" => "create",
65
- "page_editor" => "basic",
66
- "registers" => "view",
67
- "organization" => nil,
68
- "person_core" => "attach",
69
- "person_core_edit" => nil,
70
- "person_core_create" => nil,
71
- "person_details" => "view",
72
- "person_account" => nil
73
- }
74
- end
75
-
76
46
  def defid
77
47
  @defid ||= policy_groups.to_id([default_group]).compact
78
48
  end
@@ -61,28 +61,9 @@ module Eco
61
61
  self
62
62
  end
63
63
 
64
- # Builds the presets using the usergroup ids of the input.
65
- # @note for each flag/ability it will take the highest among those mapped for the present usergroups.
66
- # @param input [Ecoportal::API::Internal::Person, Array<String>] the array should be of usegroup names or ids.
67
- # @return [Hash] with custom presets.
68
- def new_preset(input)
69
- case input
70
- when Ecoportal::API::Internal::Person
71
- presets_factory.new(*input&.account&.policy_group_ids)
72
- when Array
73
- presets_factory.new(*input)
74
- else
75
- presets_factory.new(input)
76
- end
77
- end
78
-
79
64
  # Helper to state the abilities that a person should have with given their usergroups
80
65
  def presets_factory
81
- @presets_factory ||= Eco::API::Organization::PresetsFactory.new({
82
- presets_custom: file_manager.dir.file(config.people.presets_custom, should_exist: true),
83
- presets_map: file_manager.dir.file(config.people.presets_map, should_exist: true),
84
- enviro: enviro
85
- })
66
+ @presets_factory ||= Eco::API::Organization::PresetsFactory.new(enviro: enviro)
86
67
  end
87
68
 
88
69
  # Helper to obtain a EntryFactory
@@ -105,25 +105,57 @@ module Eco
105
105
  iterations = (data.length.to_f / per_page).ceil
106
106
 
107
107
  Eco::API::Session::Batch::Status.new(enviro, queue: data, method: method).tap do |status|
108
+ start_time = Time.now
109
+ start_slice = Time.now; slice = []
108
110
  data.each_slice(per_page) do |slice|
109
- msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, with #{slice.length} entries of #{data.length} -- #{done} done"
111
+ msg = "starting batch '#{method}' iteration #{iteration}/#{iterations},"
112
+ msg += " with #{slice.length} entries of #{data.length} -- #{done} done"
113
+ msg += " (last: #{str_stats(start_slice, slice.length)}; total: #{str_stats(start_time, done)})"
110
114
  logger.info(msg) unless silent
111
115
 
112
- people_api.batch do |batch|
113
- slice.each do |person|
114
- batch.public_send(method, person) do |response|
115
- faltal("Request with no response") unless !!response
116
- status[person] = response
116
+ start_slice = Time.now
117
+ offer_retry_on(Ecoportal::API::Errors::TimeOut) do
118
+ people_api.batch do |batch|
119
+ slice.each do |person|
120
+ batch.public_send(method, person) do |response|
121
+ faltal("Request with no response") unless !!response
122
+ status[person] = response
123
+ end
117
124
  end
118
- end
119
- end # next batch
125
+ end # end batch
126
+ end
120
127
 
121
- iteration += 1
122
- done += slice.length
128
+ iteration += 1
129
+ done += slice.length
123
130
  end # next slice
124
131
  end
125
132
  end
126
133
 
134
+ def offer_retry_on(error_type, retries_left = 3, &block)
135
+ begin
136
+ block.call
137
+ rescue error_type => e
138
+ raise unless retries_left > 0
139
+ print "Batch TimeOut. You have #{retries_left} retries left. Do you want to retry (y/N)? "
140
+ if (res = STDIN.gets.chomp) && res[0].downcase == "y"
141
+ offer_retry_on(error_type, retries_left - 1, &block)
142
+ else
143
+ raise
144
+ end
145
+ end
146
+ end
147
+
148
+ def str_stats(start, count)
149
+ now = Time.now
150
+ secs = (now - start).round(3)
151
+ if secs > 0.0
152
+ per_sec = (count.to_f / secs).round(2)
153
+ "#{secs}s -> #{per_sec} people/s"
154
+ else
155
+ " -- "
156
+ end
157
+ end
158
+
127
159
  end
128
160
  end
129
161
  end