eco-helpers 2.0.21 → 2.0.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -4
  3. data/eco-helpers.gemspec +0 -1
  4. data/lib/eco/api/common.rb +0 -1
  5. data/lib/eco/api/common/loaders.rb +2 -0
  6. data/lib/eco/api/common/loaders/base.rb +58 -0
  7. data/lib/eco/api/common/loaders/case_base.rb +33 -0
  8. data/lib/eco/api/common/loaders/error_handler.rb +2 -2
  9. data/lib/eco/api/common/loaders/parser.rb +30 -5
  10. data/lib/eco/api/common/loaders/policy.rb +1 -1
  11. data/lib/eco/api/common/loaders/use_case.rb +1 -1
  12. data/lib/eco/api/common/people/default_parsers.rb +1 -0
  13. data/lib/eco/api/common/people/default_parsers/csv_parser.rb +93 -1
  14. data/lib/eco/api/common/people/default_parsers/xls_parser.rb +53 -0
  15. data/lib/eco/api/common/people/entries.rb +83 -14
  16. data/lib/eco/api/common/people/entry_factory.rb +36 -21
  17. data/lib/eco/api/common/people/person_attribute_parser.rb +8 -0
  18. data/lib/eco/api/common/people/person_factory.rb +4 -2
  19. data/lib/eco/api/common/people/person_parser.rb +8 -2
  20. data/lib/eco/api/common/people/supervisor_helpers.rb +1 -1
  21. data/lib/eco/api/common/version_patches/ecoportal_api/external_person.rb +0 -8
  22. data/lib/eco/api/common/version_patches/ecoportal_api/internal_person.rb +0 -8
  23. data/lib/eco/api/microcases/set_core_with_supervisor.rb +4 -2
  24. data/lib/eco/api/microcases/set_supervisor.rb +29 -8
  25. data/lib/eco/api/microcases/with_each.rb +7 -3
  26. data/lib/eco/api/microcases/with_each_starter.rb +3 -2
  27. data/lib/eco/api/organization/people.rb +7 -1
  28. data/lib/eco/api/session.rb +18 -7
  29. data/lib/eco/api/session/batch.rb +1 -1
  30. data/lib/eco/api/session/batch/job.rb +42 -9
  31. data/lib/eco/api/usecases.rb +2 -2
  32. data/lib/eco/api/usecases/base_case.rb +2 -2
  33. data/lib/eco/api/usecases/base_io.rb +17 -4
  34. data/lib/eco/api/usecases/default_cases/create_case.rb +10 -1
  35. data/lib/eco/api/usecases/default_cases/create_details_case.rb +10 -1
  36. data/lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb +10 -1
  37. data/lib/eco/api/usecases/default_cases/hris_case.rb +25 -1
  38. data/lib/eco/api/usecases/default_cases/upsert_case.rb +10 -1
  39. data/lib/eco/cli/config/default/input.rb +63 -10
  40. data/lib/eco/cli/config/default/options.rb +40 -8
  41. data/lib/eco/cli/config/default/usecases.rb +16 -0
  42. data/lib/eco/cli/config/default/workflow.rb +7 -4
  43. data/lib/eco/cli/config/filters.rb +6 -2
  44. data/lib/eco/cli/config/filters/input_filters.rb +3 -2
  45. data/lib/eco/cli/config/filters/people_filters.rb +3 -2
  46. data/lib/eco/cli/config/help.rb +1 -1
  47. data/lib/eco/cli/config/options_set.rb +6 -4
  48. data/lib/eco/cli/config/use_cases.rb +6 -3
  49. data/lib/eco/cli/scripting/args_helpers.rb +2 -2
  50. data/lib/eco/csv.rb +2 -0
  51. data/lib/eco/language/models/collection.rb +5 -2
  52. data/lib/eco/version.rb +1 -1
  53. metadata +4 -22
  54. data/lib/eco/api/common/base_loader.rb +0 -68
