Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop

This commit is contained in:
Seth Call 2015-08-13 11:10:04 -05:00
commit ee6493afd3
27 changed files with 2516 additions and 1157 deletions

View File

@ -239,7 +239,9 @@ require "jam_ruby/jmep_manager"
require "jam_ruby/models/performance_sample"
require "jam_ruby/models/online_presence"
require "jam_ruby/models/json_store"
require "jam_ruby/models/base_search"
require "jam_ruby/models/musician_search"
require "jam_ruby/models/band_search"
require "jam_ruby/import/tency_stem_mapping"
include Jampb

View File

@ -71,6 +71,7 @@ module JamRuby
end
def follower_count
# FIXME: this could be a lot of followers; calling size loads all the data into memory
self.followers.size
end

View File

@ -0,0 +1,428 @@
module JamRuby
class BandSearch < BaseSearch
cattr_accessor :jschema, :search_meta
attr_accessor :user_counters
serialize :data_blob, JSON
KEY_BAND_SEARCH_TYPE = 'band_search_type'
KEY_BAND_TYPE = 'band_type'
KEY_BAND_STATUS = 'band_status'
KEY_PLAY_COMMIT = 'play_commitment'
KEY_TOUR_OPTION = 'touring_option'
KEY_PERF_SAMPLES = 'performance_samples'
KEY_HIRE_MAX_COST = 'max_cost'
KEY_HIRE_FREE = 'free_gigs'
TO_JOIN = 'to_join'
TO_HIRE = 'to_hire'
BAND_SEARCH_TYPE_VALS = [TO_JOIN, TO_HIRE]
BAND_SEARCH_TYPES = {
TO_JOIN => 'search bands',
TO_HIRE => 'search bands to hire',
}
BAND_TYPE_VAL_STRS = [ANY_VAL_STR, 'amateur', 'professional']
BAND_TYPES = {
BAND_TYPE_VAL_STRS[0] => BAND_TYPE_VAL_STRS[0].camelcase,
BAND_TYPE_VAL_STRS[1] => BAND_TYPE_VAL_STRS[1].camelcase,
BAND_TYPE_VAL_STRS[2] => BAND_TYPE_VAL_STRS[2].camelcase,
}
SORT_VALS = %W{ distance }
SORT_ORDERS = {
SORT_VALS[0] => 'Distance to Me'
}
# SORT_VALS = %W{ distance latency }
# SORT_ORDERS = {
# SORT_VALS[0] => 'Distance to Me'
# SORT_VALS[1] => 'Latency to Me',
# }
HIRE_SORT_VALS = %W{ distance price_asc price_desc }
HIRE_SORT_ORDERS = {
HIRE_SORT_VALS[0] => 'Distance to Me',
HIRE_SORT_VALS[1] => 'Gig Minimum Price (Low to High)',
HIRE_SORT_VALS[2] => 'Gig Minimum Price (High to Low)',
}
BAND_STATUS_VALS = [ANY_VAL_STR,
GenrePlayer::VIRTUAL_BAND,
GenrePlayer::TRADITIONAL_BAND,
]
BAND_STATUS = {
BAND_STATUS_VALS[0] => 'Any',
BAND_STATUS_VALS[1] => 'Virtual Band',
BAND_STATUS_VALS[2] => 'Traditional Band',
}
PLAY_COMMIT_VALS = [ANY_VAL_STR,
'1',
'2',
'3',
'4',
]
PLAY_COMMITS = {
PLAY_COMMIT_VALS[0] => 'Any',
PLAY_COMMIT_VALS[1] => 'Infrequent',
PLAY_COMMIT_VALS[2] => 'Once a Week',
PLAY_COMMIT_VALS[3] => '2-3 Times Per Week',
PLAY_COMMIT_VALS[4] => '4+ Times Per Week',
}
TOUR_OPTION_VALS = [ANY_VAL_STR,
VAL_YES,
VAL_NO,
]
TOUR_OPTIONS = {
TOUR_OPTION_VALS[0] => 'Any',
TOUR_OPTION_VALS[1] => VAL_YES,
TOUR_OPTION_VALS[2] => VAL_NO,
}
PERF_SAMPLES_VALS = TOUR_OPTION_VALS.clone
PERF_SAMPLES = TOUR_OPTIONS.clone
COUNT_FOLLOW = :count_follow
COUNT_RECORD = :count_record
COUNT_SESSION = :count_session
def self.json_schema
return @@jschema if @@jschema
@@jschema = {
TO_JOIN => BaseSearch.json_schema.merge({
KEY_SORT_ORDER => self::SORT_VALS[0],
KEY_BAND_TYPE => self::BAND_TYPE_VAL_STRS[0].to_s,
KEY_BAND_STATUS => BAND_STATUS_VALS[0],
KEY_PLAY_COMMIT => PLAY_COMMIT_VALS[0],
KEY_TOUR_OPTION => TOUR_OPTION_VALS[0],
}),
TO_HIRE => {
KEY_SORT_ORDER => self::HIRE_SORT_VALS[0],
KEY_GENRES => [],
KEY_GIGS => self::GIG_COUNTS[0].to_s,
KEY_BAND_STATUS => BAND_STATUS_VALS[0],
KEY_PERF_SAMPLES => self::PERF_SAMPLES_VALS[0],
KEY_HIRE_MAX_COST => 0,
KEY_HIRE_FREE => 0,
},
}
end
def self.search_filter_meta
return @@search_meta if @@search_meta
toJoinMeta = super(self.json_schema[TO_JOIN])
toJoinMeta.merge!({
KEY_BAND_TYPE => { keys: BAND_TYPE_VAL_STRS, map: BAND_TYPES },
KEY_BAND_STATUS => { keys: BAND_STATUS_VALS, map: BAND_STATUS },
KEY_PLAY_COMMIT => { keys: PLAY_COMMIT_VALS, map: PLAY_COMMITS },
KEY_TOUR_OPTION => { keys: TOUR_OPTION_VALS, map: TOUR_OPTIONS }
})
toHireMeta = super(self.json_schema[TO_HIRE],
{ keys: HIRE_SORT_VALS, map: HIRE_SORT_ORDERS })
toHireMeta.merge!({
KEY_BAND_STATUS => { keys: BAND_STATUS_VALS, map: BAND_STATUS },
KEY_PERF_SAMPLES => { keys: PERF_SAMPLES_VALS, map: PERF_SAMPLES },
})
@@search_meta = {
TO_JOIN => toJoinMeta,
TO_HIRE => toHireMeta,
}
end
def self.search_target_class
Band
end
def _genres(rel, filter)
super(rel, filter)
end
def _concert_gigs(rel, filter)
gg = filter[KEY_GIGS].to_i
rel = rel.where(concert_count: gg) if 0 <= gg
rel
end
def _band_status(rel, filter)
case filter[KEY_BAND_STATUS]
when GenrePlayer::VIRTUAL_BAND
rel.where(band_status: GenrePlayer::VIRTUAL_BAND.sub('_band',''))
when GenrePlayer::TRADITIONAL_BAND
rel.where(band_status: GenrePlayer::TRADITIONAL_BAND.sub('_band',''))
else
rel
end
end
def _play_commitment(rel, filter)
unless ANY_VAL_STR == filter[KEY_PLAY_COMMIT]
rel = rel.where(play_commitment: filter[KEY_PLAY_COMMIT].to_i)
end
rel
end
def _touring_option(rel, filter)
case filter[KEY_TOUR_OPTION]
when VAL_YES
rel.where(touring_option: true)
when VAL_NO
rel.where(touring_option: false)
else
rel
end
end
def _performance_samples(rel, filter)
case filter[KEY_PERF_SAMPLES]
when VAL_YES
rel.joins("LEFT OUTER JOIN performance_samples AS ps ON ps.player_id = bands.id AND player_type = '#{Band.name}'").where(["ps.id IS NOT NULL"])
when VAL_NO
rel.joins("LEFT OUTER JOIN performance_samples AS ps ON ps.player_id = bands.id AND player_type = '#{Band.name}'").where(["ps.id IS NULL"])
else
rel
end
end
def _max_cost(rel, filter)
if 0 < (max_cost = filter[KEY_HIRE_MAX_COST].to_i)
col = Band.arel_table[:gig_minimum]
rel = rel.where(col.lteq(max_cost)).where(col.gt(0))
end
rel
end
def _free_gigs(rel, filter)
case filter[KEY_HIRE_FREE]
when VAL_YES
rel.where(free_gigs: true)
when VAL_NO
rel.where(free_gigs: false)
else
rel
end
end
def _band_type(rel, filter)
case filter[KEY_BAND_TYPE]
when BAND_TYPE_VAL_STRS[1]
rel.where(band_type: BAND_TYPE_VAL_STRS[1])
when BAND_TYPE_VAL_STRS[2]
rel.where(band_type: BAND_TYPE_VAL_STRS[2])
else
rel
end
end
def _sort_order(rel, filter)
val = filter[KEY_SORT_ORDER]
if 'distance' == val || val.blank?
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 other_geo.latitude = bands.lat AND other_geo.longitude = bands.lng")
rel = rel.group("bands.id, my_geo.geog, other_geo.geog")
rel = rel.order('st_distance(my_geo.geog, other_geo.geog)')
elsif 'price_asc' == val
rel = rel.order('gig_minimum ASC')
elsif 'price_desc' == val
rel = rel.order('gig_minimum DESC')
end
rel
end
def do_search(filter)
rel = Band.unscoped
filter.keys.each do |fkey|
mname = "_#{fkey}"
if self.respond_to?(mname)
rel = self.send(mname.to_sym, rel, filter)
end
end
rel
end
def search_includes(rel, subtype=TO_JOIN)
TO_JOIN == subtype ? rel.includes([:instruments]) : rel
end
def _process_results_page(_results)
@results = _results
if user
@user_counters = @results.inject({}) { |hh,val| hh[val.id] = {}; hh }
# this gets counts for each search result
@results.each do |bb|
counters = {
COUNT_FOLLOW => Follow.where(:followable_id => bb.id).count,
COUNT_RECORD => Recording.where(:band_id => bb.id).count,
COUNT_SESSION => MusicSession.where(:band_id => bb.id).count
}
@user_counters[bb.id] = counters
end
else
@user_counters = {}
end
self
end
private
def _count(band, key)
if mm = @user_counters[band.id]
return mm[key]
end if @user_counters
0
end
public
def follow_count(band)
_count(band, COUNT_FOLLOW)
end
def record_count(band)
_count(band, COUNT_RECORD)
end
def session_count(band)
_count(band, COUNT_SESSION)
end
def is_follower?(band)
if mm = @user_counters[band.id]
return mm.include?(RESULT_FOLLOW)
end if @user_counters
false
end
def search_type
self.class.to_s
end
def is_blank?(subtype=TO_JOIN)
self.search_filter_for_subtype(subtype) == self.class.json_schema[subtype]
end
def reset_filter(subtype, data=nil)
data ||= self.class.json_schema[subtype]
dblob = self.data_blob
dblob[subtype] = data
self.data_blob = dblob
self.save
end
def reset_search_results(subtype=TO_JOIN)
reset_filter(subtype)
search_results_page(subtype)
end
def self.search_filter_json(user, subtype=TO_JOIN)
self.user_search_filter(user).json[subtype]
end
def search_filter_for_subtype(subtype)
self.data_blob[subtype]
end
def search_results_page(subtype=TO_JOIN, filter=nil, page=1)
if filter
reset_filter(subtype, filter)
else
filter = self.search_filter_for_subtype(subtype)
end
rel = do_search(filter)
@page_number = [page.to_i, 1].max
rel = rel.paginate(:page => @page_number, :per_page => self.class::PER_PAGE)
rel = self.search_includes(rel, subtype)
@page_count = rel.total_pages
_process_results_page(rel.all)
end
def _add_description(descrip, add)
descrip += "; " if 0 < descrip.length
descrip + add
end
def description(subtype=TO_JOIN)
return '' if self.is_blank?(subtype)
filter = search_filter_for_subtype(subtype)
str = ''
if filter.has_key?(KEY_SORT_ORDER)
str += 'Sort = '
case sort = filter[KEY_SORT_ORDER]
when 'distance'
str += SORT_ORDERS[sort]
when 'latency'
str += SORT_ORDERS[sort]
when 'price_asc'
str += HIRE_SORT_ORDERS[sort]
when 'price_desc'
str += HIRE_SORT_ORDERS[sort]
end
end
if (val = filter[KEY_BAND_TYPE]) != ANY_VAL_STR
str = _add_description(str, "Band type = #{BAND_TYPES[val]}")
end if filter.has_key?(KEY_BAND_TYPE)
if (val = filter[KEY_BAND_STATUS]) != ANY_VAL_STR
str = _add_description(str, "Band status = #{BAND_STATUS[val]}")
end if filter.has_key?(KEY_BAND_STATUS)
if (val = filter[KEY_PLAY_COMMIT]) != ANY_VAL_STR
str = _add_description(str, "Play commitment = #{PLAY_COMMITS[val]}")
end if filter.has_key?(KEY_PLAY_COMMIT)
if (val = filter[KEY_TOUR_OPTION]) != ANY_VAL_STR
str = _add_description(str, "Touring options = #{TOUR_OPTIONS[val]}")
end if filter.has_key?(KEY_TOUR_OPTION)
if (val = filter[KEY_PERF_SAMPLES]) != ANY_VAL_STR
str = _add_description(str, "Performance samples = #{PERF_SAMPLES[val]}")
end if filter.has_key?(KEY_PERF_SAMPLES)
if (val = filter[KEY_HIRE_MAX_COST].to_i) > 0
str = _add_description(str, "Maximum gig cost = $#{val}")
end if filter.has_key?(KEY_HIRE_MAX_COST)
if 0 < filter[KEY_HIRE_FREE]
str = _add_description(str, "Bands playing free gigs")
end if filter.has_key?(KEY_HIRE_FREE)
if (val = filter[KEY_GIGS].to_i) != GIG_COUNTS[0]
str = _add_description(str, "Concert gigs = #{GIG_LABELS[val]}")
end if filter.has_key?(KEY_GIGS)
if 0 < (val = filter[KEY_GENRES]).length
gstr = "Genres = "
genres = Genre.where(["id IN (?)", val]).order('description').pluck(:description)
gstr += genres.join(', ')
str = _add_description(str, gstr)
end if filter.has_key?(KEY_GENRES)
if 0 < ((val = filter[KEY_INSTRUMENTS]) || '').length
istr = "Instruments = "
instr_ids = val.collect { |vv| vv['instrument_id'] }
instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description)
instrs.each_with_index do |ii, idx|
proficiency = val.detect { |vv| vv['instrument_id'] == ii.id }['proficiency_level']
istr += "#{ii.description} (#{INSTRUMENT_PROFICIENCY[proficiency.to_i]})"
istr += ', ' unless idx==(instrs.length-1)
end
str = _add_description(str, istr)
end if filter.has_key?(KEY_INSTRUMENTS)
str = "Current Search: #{str}"
str
end
end
end

