module Eco module API module Organization class People < Eco::Language::Models::Collection # Error class that allows to handle cases where multiple people were found for the same criterion. # @note its main purpose to prevent the creation of duplicates or override information between different people. class MultipleSearchResults < StandardError attr_reader :candidates, :property # @param msg [String] the basic message error. # @param candiates [Array] the people that match the same search criterion. # @param property [String] the property of the person model that triggered the error (base of the search criterion). def initialize(msg, candidates: [], property: "email") @candidates = candidates @property = property super(msg + " " + candidates_summary) end # @param with_index [Boolean] to add an index to each candidate description. # @return [Array] the `candidates` identified def identify_candidates(with_index: false) candidates.map.each_with_index do |person, i| index = with_index ? "#{i}. " : "" msg = person.account ? (person.account_added? ? "(new user)" : "(user)") : "(no account)" "#{index}#{msg} #{person.identify}" end end # @return [Person] the `candidate` in the `index` position def candidate(index) candidates[index] end private def candidates_summary lines = ["The following people have the same '#{property}':"] lines.concat(identify_candidates(with_index: true)).join("\n ") end end # build the shortcuts of Collection attr_presence :account, :details attr_collection :id, :external_id, :email, :name, :supervisor_id alias_method :people, :to_a def initialize(people = [], klass: Ecoportal::API::Internal::Person) @klass = Ecoportal::API::Internal::Person unless klass == Ecoportal::API::V1::Person super(people, klass: @klass) @caches_init = false end # @!group Main identifier helpers def id(*args) attr('id', *args).first end def external_id(*args) attr('external_id', *args).first end def [](id_or_ext) id(id_or_ext) || external_id(id_or_ext) end # @!endgroup # @!group Special filters def users account_present(true) end def contacts details_present(true) end def non_users account_present(false) end # Returns the people that are being or have been updated and/or created. def updated_or_created select do |person| !person.as_update(:total).empty? end.yield_self do |persons| newFrom persons end end def supervisors sup_ids = self.ids & self.supervisor_ids sup_ids.map do |id| person(id: id, strict: true) end.yield_self do |supervisors| newFrom supervisors end end def missing_supervisors_ids sup_ids = self.supervisor_ids sup_ids - (sup_ids & self.ids) end def filter_tags_any(tags) attr("filter_tags", tags, default_modifier.any.insensitive) end def filter_tags_all(tags) attr("filter_tags", tags, default_modifier.all.insensitive) end def policy_group_ids_any(ids) attr("policy_group_ids", tags, default_modifier.any.insensitive) end def policy_group_ids_all(ids) attr("policy_group_ids", tags, default_modifier.all.insensitive) end # @!endgroup # @!group Searchers # It searches a person using the parameters given. # @note This is how the search function actually works: # 1. if eP `id` is given, returns the person (if found), otherwise... # 2. if `external_id` is given, returns the person (if found), otherwise... # 3. if `strict` is `false` and `email` is given: # - if there is only 1 person with that email, returns that person, otherwise... # - if found but, there are many candidates, it raises MultipleSearchResults error # - if person `external_id` matches `email`, returns that person # @raise MultipleSearchResults if there are multiple people with the same `email` # and there's no other criteria to find the person. It only gets to this point if # `external_id` was **not** provided and we are **not** in 'strict' search mode. # However, it could be we were in `strict` mode and `external_id` was not provided. # @param id [String] the `internal id` of the person # @param external_id [String] the `exernal_id` of the person # @param email [String] the `email` of the person # @param strict [Boolean] if should perform a `:soft` or a `:strict` search. `strict` will avoid repeated email addresses. # @return [Person, nil] the person we were searching, or `nil` if not found. def person(id: nil, external_id: nil, email: nil, strict: false) init_caches # normalize values ext_id = !external_id.to_s.strip.empty? && external_id.strip email = !email.to_s.strip.empty? && email.downcase.strip pers = nil pers ||= @by_id[id]&.first pers ||= @by_external_id[ext_id]&.first pers ||= person_by_email(email) unless strict && ext_id pers end # @see Eco::API::Organization::People#person def find(object, strict: false) id = attr_value(object, "id") external_id = attr_value(object, "external_id") email = attr_value(object, "email") person(id: id, external_id: external_id, email: email, strict: strict) end # @!endgroup # @!group Basic Collection Methods def to_json to_a.to_json end def newFrom(data) self.class.new(data, klass: @klass) end def uniq(strict: false, include_unsearchable: false) unsearchable = [] to_a.each_with_object([]) do |person, people| if found = find(person, strict: strict) people << found else unsearchable << person end end.yield_self do |found| found += unsearchable if include_unsearchable newFrom found end end def merge(data, strict: false, uniq: true) list = uniq ? exclude_people(data, strict: strict).to_a : to_a data = data.to_a unless data.is_a?(Array) newFrom list + data end def exclude(object, strict: false) exclude_people(into_a(object), strict: strict) end def exclude!(object, strict: false) self < exclude(object, strict: strict) end def exclude_people(list, strict: false) list.map do |person| find(person, strict: strict) end.compact.yield_self do |discarded| newFrom to_a - discarded end end # @!endgroup # @!group Groupping methods def email_id_maps users.group_by(:email).transform_values { |person| person.id } end def group_by_supervisor to_h(:supervisor_id) end def group_by_schema to_h do |person| person.details && person.details.schema_id end end def to_h(attr = "id") super(attr || "id") end # @!endgroup # @!group Helper methods def similarity Eco::API::Organization::PeopleSimilarity.new(self.to_a) end # @!endgroup protected def on_change @caches_init = false end private def person_by_email(email, prevent_duplicates: true) return nil unless email candidates = @by_non_users_email[email] || [] email_users = @by_users_email[email] || [] if pers = email_users.first return pers if candidates.empty? candidates = [pers] + candidates elsif candidates.length == 1 return candidates.first end if prevent_duplicates && !candidates.empty? msg = "Multiple search results match the criteria." raise MultipleSearchResults.new(msg, candidates: candidates, property: "email") end @by_external_id[email]&.first end def init_caches return if @caches_init @by_id = no_nil_key(to_h) @by_external_id = no_nil_key(to_h('external_id')) @by_users_email = no_nil_key(existing_users.to_h('email')) @by_non_users_email = no_nil_key(non_users.to_h('email')) @by_email = no_nil_key(to_h('email')) @caches_init = true end def existing_users newFrom users.select {|u| !u.account_added?(:original)} end def no_nil_key(hash) hash.tap {|h| h.delete(nil)} end end end end end