@@ -0,0 +1,53 @@
1
+ class Eco::API::Common::People::DefaultParsers::XLSParser < Eco::API::Common::Loaders::Parser
2
+ attribute :xls
3
+
4
+ attr_accessor :already_required
5
+ attr_reader :file
6
+
7
+ def parser(file, deps)
8
+ @file = file
9
+ rows.tap {|r| @file = nil}
10
+ end
11
+
12
+ def serializer(array_hash, deps)
13
+ raise "Not implemented. TODO: using axlsx or rubyXL gems. See: https://spin.atomicobject.com/2017/03/22/parsing-excel-files-ruby/"
14
+ end
15
+
16
+ private
17
+
18
+ def headers
19
+ raise "You should implement this method"
20
+ end
21
+
22
+ def sheet_name
23
+ 0
24
+ end
25
+
26
+ def workbook
27
+ require_reading_libs!
28
+ Roo::Spreadsheet.open(file)
29
+ end
30
+
31
+ def spreadheet(name_or_index = sheet_name)
32
+ workbook.sheet(name_or_index)
33
+ end
34
+
35
+ def rows(target = headers)
36
+ begin
37
+ spreadheet.parse(header_search: target)
38
+ rescue Roo::HeaderRowNotFoundError => e
39
+ missing = JSON.parse(e.message)
40
+ logger.warn("The input file is missing these headers: #{missing}")
41
+ present = target - missing
42
+ rows(present)
43
+ end
44
+ end
45
+
46
+ def require_reading_libs!
47
+ return if already_required
48
+ require 'roo'
49
+ require 'roo-xls'
50
+ already_required = true
51
+ end
52
+
53
+ end
@@ -5,6 +5,42 @@ module Eco
5
5
  # Class meant to offer a _collection_ of entries, normally used to get parsed input data.
6
6
  # @attr_reader entries [Array<Eco::API::Common::PeopleEntry] a pure `Array` object.
7
7
  class Entries < Eco::Language::Models::Collection
8
+ # Error class that allows to handle cases where multiple entries were found for the same criterion.
9
+ # @note its main purpose to prevent the false pairing of duplicates or override information between different people.
10
+ class MultipleSearchResults < StandardError
11
+ attr_reader :candidates, :property
12
+ # @param msg [String] the basic message error.
13
+ # @param candiates [Array<PersonEntry>] the entries that match the same search criterion.
14
+ # @param property [String] the property of the entry model that triggered the error (base of the search criterion).
15
+ def initialize(msg, candidates: [], property: "email")
16
+ @candidates = candidates
17
+ @property = property
18
+ super(msg + " " + candidates_summary)
19
+ end
20
+
21
+ # @param with_index [Boolean] to add an index to each candidate description.
22
+ # @return [Array<String>] the `candidates` identified
23
+ def identify_candidates(with_index: false)
24
+ candidates.map.each_with_index do |entry, i|
25
+ index = with_index ? "#{i}. " : ""
26
+ "#{index} #{entry.identify}"
27
+ end
28
+ end
29
+
30
+ # @return [Person] the `candidate` in the `index` position
31
+ def candidate(index)
32
+ candidates[index]
33
+ end
34
+
35
+ private
36
+
37
+ def candidates_summary
38
+ lines = ["The following entries have the same '#{property}':"]
39
+ lines.concat(identify_candidates(with_index: true)).join("\n ")
40
+ end
41
+
42
+ end
43
+
8
44
  # build the shortcuts of Collection
9
45
  attr_collection :id, :external_id, :email, :name, :supervisor_id
10
46
 
@@ -54,19 +90,34 @@ module Eco
54
90
  # @!group Searchers
55
91
 
56
92
  # Search function to find an `entry` based on one of different options
93
+ # It searches an entry using the parameters given.
94
+ # @note This is how the search function actually works:
95
+ # 1. if eP `id` is given, returns the entry (if found), otherwise...
96
+ # 2. if `external_id` is given, returns the entry (if found), otherwise...
97
+ # 3. if `strict` is `false` and `email` is given:
98
+ # - if there is only 1 entry with that email, returns that entry, otherwise...
99
+ # - if found but, there are many candidate entries, it raises MultipleSearchResults error
100
+ # - if entry `external_id` matches `email`, returns that entry
101
+ # @raise MultipleSearchResults if there are multiple entries with the same `email`
102
+ # and there's no other criteria to find the entry. It only gets to this point if
103
+ # `external_id` was **not** provided and we are **not** in 'strict' search mode.
104
+ # However, it could be we were in `strict` mode and `external_id` was not provided.
105
+ # @param id [String] the `internal id` of the person
106
+ # @param external_id [String] the `exernal_id` of the person
107
+ # @param email [String] the `email` of the person
108
+ # @param strict [Boolean] if should perform a `:soft` or a `:strict` search. `strict` will avoid repeated email addresses.
109
+ # @return [Entry, nil] the entry we were searching, or `nil` if not found.
57
110
  def entry(id: nil, external_id: nil, email: nil, strict: false)