View File

@ -0,0 +1,196 @@
module JamRuby
class BaseSearch < JsonStore
attr_accessor :page_count, :results, :page_number
ANY_VAL_STR = 'any'
ANY_VAL_INT = -1
VAL_YES = 'yes'
VAL_NO = 'no'
PER_PAGE = 10
PG_SMALLINT_MAX = 32767
KEY_SKILL = 'skill_level'
KEY_GENRES = 'genres'
KEY_INSTRUMENTS = 'instruments'
KEY_GIGS = 'concert_gigs'
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'
}
INSTRUMENT_PROFICIENCY = {
1 => 'Beginner',
2 => 'Intermediate',
3 => 'Expert',
}
def self.json_schema
{
KEY_SORT_ORDER => self::SORT_VALS[0],
KEY_INSTRUMENTS => [],
KEY_GENRES => [],
KEY_GIGS => self::GIG_COUNTS[0].to_s,
}
end
def self.search_filter_meta(jschema=nil, sort_order=nil)
jschema ||= self.json_schema
schema_keys = jschema.keys
sort_order ||= { keys: self::SORT_VALS, map: self::SORT_ORDERS }
multi_keys = jschema.collect { |kk,vv| vv.is_a?(Array) ? kk : nil }.compact
{
per_page: self::PER_PAGE,
filter_keys: {
keys: schema_keys,
multi: multi_keys,
single: schema_keys - multi_keys,
},
sort_order: sort_order
}
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 self.user_search_filter(user)
unless ss = user.send(self.name.demodulize.tableize.singularize)
ss = self.create_search(user)
end
ss
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 = self.json_schema
ms.save!
ms
end
def self.search_target_class
end
# FIXME: SQL INJECTION
def _genres(rel, query_data=json)
gids = query_data[KEY_GENRES]
unless gids.blank?
gidsql = gids.join("','")
gpsql = "SELECT player_id FROM genre_players WHERE (player_type = '#{self.class.search_target_class.name}' AND genre_id IN ('#{gidsql}'))"
rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{gpsql})")
end
rel
end
# FIXME: SQL INJECTION
def _instruments(rel, query_data=json)
unless (instruments = query_data[KEY_INSTRUMENTS]).blank?
instsql = "SELECT player_id FROM musicians_instruments WHERE (("
instsql += instruments.collect do |inst|
"instrument_id = '#{inst['instrument_id']}' AND proficiency_level = #{inst['proficiency_level']}"
end.join(") OR (")
instsql += "))"
rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{instsql})")
end
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 _sort_order(rel)
end
def do_search(params={})
end
def process_results_page(objs)
end
def search_includes(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 => self.class::PER_PAGE)
rel = self.search_includes(rel)
@page_count = rel.total_pages
process_results_page(rel.all)
end
def reset_filter
self.data_blob = self.class.json_schema
self.save
end
def reset_search_results
reset_filter
search_results_page
end
def search_type
self.class.to_s
end
def is_blank?
self.data_blob == self.class.json_schema
end
def description
end
end
end

View File

@ -1,22 +1,12 @@
module JamRuby
class MusicianSearch < JsonStore
class MusicianSearch < BaseSearch
attr_accessor :page_count, :results, :user_counters, :page_number
cattr_accessor :jschema, :search_meta
attr_accessor :user_counters
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 = {
@ -24,22 +14,6 @@ module JamRuby
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',
@ -74,81 +48,28 @@ module JamRuby
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
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_json(user)
self.user_search_filter(user).json
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.create_search(user)
ms = self.new
ms.user = user
ms.data_blob = JSON_SCHEMA
ms.save!
ms
def self.search_target_class
User
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 }
@ -193,7 +114,7 @@ module JamRuby
def _skills(rel)
if 0 < (val = json[KEY_SKILL].to_i)
rel = rel.where(['skill_level = ?', val])
rel = rel.where(skill_level: val)
end
rel
end
@ -207,8 +128,8 @@ module JamRuby
end
def _sort_order(rel)
val = json[KEY_SORT_ORDER]
if SORT_VALS[1] == val
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}")
@ -235,45 +156,11 @@ module JamRuby
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
def search_includes(rel)
rel.includes([:instruments, :followings, :friends])
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)
def process_results_page(_results)
@results = _results
@user_counters = {} and return self unless user
@ -351,7 +238,7 @@ module JamRuby
end
def is_blank?
self.data_blob == JSON_SCHEMA
self.data_blob == self.class.json_schema
end
def description

View File

@ -172,6 +172,7 @@ module JamRuby
has_many :performance_samples, :class_name => "JamRuby::PerformanceSample", :foreign_key=> 'player_id'
has_one :musician_search, :class_name => 'JamRuby::MusicianSearch'
has_one :band_search, :class_name => 'JamRuby::BandSearch'
before_save :create_remember_token, :if => :should_validate_password?
before_save :stringify_avatar_info , :if => :updating_avatar

View File

