module JamRuby class MusicianSearch < BaseSearch cattr_accessor :jschema, :search_meta attr_accessor :user_counters KEY_STUDIOS = 'studio_sessions' KEY_AGES = 'ages' KEY_INTERESTS = 'interests' SORT_VALS = %W{ latency distance } SORT_ORDERS = { SORT_VALS[0] => 'Latency to Me', SORT_VALS[1] => 'Distance to Me' } STUDIO_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, 4] STUDIOS_LABELS = { STUDIO_COUNTS[0] => 'Any', STUDIO_COUNTS[1] => 'under 10', STUDIO_COUNTS[2] => '10 to 50', STUDIO_COUNTS[3] => '50 to 100', STUDIO_COUNTS[4] => 'over 100' } AGE_COUNTS = [10, 20, 30, 40, 50] AGES = { AGE_COUNTS[0] => 'Teens', AGE_COUNTS[1] => "20's", AGE_COUNTS[2] => "30's", AGE_COUNTS[3] => "40's", AGE_COUNTS[4] => "50+" } INTEREST_VALS = [ANY_VAL_STR, GenrePlayer::VIRTUAL_BAND, GenrePlayer::TRADITIONAL_BAND, GenrePlayer::PAID_SESSION, GenrePlayer::FREE_SESSION, GenrePlayer::COWRITING, ] INTERESTS = { INTEREST_VALS[0] => 'Any', INTEREST_VALS[1] => 'Virtual Band', INTEREST_VALS[2] => 'Traditional Band', INTEREST_VALS[3] => 'Paid Sessions', INTEREST_VALS[4] => 'Free Sessions', INTEREST_VALS[5] => 'Co-Writing' } def self.json_schema return @@jschema if @@jschema @@jschema = BaseSearch.json_schema.merge({ KEY_INTERESTS => INTEREST_VALS[0], KEY_STUDIOS => STUDIO_COUNTS[0].to_s, KEY_AGES => [], KEY_SKILL => self::SKILL_VALS[0].to_s, }) end def self.search_filter_meta return @@search_meta if @@search_meta @@search_meta = super.merge({ interests: { keys: INTEREST_VALS, map: INTERESTS }, ages: { keys: AGE_COUNTS, map: AGES } }) end def self.search_target_class User end def _ages(rel) unless (vals = json[KEY_AGES]).blank? return rel if vals.detect { |vv| ANY_VAL_INT == vv } arels = [] vals.each do |val| today = Date.today case val.to_i when 10 arels << User.where("birth_date >= ? AND birth_date < ?", today - 20.years, today - 10.years) when 20 arels << User.where("birth_date >= ? AND birth_date < ?", today - 30.years, today - 20.years) when 30 arels << User.where("birth_date >= ? AND birth_date < ?", today - 40.years, today - 50.years) when 40 arels << User.where("birth_date >= ? AND birth_date < ?", today - 50.years, today - 40.years) when 50 arels << User.where("birth_date <= ?", today - 50.years) end end rel = rel.where("birth_date IS NOT NULL") sql = "(#{arels.map(&:where_values).flatten.join(') OR (')})" rel = rel.where(sql) end rel end def _studios(rel) ss = json[KEY_STUDIOS].to_i rel = rel.where('studio_session_count = ?', ss) if 0 <= ss rel end def _gigs(rel) gg = json[KEY_GIGS].to_i rel = rel.where('concert_count = ?',gg) if 0 <= gg rel end def _skills(rel) if 0 < (val = json[KEY_SKILL].to_i) rel = rel.where(skill_level: val) end rel end def _interests(rel) val = json[KEY_INTERESTS] if val.present? && ANY_VAL_STR != val rel = rel.where("#{val} = ?", true) end rel end def _active_within(rel) if json[KEY_ACTIVE_WITHIN].present? && 0 <= (val = json[KEY_ACTIVE_WITHIN].to_i) rel = rel.where("users.id IN (SELECT users.id FROM users GROUP BY id HAVING GREATEST(updated_at, last_jam_updated_at) >= ?)", val.days.ago.at_beginning_of_day) end rel end def _sort_order(rel) val = json[self.class::KEY_SORT_ORDER] if self.class::SORT_VALS[1] == val #locidispid = self.user.last_jam_locidispid || 0 #my_locid = locidispid / 1000000 #rel = rel.joins("LEFT JOIN geoiplocations AS my_geo ON my_geo.locid = #{my_locid}") #rel = rel.joins("LEFT JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid") #rel = rel.group("users.id, my_geo.geog, other_geo.geog") #rel = rel.order('st_distance(my_geo.geog, other_geo.geog)') else rel = rel.joins("LEFT JOIN current_scores ON current_scores.a_userid = users.id AND current_scores.b_userid = '#{self.user.id}'") rel = rel.order('current_scores.full_score ASC') end rel end def _sort_by_ids_ordinality(rel, ids) return rel if ids.empty? values = [] ids.each_with_index do |id, index| values << "('#{id}', #{index + 1})" end rel = rel.joins("JOIN (VALUES #{values.join(",")}) as x (id, ordering) ON users.id = x.id") rel.order('x.ordering') end def do_filter(user_ids) rel = User.musicians.where('users.id <> ?', self.user.id) # rel = self._genres(rel) # rel = self._instruments(rel) # rel = self._joined_within(rel) # rel = self._active_within(rel) #user_ids: users returned from neo4j latency data service. #if this variable is not null, that means we are calling this method with neo4j latency data (currently this call only comes from api_search_controller#filter) #debugger unless user_ids.nil? if user_ids.empty? rel = User.none # no user_ids have been passed from latency service. which means no results for the latency based filter conditions. So we return an empty data set else rel = rel.where(id: user_ids) #following line does not work in postgresql 9.3 - array_position function was intruduced in postgresql 9.4 #NOTE: we can change to this once we upgrade postgresql #rel = rel.where(id: user_ids).where('users.id <> ?', self.user.id).order("array_position(ARRAY[#{user_ids.map { |i| "'#{i}'" }.join(',')}], id::TEXT)") #rel = self._sort_by_ids_ordinality(rel, user_ids) end end rel = rel.select("users.*, (SELECT count(friendships.id) > 0 AS is_friend FROM friendships WHERE friendships.user_id = users.id AND friendships.friend_id = '#{self.user.id}'), (SELECT count(follows.id) > 0 AS is_following FROM follows WHERE follows.user_id = users.id AND follows.followable_id = '#{self.user.id}' AND follows.followable_type = 'JamRuby::User'), (SELECT count(friend_requests.id) > 0 as pending_friend_request from friend_requests WHERE (friend_requests.user_id = '#{self.user.id}' AND friend_requests.friend_id = users.id) OR (friend_requests.user_id = users.id AND friend_requests.friend_id = '#{self.user.id}') )") rel end def do_search(params={}, user_ids) rel = User.musicians.where('users.id <> ?', self.user.id) rel = Search.scope_schools_together(rel, self.user) rel = self._genres(rel) rel = self._ages(rel) rel = self._studios(rel) rel = self._gigs(rel) rel = self._skills(rel) rel = self._instruments(rel) rel = self._interests(rel) #rel = self._sort_order(rel) rel = self._joined_within(rel) rel = self._active_within(rel) rel end def search_includes(rel) rel.includes([:instruments, :followings, :friends]) end def filter_includes(rel) rel.includes([:instruments, :genres]) end def process_results_page(_results, skip_counters = false) @results = _results @user_counters = {} and return self unless user unless skip_counters @user_counters = @results.inject({}) { |hh,val| hh[val.id] = []; hh } mids = "'#{@results.map(&:id).join("','")}'" # this gets counts for each search result on friends/follows/records/sessions @results.each do |uu| counters = { } counters[COUNT_FRIEND] = Friendship.where(:user_id => uu.id).count counters[COUNT_FOLLOW] = Follow.where(:followable_id => uu.id).count counters[COUNT_RECORD] = ClaimedRecording.where(:user_id => uu.id).count counters[COUNT_SESSION] = MusicSession.where(:user_id => uu.id).count @user_counters[uu.id] << counters end # this section determines follow/like/friend status for each search result # so that action links can be activated or not rel = User.select("users.id AS uid") rel = rel.joins("LEFT JOIN follows ON follows.user_id = '#{user.id}'") rel = rel.where(["users.id IN (#{mids}) AND follows.followable_id = users.id"]) rel.all.each { |val| @user_counters[val.uid] << RESULT_FOLLOW } rel = User.select("users.id AS uid") rel = rel.joins("LEFT JOIN friendships AS friends ON friends.friend_id = '#{user.id}'") rel = rel.where(["users.id IN (#{mids}) AND friends.user_id = users.id"]) rel.all.each { |val| @user_counters[val.uid] << RESULT_FRIEND } end self end private def _count(musician, key) if mm = @user_counters[musician.id] return mm.detect { |ii| ii.is_a?(Hash) }[key] end if @user_counters 0 end public def follow_count(musician) _count(musician, COUNT_FOLLOW) end def friend_count(musician) _count(musician, COUNT_FRIEND) end def record_count(musician) _count(musician, COUNT_RECORD) end def session_count(musician) _count(musician, COUNT_SESSION) end def is_friend?(musician) if mm = @user_counters[musician.id] return mm.include?(RESULT_FRIEND) end if @user_counters false end def is_follower?(musician) if mm = @user_counters[musician.id] return mm.include?(RESULT_FOLLOW) end if @user_counters false end def search_type self.class.to_s end def is_blank? self.data_blob == self.class.json_schema end def description if self.is_blank? return 'Click search button to look for musicians with similar interests, skill levels, etc.' end jj = self.json str = '' if 0 < (val = jj[KEY_INSTRUMENTS]).length str += ", Instruments = " instr_ids = val.collect { |stored_instrument| stored_instrument['id'] } instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description) instrs.each_with_index do |ii, idx| proficiency = val.detect { |stored_instrument| stored_instrument['id'] == ii.id }['level'] str += "#{ii.description} / #{INSTRUMENT_PROFICIENCY[proficiency.to_i]}" str += ', ' unless idx==(instrs.length-1) end end if (val = jj[KEY_INTERESTS]) != INTEREST_VALS[0] str += ", Interest = #{INTERESTS[val]}" end if (val = jj[KEY_SKILL].to_i) != SKILL_VALS[0] str += ", Skill = #{SKILL_LEVELS[val]}" end if (val = jj[KEY_STUDIOS].to_i) != STUDIO_COUNTS[0] str += ", Studio Sessions = #{STUDIOS_LABELS[val]}" end if (val = jj[KEY_GIGS].to_i) != GIG_COUNTS[0] str += ", Concert Gigs = #{GIG_LABELS[val]}" end val = jj[KEY_AGES].map(&:to_i) val.sort! if !val.blank? str += ", Ages = " val.each_with_index do |vv, idx| str += "#{AGES[vv]}" str += ', ' unless idx==(val.length-1) end end if 0 < (val = jj[KEY_GENRES]).length str += ", Genres = " genres = Genre.where(["id IN (?)", val]).order('description').pluck(:description) genres.each_with_index do |gg, idx| str += "#{gg}" str += ', ' unless idx==(genres.length-1) end end str += ", Sort = #{SORT_ORDERS[json_value(MusicianSearch::KEY_SORT_ORDER)]}" if str.start_with?(', ') # trim off any leading , str = str[2..-1] end str = 'Current Search: ' + str str end end end