58
111
  init_caches
59
- pers = nil
60
- pers = @by_id[id]&.first if id
61
- pers = @by_external_id[external_id&.strip]&.first if !pers && !external_id.to_s.strip.empty?
62
-
63
- # strict prevents taking existing user for searched person with same email
64
- # specially useful if the organisation ensures all have external id (no need for email search)
65
- if !pers && (!strict || external_id.to_s.strip.empty?)
66
- pers = @by_email[email&.downcase.strip]&.first if !pers && !email.to_s.strip.empty?
67
- pers = @by_external_id[email&.downcase.strip]&.first if !pers && !email.to_s.strip.empty?
68
- end
69
- pers
112
+ # normalize values
113
+ ext_id = !external_id.to_s.strip.empty? && external_id.strip
114
+ email = !email.to_s.strip.empty? && email.downcase.strip
115
+
116
+ e = nil
117
+ e ||= @by_id[id]&.first
118
+ e ||= @by_external_id[ext_id]&.first
119
+ e ||= entry_by_email(email) unless strict && ext_id
120
+ e
70
121
  end
71
122
 
72
123
  # Search function to find an `entry` based on one of different options
@@ -136,15 +187,33 @@ module Eco
136
187
 
137
188
  private
138
189
 
190
+ def entry_by_email(email, prevent_multiple_match: false)
191
+ return nil unless email
192
+
193
+ candidates = @by_email[email] || []
194
+ return candidates.first if candidates.length == 1
195
+
196
+ if prevent_multiple_match && !candidates.empty?
197
+ msg = "Multiple search results match the criteria."
198
+ raise MultipleSearchResults.new(msg, candidates: candidates, property: "email")
199
+ end
200
+
201
+ @by_external_id[email]&.first
202
+ end
203
+
139
204
  def init_caches
140
205
  return if @caches_init
141
- @by_id = to_h
142
- @by_external_id = to_h('external_id')
143
- @by_email = to_h('email')
206
+ @by_id = no_nil_key(to_h)
207
+ @by_external_id = no_nil_key(to_h('external_id'))
208
+ @by_email = no_nil_key(to_h('email'))
144
209
  @array_supers = sort_by_supervisors(@items)
145
210
  @caches_init = true
146
211
  end
147
212
 
213
+ def no_nil_key(hash)
214
+ hash.tap {|h| h.delete(nil)}
215
+ end
216
+
148
217
  end
149
218
  end
150
219
  end
@@ -80,54 +80,69 @@ module Eco
80
80
  # @param data [Array<Hash>] data to be parsed. It cannot be used alongside with `file:`
81
81
  # @param file [String] absolute or relative path to the input file. It cannot be used alongside with `data:`.
82
82
  # @param format [Symbol] it must be used when you use the option `file:` (i.e. `:xml`, `:csv`), as it specifies the format of the input `file:`.
83
- # @param encoding [String] optional parameter to read `file:` by expecting certain encoding.
83
+ # @param options [Hash] further options.
84
+ # @option options [String] :encoding optional parameter to read `file:` by expecting certain encoding.
85
+ # @option options [Boolean] :check_headers signals if the `csv` file headers should be expected.
84
86
  # @return [Eco::API::Common::People::Entries] collection of `Eco::API::Common::People::PersonEntry`.
85
- def entries(data: (no_data = true; nil), file: (no_file = true; nil), format: (no_format = true; nil), encoding: nil)
87
+ def entries(data: (no_data = true; nil), file: (no_file = true; nil), format: (no_format = true; nil), **options)
86
88
  fatal("You should at least use data: or file:, but not both") if no_data == no_file