@ -1,6 +1,19 @@
require 'spec_helper'
describe 'Band search' do
describe 'Band Search Model' do
let!(:searcher) { FactoryGirl.create(:austin_user) }
let!(:search) { BandSearch.user_search_filter(searcher) }
let!(:austin_user) { FactoryGirl.create(:austin_user) }
let!(:dallas_user) { FactoryGirl.create(:dallas_user) }
let!(:miami_user) { FactoryGirl.create(:miami_user) }
let!(:seattle_user) { FactoryGirl.create(:seattle_user) }
let!(:user_types) { [:austin_user, :dallas_user, :miami_user, :seattle_user] }
let!(:to_join) { search.search_filter_for_subtype(BandSearch::TO_JOIN) }
let!(:to_hire) { search.search_filter_for_subtype(BandSearch::TO_HIRE) }
before(:all) do
Recording.delete_all
@ -20,213 +33,310 @@ describe 'Band search' do
FactoryGirl.create(:band_musician, :band => bb, :user => FactoryGirl.create(:user))
end
end
end
describe "creates search obj" do
before(:all) do
User.delete_all
end
it "associates to user" do
expect(search.user).to eq(searcher)
searcher.reload
expect(searcher.band_search).to eq(search)
end
it "sets json" do
expect(search.search_filter_for_subtype(BandSearch::TO_JOIN)).to eq(BandSearch.json_schema[BandSearch::TO_JOIN])
expect(search.search_filter_for_subtype(BandSearch::TO_HIRE)).to eq(BandSearch.json_schema[BandSearch::TO_HIRE])
end
it "loads all bands by default" do
search.search_results_page
expect(search.results.count).to eq(Band.count)
end
it "has follower counts" do
Follow.create(user_id: searcher.id, followable: Band.first)
search.search_results_page
expect(search.follow_count(Band.first)).to eq(1)
end
end
context 'default filter settings' do
describe "generates description" do
it "finds all bands" do
# expects all the bands
num = Band.count
results = Search.band_filter({ :per_page => num })
expect(results.results.count).to eq(num)
def check_description(_filter, subtype, key, value, lookup, label)
_filter[key] = value
search.search_results_page(subtype, _filter)
expect(search.description(subtype)).to match(/Current Search: Sort = .*; #{label} = #{lookup[value]}$/)
end
it "finds bands with proper ordering" do
# the ordering should be create_at since no followers exist
expect(Follow.count).to eq(0)
results = Search.band_filter({ :per_page => Band.count })
it "renders no description for blank" do
search.search_results_page
expect(search.description).to eq('')
end
it "renders description for sort order" do
to_join[BandSearch::KEY_TOUR_OPTION] = BandSearch::VAL_YES
search.search_results_page(BandSearch::TO_JOIN, to_join)
search.search_results_page
expect(search.description).to match(/Sort =/)
end
context "to_join" do
it "renders description for band type" do
check_description(to_join,
BandSearch::TO_JOIN,
BandSearch::KEY_BAND_TYPE,
BandSearch::BAND_TYPE_VAL_STRS[1],
BandSearch::BAND_TYPES,
'Band type')
end
it "renders description for band status" do
check_description(to_join,
BandSearch::TO_JOIN,
BandSearch::KEY_BAND_STATUS,
BandSearch::SKILL_VALS[1],
BandSearch::BAND_STATUS,
'Band status')
end
it "renders description for concert gigs" do
check_description(to_join,
BandSearch::TO_JOIN,
BandSearch::KEY_GIGS,
BandSearch::GIG_COUNTS[1],
BandSearch::GIG_LABELS,
'Concert gigs')
end
it "renders description for play commitment" do
check_description(to_join,
BandSearch::TO_JOIN,
BandSearch::KEY_PLAY_COMMIT,
BandSearch::PLAY_COMMIT_VALS[1],
BandSearch::PLAY_COMMITS,
'Play commitment')
end
it "renders description for tour option" do
check_description(to_join,
BandSearch::TO_JOIN,
BandSearch::KEY_TOUR_OPTION,
BandSearch::VAL_YES,
BandSearch::TOUR_OPTIONS,
'Touring options')
end
it "renders description for genres" do
to_join[BandSearch::KEY_GENRES] = [Genre.first.id]
search.search_results_page(BandSearch::TO_JOIN, to_join)
expect(search.description).to match(/Current Search: Sort = .*; Genres = #{Genre.first.description}$/)
end
it "renders description for instruments" do
to_join[BandSearch::KEY_INSTRUMENTS] = [{ 'instrument_id' => Instrument.first.id, 'proficiency_level' => 1 }]
search.search_results_page(BandSearch::TO_JOIN, to_join)
expect(search.description).to match(/Current Search: Sort = .*; Instruments = #{Instrument.first.description} \(#{BandSearch::INSTRUMENT_PROFICIENCY[1]}\)$/)
end
end
context "to_hire" do
it "renders description for genres" do
to_hire[BandSearch::KEY_GENRES] = [Genre.first.id]
search.search_results_page(BandSearch::TO_HIRE, to_hire)
expect(search.description(BandSearch::TO_HIRE)).to match(/Current Search: Sort = .*; Genres = #{Genre.first.description}$/)
end
it "renders description for band status" do
check_description(to_hire,
BandSearch::TO_HIRE,
BandSearch::KEY_BAND_STATUS,
BandSearch::BAND_STATUS_VALS[1],
BandSearch::BAND_STATUS,
'Band status')
end
it "renders description for concert gigs" do
check_description(to_hire,
BandSearch::TO_HIRE,
BandSearch::KEY_GIGS,
BandSearch::GIG_COUNTS[1],
BandSearch::GIG_LABELS,
'Concert gigs')
end
it "renders description for performance samples" do
check_description(to_hire,
BandSearch::TO_HIRE,
BandSearch::KEY_PERF_SAMPLES,
BandSearch::PERF_SAMPLES_VALS[1],
BandSearch::PERF_SAMPLES,
'Performance samples')
end
it "renders description max cost" do
to_hire[BandSearch::KEY_HIRE_MAX_COST] = 100
search.search_results_page(BandSearch::TO_HIRE, to_hire)
expect(search.description(BandSearch::TO_HIRE)).to match(/Current Search: Sort = .*; Maximum gig cost = \$100$/)
end
it "renders description free gigs" do
to_hire[BandSearch::KEY_HIRE_FREE] = 1
search.search_results_page(BandSearch::TO_HIRE, to_hire)
expect(search.description(BandSearch::TO_HIRE)).to match(/Current Search: Sort = .*; Bands playing free gigs$/)
end
end
end
describe "filtering by keys" do
let!(:band) { Band.first }
context 'all search keys' do
let!(:filter) { to_join }
it "filters by gigs" do
band.update_attribute(:concert_count, BandSearch::GIG_COUNTS[2])
filter[BandSearch::KEY_GIGS] = BandSearch::GIG_COUNTS[2]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
it "filters by genre" do
band_id = band.id
filter[BandSearch::KEY_GENRES] = [band_id]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(Band.all.map(&:genres).flatten.select { |bb| bb.id == band_id }.count)
end
it "filters by band_type" do
band.update_attribute(:band_type, BandSearch::BAND_TYPE_VAL_STRS[1])
filter[BandSearch::KEY_BAND_TYPE] = BandSearch::BAND_TYPE_VAL_STRS[1]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
it "filters by instruments" do
minst = FactoryGirl.create(:musician_instrument)
band.musician_instruments << minst
band.save
filter[BandSearch::KEY_INSTRUMENTS] = [{'instrument_id' => minst.instrument_id,
'proficiency_level' => minst.proficiency_level
}]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
filter[BandSearch::KEY_INSTRUMENTS] = [{'instrument_id' => minst.instrument_id,
'proficiency_level' => minst.proficiency_level + 1
}]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(0)
end
end
context 'to_join' do
let!(:filter) { to_join }
it "sorts by distance" do
bands = Band.all.reverse
bb = bands.first
bb.lat, bb.lng = austin_geoip[:geoiplocation].latitude, austin_geoip[:geoiplocation].longitude
bb.save!
bb = bands.second
bb.lat, bb.lng = dallas_geoip[:geoiplocation].latitude, dallas_geoip[:geoiplocation].longitude
bb.save!
bb = bands.third
bb.lat, bb.lng = miami_geoip[:geoiplocation].latitude, miami_geoip[:geoiplocation].longitude
bb.save!
bb = bands.fourth
bb.lat, bb.lng = seattle_geoip[:geoiplocation].latitude, seattle_geoip[:geoiplocation].longitude
bb.save!
filter[BandSearch::KEY_SORT_ORDER] = BandSearch::SORT_VALS[0]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(Band.count)
expect(search.results.first.id).to eq(bands.first.id)
expect(search.results.second.id).to eq(bands.second.id)
expect(search.results.third.id).to eq(bands.third.id)
expect(search.results.fourth.id).to eq(bands.fourth.id)
end
it "filters by play commitment" do
band.update_attribute(BandSearch::KEY_PLAY_COMMIT, BandSearch::PLAY_COMMIT_VALS[1].to_i)
filter[BandSearch::KEY_PLAY_COMMIT] = BandSearch::PLAY_COMMIT_VALS[1]
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
it "filters by tour option" do
band.update_attribute(BandSearch::KEY_TOUR_OPTION, true)
filter[BandSearch::KEY_TOUR_OPTION] = BandSearch::VAL_YES
search.search_results_page(BandSearch::TO_JOIN, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
end
context 'to_hire' do
rbands = @bands.reverse
results.results.each_with_index do |uu, idx|
expect(uu.id).to eq(@bands.reverse[idx].id)
end
end
let!(:filter) { to_hire }
it "sorts bands by followers" do
users = []
4.downto(1) { |nn| users << FactoryGirl.create(:user) }
users.each_with_index do |u, index|
if index != 0
f1 = Follow.new
f1.user = u
f1.followable = @band4
f1.save
end
it "filters by free gigs" do
band.update_attribute(BandSearch::KEY_HIRE_FREE, true)
filter[BandSearch::KEY_HIRE_FREE] = BandSearch::VAL_YES
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
users.each_with_index do |u, index|
if index != 0
f1 = Follow.new
f1.user = u
f1.followable = @band3
f1.save
end
it "filters by max cost" do
band.update_attribute(:gig_minimum, 10)
filter[BandSearch::KEY_HIRE_MAX_COST] = 5
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(0)
filter[BandSearch::KEY_HIRE_MAX_COST] = 15
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(1)
end
f1 = Follow.new
f1.user = users.first
f1.followable = @band2
f1.save
it "filters by perform samples" do
filter[BandSearch::KEY_PERF_SAMPLES] = BandSearch::VAL_YES
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(0)
# establish sorting order
# @band4.followers.concat(users[1..-1])
# @band3.followers.concat(users[1..3])
# @band2.followers.concat(users[0])
@bands.map(&:reload)
filter[BandSearch::KEY_PERF_SAMPLES] = BandSearch::VAL_NO
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(Band.count)
expect(@band4.followers.count).to be 3
expect(Follow.count).to be 7
# refresh the order to ensure it works right
users.each_with_index do |u, index|
if index != 0
f1 = Follow.new
f1.user = u
f1.followable = @band2
f1.save
end
ps = PerformanceSample.new
ps.player = band
ps.service_type = 'youtube'
ps.service_id = 'abc123'
ps.save
# PerformanceSample.create(player: band, service_type: 'youtube', service_id: 'abc123')
filter[BandSearch::KEY_PERF_SAMPLES] = BandSearch::VAL_YES
search.search_results_page(BandSearch::TO_HIRE, filter)
expect(search.results.count).to eq(1)
expect(search.results[0].id).to eq(band.id)
end
# @band2.followers.concat(users[1..-1])
results = Search.band_filter({ :per_page => @bands.size }, users[0])
expect(results.results[0].id).to eq(@band2.id)
# check the follower count for given entry
expect(results.results[0].search_follow_count.to_i).not_to eq(0)
# check the follow relationship between current_user and result
expect(results.is_follower?(@band2)).to be true
end
it 'paginates properly' do
# make sure pagination works right
params = { :per_page => 2, :page => 1 }
results = Search.band_filter(params)
expect(results.results.count).to be 2
end
end
def make_session(band)
usr = band.users[0]
session = FactoryGirl.create(:active_music_session, :creator => usr, :description => "Session", :band => band)
FactoryGirl.create(:connection, :user => usr, :music_session => session)
user = FactoryGirl.create(:user)
session
end
context 'band stat counters' do
it "follow stat shows follower count" do
users = []
2.downto(1) { |nn| users << FactoryGirl.create(:user) }
users.each do |u|
f1 = Follow.new
f1.user = u
f1.followable = @band1
f1.save
end
# establish sorting order
# @band1.followers.concat(users)
results = Search.band_filter({},@band1)
uu = results.results.detect { |mm| mm.id == @band1.id }
expect(uu).to_not be_nil
expect(results.follow_count(uu)).to eq(users.count)
end
it "session stat shows session count" do
make_session(@band1)
@band1.reload
results = Search.band_filter({},@band1)
uu = results.results.detect { |mm| mm.id == @band1.id }
expect(uu).to_not be_nil
expect(results.session_count(uu)).to be 1
end
end
context 'band sorting' do
it "by plays" do
make_session(@band2)
make_session(@band2)
make_session(@band2)
make_session(@band1)
# order results by num recordings
results = Search.band_filter({ :orderby => 'plays' })
expect(results.results[0].id).to eq(@band2.id)
expect(results.results[1].id).to eq(@band1.id)
end
it "by now playing" do
# should get 1 result with 1 active session
session = make_session(@band3)
#FactoryGirl.create(:active_music_session, :music_session => session)
results = Search.band_filter({ :orderby => 'playing' })
expect(results.results.count).to be 1
expect(results.results.first.id).to eq(@band3.id)
# should get 2 results with 2 active sessions
# sort order should be created_at DESC
session = make_session(@band4)
#FactoryGirl.create(:active_music_session, :music_session => session)
results = Search.band_filter({ :orderby => 'playing' })
expect(results.results.count).to be 2
expect(results.results[0].id).to eq(@band4.id)
expect(results.results[1].id).to eq(@band3.id)
end
end
context 'filter settings' do
it "searches bands for a genre" do
genre = FactoryGirl.create(:genre)
@band1.genres << genre
@band1.reload
ggg = @band1.genres.detect { |gg| gg.id == genre.id }
expect(ggg).to_not be_nil
results = Search.band_filter({ :genre => ggg.id })
results.results.each do |rr|
expect(rr.genres.detect { |gg| gg.id==ggg.id }.id).to eq(genre.id)
end
expect(results.results.count).to be 1
end
it "finds bands within a given distance of given location" do
pending 'distance search changes'
num = Band.count
expect(@band1.lat).to_not be_nil
# short distance
results = Search.band_filter({ :per_page => num,
:distance => 10,
:city => 'Apex' }, @band1)
expect(results.results.count).to be num
# long distance
results = Search.band_filter({ :per_page => num,
:distance => 1000,
:city => 'Miami',
:state => 'FL' }, @band1)
expect(results.results.count).to be num
end
it "finds bands within a given distance of bands location" do
pending 'distance search changes'
expect(@band1.lat).to_not be_nil
# uses the location of @band1
results = Search.band_filter({ :distance => 10, :per_page => Band.count }, @band1)
expect(results.results.count).to be Band.count
end
it "finds no bands within a given distance of location" do
pending 'distance search changes'
expect(@band1.lat).to_not be_nil
results = Search.band_filter({ :distance => 10, :city => 'San Francisco' }, @band1)
expect(results.results.count).to be 0
end
end

View File

@ -115,7 +115,8 @@ group :development, :test do
gem 'test-unit'
# gem 'teaspoon'
# gem 'teaspoon-jasmine'
# gem 'puma'
gem 'puma'
gem 'byebug'
end
group :unix do
gem 'therubyracer' #, '0.11.0beta8'

View File

@ -1,257 +0,0 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.FindBandScreen = function(app) {
var logger = context.JK.logger;
var bands = {};
var bandList;
var instrument_logo_map = context.JK.getInstrumentIconMap24();
var did_show_band_page = false;
var page_num=1, page_count=0;
var helpBubble = context.JK.HelpBubbleHelper;
var $screen = $('#bands-screen');
var $results = $screen.find('#band-filter-results');
function loadBands(queryString) {
// squelch nulls and undefines
queryString = !!queryString ? queryString : "";
$.ajax({
type: "GET",
url: "/api/search.json?" + queryString,
success: afterLoadBands,
error: app.ajaxError
});
}
function search() {
did_show_band_page = true;
var queryString = 'srch_b=1&page='+page_num+'&';
// order by
var orderby = $('#band_order_by').val();
if (typeof orderby != 'undefined' && orderby.length > 0) {
queryString += "orderby=" + orderby + '&';
}
// genre filter
var genre = $('#band_genre').val();
if (typeof genre != 'undefined' && !(genre === '')) {
queryString += "genre=" + genre + '&';
}
// distance filter
var query_param = $('#band_query_distance').val();
if (query_param !== null && query_param.length > 0) {
var matches = query_param.match(/(\d+)/);
if (0 < matches.length) {
var distance = matches[0];
queryString += "distance=" + distance + '&';
}
}
loadBands(queryString);
}
function refreshDisplay() {
clearResults();
search();
}
function afterLoadBands(mList) {
// display the 'no bands' banner if appropriate
var $noBandsFound = $('#bands-none-found');
bandList = mList;
if(bandList.length == 0) {
$noBandsFound.show();
bands = [];
}
else {
$noBandsFound.hide();
bands = bandList['bands'];
if (!(typeof bands === 'undefined')) {
$('#band-filter-city').text(bandList['city']);
if (0 == page_count) {
page_count = bandList['page_count'];
}
renderBands();
}
}
}
function renderBands() {
var ii, len;
var mTemplate = $('#template-find-band-row').html();
var pTemplate = $('#template-band-player-info').html();
var aTemplate = $('#template-band-action-btns').html();
var eTemplate = $('#template-band-edit-btns').html();
var bVals, bb, renderings='';
var instr_logos, instr;
var players, playerVals, aPlayer, isMember;
for (ii=0, len=bands.length; ii < len; ii++) {
bb = bands[ii];
instr_logos = '';
players = '';
playerVals = {};
isMember = false;
for (var jj=0, ilen=bb['players'].length; jj<ilen; jj++) {
var toolTip = '';
aPlayer = bb['players'][jj];
var player_instrs = '';
var iter_pinstruments = aPlayer['instruments'].split(',');
for (var kk=0, klen=iter_pinstruments.length; kk<klen; kk++) {
var pinstr = iter_pinstruments[kk];
var toolTip = '';
if (pinstr in instrument_logo_map) {
instr = instrument_logo_map[pinstr].asset;
toolTip = pinstr;
}
player_instrs += '<img src="' + instr + '" title="' + toolTip + '"/>';
}
if (!isMember) {
isMember = aPlayer.user_id == context.JK.currentUserId;
}
playerVals = {
user_id: aPlayer.user_id,
player_name: aPlayer.name,
profile_url: '/client#/profile/' + aPlayer.user_id,
avatar_url: context.JK.resolveAvatarUrl(aPlayer.photo_url),
player_instruments: player_instrs
};
players += context.JK.fillTemplate(pTemplate, playerVals);
}
var actionVals, band_actions;
if (isMember) {
actionVals = {
profile_url: "/client#/bandProfile/" + bb.id,
band_edit_url: "/client#/band/setup/" + bb.id + '/step1',
band_member_url: "/client#/band/setup/" + bb.id + '/step2'
};
band_actions = context.JK.fillTemplate(eTemplate, actionVals);
} else {
actionVals = {
profile_url: "/client#/bandProfile/" + bb.id,
button_follow: bb['is_following'] ? '' : 'button-orange',
button_message: 'button-orange'
};
band_actions = context.JK.fillTemplate(aTemplate, actionVals);
}
var bgenres = '';
for (jj=0, ilen=bb['genres'].length; jj<ilen; jj++) {
bgenres += bb['genres'][jj]['description'] + '<br />';
}
bgenres += '<br />';
bVals = {
avatar_url: context.JK.resolveBandAvatarUrl(bb.photo_url),
profile_url: "/client#/bandProfile/" + bb.id,
band_name: bb.name,
band_location: bb.city + ', ' + bb.state,
genres: bgenres,
instruments: instr_logos,
biography: bb['biography'],
follow_count: bb['follow_count'],
recording_count: bb['recording_count'],
session_count: bb['session_count'],
band_id: bb['id'],
band_player_template: players,
band_action_template: band_actions
};
var $rendering = $(context.JK.fillTemplate(mTemplate, bVals))
var $offsetParent = $results.closest('.content');
var data = {entity_type: 'band'};
var options = {positions: ['top', 'bottom', 'right', 'left'], offsetParent: $offsetParent};
context.JK.helpBubble($('.follower-count', $rendering), 'follower-count', data, options);
context.JK.helpBubble($('.recording-count', $rendering), 'recording-count', data, options);
context.JK.helpBubble($('.session-count', $rendering), 'session-count', data, options);
$results.append($rendering);
}
$('.search-m-follow').on('click', followBand);
context.JK.bindHoverEvents();
}
function beforeShow(data) {
}
function afterShow(data) {
if (!did_show_band_page) {
refreshDisplay();
}
}
function clearResults() {
bands = {};
$('#band-filter-results').empty();
page_num = 1;
page_count = 0;
}
function followBand(evt) {
// if the band is already followed, remove the button-orange class, and prevent
// the link from working
if (0 == $(this).closest('.button-orange').size()) return false;
$(this).click(function(ee) {ee.preventDefault();});
evt.stopPropagation();
var newFollowing = {};
newFollowing.band_id = $(this).parent().data('band-id');
var url = "/api/users/" + context.JK.currentUserId + "/followings";
$.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: url,
data: JSON.stringify(newFollowing),
processData: false,
success: function(response) {
// remove the orange look to indicate it's not selectable
// @FIXME -- this will need to be tweaked when we allow unfollowing
$('div[data-band-id='+newFollowing.band_id+'] .search-m-follow').removeClass('button-orange').addClass('button-grey');
},
error: app.ajaxError(arguments)
});
}
function events() {
$('#band_query_distance').change(refreshDisplay);
$('#band_genre').change(refreshDisplay);
$('#band_order_by').change(refreshDisplay);
$('#band-filter-results').closest('.content-body-scroller').bind('scroll', function() {
if ($(this).scrollTop() + $(this).innerHeight() >= $(this)[0].scrollHeight) {
if (page_num < page_count) {
page_num += 1;
search();
}
}
});
}
function initialize() {
var screenBindings = {
'beforeShow': beforeShow,
'afterShow': afterShow
};
app.bindScreen('bands', screenBindings);
events();
}
this.initialize = initialize;
this.renderBands = renderBands;
this.afterShow = afterShow;
this.clearResults = clearResults;
return this;
}
})(window,jQuery);

View File

@ -1746,6 +1746,19 @@
});
}
function getBandSearchFilter(query) {
var qarg = query === undefined ? '' : query;
return $.get("/api/search/bands.json?"+qarg);
}
function postBandSearchFilter(query) {
return $.ajax({
type: "POST",
url: "/api/search/bands.json",
data: query
});
}
function getMount(options) {
var id = getId(options);
return $.ajax({
@ -1991,6 +2004,8 @@
this.addRecordingTimeline = addRecordingTimeline;
this.getMusicianSearchFilter = getMusicianSearchFilter;
this.postMusicianSearchFilter = postMusicianSearchFilter;
this.getBandSearchFilter = getBandSearchFilter;
this.postBandSearchFilter = postBandSearchFilter;
this.playJamTrack = playJamTrack;
this.createSignupHint = createSignupHint;
this.createAlert = createAlert;

View File

@ -0,0 +1,858 @@
$ = jQuery
context = window
context.JK ||= {};
context.JK.BaseSearchFilter = class BaseSearchFilter
constructor: () ->
@rest = context.JK.Rest()
@logger = context.JK.logger
@searchFilter = null
@profileUtils = context.JK.ProfileUtils
@helpBubble = context.JK.HelpBubbleHelper
@searchResults = null
@isSearching = false
@pageNumber = 1
@instrument_logo_map = context.JK.getInstrumentIconMap24()
@searchType = ''
@searchTypeS = ''
@restGet = null
@restPost = null
@searchMeta = null
init: (app) =>
@app = app
@screenBindings = { 'afterShow': this.afterShow, 'afterHide': this.afterHide }
@app.bindScreen(@searchTypeS, @screenBindings)
@screen = $('#'+@searchTypeS+'-screen')
@resultsListContainer = @screen.find('#'+@searchType+'-search-filter-results-list')
@spinner = @screen.find('.paginate-wait')
this.registerResultsPagination()
afterShow: () =>
@screen.find('#'+@searchType+'-search-filter-results').show()
@screen.find('#'+@searchType+'-search-filter-builder').hide()
this.getUserFilterResults()
showBuilder: () =>
@screen.find('#'+@searchType+'-search-filter-results').hide()
@screen.find('#'+@searchType+'-search-filter-builder').show()
@resultsListContainer.empty()
afterHide: () =>
@resultsListContainer.empty()
searchMetaData: () =>
@searchMeta
filterData: () =>
@searchFilter.data_blob
renderSearchFilter: () =>
$.when(@restGet()).done (sFilter) =>
this.loadSearchFilter(sFilter)
loadSearchFilter: (sFilter) =>
_populateSelectWithKeys: (struct, selection, keys, element) =>
element.children().remove()
$.each keys, (idx, value) =>
label = struct[value]
blankOption = $ '<option value=""></option>'
blankOption.text label
blankOption.attr 'value', value
blankOption.attr 'selected', '' if value == selection
element.append(blankOption)
context.JK.dropdown(element)
_populateSelectIdentifier: (identifier) =>
elem = $ '#'+@searchType+'-search-filter-builder select[name='+identifier+']'
struct = this.searchMetaData()[identifier]['map']
keys = this.searchMetaData()[identifier]['keys']
this._populateSelectWithKeys(struct, this.filterData()[identifier], keys, elem)
_populateSelectWithInt: (sourceStruct, selection, element) =>
struct =
'-1': 'Any'
$.extend(struct, sourceStruct)
this._populateSelectWithKeys(struct, selection, Object.keys(struct).sort(), element)
_populateSortOrder: () =>
this._populateSelectIdentifier('sort_order')
_populateGigs: () =>
elem = $ '#'+@searchType+'-search-filter-builder select[name=concert_gigs]'
this._populateSelectWithInt(@profileUtils.gigMap, this.filterData().concert_gigs.toString(), elem)
_populateGenres: () =>
@screen.find('#search-filter-genres').empty()
@rest.getGenres().done (genres) =>
genreTemplate = @screen.find('#template-search-filter-setup-genres').html()
filterGenres = this.filterData().genres
$.each genres, (index, genre) =>
if 0 < filterGenres.length
genreMatch = $.grep(filterGenres, (n, i) ->
n == genre.id)
else
genreMatch = []
if genreMatch.length > 0 then selected = 'checked' else selected = ''
genreHtml = context.JK.fillTemplate(genreTemplate,
id: genre.id
description: genre.description
checked: selected)
@screen.find('#search-filter-genres').append genreHtml
_populateInstruments: () =>
@screen.find('#search-filter-instruments').empty()
@rest.getInstruments().done (instruments) =>
$.each instruments, (index, instrument) =>
instrumentTemplate = @screen.find('#template-search-filter-setup-instrument').html()
selected = ''
proficiency = '1'
if 0 < this.filterData().instruments.length
instMatch = $.grep(this.filterData().instruments, (inst, i) ->
yn = inst.instrument_id == instrument.id
proficiency = inst.proficiency_level if yn
yn)
selected = 'checked' if instMatch.length > 0
instrumentHtml = context.JK.fillTemplate(instrumentTemplate,
id: instrument.id
description: instrument.description
checked: selected)
@screen.find('#search-filter-instruments').append instrumentHtml
profsel = '#search-filter-instruments tr[data-instrument-id="'+instrument.id+'"] select'
jprofsel = @screen.find(profsel)
jprofsel.val(proficiency)
context.JK.dropdown(jprofsel)
return true
_builderSelectValue: (identifier) =>
elem = $ '#'+@searchType+'-search-filter-builder select[name='+identifier+']'
elem.val()
_builderSelectMultiValue: (identifier) =>
vals = []
elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked'
if 'instruments' == identifier
elem.each (idx) ->
row = $(this).parent().parent()
instrument =
instrument_id: row.data('instrument-id')
proficiency_level: row.find('select').val()
vals.push instrument
else
elem.each (idx) ->
if $(this).prop('checked')
vals.push $(this).val()
vals
willSearch: (reload) =>
return false if @isSearching
@isSearching = true
if reload
@pageNumber = 1
@screen.find('#'+@searchType+'-search-filter-spinner').show()
@resultsListContainer.empty()
@screen.find('#'+@searchType+'-search-filter-builder').hide()
@screen.find('#'+@searchType+'-search-filter-results').show()
true
didSearch: (response) =>
this.loadSearchFilter(response.filter_json)
@searchResults = response
@screen.find('#'+@searchType+'-search-filter-spinner').hide()
this.renderResultsPage()
@screen.find('.paginate-wait').hide()
@isSearching = false
resetFilter: () =>
if this.willSearch(true)
@restPost({ filter: 'reset' }).done(this.didSearch)
cancelFilter: () =>
this.resetFilter()
getUserFilterResults: () =>
if this.willSearch(true)
this.doRestGet()
doRestGet: (query) =>
query2 = 'results=true&'
unless (typeof query == "undefined")
query2 += query
@restGet(query2).done(this.didSearch)
performSearch: () =>
if this.willSearch(true)
$.each this.searchMetaData().filter_keys.single, (index, key) =>
this.filterData()[key] = this._builderSelectValue(key)
$.each this.searchMetaData().filter_keys.multi, (index, key) =>
this.filterData()[key] = this._builderSelectMultiValue(key)
@restPost({ filter: JSON.stringify(this.filterData()), page: @pageNumber }).done(this.didSearch)
renderResultsHeader: () =>
renderResultsPage: () =>
_formatLocation: (band) ->
if band.city and band.state
band.city + ', ' + band.state
else if band.city
band.city
else if band.regionname
band.regionname
else
'Location Unavailable'
friendRequestCallback: (user_id)=>
# TODO:
paginate: () =>
if @pageNumber < @searchResults.page_count && this.willSearch(false)
@screen.find('.paginate-wait').show()
@pageNumber += 1
@restPost({ filter: JSON.stringify(this.filterData()), page: @pageNumber }).done(this.didSearch)
return true
false
registerResultsPagination: () =>
_resultsListContainer = @resultsListContainer
_headerHeight = @screen.find('#'+@searchType+'-search-filter-results-header').height()
_paginator = this.paginate
@screen.find('.content-body-scroller').scroll ->
if _resultsListContainer.is(':visible')
jthis = $(this)
wintop = jthis.scrollTop()
winheight = jthis.innerHeight()
docheight = jthis[0].scrollHeight - _headerHeight
scrollTrigger = 0.98;
if ((wintop / (docheight - winheight)) >= scrollTrigger)
_paginator()
context.JK.MusicianSearchFilter = class MusicianSearchFilter extends BaseSearchFilter
constructor: () ->
super()
@searchType = 'musician'
@searchTypeS = @searchType+'s'
@restGet = @rest.getMusicianSearchFilter
@restPost = @rest.postMusicianSearchFilter
@searchMeta = gon.musician_search_meta
init: (app) =>
super(app)
@screen.find('#btn-'+@searchType+'-search-builder').on 'click', =>
this.showBuilder()
@screen.find('#btn-'+@searchType+'-search-reset').on 'click', =>
this.resetFilter()
renderSearchFilter: () =>
super()
loadSearchFilter: (sFilter) =>
super(sFilter)
@searchFilter = JSON.parse(sFilter)
args =
interests: this.filterData().interests
skill_level: this.filterData().skill_level
studio_sessions: this.filterData().studio_sessions
concert_gigs: this.filterData().concert_gigs
template = context.JK.fillTemplate(@screen.find('#template-musician-search-filter').html(), args)
content_root = @screen.find('#musician-search-filter-builder')
content_root.html template
@screen.find('#btn-perform-musician-search').on 'click', =>
this.performSearch()
@screen.find('#btn-musician-search-cancel').on 'click', =>
this.cancelFilter()
this._populateSkill()
this._populateStudio()
this._populateGigs()
this._populateInterests()
this._populateAges()
this._populateGenres()
this._populateInstruments()
this._populateSortOrder()
_populateSortOrder: () =>
this._populateSelectIdentifier('sort_order')
_populateInterests: () =>
this._populateSelectIdentifier('interests')
_populateStudio: () =>
elem = $ '#musician-search-filter-builder select[name=studio_sessions]'
this._populateSelectWithInt(@profileUtils.studioMap, this.filterData().studio_sessions.toString(), elem)
_populateAges: () =>
@screen.find('#search-filter-ages').empty()
ages_map = this.searchMetaData()['ages']['map']
$.each this.searchMetaData()['ages']['keys'], (index, key) =>
ageTemplate = @screen.find('#template-search-filter-setup-ages').html()
selected = ''
ageLabel = ages_map[key]
if 0 < this.filterData().ages.length
key_val = key.toString()
ageMatch = $.grep(this.filterData().ages, (n, i) ->
n == key_val)
selected = 'checked' if ageMatch.length > 0
ageHtml = context.JK.fillTemplate(ageTemplate,
id: key
description: ageLabel
checked: selected)
@screen.find('#search-filter-ages').append ageHtml
_populateGenres: () =>
super()
_populateSkill: () =>
elem = $ '#'+@searchType+'-search-filter-builder select[name=skill_level]'
this._populateSelectWithInt(@profileUtils.skillLevelMap, this.filterData().skill_level.toString(), elem)
_populateInstruments: () =>
super()
willSearch: (reload) =>
super(reload)
didSearch: (response) =>
super(response)
resetFilter: () =>
super()
cancelFilter: () =>
super()
getUserFilterResults: () =>
super()
performSearch: () =>
super()
renderResultsHeader: () =>
@screen.find('#'+@searchType+'-search-filter-description').html(@searchResults.description)
if @searchResults.is_blank_filter
@screen.find('#btn-'+@searchType+'-search-reset').hide()
else
@screen.find('#btn-'+@searchType+'-search-reset').show()
renderResultsPage: () =>
super()
this.renderResultsHeader() if @pageNumber == 1
musicians = @searchResults.musicians
len = musicians.length
if 0 == len
@screen.find('#musician-search-filter-results-list-blank').show()
@screen.find('#musician-search-filter-results-list-blank').html('No results found')
return
else
@screen.find('#musician-search-filter-results-list-blank').hide()
ii = 0
mTemplate = @screen.find('#template-search-musician-row').html()
aTemplate = @screen.find('#template-search-musician-action-btns').html()
mVals = undefined
musician = undefined
renderings = ''
instr_logos = undefined
follows = undefined
followVals = undefined
aFollow = undefined
myAudioLatency = @searchResults.my_audio_latency
while ii < len
musician = musicians[ii]
if context.JK.currentUserId == musician.id
ii++
continue
instr_logos = ''
jj = 0
ilen = musician['instruments'].length
while jj < ilen
instr_id = musician['instruments'][jj].instrument_id
if instr_img = @instrument_logo_map[instr_id]
instr_logos += '<img height="24" width="24" src="' + instr_img.asset + '" title="' + instr_id + '"/>'
jj++
actionVals =
profile_url: '/client#/profile/' + musician.id
friend_class: 'button-' + (if musician['is_friend'] then 'grey' else 'orange')
friend_caption: (if musician.is_friend then 'DIS' else '') + 'CONNECT'
follow_class: 'button-' + (if musician['is_following'] then 'grey' else 'orange')
follow_caption: (if musician.is_following then 'UN' else '') + 'FOLLOW'
message_class: 'button-orange'
message_caption: 'MESSAGE'
button_message: 'button-orange'
musician_actions = context.JK.fillTemplate(aTemplate, actionVals)
latencyBadge = context._.template($("#template-account-session-latency").html(), $.extend(sessionUtils.createLatency(musician), musician), variable: 'data')
mVals =
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url)
profile_url: '/client#/profile/' + musician.id
musician_name: musician.name
musician_location: this._formatLocation(musician)
instruments: instr_logos
biography: musician['biography']
follow_count: musician['follow_count']
friend_count: musician['friend_count']
recording_count: musician['recording_count']
session_count: musician['session_count']
musician_id: musician['id']
musician_action_template: musician_actions
latency_badge: latencyBadge
musician_first_name: musician['first_name']
$rendering = $(context.JK.fillTemplate(mTemplate, mVals))
$offsetParent = @resultsListContainer.closest('.content')
data = entity_type: 'musician'
options =
positions: [
'top'
'bottom'
'right'
'left'
]
offsetParent: $offsetParent
scoreOptions = offsetParent: $offsetParent
context.JK.helpBubble($('.follower-count', $rendering), 'follower-count', data, options);
context.JK.helpBubble($('.friend-count', $rendering), 'friend-count', data, options);
context.JK.helpBubble($('.recording-count', $rendering), 'recording-count', data, options);
context.JK.helpBubble($('.session-count', $rendering), 'session-count', data, options);
@helpBubble.scoreBreakdown $('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions
@resultsListContainer.append $rendering
$rendering.find('.biography').dotdotdot()
ii++
this._bindMessageMusician()
this._bindFriendMusician()
this._bindFollowMusician()
context.JK.bindHoverEvents()
return
_bindMessageMusician: () =>
objThis = this
@screen.find('.search-m-message').on 'click', (evt) ->
userId = $(this).parent().data('musician-id')
objThis.app.layout.showDialog 'text-message', d1: userId
_bindFriendMusician: () =>
objThis = this
@screen.find('.search-m-friend').on 'click', (evt) ->
# if the musician is already a friend, remove the button-orange class, and prevent the link from working
if 0 == $(this).closest('.button-orange').size()
return false
$(this).click (ee) ->
ee.preventDefault()
return
evt.stopPropagation()
uid = $(this).parent().data('musician-id')
objThis.rest.sendFriendRequest objThis.app, uid, this.friendRequestCallback
_bindFollowMusician: () =>
objThis = this
@screen.find('.search-m-follow').on 'click', (evt) ->
# if the musician is already followed, remove the button-orange class, and prevent the link from working
if 0 == $(this).closest('.button-orange').size()
return false
$(this).click (ee) ->
ee.preventDefault()
return
evt.stopPropagation()
newFollowing = {}
newFollowing.user_id = $(this).parent().data('musician-id')
url = '/api/users/' + context.JK.currentUserId + '/followings'
$.ajax
type: 'POST'
dataType: 'json'
contentType: 'application/json'
url: url
data: JSON.stringify(newFollowing)
processData: false
success: (response) ->
# remove the orange look to indicate it's not selectable
# @FIXME -- this will need to be tweaked when we allow unfollowing
objThis.screen.find('div[data-musician-id=' + newFollowing.user_id + '] .search-m-follow').removeClass('button-orange').addClass 'button-grey'
return
error: objThis.app.ajaxError
_formatLocation: (musician) ->
if musician.city and musician.state
musician.city + ', ' + musician.state
else if musician.city
musician.city
else if musician.regionname
musician.regionname
else
'Location Unavailable'
friendRequestCallback: (user_id)=>
# TODO:
paginate: () =>
super()
registerResultsPagination: () =>
super()
context.JK.BandSearchFilter = class BandSearchFilter extends BaseSearchFilter
constructor: () ->
super()
@searchType = 'band'
@searchTypeS = @searchType+'s'
@restGet = @rest.getBandSearchFilter
@restPost = @rest.postBandSearchFilter
@searchMeta = gon.band_search_meta
@searchSubType = 'to_join'
init: (app) =>
super(app)
@screen.find('#btn-'+@searchType+'-search-builder-to_join').on 'click', =>
this.showBuilderToJoin()
@screen.find('#btn-'+@searchType+'-search-builder-to_hire').on 'click', =>
this.showBuilderToHire()
@screen.find('#btn-'+@searchType+'-search-builder').on 'click', =>
this.showBuilderActive()
@screen.find('#btn-'+@searchType+'-search-reset').on 'click', =>
this.resetFilter()
@resultsList = @screen.find('#band-search-filter-results-list')
@screen.find('#band-search-filter-results-filtered').hide()
isToHire: () =>
'to_hire' == @searchSubType
showBuilderActive: () =>
if this.isToHire()
this.showBuilderToHire()
else
this.showBuilderToJoin()
showBuilderToJoin: () =>
@screen.find('.band-search-filter-builder-top-to_join').show()
@screen.find('.band-search-filter-builder-top-to_hire').hide()
@searchSubType = 'to_join'
this.showBuilder()
this._loadSearchFilter() if @searchFilter
# @screen.find('.band-search-filter-builder-top-to_join h2').html('search bands')
showBuilderToHire: () =>
@screen.find('.band-search-filter-builder-top-to_join').hide()
@screen.find('.band-search-filter-builder-top-to_hire').show()
@searchSubType = 'to_hire'
this.showBuilder()
this._loadSearchFilter() if @searchFilter
# @screen.find('.band-search-filter-builder-top-to_hire h2').html('search bands to hire')
searchMetaData: () =>
@searchMeta[@searchSubType]
renderSearchFilter: () =>
super()
_searchFilterArgsToJoin: () =>
args =
touring_option: this.filterData().touring_option
band_status: this.filterData().band_status
play_commitment: this.filterData().play_commitment
band_type: this.filterData().band_type
concert_gigs: this.filterData().concert_gigs
_searchFilterArgsToHire: () =>
if 0 < this.filterData().max_cost
has_max_cost = 'checked'
else
has_max_cost = ''
if 1==this.filterData().free_gigs
has_free_gigs = 'checked'
else
has_free_gigs = ''
args =
band_status: this.filterData().band_status
concert_gigs: this.filterData().concert_gigs
performance_samples: this.filterData().performance_samples
has_max_cost: has_max_cost
max_cost: this.filterData().max_cost
has_free_gigs: has_free_gigs
_populateSearchFilterToJoin: () =>
this._populateInstruments()
this._populateSkill()
this._populateGigs()
this._populatePlayCommit()
this._populateTourOption()
this._populateBandStatus()
_populateSearchFilterToHire: () =>
this._populateBandStatus()
this._populateGigs()
this._populatePerformSamples()
loadSearchFilter: (sFilter) =>
super(sFilter)
@searchFilter = JSON.parse(sFilter)
this._loadSearchFilter()
_loadSearchFilter: () =>
switch @searchSubType
when 'to_join' then args = this._searchFilterArgsToJoin()
when 'to_hire' then args = this._searchFilterArgsToHire()
template = context.JK.fillTemplate(@screen.find('#template-band-search-filter-'+@searchSubType).html(), args)
content_root = @screen.find('#band-search-filter-builder')
content_root.html template
@screen.find('#btn-perform-band-search').on 'click', =>
this.performSearch()
@screen.find('#btn-band-search-cancel').on 'click', =>
this.cancelFilter()
this._populateGenres()
this._populateSortOrder() if this.isToHire()
switch @searchSubType
when 'to_join' then this._populateSearchFilterToJoin()
when 'to_hire' then this._populateSearchFilterToHire()
_populateSkill: () =>
this._populateSelectIdentifier('band_type')
_populateBandStatus: () =>
this._populateSelectIdentifier('band_status')
_populatePlayCommit: () =>
this._populateSelectIdentifier('play_commitment')
_populateTourOption: () =>
this._populateSelectIdentifier('touring_option')
_populateSortOrder: () =>
this._populateSelectIdentifier('sort_order')
_populatePerformSamples: () =>
this._populateSelectIdentifier('performance_samples')
_populateGenres: () =>
super()
_populateInstruments: () =>
super()
willSearch: (reload) =>
super(reload)
didSearch: (response) =>
super(response)
resetFilter: () =>
if this.willSearch(true)
@screen.find('#band-search-filter-results-blank').show()
@screen.find('#band-search-filter-results-filtered').hide()
@screen.find('#band-search-filter-description').html('')
@restPost({ filter: 'reset', subtype: @searchSubType }).done(this.didSearch)
cancelFilter: () =>
super()
doRestGet: (query) =>
super('subtype='+@searchSubType)
performSearch: () =>
if this.willSearch(true)
filterPost = this.filterData()
$.each this.searchMetaData().filter_keys.single, (index, key) =>
filterPost[key] = this._builderSelectValue(key)
$.each this.searchMetaData().filter_keys.multi, (index, key) =>
filterPost[key] = this._builderSelectMultiValue(key)
if this.isToHire()
filterPost['max_cost'] = parseInt($('#max_cost_amount').val())
filterPost['free_gigs'] = if $('#free_gigs').prop('checked') then 1 else 0
postData = { subtype: @searchSubType, filter: JSON.stringify(filterPost), page: @pageNumber }
@restPost(postData).done(this.didSearch)
renderResultsHeader: () =>
if @searchResults.is_blank_filter
@screen.find('#band-search-filter-results-blank').show()
@screen.find('#band-search-filter-results-filtered').hide()
else
@screen.find('#band-search-filter-results-blank').hide()
@screen.find('#band-search-filter-results-filtered').show()
@screen.find('#band-search-filter-description').html(@searchResults.description)
renderResultsPage: () =>
super()
this.renderResultsHeader() if @pageNumber == 1
bands = @searchResults.bands
len = bands.length
if 0 == len
@screen.find('#band-search-filter-results-list').hide()
@screen.find('#band-search-filter-results-list-blank').show()
@screen.find('#band-search-filter-results-list-blank').html('No results found')
return
else
@screen.find('#band-search-filter-results-list-blank').hide()
@screen.find('#band-search-filter-results-list').show()
this.renderBands(bands)
renderBands: (bands) =>
toolTip = undefined
ii = undefined
len = undefined
mTemplate = $('#template-find-band-row').html()
pTemplate = $('#template-band-player-info').html()
aTemplate = $('#template-band-action-btns').html()
eTemplate = $('#template-band-edit-btns').html()
bVals = undefined
bb = undefined
renderings = ''
instr_logos = undefined
instr = undefined
players = undefined
playerVals = undefined
aPlayer = undefined
isMember = undefined
ii = 0
len = bands.length
while ii < len
bb = bands[ii]
instr_logos = ''
players = ''
playerVals = {}
isMember = false
jj = 0
ilen = bb['players'].length
while jj < ilen
toolTip = ''
aPlayer = bb['players'][jj]
player_instrs = ''
iter_pinstruments = aPlayer['instruments'].split(',')
kk = 0
klen = iter_pinstruments.length
while kk < klen
pinstr = iter_pinstruments[kk]
toolTip = ''
if pinstr of @instrument_logo_map
instr = @instrument_logo_map[pinstr].asset
toolTip = pinstr
player_instrs += '<img src="' + instr + '" title="' + toolTip + '"/>'
kk++
if !isMember
isMember = aPlayer.user_id == context.JK.currentUserId
playerVals =
user_id: aPlayer.user_id
player_name: aPlayer.name
profile_url: '/client#/profile/' + aPlayer.user_id
avatar_url: context.JK.resolveAvatarUrl(aPlayer.photo_url)
player_instruments: player_instrs
players += context.JK.fillTemplate(pTemplate, playerVals)
jj++
actionVals = undefined
band_actions = undefined
if isMember
actionVals =
profile_url: '/client#/bandProfile/' + bb.id
band_edit_url: '/client#/band/setup/' + bb.id + '/step1'
band_member_url: '/client#/band/setup/' + bb.id + '/step2'
band_actions = context.JK.fillTemplate(eTemplate, actionVals)
else
actionVals =
profile_url: '/client#/bandProfile/' + bb.id
button_follow: if bb['is_following'] then '' else 'button-orange'
button_message: 'button-orange'
band_actions = context.JK.fillTemplate(aTemplate, actionVals)
bgenres = ''
jj = 0
ilen = bb['genres'].length
while jj < ilen
bgenres += bb['genres'][jj]['description'] + '<br />'
jj++
bgenres += '<br />'
bVals =
avatar_url: context.JK.resolveBandAvatarUrl(bb.photo_url)
profile_url: '/client#/bandProfile/' + bb.id
band_name: bb.name
band_location: bb.city + ', ' + bb.state
genres: bgenres
instruments: instr_logos
biography: bb['biography']
follow_count: bb['follow_count']
recording_count: bb['recording_count']
session_count: bb['session_count']
band_id: bb['id']
band_player_template: players
band_action_template: band_actions
$rendering = $(context.JK.fillTemplate(mTemplate, bVals))
$offsetParent = @resultsList.closest('.content')
data = entity_type: 'band'
options =
positions: [
'top'
'bottom'
'right'
'left'
]
offsetParent: $offsetParent
context.JK.helpBubble $('.follower-count', $rendering), 'follower-count', data, options
context.JK.helpBubble $('.recording-count', $rendering), 'recording-count', data, options
context.JK.helpBubble $('.session-count', $rendering), 'session-count', data, options
@resultsList.append $rendering
ii++
this._bindFollowBand()
context.JK.bindHoverEvents()
return
_bindFollowBand: () =>
objThis = this
@screen.find('.search-m-follow').on 'click', (evt) ->
if 0 == $(this).closest('.button-orange').size()
return false
$(this).click (ee) ->
ee.preventDefault()
return
evt.stopPropagation()
newFollowing = {}
newFollowing.band_id = $(this).parent().data('band-id')
url = '/api/users/' + context.JK.currentUserId + '/followings'
$.ajax
type: 'POST'
dataType: 'json'
contentType: 'application/json'
url: url
data: JSON.stringify(newFollowing)
processData: false
success: (response) ->
$('div[data-band-id=' + newFollowing.band_id + '] .search-m-follow').removeClass('button-orange').addClass 'button-grey'
return
error: objThis.app.ajaxError(arguments)
_formatLocation: (band) ->
friendRequestCallback: (user_id)=>
# TODO:
paginate: () =>
super()
registerResultsPagination: () =>
super()
filterData: () =>
super()[@searchSubType]

View File

@ -1,412 +0,0 @@
$ = jQuery
context = window
context.JK ||= {};
context.JK.MusicianSearchFilter = class MusicianSearchFilter
constructor: () ->
@rest = context.JK.Rest()
@logger = context.JK.logger
@searchFilter = null
@profileUtils = context.JK.ProfileUtils
@helpBubble = context.JK.HelpBubbleHelper
@searchResults = null
@isSearching = false
@pageNumber = 1
@instrument_logo_map = context.JK.getInstrumentIconMap24()
@instrumentSelector = null
init: (app) =>
@app = app
@screenBindings = { 'afterShow': this.afterShow, 'afterHide': this.afterHide }
@app.bindScreen('musicians', @screenBindings)
@screen = $('#musicians-screen')
@resultsListContainer = @screen.find('#musician-search-filter-results-list')
@spinner = @screen.find('.paginate-wait')
@instrumentSelector = new context.JK.InstrumentSelector(JK.app)
@instrumentSelector.initialize(false, true)
this.registerResultsPagination()
@screen.find('#btn-musician-search-builder').on 'click', =>
this.showBuilder()
false
@screen.find('#btn-musician-search-reset').on 'click', =>
this.resetFilter()
false
afterShow: () =>
@screen.find('#musician-search-filter-results').show()
@screen.find('#musician-search-filter-builder').hide()
this.getUserFilterResults()
showBuilder: () =>
@screen.find('#musician-search-filter-results').hide()
@screen.find('#musician-search-filter-builder').show()
@resultsListContainer.empty()
afterHide: () =>
@resultsListContainer.empty()
renderSearchFilter: () =>
$.when(this.rest.getMusicianSearchFilter()).done (sFilter) =>
this.loadSearchFilter(sFilter)
loadSearchFilter: (sFilter) =>
@searchFilter = JSON.parse(sFilter)
args =
interests: @searchFilter.data_blob.interests
skill_level: @searchFilter.data_blob.skill_level
studio_sessions: @searchFilter.data_blob.studio_sessions
concert_gigs: @searchFilter.data_blob.concert_gigs
template = context.JK.fillTemplate(@screen.find('#template-musician-search-filter').html(), args)
content_root = @screen.find('#musician-search-filter-builder')
content_root.html template
@screen.find('#btn-perform-musician-search').on 'click', =>
this.performSearch()
false
@screen.find('#btn-musician-search-cancel').on 'click', =>
this.cancelFilter()
false
this._populateSkill()
this._populateStudio()
this._populateGigs()
this._populateInterests()
this._populateAges()
this._populateGenres()
this._populateInstruments()
this._populateSortOrder()
_populateSelectWithKeys: (struct, selection, keys, element) =>
element.children().remove()
$.each keys, (idx, value) =>
label = struct[value]
blankOption = $ '<option value=""></option>'
blankOption.text label
blankOption.attr 'value', value
element.append(blankOption)
element.val(selection)
context.JK.dropdown(element)
_populateSelectIdentifier: (identifier) =>
elem = $ '#musician-search-filter-builder select[name='+identifier+']'
struct = gon.musician_search_meta[identifier]['map']
keys = gon.musician_search_meta[identifier]['keys']
console.log("@searchFilter", @searchFilter, identifier)
this._populateSelectWithKeys(struct, @searchFilter.data_blob[identifier], keys, elem)
_populateSelectWithInt: (sourceStruct, selection, element) =>
struct =
'-1': 'Any'
$.extend(struct, sourceStruct)
this._populateSelectWithKeys(struct, selection, Object.keys(struct).sort(), element)
_populateSortOrder: () =>
this._populateSelectIdentifier('sort_order')
_populateInterests: () =>
this._populateSelectIdentifier('interests')
_populateStudio: () =>
elem = $ '#musician-search-filter-builder select[name=studio_sessions]'
this._populateSelectWithInt(@profileUtils.studioMap, @searchFilter.data_blob.studio_sessions.toString(), elem)
_populateGigs: () =>
elem = $ '#musician-search-filter-builder select[name=concert_gigs]'
this._populateSelectWithInt(@profileUtils.gigMap, @searchFilter.data_blob.concert_gigs.toString(), elem)
_populateSkill: () =>
elem = $ '#musician-search-filter-builder select[name=skill_level]'
this._populateSelectWithInt(@profileUtils.skillLevelMap, @searchFilter.data_blob.skill_level.toString(), elem)
_populateAges: () =>
@screen.find('#search-filter-ages').empty()
ages_map = gon.musician_search_meta['ages']['map']
$.each gon.musician_search_meta['ages']['keys'], (index, key) =>
ageTemplate = @screen.find('#template-search-filter-setup-ages').html()
ageLabel = ages_map[key]
if 0 < @searchFilter.data_blob.ages.length
key_val = key.toString()
ageMatch = $.grep(@searchFilter.data_blob.ages, (n, i) ->
n == key_val)
selected = 'checked' if ageMatch.length > 0
ageHtml = context._.template(ageTemplate,
{ id: key
description: ageLabel
checked: selected},
{variable: 'data'})
@screen.find('#search-filter-ages').append ageHtml
_populateGenres: () =>
@screen.find('#search-filter-genres').empty()
@rest.getGenres().done (genres) =>
genreTemplate = @screen.find('#template-search-filter-setup-genres').html()
$.each genres, (index, genre) =>
if 0 < @searchFilter.data_blob.genres.length
genreMatch = $.grep(@searchFilter.data_blob.genres, (n, i) ->
n == genre.id)
else
genreMatch = []
selected = 'checked' if genreMatch.length > 0
genreHtml = context._.template(genreTemplate,
{ id: genre.id
description: genre.description
checked: selected },
{ variable: 'data' })
@screen.find('#search-filter-genres').append genreHtml
_populateInstruments: () =>
# TODO hydrate user's selection from json_store
@instrumentSelector.render(@screen.find('.session-instrumentlist'), [])
@instrumentSelector.setSelectedInstruments(@searchFilter.data_blob.instruments)
_builderSelectValue: (identifier) =>
elem = $ '#musician-search-filter-builder select[name='+identifier+']'
elem.val()
_builderSelectMultiValue: (identifier) =>
vals = []
elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked'
if 'instruments' == identifier
vals = @instrumentSelector.getSelectedInstruments()
else
elem.each (idx) ->
vals.push $(this).val()
vals
willSearch: (reload) =>
return false if @isSearching
@isSearching = true
if reload
@pageNumber = 1
@screen.find('#musician-search-filter-spinner').show()
@resultsListContainer.empty()
@screen.find('#musician-search-filter-builder').hide()
@screen.find('#musician-search-filter-results').show()
true
didSearch: (response) =>
this.loadSearchFilter(response.filter_json)
@searchResults = response
@screen.find('#musician-search-filter-spinner').hide()
this.renderMusicians()
@screen.find('.paginate-wait').hide()
@isSearching = false
resetFilter: () =>
if this.willSearch(true)
@rest.postMusicianSearchFilter({ filter: 'reset' }).done(this.didSearch)
cancelFilter: () =>
this.resetFilter()
getUserFilterResults: () =>
if this.willSearch(true)
@rest.getMusicianSearchFilter('results=true').done(this.didSearch)
performSearch: () =>
if this.willSearch(true)
filter = {}
$.each gon.musician_search_meta.filter_keys.single, (index, key) =>
filter[key] = this._builderSelectValue(key)
$.each gon.musician_search_meta.filter_keys.multi, (index, key) =>
filter[key] = this._builderSelectMultiValue(key)
@rest.postMusicianSearchFilter({ filter: JSON.stringify(filter), page: @pageNumber }).done(this.didSearch)
renderResultsHeader: () =>
if @searchResults.is_blank_filter
@screen.find('#btn-musician-search-reset').hide()
@screen.find('.musician-search-text').text('Click search button to look for musicians with similar interests, skill levels, etc.')
else
@screen.find('#btn-musician-search-reset').show()
@screen.find('.musician-search-text').text(@searchResults.summary)
renderMusicians: () =>
this.renderResultsHeader() if @pageNumber == 1
musicians = @searchResults.musicians
len = musicians.length
if 0 == len
@screen.find('#musician-search-filter-results-list-blank').show()
@screen.find('#musician-search-filter-results-list-blank').html('No results found')
return
else
@screen.find('#musician-search-filter-results-list-blank').hide()
ii = 0
mTemplate = @screen.find('#template-search-musician-row').html()
aTemplate = @screen.find('#template-search-musician-action-btns').html()
mVals = undefined
musician = undefined
renderings = ''
instr_logos = undefined
follows = undefined
followVals = undefined
aFollow = undefined
myAudioLatency = @searchResults.my_audio_latency
while ii < len
musician = musicians[ii]
if context.JK.currentUserId == musician.id
ii++
continue
instr_logos = ''
jj = 0
ilen = musician['instruments'].length
while jj < ilen
instr_id = musician['instruments'][jj].instrument_id
if instr_img = @instrument_logo_map[instr_id]
instr_logos += '<img height="24" width="24" src="' + instr_img.asset + '" title="' + instr_id + '"/>'
jj++
actionVals =
profile_url: '/client#/profile/' + musician.id
friend_class: 'button-' + (if musician['is_friend'] then 'grey' else 'orange')
friend_caption: (if musician.is_friend then 'DIS' else '') + 'CONNECT'
follow_class: 'button-' + (if musician['is_following'] then 'grey' else 'orange')
follow_caption: (if musician.is_following then 'UN' else '') + 'FOLLOW'
message_class: 'button-orange'
message_caption: 'MESSAGE'
button_message: 'button-orange'
musician_actions = context.JK.fillTemplate(aTemplate, actionVals)
latencyBadge = context._.template($("#template-account-session-latency").html(), $.extend(sessionUtils.createLatency(musician), musician), variable: 'data')
mVals =
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url)
profile_url: '/client#/profile/' + musician.id
musician_name: musician.name
musician_location: this._formatLocation(musician)
instruments: instr_logos
biography: musician['biography']
follow_count: musician['follow_count']
friend_count: musician['friend_count']
recording_count: musician['recording_count']
session_count: musician['session_count']
musician_id: musician['id']
musician_action_template: musician_actions
latency_badge: latencyBadge
musician_first_name: musician['first_name']
$rendering = $(context.JK.fillTemplate(mTemplate, mVals))
$offsetParent = @resultsListContainer.closest('.content')
data = entity_type: 'musician'
options =
positions: [
'top'
'bottom'
'right'
'left'
]
offsetParent: $offsetParent
scoreOptions = offsetParent: $offsetParent
context.JK.helpBubble($('.follower-count', $rendering), 'follower-count', data, options);
context.JK.helpBubble($('.friend-count', $rendering), 'friend-count', data, options);
context.JK.helpBubble($('.recording-count', $rendering), 'recording-count', data, options);
context.JK.helpBubble($('.session-count', $rendering), 'session-count', data, options);
@helpBubble.scoreBreakdown $('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions
@resultsListContainer.append $rendering
$rendering.find('.biography').dotdotdot()
ii++
this._bindMessageMusician()
this._bindFriendMusician()
this._bindFollowMusician()
context.JK.bindHoverEvents()
return
_bindMessageMusician: () =>
objThis = this
@screen.find('.search-m-message').on 'click', (evt) ->
userId = $(this).parent().data('musician-id')
objThis.app.layout.showDialog 'text-message', d1: userId
return false
_bindFriendMusician: () =>
objThis = this
@screen.find('.search-m-friend').on 'click', (evt) =>
# if the musician is already a friend, remove the button-orange class, and prevent the link from working
$self = $(evt.target)
if 0 == $self.closest('.button-orange').size()
return false
logger.debug("evt.target", evt.target)
$self.click (ee) ->
ee.preventDefault()
return false
evt.stopPropagation()
uid = $self.parent().data('musician-id')
objThis.rest.sendFriendRequest objThis.app, uid, this.friendRequestCallback
@app.notify({text: 'A friend request has been sent.'})
return false
_bindFollowMusician: () =>
objThis = this
@screen.find('.search-m-follow').on 'click', (evt) ->
# if the musician is already followed, remove the button-orange class, and prevent the link from working
if 0 == $(this).closest('.button-orange').size()
return false
$(this).click (ee) ->
ee.preventDefault()
return false
evt.stopPropagation()
newFollowing = {}
newFollowing.user_id = $(this).parent().data('musician-id')
url = '/api/users/' + context.JK.currentUserId + '/followings'
$.ajax
type: 'POST'
dataType: 'json'
contentType: 'application/json'
url: url
data: JSON.stringify(newFollowing)
processData: false
success: (response) ->
# remove the orange look to indicate it's not selectable
# @FIXME -- this will need to be tweaked when we allow unfollowing
objThis.screen.find('div[data-musician-id=' + newFollowing.user_id + '] .search-m-follow').removeClass('button-orange').addClass 'button-grey'
return false
error: objThis.app.ajaxError
return false
_formatLocation: (musician) ->
if musician.city and musician.state
musician.city + ', ' + musician.state
else if musician.city
musician.city
else if musician.regionname
musician.regionname
else
'Location Unavailable'
friendRequestCallback: (user_id)=>
# TODO:
paginate: () =>
if @pageNumber < @searchResults.page_count && this.willSearch(false)
@screen.find('.paginate-wait').show()
@pageNumber += 1
@rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter.data_blob), page: @pageNumber }).done(this.didSearch)
return true
false
registerResultsPagination: () =>
_resultsListContainer = @resultsListContainer
_headerHeight = @screen.find('#musician-search-filter-results-header').height()
_paginator = this.paginate
@screen.find('.content-body-scroller').scroll ->
if _resultsListContainer.is(':visible')
jthis = $(this)
wintop = jthis.scrollTop()
winheight = jthis.innerHeight()
docheight = jthis[0].scrollHeight - _headerHeight
scrollTrigger = 0.98;
if ((wintop / (docheight - winheight)) >= scrollTrigger)
_paginator()

