PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/contact_searcher.rb

https://github.com/dwaynemac/contacts
Ruby | 294 lines | 241 code | 18 blank | 35 comment | 22 complexity | 73f159f37b859f4920eac43227fe9c0e MD5 | raw file
  1. ##
  2. # ContactSearcher converts a simple Hash that can be sent through API
  3. # into mongo query.
  4. #
  5. class ContactSearcher
  6. attr_accessor :initial_scope
  7. attr_accessor :account_id
  8. attr_accessor :new_selector
  9. # @param scope [Mongoid::Criteria]
  10. # @param acc_id [String] account_id
  11. def initialize(scope=Contact, acc_id=nil)
  12. self.initial_scope = scope
  13. self.account_id = acc_id
  14. end
  15. LOCAL_ATTRIBUTE_META_ACCESSOR_REGEX = /^(.+)_for_([^=]+)$/
  16. CUSTOM_ATTRIBUTE_META_ACCESSOR_REGEX = /^custom_(.+)$/
  17. # This is same as #where but will make some transformations on selector.
  18. #
  19. # first_name and last_name are converted to Regex
  20. #
  21. # @param selector [ Hash ] query
  22. #
  23. # @option selector :nucleo_unit_id, scopes to accounts with given nucleo_id
  24. # @option selector :telephone, searches within all telephones
  25. # @option selector :email, searches within all emails
  26. # @option selector :address
  27. # @option selector :custom_attribute
  28. # @option selector :local_status only considered if account_id is specified or nucleo_unit_id
  29. # @option selector :local_teacher only considered if account_id is specified or nucleo_unit_id
  30. # @option selector :last_seen_at only considered if account_id is specified or nucleo_unit_id
  31. # @option selector :younger_than
  32. # @option selector :older_than
  33. # @option selector :attribute_value_at [Hash] keys: attribute, value, ref_date
  34. #
  35. # @return [Mongoid::Criteria]
  36. def api_where(selector=nil)
  37. return self.initial_scope if selector.nil?
  38. selector.stringify_keys!
  39. self.new_selector = {'$and' => []}
  40. selector.each do |k,v|
  41. unless v.blank?
  42. k = k.to_s
  43. case k
  44. when 'older_than'
  45. bdate = v.to_i.years.ago.to_date
  46. andit(
  47. "$or" => [
  48. {
  49. contact_attributes: {
  50. '$elemMatch' => {
  51. _type: "DateAttribute",
  52. category: 'birthday',
  53. year: bdate.year,
  54. month: bdate.month,
  55. day: { "$lte" => bdate.day }
  56. }
  57. }
  58. },
  59. {
  60. contact_attributes: {
  61. '$elemMatch' => {
  62. _type: "DateAttribute",
  63. category: 'birthday',
  64. year: bdate.year,
  65. month: { "$lt" => bdate.month }
  66. }
  67. }
  68. },
  69. {
  70. contact_attributes: {
  71. '$elemMatch' => {
  72. _type: "DateAttribute",
  73. category: 'birthday',
  74. year: { "$lt" => bdate. year }
  75. }
  76. }
  77. },
  78. {
  79. estimated_age: { "$gt" => v }
  80. # TODO consider estimated_age_on
  81. }
  82. ]
  83. )
  84. when 'younger_than'
  85. bdate = v.to_i.years.ago.to_date
  86. andit(
  87. "$or" => [
  88. {
  89. contact_attributes: {
  90. '$elemMatch' => {
  91. _type: "DateAttribute",
  92. category: 'birthday',
  93. year: bdate.year,
  94. month: bdate.month,
  95. day: { "$gte" => bdate.day }
  96. }
  97. }
  98. },
  99. {
  100. contact_attributes: {
  101. '$elemMatch' => {
  102. _type: "DateAttribute",
  103. category: 'birthday',
  104. year: bdate.year,
  105. month: { "$gt" => bdate.month }
  106. }
  107. }
  108. },
  109. {
  110. contact_attributes: {
  111. '$elemMatch' => {
  112. _type: "DateAttribute",
  113. category: 'birthday',
  114. year: { "$gt" => bdate. year }
  115. }
  116. }
  117. },
  118. {
  119. estimated_age: { "$lt" => v }
  120. # TODO consider estimated_age_on
  121. }
  122. ]
  123. )
  124. when 'nucleo_unit_id'
  125. andit({
  126. account_ids: nucleo_id_to_account_id(v)
  127. })
  128. when 'telephone', 'email', 'address', 'custom_attribute', 'occupation', 'identification'
  129. andit({
  130. :contact_attributes => { '$elemMatch' => { "_type" => k.camelize, "value" => Regexp.new(v.to_s,Regexp::IGNORECASE)}}
  131. })
  132. when 'country', 'state', 'city', 'postal_code', 'neighborhood'
  133. andit({:contact_attributes => { '$elemMatch' => { "_type" => "Address", k => Regexp.new(v.to_s)}}})
  134. when 'contact_attributes'
  135. andit({k => v})
  136. when 'date_attributes'
  137. v.each do |sv|
  138. aux = DateAttribute.convert_selector(sv)
  139. andit(aux) unless aux.nil?
  140. end
  141. when 'date_attribute'
  142. aux = DateAttribute.convert_selector(v)
  143. andit(aux) unless aux.nil?
  144. when 'local_status', 'coefficient', 'local_teacher'
  145. ref_id = ref_account_id(selector)
  146. if ref_id.present?
  147. if k == 'coefficient'
  148. filter_by_coefficient v, ref_id
  149. else
  150. andit({
  151. :local_unique_attributes => {'$elemMatch' => {_type: k.camelcase,
  152. value: {'$in' => v.to_a},
  153. account_id: ref_id}}
  154. })
  155. end
  156. end
  157. when 'professional_training_level'
  158. if v.is_a? Array
  159. andit({:professional_training_level => { '$in' => v.map{|lvl| lvl.to_i }}})
  160. else
  161. andit({:professional_training_level => v.to_i})
  162. end
  163. when 'level' # convert level name to level number
  164. if v.is_a? Array
  165. # ignore filter if all levels are considered
  166. unless v.select{|lvl| lvl != ''}.size == Contact::VALID_LEVELS.size
  167. andit({:level => { '$in' => v.map {|lvl| Contact::VALID_LEVELS[lvl]} }})
  168. end
  169. else
  170. andit({:level => Contact::VALID_LEVELS[v]})
  171. end
  172. when LOCAL_ATTRIBUTE_META_ACCESSOR_REGEX
  173. local_attribute = $1
  174. a = get_account($2)
  175. if a
  176. if local_attribute.to_s == 'coefficient'
  177. filter_by_coefficient v, a.id
  178. else
  179. andit({
  180. :local_unique_attributes => {'$elemMatch' => {_type: local_attribute.to_s.camelcase, value: {'$in' => v.to_a}, account_id: a.id}}
  181. })
  182. end
  183. end
  184. when 'first_name', 'last_name'
  185. self.new_selector[k] = v.is_a?(String)? Regexp.new(v,Regexp::IGNORECASE) : v
  186. when 'tags'
  187. cs = Tag.find(v).map(&:contact_ids).flatten.uniq
  188. andit({
  189. :_id => {'$in' => cs}
  190. })
  191. when 'updated_at'
  192. andit({:updated_at => { '$gt' => v }})
  193. when 'last_seen_at'
  194. if account_id.present?
  195. andit({:local_unique_attributes => {'$elemMatch' => {_type: "LastSeenAt",
  196. value: {'$lt' => DateTime.parse(v).to_time_in_current_zone.utc},
  197. account_id: account_id}}
  198. })
  199. end
  200. when CUSTOM_ATTRIBUTE_META_ACCESSOR_REGEX
  201. custom_attribute_key = $1
  202. if account_id.present?
  203. andit({
  204. :contact_attributes => { '$elemMatch' => {
  205. _type: "CustomAttribute",
  206. key: custom_attribute_key,
  207. value: Regexp.new(v.to_s,Regexp::IGNORECASE),
  208. account_id: account_id
  209. }}
  210. })
  211. end
  212. else
  213. self.new_selector[k] = v
  214. end
  215. end
  216. end
  217. clean_selector
  218. self.initial_scope.where(self.new_selector)
  219. rescue Exceptions::ForceEmptyQuery
  220. Contact.where(id: 'force-empty-query') # force an empty result
  221. end
  222. private
  223. def andit(hsh)
  224. self.new_selector['$and'] << hsh
  225. end
  226. def clean_selector
  227. if self.new_selector['$and'].empty?
  228. self.new_selector.delete('$and')
  229. elsif self.new_selector['$and'].size == 1
  230. aux = self.new_selector.delete('$and')[0]
  231. self.new_selector = self.new_selector.merge(aux)
  232. end
  233. end
  234. #
  235. # Reference account for local_unique_attributes
  236. #
  237. def ref_account_id(selector=nil)
  238. if self.account_id
  239. self.account_id
  240. elsif selector
  241. if selector['nucleo_unit_id'].present?
  242. nucleo_id_to_account_id(selector['nucleo_unit_id'])
  243. end
  244. end
  245. end
  246. def nucleo_id_to_account_id(nucleo_id)
  247. account = PadmaAccount.find_by_nucleo_id(nucleo_id)
  248. if account
  249. local_account = get_account(account.name)
  250. local_account.id
  251. else
  252. # Mongo Queries can get slow. If account doesnt exist avoid querying.
  253. raise Exceptions::ForceEmptyQuery
  254. end
  255. end
  256. # Will search for account with given name and cache it
  257. # or read it from cache of sucesive calls
  258. # @param account_name [String]
  259. def get_account(account_name)
  260. sanitized_account_name = account_name.gsub(/\.|-/, '_')
  261. if (a = instance_variable_get("@cached_account_#{sanitized_account_name}")).blank?
  262. a = Account.where(name: account_name).first
  263. instance_variable_set("@cached_account_#{sanitized_account_name}", a)
  264. end
  265. a
  266. end
  267. def filter_by_coefficient(value,account_id)
  268. unless value.is_a?(Array) && value.select{|coef| coef != ''}.size == Coefficient::VALID_VALUES.size
  269. andit({
  270. :local_unique_attributes => {'$elemMatch' => {_type: 'Coefficient',
  271. value: {'$in' => value.to_a},
  272. account_id: account_id}}
  273. })
  274. end
  275. end
  276. end