87
89
  fatal("You must specify a valid format: (symbol) when you use file.") if file && no_format
88
90
  fatal("Format should be a Symbol. Given '#{format}'") if format && !format.is_a?(Symbol)
89
91
  fatal("There is no parser/serializer for format ':#{format.to_s}'") unless no_format || @person_parser.defined?(format)
90
92
 
91
- kargs = {}
92
- kargs.merge!(content: data) unless no_data
93
- kargs.merge!(file: file) unless no_file
94
- kargs.merge!(format: format) unless no_format
95
- kargs.merge!(encoding: encoding) if encoding
93
+ options.merge!(content: data) unless no_data
94
+ options.merge!(file: file) unless no_file
95
+ options.merge!(format: format) unless no_format
96
96
 
97
- Entries.new(to_array_of_hashes(**kargs), klass: PersonEntry, factory: self)
97
+ Entries.new(to_array_of_hashes(**options), klass: PersonEntry, factory: self)
98
98
  end
99
99
 
100
100
  def to_array_of_hashes(**kargs)
101
101
  data = []
102
102
  content, file, encoding, format = kargs.values_at(:content, :file, :encoding, :format)
103
103
 
104
- content = get_file_content(file, format, encoding) if file
104
+ # Support for multiple file
105
+ if file.is_a?(Array)
106
+ return file.each_with_object([]) do |f, out|
107
+ logger.info("Parsing file '#{f}'")
108
+ curr = to_array_of_hashes(**kargs.merge(file: f))
109
+ out.concat(curr)
110
+ end
111
+ end
112
+ # Get content only when it's not :xls
113
+ # note: even if content was provided, file takes precedence
114
+ content = get_file_content(file, format, encoding) if (format != :xls) && file
105
115
 
106
116
  case content
107
- when !content
108
- logger.error("Could not obtain any data out of these: #{kargs}")
109
- exit(1)
110
117
  when Hash
111
118
  logger.error("Input data as 'Hash' not supported. Expecting 'Enumerable' or 'String'")
112
119
  exit(1)
113
120
  when String
114
- data = person_parser.parse(format, content).map.each_with_index do |entry_hash, i|
115
- j = (format == :csv)? i + 2 : i + 1
116
- entry_hash.tap {|hash| hash["idx"] = j}
117
- end
118
- to_array_of_hashes(content: data)
121
+ deps = {check_headers: true} if kargs[:check_headers]
122
+ to_array_of_hashes(content: person_parser.parse(format, content, deps: deps || {}))
119
123
  when Enumerable
120
124
  sample = content.to_a.first
121
125
  case sample
122
126
  when Hash, Array, ::CSV::Row
123
127
  Eco::CSV::Table.new(content).to_array_of_hashes
124
128
  else
125
- logger.error("Input 'Array' of '#{sample.class}' is not supported.")
129
+ logger.error("Input content 'Array' of '#{sample.class}' is not supported.")
126
130
  end
127
131
  else
128
- logger.error("Could not obtain any data out of content: '#{content.class}'")
129
- exit(1)
132
+ if file && format == :xls
133
+ person_parser.parse(format, file)
134
+ else
135
+ logger.error("Could not obtain any data out of these: #{kargs}. Given content: '#{content.class}'")
136
+ exit(1)
137
+ end
138
+ end.tap do |out_array|
139
+ start_from_two = (format == :csv) || format == :xls
140
+ out_array.each_with_index do |entry_hash, i|
141
+ entry_hash["idx"] = start_from_two ? i + 2 : i + 1
142
+ entry_hash["source_file"] = file
143
+ end
130
144
  end
145
+
131
146
  end
132
147
 
133
148
 
@@ -150,7 +165,7 @@ module Eco
150
165
 
151
166
  run = true
152
167
  if Eco::API::Common::Session::FileManager.file_exists?(file)
153
- prompt_user("The file '#{file}' already exists. Do you want to overwrite it? (Y/n):", default: "Y") do |response|
168
+ prompt_user("Do you want to overwrite it? (Y/n):", explanation: "The file '#{file}' already exists.", default: "Y") do |response|
154
169
  run = (response == "") || reponse.upcase.start_with?("Y")