View File

@ -1,6 +1,6 @@
@import "client/common.css.scss";
#band-setup, #band-profile {
#band-setup, #band-profile, #bands-filter-to_hire, #bands-filter-to_join {
font-family: Raleway, Arial, Helvetica, verdana, arial, sans-serif;
.band-field {
input[type="text"], select, textarea {
@ -495,10 +495,178 @@
label {
font-size: 1.05em;
}
}
label.strong-label {
font-weight: bold;
font-size: 1.1em;
#bands-screen {
.col-left {
float: left;
width: 50%;
margin-left: auto;
margin-right: auto;
}
.col-right {
float: right;
width: 50%;
margin-left: auto;
margin-right: auto;
}
#band-search-filter-spinner {
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 40px;
}
#band-search-filter-results-list {
.profile-band-list-result {
padding-top: 5px;
}
}
.builder-section {
padding: 10px;
margin-bottom: 20px;
}
.builder-sort-order {
.text-label {
float: right;
margin-right: 10px;
padding-top: 5px;
}
.sort-order-selector {
float: right;
.easydropdown-wrapper {
width: 140px;
}
}
}
#band-search-filter-builder {
margin-left: 5px;
}
.band-search-filter-builder-bottom {
text-align: right;
#btn-perform-band-search {
width: 120px;
margin-left: 20px;
}
}
tr:nth-child(even) td {
padding-bottom: 0;
}
#band-search-filter-results-header {
padding: 10px 10px 10px 10px;
}
#band-search-filter-description {
padding: 5px 5px 5px 5px;
display: inline;
margin-left: auto;
margin-right: auto;
}
#btn-band-search-builder {
float: left;
margin-right: 20px;
}
#btn-band-search-reset {
float: right;
margin-left: 20px;
}
#bands-filter-to_hire {
width: 100%;
.builder-selector {
margin-top: 10px;
}
#max_cost_amount {
width: 60px;
}
.col-left {
float: left;
width: 45%;
margin-left: auto;
margin-right: auto;
padding-right: 20px;
}
.col-right {
float: right;
width: 45%;
margin-left: auto;
margin-right: auto;
.easydropdown-wrapper {
width: 100%;
}
}
}
#bands-filter-to_join {
width: 100%;
.builder-selector {
margin-top: 10px;
}
.lhs {
float: left;
width: 40%;
.easydropdown-wrapper {
width: 100%;
}
}
.rhs {
float: right;
width: 40%;
.easydropdown-wrapper {
width: 100%;
}
}
.col-left {
float: left;
width: 30%;
margin-left: auto;
margin-right: auto;
padding-right: 20px;
.easydropdown-wrapper {
width: 100%;
}
}
.col-right {
float: right;
width: 66%;
margin-left: auto;
margin-right: auto;
}
.search-filter-setup-genres {
}
.band-setup-genres {
.easydropdown-wrapper {
width: 100%;
}
}
.builder-section {
padding: 10px;
margin-bottom: 20px;
}
}
}

