module JamRuby class MusicianSearch < JsonStore attr_accessor :page_count, :results, :user_counters, :page_number ANY_VAL_STR = 'any' ANY_VAL_INT = -1 PER_PAGE = 10 PG_SMALLINT_MAX = 32767 KEY_GIGS = 'concert_gigs' KEY_STUDIOS = 'studio_sessions' KEY_AGES = 'ages' KEY_SKILL = 'skill_level' KEY_GENRES = 'genres' KEY_INSTRUMENTS = 'instruments' KEY_INTERESTS = 'interests' KEY_SORT_ORDER = 'sort_order' SORT_VALS = %W{ latency distance } SORT_ORDERS = { SORT_VALS[0] => 'Latency to Me', SORT_VALS[1] => 'Distance to Me' } SKILL_VALS = [ANY_VAL_INT, 1, 2] SKILL_LEVELS = { SKILL_VALS[0] => 'Any', SKILL_VALS[1] => 'Amateur', SKILL_VALS[2] => 'Pro', } GIG_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, 4] GIG_LABELS = { GIG_COUNTS[0] => 'Any', GIG_COUNTS[1] => 'under 10', GIG_COUNTS[2] => '10 to 50', GIG_COUNTS[3] => '50 to 100', GIG_COUNTS[4] => 'over 100' } 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' } INSTRUMENT_PROFICIENCY = { 1 => 'Beginner', 2 => 'Intermediate', 3 => 'Expert', } JSON_SCHEMA = { KEY_SORT_ORDER => SORT_VALS[0], KEY_INSTRUMENTS => [], KEY_INTERESTS => INTEREST_VALS[0], KEY_GENRES => [], KEY_GIGS => GIG_COUNTS[0].to_s, KEY_STUDIOS => STUDIO_COUNTS[0].to_s, KEY_SKILL => SKILL_VALS[0].to_s, KEY_AGES => [] } JSON_SCHEMA_KEYS = JSON_SCHEMA.keys MULTI_VALUE_KEYS = JSON_SCHEMA.collect { |kk,vv| vv.is_a?(Array) ? kk : nil }.compact SINGLE_VALUE_KEYS = JSON_SCHEMA.keys - MULTI_VALUE_KEYS SEARCH_FILTER_META = { per_page: PER_PAGE, filter_keys: { keys: JSON_SCHEMA_KEYS, multi: MULTI_VALUE_KEYS, single: SINGLE_VALUE_KEYS, }, sort_order: { keys: SORT_VALS, map: SORT_ORDERS }, interests: { keys: INTEREST_VALS, map: INTERESTS }, ages: { keys: AGE_COUNTS, map: AGES } } def self.user_search_filter(user) unless ms = user.musician_search ms = self.create_search(user) end ms end def self.search_filter_json(user) self.user_search_filter(user).json end def self.create_search(user) ms = self.new ms.user = user ms.data_blob = JSON_SCHEMA ms.save! ms end # XXX SQL INJECTION def _genres(rel) gids = json[KEY_GENRES] unless gids.blank? gidsql = gids.join("','") gpsql = "SELECT player_id FROM genre_players WHERE (player_type = 'JamRuby::User' AND genre_id IN ('#{gidsql}'))" rel = rel.where("users.id IN (#{gpsql})") end rel end # XXX SQL INJECTION def _instruments(rel) unless (instruments = json['instruments']).blank? instsql = "SELECT player_id FROM musicians_instruments WHERE ((" instsql += instruments.collect do |inst| "instrument_id = '#{inst['id']}' AND proficiency_level = #{inst['level']}" end.join(") OR (") instsql += "))" rel = rel.where("users.id IN (#{instsql})") end rel 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 _sort_order(rel) val = json[KEY_SORT_ORDER] if 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 do_search(params={}) rel = User.musicians.where('users.id <> ?', self.user.id) 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 end def search_results_page(filter=nil, page=1) if filter self.data_blob = filter self.save else filter = self.data_blob end rel = do_search(filter) @page_number = [page.to_i, 1].max rel = rel.paginate(:page => @page_number, :per_page => PER_PAGE) rel = rel.includes([:instruments, :followings, :friends]) @page_count = rel.total_pages musician_results(rel.all) end def reset_filter self.data_blob = JSON_SCHEMA self.save end def reset_search_results reset_filter search_results_page end RESULT_FOLLOW = :follows RESULT_FRIEND = :friends COUNT_FRIEND = :count_friend COUNT_FOLLOW = :count_follow COUNT_RECORD = :count_record COUNT_SESSION = :count_session COUNTERS = [COUNT_FRIEND, COUNT_FOLLOW, COUNT_RECORD, COUNT_SESSION] def musician_results(_results) @results = _results @user_counters = {} and return self unless user @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 } 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 == 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