155
170
  end
156
171
  end
@@ -6,6 +6,14 @@ module Eco
6
6
  # Class to define a parser/serializer.
7
7
  class PersonAttributeParser < Eco::Language::Models::ParserSerializer
8
8
 
9
+ # @note
10
+ # - This was introduced at a later stage and might not be available for certain org-parsers configs
11
+ # @return [RequiredAttrs]
12
+ def required_attrs
13
+ @required_attrs ||= @dependencies[:required_attrs]
14
+ #@required_attrs ||= RequiredAttrs.new(attr, :unkown, [attr])
15
+ end
16
+
9
17
  # @see Eco::Language::Models::ParserSerializer#def_parser
10
18
  # @note
11
19
  # - additionally, you can declare a callback `active:` to determine if when the
@@ -10,7 +10,7 @@ module Eco
10
10
 
11
11
  attr_reader :schema, :schema_attrs
12
12
 
13
- def initialize(person: {}, schema: {}, account: {}, modifier: Common::People::PersonModifier.new)
13
+ def initialize(person: nil, schema: {}, account: {}, modifier: Common::People::PersonModifier.new)
14
14
  @modifier = Common::People::PersonModifier.new(modifier)
15
15
  @person = person
16
16
  @account = account
@@ -77,7 +77,9 @@ module Eco
77
77
  when Hash
78
78
  JSON.parse(person.to_json)
79
79
  else
80
- {}
80
+ {
81
+ "subordinates" => 0
82
+ }
81
83
  end
82
84
  end
83
85
 
@@ -16,7 +16,7 @@ module Eco
16
16
  CORE_ATTRS = ["id", "external_id", "email", "name", "supervisor_id", "filter_tags", "freemium"]
17
17
  ACCOUNT_ATTRS = ["policy_group_ids", "default_tag", "send_invites", "landing_page_id", "login_provider_ids"]
18
18
  TYPE = [:select, :text, :date, :number, :phone_number, :boolean, :multiple]
19
- FORMAT = [:csv, :xml, :json]
19
+ FORMAT = [:csv, :xml, :json, :xls]
20
20
 
21
21
  attr_reader :schema
22
22
  attr_reader :details_attrs, :all_model_attrs
@@ -59,9 +59,15 @@ module Eco
59
59
 
60
60
  # @!group Scopping attributes (identifying, presence & active)
61
61
 
62
+ # @return [Array<Eco::API::Common::Loaders::Parser::RequiredAttrs>]
63
+ def required_attrs
64
+ @parsers.values_at(*all_attrs(include_defined_parsers: true)).compact.map(&:required_attrs).compact
65
+ end
66
+
62
67
  # All the internal name attributes, including _core_, _account_ and _details_.
63
68
  def all_attrs(include_defined_parsers: false)
64
- all_model_attrs | defined_model_attrs
69
+ return all_model_attrs | defined_model_attrs if include_defined_parsers
70
+ all_model_attrs
65
71
  end
66
72
 
67
73
  # Scopes `source_attrs` using the _**core** attributes_.
@@ -15,7 +15,7 @@ module Eco
15
15
  # Reorders as follows:
16
16
  # 1. supervisors, people with no supervisor or where their supervisor not present
17
17
  # 2. subordinates
18
- # @return [Array<Entry>] `values` sorted by supervisors/subordinates
18
+ # @return [Array<PersonEntry>] `values` sorted by supervisors/subordinates
19
19
  def sort_by_supervisors(values, supervisors_first: true)
20
20
  raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash)
21
21
  return [] unless values && values.is_a?(Enumerable)
@@ -5,14 +5,6 @@ module Ecoportal
5
5
  class Person
6
6
  attr_accessor :entry
7
7
 
8
- def reset_details!
9
- doc["details"] = JSON.parse(original_doc["details"])
10
- end
11
-
12
- def consolidate_details!
13
- original_doc["details"] = JSON.parse(doc["details"])
14
- end
15
-
16
8
  def identify(section = :person)
17
9
  if entry && section == :entry