View File

@ -254,15 +254,15 @@
.col-left {
@include border_box_sizing;
float: left;
width: 33%;
width: 45%;
margin-left: auto;
margin-right: auto;
padding-right: 20px;
}
.col-right {
float: right;
width: 67%;
@include border_box_sizing;
width: 45%;
margin-left: auto;
margin-right: auto;
@ -306,6 +306,14 @@
}
.builder-ages {
width: 100%;
height: 100%;
background-color:#c5c5c5;
border:none;
-webkit-box-shadow: inset 2px 2px 3px 0px #888;
box-shadow: inset 2px 2px 3px 0px #888;
color:#000;
overflow:auto;
font-size:12px;
}
.builder-instruments {
width: 100%;
@ -321,6 +329,21 @@
}
}
.musician-setup-genres {
width:100%;
height:200px;
background-color:#c5c5c5;
border:none;
-webkit-box-shadow: inset 2px 2px 3px 0px #888;
box-shadow: inset 2px 2px 3px 0px #888;
color:#000;
overflow:auto;
font-size:12px;
.easydropdown-wrapper {
width: 100%;
}
}
}
.filter-element {

View File

@ -7,7 +7,7 @@ class ApiSearchController < ApiController
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
query = params.clone
query = parasobj.clone
query[:remote_ip] = request.remote_ip
if 1 == query[Search::PARAM_MUSICIAN].to_i
@search = Search.musician_filter(query, current_user)
@ -32,13 +32,35 @@ class ApiSearchController < ApiController
end
elsif request.post?
ms = MusicianSearch.user_search_filter(current_user)
sobj = MusicianSearch.user_search_filter(current_user)
filter = params[:filter]
if filter == 'reset'
@search = ms.reset_search_results
@search = sobj.reset_search_results
else
json = JSON.parse(filter, :create_additions => false)
@search = ms.search_results_page(json, [params[:page].to_i, 1].max)
@search = sobj.search_results_page(json, [params[:page].to_i, 1].max)
end
respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/index'
end
end
def bands
if request.get?
if params[:results]
@search = BandSearch.user_search_filter(current_user).search_results_page(params[:subtype])
respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/index'
else
render :json => BandSearch.search_filter_json(current_user, params[:subtype]), :status => 200
end
elsif request.post?
sobj = BandSearch.user_search_filter(current_user)
filter = params[:filter]
if filter == 'reset'
@search = sobj.reset_search_results(params[:subtype])
else
json = JSON.parse(filter, :create_additions => false)
@search = sobj.search_results_page(params[:subtype], json, [params[:page].to_i, 1].max)
end
respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/index'
end

View File

@ -69,7 +69,12 @@ class SpikesController < ApplicationController
end
def musician_search_filter
# gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_META
# gon.musician_search_meta = MusicianSearch.search_filter_meta
render :layout => 'web'
end
def band_search_filter
gon.band_search_meta = BandSearch.search_filter_meta
render :layout => 'web'
end

View File

@ -67,7 +67,8 @@ module ClientHelper
gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration
gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients
gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency
gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_META
gon.musician_search_meta = MusicianSearch.search_filter_meta
gon.band_search_meta = BandSearch.search_filter_meta
# is this the native client or browser?
@nativeClient = is_native_client?

View File

@ -1,8 +1,6 @@
object @search
node :search_type do |ss| ss.search_type end
if @search.is_a?(MusicianSearch)
if @search.is_a?(BaseSearch)
node :page_count do |foo|
@search.page_count
@ -12,18 +10,19 @@ if @search.is_a?(MusicianSearch)
current_user.last_jam_audio_latency.round if current_user.last_jam_audio_latency
end
node :is_blank_filter do |foo|
@search.is_blank?
end
node :filter_json do |foo|
@search.to_json
end
node :summary do |foo|
@search.description
end
if @search.is_a? MusicianSearch
node :description do |foo|
@search.description
end
node :is_blank_filter do |foo|
@search.is_blank?
end
child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score
@ -53,8 +52,8 @@ if @search.is_a?(MusicianSearch)
node :name do |uu| uu.name end
end
node :follow_count do |musician| @search.follow_count(musician) end
node :friend_count do |musician| @search.friend_count(musician) end
node :follow_count do |musician| @search.follow_count(musician) end
node :recording_count do |musician| @search.record_count(musician) end
node :session_count do |musician| @search.session_count(musician) end
@ -62,8 +61,49 @@ if @search.is_a?(MusicianSearch)
last_jam_audio_latency(musician)
end
}
elsif @search.is_a?(BandSearch)
node :is_blank_filter do |foo|
@search.is_blank?(params[:subtype])
end
node :description do |foo|
@search.description(params[:subtype])
end
child(:results => :bands) {
attributes :id, :name, :city, :state, :country, :photo_url, :biography, :logo_url, :website
node :is_following do |band|
@search.is_follower?(band)
end
node :biography do |band|
band.biography.nil? ? "" : band.biography
end
child :genres => :genres do
attributes :genre_id, :description
end
child :users => :players do |pl|
node :user_id do |uu| uu.id end
node :photo_url do |uu| uu.photo_url end
node :name do |uu| uu.name end
node :instruments do |uu| uu.instruments.map(&:id).join(',') end
end
node :follow_count do |band| @search.follow_count(band) end
node :recording_count do |band| @search.record_count(band) end
node :session_count do |band| @search.session_count(band) end
}
end
else
node :search_type do |ss| ss.search_type end
if @search.session_invite_search?
child(:results => :suggestions) {
node :value do |uu| uu.name end

View File

@ -0,0 +1,216 @@
.content-body-scroller
div#band-search-filter-builder.content-wrapper
div#band-search-filter-results.content-wrapper
div#band-search-filter-results-header
#band-search-filter-results-blank
a.button-orange#btn-band-search-builder-to_join href="#" SEARCH FOR BAND TO JOIN
|&nbsp;-&nbsp;or&nbsp;-&nbsp;
a.button-orange#btn-band-search-builder-to_hire href="#" SEARCH FOR BAND TO HIRE
#band-search-filter-results-filtered
a.button-orange#btn-band-search-builder href="#" SEARCH
a.button-grey#btn-band-search-reset href="#" RESET
div#band-search-filter-description
div.clearall
div#band-search-filter-spinner.spinner-large
div#band-search-filter-results-wrapper
div#band-search-filter-results-list-blank
div.content-wrapper#band-search-filter-results-list
div.paginate-wait
Fetching more results...
div.spinner-small
script#template-band-search-filter-to_join type="text/template"
#bands-filter-to_join
.band-search-filter-builder-top.builder-section
.col-left
h2 search bands
/ .col-right.builder-sort-order
/ .sort-order-selector
/ select.easydropdown name="sort_order"
/ option selected="selected" value="{sort_order}" {sort_order}
/ .text-label Sort Results By:
.clearall
.band-search-filter-builder-middle1.builder-section
.col-left
.field
label for="search-filter-genres" Genres:
.search-filter-setup-genres.band-setup-genres
table#search-filter-genres cellpadding="10" cellspacing="6" width="100%"
.col-right
.field
label for="search-filter-instruments"
| Looking for New Members with These Skills:
.search-filter-setup-instruments.band-setup-genres.builder-instruments
table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%"
.clearall
.band-search-filter-builder-middle2.builder-section
.col-left
.field.builder-selector
label Type:
select.easydropdown name="band_type"
option selected="selected" value="{band_type}" {band_type}
.field.builder-selector
label Play Commitment:
select.easydropdown name="play_commitment"
option selected="selected" value="{play_commitment}" {play_commitment}
.col-right
.field.builder-selector
.lhs
label Status:
select.easydropdown name="band_status"
option selected="selected" value="{band_status}" {band_status}
.field.builder-selector
.rhs
label Concert Gigs Played:
select.easydropdown name="concert_gigs"
option selected="selected" value="{concert_gigs}" {concert_gigs}
.clearall
.field.builder-selector
.lhs
label Touring Option:
select.easydropdown name="touring_option"
option selected="selected" value="{touring_option}" {touring_option}
.clearall
.clearall
.band-search-filter-builder-bottom.builder-section.builder-action-buttons
.col-right
a#btn-band-search-cancel.builder-button.button-grey href="#" CANCEL
a#btn-perform-band-search.builder-button.button-orange href="#" SEARCH
script#template-band-search-filter-to_hire type="text/template"
#bands-filter-to_hire
.band-search-filter-builder-top-to_hire.builder-section
.col-left
h2 search bands to hire
.col-right.builder-sort-order
.sort-order-selector
select.easydropdown name="sort_order"
option selected="selected" value="{sort_order}" {sort_order}
.text-label Sort Results By:
.clearall
.band-search-filter-builder-middle1.builder-section
.col-left
.field
label for="search-filter-genres" Genres:
.search-filter-setup-genres.band-setup-genres
table#search-filter-genres cellpadding="10" cellspacing="6" width="100%"
.col-right
.field.builder-selector
label Status:
select.easydropdown name="band_status"
option selected="selected" value="{band_status}" {band_status}
.field.builder-selector
label Concert Gigs Played:
select.easydropdown name="concert_gigs"
option selected="selected" value="{concert_gigs}" {concert_gigs}
.field.builder-selector
label Performance Sample Available:
select.easydropdown name="performance_samples"
option selected="selected" value="{performance_samples}" {performance_samples}
.clearall
.band-search-filter-builder-middle2.builder-section
.field.builder-selector
<input type="checkbox" id="has_max_cost" {has_max_cost} >&nbsp;Find bands to play a paid gig at a cost not to exceed&nbsp;&nbsp;$
input type="text" id="max_cost_amount" name="max_cost" value="{max_cost}"
.field.builder-selector
<input type="checkbox" id="free_gigs" name="free_gigs" {has_free_gigs} >&nbsp;Find bands that will play free gigs
.clearall
.clearall
.band-search-filter-builder-bottom.builder-section.builder-action-buttons
.col-right
a#btn-band-search-cancel.builder-button.button-grey href="#" CANCEL
a#btn-perform-band-search.builder-button.button-orange href="#" SEARCH
script#template-search-filter-setup-instrument type="text/template"
tr data-instrument-id="{id}"
td <input type="checkbox" {checked} />{description}
td align="right" width="50%"
select.proficiency_selector name="proficiency"
option value="1" Beginner
option value="2" Intermediate
option value="3" Expert
script#template-search-filter-setup-genres type="text/template"
tr
td <input value="{id}" {checked} type="checkbox" />{description}
script#template-find-band-row type="text/template"
.profile-band-list-result.band-list-result
.f11
.left style="width:63px;margin-top:-12px;"
.avatar-small
img src="{avatar_url}" /
.right.mband-players style="width:265px; margin-top:-4px;"
table.musicians cellpadding="0" cellspacing="5"
| {band_player_template}
div style="margin-left: 63px; margin-right: 275px;margin-top: 12px;"
.first-row data-hint="top-row"
.lcol.left
.result-name
| {band_name}
.result-location
| {band_location}
br
#result_genres.nowrap.mt10
| {genres}
.whitespace
.biography
| {biography}
.clearleft
.button-row
.lcol.stats.left
span.follower-count
| {follow_count}
img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" style="margin-right:4px;"
span.recording-count
| {recording_count}
img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" style="margin-right:4px;"
span.session-count
| {session_count}
img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle"
.clearall
.result-list-button-wrapper data-band-id="{band_id}"
| {band_action_template}
.clearall
script#template-band-action-btns type="text/template"
a.button-orange.smallbutton href="{profile_url}" PROFILE
a.smallbutton.search-m-follow href="#" class="{button_follow}" FOLLOW
script#template-band-edit-btns type="text/template"
a.button-orange.smallbutton href="{profile_url}" PROFILE
a.button-orange.smallbutton href="{band_edit_url}" EDIT BAND
a.button-orange.smallbutton href="{band_member_url}" INVITE
script#template-band-player-info type="text/template"
tr
td
a.avatar-tiny href="{profile_url}" user-id="{user_id}" hoveraction="musician"
img src="{avatar_url}"
td style="padding: 0 4px;width:88px;"
a user-id="{user_id}" hoveraction="musician" href="{profile_url}"
strong
| {player_name}
td.instruments
| {player_instruments}

View File

@ -1,86 +0,0 @@
<!-- Band Screen -->
<%= content_tag(:div, :layout => 'screen', 'layout-id' => 'bands', :class => "screen secondary", :id => "bands-screen") do -%>
<%= content_tag(:div, :class => :content) do -%>
<%= content_tag(:div, :class => 'content-head') do -%>
<%= content_tag(:div, image_tag("content/icon_bands.png", {:height => 19, :width => 19}), :class => 'content-icon') %>
<%= content_tag(:h1, 'bands') %>
<%= render "screen_navigation" %>
<% end -%>
<%= content_tag(:div, :class => 'content-body') do -%>
<%= form_tag('', {:id => 'find-band-form', :class => 'inner-content'}) do -%>
<%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_BAND}) %>
<%= content_tag(:div, :class => 'filter-body') do %>
<%= content_tag(:div, :class => 'content-body-scroller') do -%>
<%= content_tag(:div, content_tag(:div, '', :id => 'band-filter-results', :class => 'filter-results'), :class => 'content-wrapper') %>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
<!-- Session Row Template -->
<script type="text/template" id="template-find-band-row"><!-- -->
<div class="profile-band-list-result band-list-result">
<div class="f11">
<div class="left" style="width:63px;margin-top:-12px;">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>
</div>
<div class="right mband-players" style="width:265px; margin-top:-4px;">
<table class="musicians" cellpadding="0" cellspacing="5">{band_player_template}</table>
</div>
<div class="" style="margin-left: 63px; margin-right: 275px;margin-top: 12px;">
<div class="first-row" data-hint="top-row">
<div class="lcol left">
<!-- name & location -->
<div class="result-name">{band_name}</div>
<div class="result-location">{band_location}</div>
<br />
<div id="result_genres" class="nowrap mt10">{genres}</div>
</div>
<div class="whitespace">
<div class="biography">{biography}</div>
</div>
<div class="clearleft"></div>
</div>
<div class="button-row">
<div class="lcol stats left">
<span class="follower-count">{follow_count} <img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" style="margin-right:4px;"/></span>
<span class="recording-count">{recording_count} <img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" style="margin-right:4px;"/></span>
<span class="session-count">{session_count} <img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" /></span>
</div>
<div class="clearall"></div>
<div class="result-list-button-wrapper" data-band-id={band_id}>
{band_action_template}
</div>
<div class="clearall"></div>
</div>
</div>
</div>
</div>
</script>
<script type="text/template" id="template-band-action-btns">
<a href="{profile_url}" class="button-orange smallbutton">PROFILE</a>
<a href="#" class="{button_follow} smallbutton search-m-follow">FOLLOW</a>
</script>
<script type="text/template" id="template-band-edit-btns">
<a href="{profile_url}" class="button-orange smallbutton">PROFILE</a>
<a href="{band_edit_url}" class="button-orange smallbutton">EDIT BAND</a>
<a href="{band_member_url}" class="button-orange smallbutton">INVITE</a>
</script>
<script type="text/template" id="template-band-player-info">
<tr>
<td>
<a href="{profile_url}" user-id="{user_id}" hoveraction="musician" class="avatar-tiny"><img src="{avatar_url}" /></a>
</td>
<td style="padding: 0 4px;width:88px;">
<a user-id="{user_id}" hoveraction="musician" href="{profile_url}"><strong>{player_name}</strong></a>
</td>
<td class="instruments">{player_instruments}</td>
</tr>
</script>