18
10
  entry.to_s(:identify)
@@ -3,14 +3,6 @@ module Ecoportal
3
3
  class Internal
4
4
  class Person
5
5
 
6
- def reset_account!
7
- doc["account"] = JSON.parse(original_doc["account"])
8
- end
9
-
10
- def consolidate_account!
11
- original_doc["account"] = JSON.parse(doc["account"])
12
- end
13
-
14
6
  def new?(doc = :initial)
15
7
  ref_doc = (doc == :original) ? original_doc : initial_doc
16
8
  !ref_doc["details"] && !ref_doc["account"]
@@ -12,9 +12,11 @@ module Eco
12
12
  unless options.dig(:exclude, :core) && !person.new?
13
13
  micro.set_core(entry, person, options)
14
14
  if entry.supervisor_id?
15
- micro.set_supervisor(entry.supervisor_id, person, people, options) do |unkown_id|
15
+ micro.set_supervisor(person, entry.supervisor_id, people, options) do |unknown_id|
16
16
  # delay setting supervisor if does not exit
17
- supers_job.add(person) {|person| person.supervisor_id = unkown_id}
17
+ supers_job.add(person) do |person|
18
+ micro.set_supervisor(person, unknown_id, people, options)
19
+ end
18
20
  end
19
21
  end
20
22
  end
@@ -1,22 +1,27 @@
1
1
  module Eco
2
2
  module API
3
3
  class MicroCases
4
- # Special snippet to decide if the `supervisor_id` is set now or in a later batch job `supers_job`.
5
- # @note delaying the setting of a `supervisor_id` can save errors when the supervisor still does not exit.
6
- # @param sup_id [nil, String] the **supervisor id** we should set on the `person`.
4
+ # Unique access point to set the `supervisor_id` value on a person.
7
5
  # @param person [Ecoportal::API::V1::Person] the person we want to update, carrying the changes to be done.
8
- # @param people [Eco::API::Organization::People] target existing _People_ of the current update.
6
+ # @param sup_id [nil, String] the **supervisor id** we should set on the `person`.
7
+ # @param people [Eco::API::Organization::People] _People_ involved in the current update.
9
8
  # @param options [Hash] the options.
10
9
  # @yield [supervisor_id] callback when the supervisor_id is **unknown** (not `nil` nor any one's in `people`).
11
10
  # @yieldparam supervisor_id [String] the **unknown** `supervisor_id`.
12
- def set_supervisor(sup_id, person, people, options)
11
+ def set_supervisor(person, sup_id, people, options)
13
12
  unless options.dig(:exclude, :core) || options.dig(:exclude, :supervisor)
14
- micro.with_supervisor(sup_id, people) do |supervisor|
13
+ cur_id = person.supervisor_id
14
+ cur_super = cur_id && with_supervisor(cur_id, people)
15
+ micro.with_supervisor(sup_id, people) do |new_super|
15
16
  if !sup_id
16
17
  person.supervisor_id = nil
17
- elsif supervisor
18
- person.supervisor_id = supervisor.id
18
+ descrease_subordinates(cur_super)
19
+ elsif new_super && id = new_super.id
20
+ person.supervisor_id = id
21
+ descrease_subordinates(cur_super)
22
+ increase_subordinates(new_super)
19
23
  elsif !block_given?
24
+ descrease_subordinates(cur_super)
20
25
  person.supervisor_id = sup_id
21
26
  else
22
27
  yield(sup_id) if block_given?
@@ -25,6 +30,22 @@ module Eco
25
30
  end
26
31
  end
27
32
 
33
+ private
34
+
35
+ def descrease_subordinates(person, by = 1)
36
+ if person.is_a?(Ecoportal::API::V1::Person)
37
+ person.subordinates -= by
38
+ #person.subordinates = 0 if person.subordinates < 0
39
+ end
40
+ end
41
+
42
+ def increase_subordinates(person, by = 1)
43
+ if person.is_a?(Ecoportal::API::V1::Person)
44
+ #person.subordinates = 0 if person.subordinates < 0
45
+ person.subordinates += by
46
+ end
47
+ end
48
+
28
49
  end
29
50
  end
30
51
  end