View File

@ -0,0 +1,140 @@
#bands-screen.screen.secondary layout="screen" layout-id="bands"
.content
.content-head
.content-icon
img alt="Icon_bands" height="19" src="/assets/content/icon_bands.png" width="19" /
h1 bands
= render "screen_navigation"
.content-body
= render "clients/band_search_filter"
script#template-band-search-filter-to_join type="text/template"
#bands-filter-to_join
#band-search-filter-builder-top.builder-section
.col-left
h2 search bands
.col-right.builder-sort-order
.text-label Sort Results By:
select.easydropdown name="sort_order"
option selected="selected" value="{sort_order}" {sort_order}
.clearall
#band-search-filter-builder-middle1.builder-section
.col-left
.field
label for="search-filter-genres" Genres:
.search-filter-setup-genres.band-setup-genres
table#search-filter-genres cellpadding="10" cellspacing="6" width="100%"
.col-right
.field
label for="search-filter-instruments"
| Instruments &amp; Skill Level:
.search-filter-setup-instruments.band-setup-genres.builder-instruments
table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%"
.clearall
#band-search-filter-builder-middle2.builder-section
.col-left
.field.builder-selector
label Type:
select.easydropdown name="band_type"
option selected="selected" value="{band_type}" {band_type}
.field.builder-selector
label Play Commitment:
select.easydropdown name="play_commit"
option selected="selected" value="{play_commit}" {play_commit}
.col-right
.field.builder-selector
label Status:
select.easydropdown name="skill_level"
option selected="selected" value="{skill_level}" {skill_level}
.field.builder-selector
label Concert Gigs Played:
select.easydropdown name="concert_gigs"
option selected="selected" value="{concert_gigs}" {concert_gigs}
.field.builder-selector
label Touring Option:
select.easydropdown name="tour_option"
option selected="selected" value="{tour_option}" {tour_option}
.clearall
.clearall
#band-search-filter-builder-bottom.builder-section.builder-action-buttons
.col-right
a#btn-perform-band-search.builder-button.button-orange href="#" SEARCH
a#btn-band-search-cancel.builder-button.button-grey href="#" CANCEL
/! Session Row Template
script#template-search-band-row type="text/template"
.profile-band-list-result.band-list-result
.f11
.left style="width:63px;margin-top:-12px;"
/! avatar
.avatar-small
img src="{avatar_url}" /
.right.mband-players style="width:265px; margin-top:-4px;"
table.musicians cellpadding="0" cellspacing="5"
| {band_player_template}
div style="margin-left: 63px; margin-right: 275px;margin-top: 12px;"
.first-row data-hint="top-row"
.lcol.left
/! name and location
.result-name
| {band_name}
.result-location
| {band_location}
br /
#result_genres.nowrap.mt10
| {genres}
.whitespace
.biography
| {biography}
.clearleft
.button-row
.lcol.stats.left
span.follower-count
| {follow_count}
img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" style="margin-right:4px;" /
span.recording-count
| {recording_count}
img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" style="margin-right:4px;" /
span.session-count
| {session_count}
img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" /
.clearall
.result-list-button-wrapper data-band-id="{band_id}"
| {band_action_template}
.clearall
script#template-search-band-action-btns type="text/template"
a.button-orange smallbutton href="{profile_url}"
PROFILE
a class="{button_follow} smallbutton search-m-follow" href="#"
FOLLOW
script#template-search-band-edit-btns type="text/template"
a href="{profile_url}" class="button-orange smallbutton"
PROFILE
a href="{band_edit_url}" class="button-orange smallbutton"
EDIT BAND
a href="{band_member_url}" class="button-orange smallbutton"
INVITE
script#template-search-band-player-info type="text/template"
tr
td
a.avatar-tiny href="{profile_url}" user-id="{user_id}" hoveraction="musician"
img src="{avatar_url}" /
td style="padding: 0 4px;width:88px"
a user-id="{user_id}" hoveraction="musician" href="{profile_url}"
strong
| {player_name}
td.instruments
| {player_instruments}

View File

@ -4,7 +4,6 @@
div#musician-search-filter-results.content-wrapper
div#musician-search-filter-results-header
a#btn-musician-search-builder.button-orange href="#" SEARCH
span.musician-search-text Click search button to look for musicians with similar interests, skill levels, etc.
a#btn-musician-search-reset.button-grey href="#" RESET
div#musician-search-filter-description
div.clearall
@ -22,51 +21,51 @@ script#template-musician-search-filter type="text/template"
.col-left
h2 search musicians
.col-right.builder-sort-order
.text-label Sort Results By:
select.easydropdown name="sort_order"
option selected="selected" value="{sort_order}" {sort_order}
.text-label Sort Results By:
select.easydropdown name="sort_order"
option selected="selected" value="{sort_order}" {sort_order}
.clearall
#musician-search-filter-builder-middle.builder-section
.col-left
.field
label for="search-filter-genres" Genres
.search-filter-setup-genres.band-setup-genres
#search-filter-genres
label for="search-filter-genres" Genres:
.search-filter-setup-genres.musician-setup-genres
table#search-filter-genres cellpadding="10" cellspacing="6" width="100%"
.field.builder-selector
label Interests
label Interests:
select.easydropdown name="interests"
option selected="selected" value="{interests}" {interests}
.field.builder-selector
label Studio Sessions Played
label Studio Sessions Played:
select.easydropdown name="studio_sessions"
option selected="selected" value="{studio_sessions}" {studio_sessions}
.col-right
.field
label for="search-filter-instruments"
| Instruments &amp; Skill Level
.search-filter-setup-instruments.band-setup-genres.builder-instruments.session-instrumentlist
| Instruments &amp; Skill Level:
.search-filter-setup-instruments.musician-setup-genres.builder-instruments
table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%"
.col-left
.field.builder-selector
label Status
label Status:
select.easydropdown name="skill_level"
option selected="selected" value="{skill_level}" {skill_label}
.field.builder-selector
label Concert Gigs Played
label Concert Gigs Played:
select.easydropdown name="concert_gigs"
option selected="selected" value="{concert_gigs}" {concert_gigs}
.col-right
.field.builder-selector
label for="search-filter-ages" Ages
.search-filter-setup-ages.band-setup-genres.builder-ages
#search-filter-ages
label for="search-filter-ages" Ages:
.search-filter-setup-ages.builder-ages
table#search-filter-ages cellpadding="10" cellspacing="6" width="100%"
.clearall
.clearall
@ -85,25 +84,13 @@ script#template-search-filter-setup-instrument type="text/template"
option value="2" Intermediate
option value="3" Expert
script#template-search-filter-setup-genres type="text/template"
.genre-option
| {% if(data.checked) { %}
input value="{{data.id}}" checked="checked" type="checkbox"
| {% } else { %}
input value="{{data.id}}" type="checkbox"
| {% } %}
label
| {{data.description}}
script#template-search-filter-setup-genres type="text/template"
tr
td <input value="{id}" {checked} type="checkbox" />{description}
script#template-search-filter-setup-ages type="text/template"
.age-option
| {% if(data.checked) { %}
input value="{{data.id}}" checked="checked" type="checkbox"
| {% } else { %}
input value="{{data.id}}" type="checkbox"
| {% } %}
label
| {{data.description}}
script#template-search-filter-setup-ages type="text/template"
tr
td <input value="{id}" {checked} type="checkbox" />{description}
/! Session Row Template
script#template-search-musician-row type="text/template"

View File

@ -310,21 +310,17 @@
// var findMusicianScreen = new JK.FindMusicianScreen(JK.app);
//findMusicianScreen.initialize(JK.TextMessageDialogInstance);
var findBandScreen = new JK.BandSearchFilter();
findBandScreen.init(JK.app);
var redeemSignUpScreen = new JK.RedeemSignUpScreen(JK.app);
redeemSignUpScreen.initialize();
var redeemCompleteScreen = new JK.RedeemCompleteScreen(JK.app);
redeemCompleteScreen.initialize();
var findBandScreen = new JK.FindBandScreen(JK.app);
findBandScreen.initialize();
//var sessionScreen = new JK.SessionScreen(JK.app);
//sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog, JK.FriendSelectorDialogInstance);
AppActions.appInit.trigger(JK.app)
var addNewGearDialog = new JK.AddNewGearDialog(JK.app);
addNewGearDialog.initialize();

View File

@ -0,0 +1,15 @@
= javascript_include_tag "profile_utils"
= javascript_include_tag "member_search_filter"
= stylesheet_link_tag "client/band"
#bands-screen
= render "clients/band_search_filter"
javascript:
var initialized = false;
$(document).on('JAMKAZAM_READY', function(e, data) {
setTimeout(function() {
window.band_search_filter = new JK.BandSearchFilter();
band_search_filter.init(data.app);
band_search_filter.afterShow();
}, 1)
});

View File

@ -1,15 +1,15 @@
= javascript_include_tag "profile_utils"
= javascript_include_tag "musician_search_filter"
= javascript_include_tag "member_search_filter"
= stylesheet_link_tag "client/musician"
#musician_search_spike
= render "clients/musician_search_filter"
#musicians-screen
= render "clients/musician_search_filter"
javascript:
var initialized = false;
$(document).on('JAMKAZAM_READY', function(e, data) {
setTimeout(function() {
window.musician_search_filter = new JK.MusicianSearchFilter();
musician_search_filter.init();
musician_search_filter.init(data.app);
musician_search_filter.afterShow();
}, 1)
});

View File

@ -118,6 +118,7 @@ SampleApp::Application.routes.draw do
match '/site_validate', to: 'spikes#site_validate'
match '/recording_source', to: 'spikes#recording_source'
match '/musician_search_filter', to: 'spikes#musician_search_filter'
match '/band_search_filter', to: 'spikes#band_search_filter'
# junk pages
match '/help', to: 'static_pages#help'
@ -478,6 +479,7 @@ SampleApp::Application.routes.draw do
# search
match '/search' => 'api_search#index', :via => :get
match '/search/musicians' => 'api_search#musicians', :via => [:get, :post]
match '/search/bands' => 'api_search#bands', :via => [:get, :post]
# join requests
match '/join_requests/:id' => 'api_join_requests#show', :via => :get, :as => 'api_join_request_detail'

View File

@ -139,7 +139,7 @@ end
def make_band_members
Band.find_each do |bb|
User.order('RANDOM()').limit(4).each do |uu|
BandMusician.create!({:user_id => uu.id, :band_id => bb.id})
BandMusician.create({:user_id => uu.id, :band_id => bb.id})
end
end
end