Merge branch 'develop' into feature/sprint_12_work

This commit is contained in:
Brian Smith 2014-09-23 21:27:52 -04:00
commit 3f1eb86684
75 changed files with 2672 additions and 845 deletions

View File

@ -70,6 +70,14 @@ module JamRuby
self.music_sessions.size
end
def latitude
lat
end
def longitude
lng
end
def recent_history
recordings = Recording
.joins(:claimed_recordings)

View File

@ -54,6 +54,19 @@ module JamRuby
{city: city, state: state, country: country, addr: addr, locidispid: (locid.nil? || ispid.nil?) ? nil : Score.compute_locidispid(locid, ispid) }
end
# returns a display- friendly bit of info about this location
def info
country_model = Country.where(countrycode: countrycode).first
region_model = Region.where(region: region, countrycode: countrycode).first
{
countrycode: countrycode,
country: country_model ? country_model.countryname : nil,
regioncode: region,
region: region_model ? region_model.regionname : nil,
city: city
}
end
def self.createx(locid, countrycode, region, city, postalcode, latitude, longitude, metrocode, areacode)
c = connection.raw_connection
c.exec_params("insert into #{self.table_name} (locid, countrycode, region, city, postalcode, latitude, longitude, metrocode, areacode, geog) values($1, $2, $3, $4, $5, $6, $7, $8, $9, ST_SetSRID(ST_MakePoint($7, $6), 4326)::geography)",
@ -82,8 +95,8 @@ module JamRuby
# it isn't reasonable for both to be 0...
latlng = [geo.latitude, geo.longitude]
end
elsif current_user and current_user.locidispid and current_user.locidispid != 0
location = GeoIpLocations.find_by_locid(current_user.locidispid/1000000)
elsif current_user and current_user.last_jam_locidispid and current_user.last_jam_locidispid != 0
location = GeoIpLocations.find_by_locid(current_user.last_jam_locidispid/1000000)
if location and location.latitude and location.longitude and (location.latitude != 0 or location.longitude != 0)
# it isn't reasonable for both to be 0...
latlng = [location.latitude, location.longitude]
@ -99,7 +112,7 @@ module JamRuby
end
if latlng
relation = relation.where(['latitude IS NOT NULL AND longitude IS NOT NULL']).within(distance, origin: latlng)
relation = relation.where(['lat IS NOT NULL AND lng IS NOT NULL']).within(distance, origin: latlng)
end
end
relation

View File

@ -99,12 +99,13 @@ module JamRuby
M_ORDER_PLAYS = ['Most Plays', :plays]
M_ORDER_PLAYING = ['Playing Now', :playing]
M_ORDER_LATENCY = ['Latency To Me', :latency]
M_ORDERINGS = [M_ORDER_LATENCY, M_ORDER_FOLLOWS, M_ORDER_PLAYS]
M_ORDER_DISTANCE = ['Distance To Me', :distance]
M_ORDERINGS = [M_ORDER_LATENCY, M_ORDER_DISTANCE, M_ORDER_FOLLOWS, M_ORDER_PLAYS]
ORDERINGS = B_ORDERINGS = [M_ORDER_FOLLOWS, M_ORDER_PLAYS, M_ORDER_PLAYING]
M_ORDERING_KEYS = M_ORDERINGS.collect { |oo| oo[1] }
B_ORDERING_KEYS = B_ORDERINGS.collect { |oo| oo[1] }
DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [['Any', 0], [1000.to_s, 1000], [500.to_s, 500], [250.to_s, 250], [100.to_s, 100], [50.to_s, 50], [25.to_s, 25]]
DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [[25.to_s, 25], [50.to_s, 50], [100.to_s, 100], [250.to_s, 250], [500.to_s, 500], [1000.to_s, 1000] ]
# the values for score ranges are raw roundtrip scores. david often talks of one way scores (<= 20 is good), but
# the client reports scores as roundtrip and the server uses those values throughout
@ -117,6 +118,7 @@ module JamRuby
ANY_SCORE = ''
M_SCORE_OPTS = [['Any', ANY_SCORE], ['Good', GOOD_SCORE], ['Moderate', MODERATE_SCORE], ['Poor', POOR_SCORE], ['Unacceptable', UNACCEPTABLE_SCORE]]
M_SCORE_DEFAULT = ANY_SCORE
M_DISTANCE_DEFAULT = 500
F_SORT_RECENT = ['Most Recent', :date]
F_SORT_OLDEST = ['Most Liked', :likes]
@ -148,10 +150,7 @@ module JamRuby
# distance - defunct!
# city - defunct!
# remote_ip - defunct!
def self.musician_filter(params={}, user=nil, conn=nil)
# puts "================ params #{params.inspect}"
# puts "================ user #{user.inspect}"
# puts "================ conn #{conn.inspect}"
def self.musician_filter(params={}, user=nil)
rel = User.musicians # not musicians_geocoded on purpose; we allow 'unknowns' to surface in the search page
rel = rel.select('users.*')
@ -173,13 +172,25 @@ module JamRuby
score_limit = l
end
# puts "================ score_limit #{score_limit}"
locidispid = user.nil? ? 0 : (user.last_jam_locidispid || 0)
locidispid = ((conn and conn.client_type == 'client') ? conn.locidispid : ((user and user.musician) ? user.last_jam_locidispid : nil))
# user can override their location with these 3 values
country = params[:country]
region = params[:region]
city = params[:city]
# puts "================ locidispid #{locidispid}"
my_locid = nil # this is used for distance searches only
unless locidispid.nil?
if country && region && city
geoiplocation = GeoIpLocations.where(countrycode: country, region: region, city: city).first
my_locid = geoiplocation.locid
end
unless my_locid
my_locid = locidispid/1000000 # if the user didn't specify a location to search on, user their account locidispid
end
if !locidispid.nil? && !user.nil?
# score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad
# this is ANY_SCORE:
score_join = 'left outer' # or 'inner'
@ -229,10 +240,18 @@ module JamRuby
end
ordering = self.order_param(params)
# puts "================ ordering #{ordering}"
case ordering
when :latency
# nothing to do. the sort added below 'current_scores.score ASC NULLS LAST' handles this
when :distance
# convert miles to meters for PostGIS functions
miles = params[:distance].blank? ? 500 : params[:distance].to_i
meters = miles * 1609.34
rel = rel.joins("INNER JOIN geoiplocations AS my_geo ON #{my_locid} = my_geo.locid")
rel = rel.joins("INNER JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid")
rel = rel.where("users.last_jam_locidispid/1000000 IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog FROM geoiplocations WHERE locid = #{my_locid}), #{meters}))")
rel = rel.group("my_geo.geog, other_geo.geog")
rel = rel.order('st_distance(my_geo.geog, other_geo.geog)')
when :plays # FIXME: double counting?
# sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}"
rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count')
@ -248,23 +267,16 @@ module JamRuby
rel = rel.where(['connections.aasm_state != ?', 'expired'])
end
unless locidispid.nil?
if !locidispid.nil? && !user.nil?
rel = rel.order('nondirected_scores.full_score ASC NULLS LAST')
end
rel = rel.order('users.created_at DESC')
# rel = rel.select(sel_str)
rel, page = self.relation_pagination(rel, params)
rel = rel.includes([:instruments, :followings, :friends])
# puts "======================== sql #{rel.to_sql}"
objs = rel.all
# puts "======================== objs #{objs.inspect}"
# if objs.length > 0
# puts "======================== attributes #{objs[0].attributes}"
# puts "======================== score #{objs[0].score}"
# end
srch = Search.new
srch.search_type = :musicians_filter

View File

@ -1003,6 +1003,11 @@ module JamRuby
self.save
end
# gets the GeoIpLocation for the user's last_jam_locidispid (where are they REALLY, vs profile info)
def geoiplocation
GeoIpLocations.find_by_locid(last_jam_locidispid / 1000000) if last_jam_locidispid
end
def update_last_jam(remote_ip, reason)
location = GeoIpLocations.lookup(remote_ip)
self.last_jam_addr = location[:addr]

View File

@ -39,6 +39,50 @@ FactoryGirl.define do
admin true
end
factory :austin_user do
first_name 'Austin'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Austin'
last_jam_locidispid { austin_geoip[:locidispid] }
last_jam_addr { austin_ip }
end
factory :dallas_user do
first_name 'Dallas'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Dallas'
last_jam_locidispid { dallas_geoip[:locidispid] }
last_jam_addr { dallas_ip }
end
factory :houston_user do
first_name 'Houston'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Houston'
last_jam_locidispid { houston_geoip[:locidispid] }
last_jam_addr { houston_ip }
end
factory :miami_user do
first_name 'Miami'
sequence(:last_name) { |n| "#{n}" }
state 'FL'
city 'Miami'
last_jam_locidispid { miami_geoip[:locidispid] }
last_jam_addr { miami_ip }
end
factory :seattle_user do
first_name 'Seattle'
sequence(:last_name) { |n| "#{n}" }
state 'WA'
city 'Seattle'
last_jam_locidispid { seattle_geoip[:locidispid] }
last_jam_addr { seattle_ip }
end
factory :single_user_session do
after(:create) do |user, evaluator|
active_music_session = FactoryGirl.create(:active_music_session, :creator => user)

View File

@ -2,356 +2,423 @@ require 'spec_helper'
describe 'Musician search' do
before(:each) do
# @geocode1 = FactoryGirl.create(:geocoder)
# @geocode2 = FactoryGirl.create(:geocoder)
t = Time.now - 10.minute
# need a data set with actual distances
describe "test set A" do
@user1 = FactoryGirl.create(:user, created_at: t+1.minute, last_jam_locidispid: 1)
@user2 = FactoryGirl.create(:user, created_at: t+2.minute, last_jam_locidispid: 2)
@user3 = FactoryGirl.create(:user, created_at: t+3.minute, last_jam_locidispid: 3)
@user4 = FactoryGirl.create(:user, created_at: t+4.minute, last_jam_locidispid: 4)
@user5 = FactoryGirl.create(:user, created_at: t+5.minute, last_jam_locidispid: 5)
@user6 = FactoryGirl.create(:user, created_at: t+6.minute) # not geocoded
@user7 = FactoryGirl.create(:user, created_at: t+7.minute, musician: false) # not musician
@musicians = []
@musicians << @user1
@musicians << @user2
@musicians << @user3
@musicians << @user4
@musicians << @user5
@musicians << @user6
@geomusicians = []
@geomusicians << @user1
@geomusicians << @user2
@geomusicians << @user3
@geomusicians << @user4
@geomusicians << @user5
# from these scores:
# user1 has scores other users in user1 location, and with user2, user3, user4
# user2 has scores with users in user3 and user4 location
# Score.delete_all
Score.createx(1, 'a', 1, 1, 'a', 1, 10)
Score.createx(1, 'a', 1, 2, 'b', 2, 20)
Score.createx(1, 'a', 1, 3, 'c', 3, 30)
Score.createx(1, 'a', 1, 4, 'd', 4, 40)
Score.createx(2, 'b', 2, 3, 'c', 3, 15)
Score.createx(2, 'b', 2, 4, 'd', 4, 70)
end
context 'default filter settings' do
it "finds all musicians" do
# expects all the musicians (geocoded)
results = Search.musician_filter({score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == @musicians.length
results.results.should eq @musicians.reverse
before(:each) do
create_phony_database
end
it "finds all musicians page 1" do
# expects all the musicians
results = Search.musician_filter({page: 1, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == @musicians.length
results.results.should eq @musicians.reverse
end
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) }
it "finds all musicians page 2" do
# expects no musicians (all fit on page 1)
results = Search.musician_filter({page: 2, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 0
end
describe "search on distance" do
it "finds all musicians page 1 per_page 3" do
# expects three of the musicians
results = Search.musician_filter({per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 3
results.results.should eq @musicians.reverse.slice(0, 3)
end
it "finds all musicians page 2 per_page 3" do
# expects two of the musicians
results = Search.musician_filter({page: 2, per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 3
results.results.should eq @musicians.reverse.slice(3, 3)
end
it "finds all musicians page 3 per_page 3" do
# expects two of the musicians
results = Search.musician_filter({page: 3, per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 0
end
it "sorts musicians by followers" do
# establish sorting order
# @user4
f1 = Follow.new
f1.user = @user2
f1.followable = @user4
f1.save
f2 = Follow.new
f2.user = @user3
f2.followable = @user4
f2.save
f3 = Follow.new
f3.user = @user4
f3.followable = @user4
f3.save
# @user3
f4 = Follow.new
f4.user = @user3
f4.followable = @user3
f4.save
f5 = Follow.new
f5.user = @user4
f5.followable = @user3
f5.save
# @user2
f6 = Follow.new
f6.user = @user1
f6.followable = @user2
f6.save
# @user4.followers.concat([@user2, @user3, @user4])
# @user3.followers.concat([@user3, @user4])
# @user2.followers.concat([@user1])
expect(@user4.followers.count).to be 3
expect(Follow.count).to be 6
# refresh the order to ensure it works right
f1 = Follow.new
f1.user = @user3
f1.followable = @user2
f1.save
f2 = Follow.new
f2.user = @user4
f2.followable = @user2
f2.save
f3 = Follow.new
f3.user = @user2
f3.followable = @user2
f3.save
# @user2.followers.concat([@user3, @user4, @user2])
results = Search.musician_filter({ :per_page => @musicians.size, score_limit: Search::TEST_SCORE, orderby: 'followed'}, @user3)
expect(results.results[0].id).to eq(@user2.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?(@user2)).to be true
end
it 'paginates properly' do
# make sure pagination works right
params = { :per_page => 2, :page => 1 , score_limit: Search::TEST_SCORE}
results = Search.musician_filter(params)
expect(results.results.count).to be 2
end
end
def make_recording(usr)
connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid)
instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
track = FactoryGirl.create(:track, :connection => connection, :instrument => instrument)
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr, 10)
recording = Recording.start(music_session, usr)
recording.stop
recording.reload
genre = FactoryGirl.create(:genre)
recording.claim(usr, "name", "description", genre, true)
recording.reload
recording
end
def make_session(usr)
connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid)
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr, 10)
end
context 'musician stat counters' do
it "displays musicians top followings" do
f1 = Follow.new
f1.user = @user4
f1.followable = @user4
f1.save
f2 = Follow.new
f2.user = @user4
f2.followable = @user3
f2.save
f3 = Follow.new
f3.user = @user4
f3.followable = @user2
f3.save
# @user4.followers.concat([@user4])
# @user3.followers.concat([@user4])
# @user2.followers.concat([@user4])
expect(@user4.top_followings.count).to eq 3
expect(@user4.top_followings.map(&:id)).to match_array((@musicians - [@user1, @user5, @user6]).map(&:id))
end
it "friends stat shows friend count" do
# create friendship record
Friendship.save(@user1.id, @user2.id)
# search on user2
results = Search.musician_filter({score_limit: Search::TEST_SCORE}, @user2)
friend = results.results.detect { |mm| mm.id == @user1.id }
expect(friend).to_not be_nil
expect(results.friend_count(friend)).to be 1
@user1.reload
expect(friend.friends?(@user2)).to be true
expect(results.is_friend?(@user1)).to be true
end
it "recording stat shows recording count" do
Recording.delete_all
recording = make_recording(@user1)
expect(recording.users.length).to be 1
expect(recording.users.first).to eq(@user1)
@user1.reload
expect(@user1.recordings.length).to be 1
expect(@user1.recordings.first).to eq(recording)
expect(recording.claimed_recordings.length).to be 1
expect(@user1.recordings.detect { |rr| rr == recording }).to_not be_nil
results = Search.musician_filter({score_limit: Search::TEST_SCORE},@user1)
# puts "====================== results #{results.inspect}"
uu = results.results.detect { |mm| mm.id == @user1.id }
expect(uu).to_not be_nil
expect(results.record_count(uu)).to be 1
expect(results.session_count(uu)).to be 1
end
end
context 'musician sorting' do
it "by plays" do
Recording.delete_all
make_recording(@user1)
# order results by num recordings
results = Search.musician_filter({ orderby: 'plays', score_limit: Search::TEST_SCORE}, @user2)
# puts "========= results #{results.inspect}"
expect(results.results.length).to eq(2)
expect(results.results[0].id).to eq(@user1.id)
expect(results.results[1].id).to eq(@user3.id)
# add more data and make sure order still correct
make_recording(@user3)
make_recording(@user3)
results = Search.musician_filter({ :orderby => 'plays', score_limit: Search::TEST_SCORE }, @user2)
expect(results.results.length).to eq(2)
expect(results.results[0].id).to eq(@user3.id)
expect(results.results[1].id).to eq(@user1.id)
end
it "by now playing" do
pending "these tests worked, so leaving them in, but we don't currently have 'Now Playing' in the find musicians screen"
# should get 1 result with 1 active session
make_session(@user1)
results = Search.musician_filter({ :orderby => 'playing', score_limit: Search::TEST_SCORE}, @user2)
expect(results.results.count).to be 1
expect(results.results.first.id).to eq(@user1.id)
# should get 2 results with 2 active sessions
# sort order should be created_at DESC
make_session(@user3)
results = Search.musician_filter({ :orderby => 'playing', score_limit: Search::TEST_SCORE}, @user2)
expect(results.results.count).to be 2
expect(results.results[0].id).to eq(@user3.id)
expect(results.results[1].id).to eq(@user1.id)
end
end
context 'filter settings' do
it "searches musicisns for an instrument" do
minst = FactoryGirl.create(:musician_instrument, {
:user => @user1,
:instrument => Instrument.find('tuba') })
@user1.musician_instruments << minst
@user1.reload
ii = @user1.instruments.detect { |inst| inst.id == 'tuba' }
expect(ii).to_not be_nil
results = Search.musician_filter({ :instrument => ii.id, score_limit: Search::TEST_SCORE })
results.results.each do |rr|
expect(rr.instruments.detect { |inst| inst.id=='tuba' }.id).to eq(ii.id)
it "finds self when very local search" do
Search.musician_filter({distance: 1, orderby: 'distance'}, austin_user).results.should == [austin_user] # just to see that distance is 0 to self
Search.musician_filter({distance: 1, orderby: 'distance'}, dallas_user).results.should == [dallas_user] # just to see that distance is 0 to self
Search.musician_filter({distance: 1, orderby: 'distance'}, miami_user).results.should == [miami_user] # just to see that distance is 0 to self
Search.musician_filter({distance: 1, orderby: 'distance'}, seattle_user).results.should == [seattle_user] # just to see that distance is 0 to self
end
it "finds dallas when in range of austin" do
expected_results = [austin_user, dallas_user]
Search.musician_filter({distance: 500, orderby: 'distance'}, austin_user).results.should == expected_results
Search.musician_filter({distance: 100, orderby: 'distance'}, austin_user).results.should == [austin_user]
end
it "finds miami when in range of austin" do
expected_results = [austin_user, dallas_user, miami_user]
Search.musician_filter({distance: 1500, orderby: 'distance'}, austin_user).results.should == expected_results
Search.musician_filter({distance: 300, orderby: 'distance'}, austin_user).results.should == [austin_user, dallas_user]
Search.musician_filter({distance: 100, orderby: 'distance'}, austin_user).results.should == [austin_user]
end
it "finds seattle when in range of austin" do
expected_results = [austin_user, dallas_user, miami_user, seattle_user]
Search.musician_filter({distance: 2000, orderby: 'distance'}, austin_user).results.should == expected_results
Search.musician_filter({distance: 1500, orderby: 'distance'}, austin_user).results.should == [austin_user, dallas_user, miami_user]
Search.musician_filter({distance: 300, orderby: 'distance'}, austin_user).results.should == [austin_user, dallas_user]
Search.musician_filter({distance: 100, orderby: 'distance'}, austin_user).results.should == [austin_user]
end
it "finds austin & dallas by user-specified location when in range" do
Search.musician_filter({distance: 500, orderby: 'distance', city: 'Austin', region: 'TX', country: 'US'}, austin_user).results.should == [austin_user, dallas_user]
end
it "finds dallas & austin by user-specified location when in range" do
Search.musician_filter({distance: 500, orderby: 'distance', city: 'Dallas', region: 'TX', country: 'US'}, austin_user).results.should == [dallas_user, austin_user]
end
it "finds miami user-specified location when in range" do
Search.musician_filter({distance: 300, orderby: 'distance', city: 'Tampa', region: 'FL', country: 'US'}, austin_user).results.should == [miami_user]
end
it "finds all users with user-specified location when in range" do
Search.musician_filter({distance: 2500, orderby: 'distance', city: 'Tampa', region: 'FL', country: 'US'}, austin_user).results.should == [miami_user, dallas_user, austin_user, seattle_user]
end
expect(results.results.count).to be 1
end
end
context 'new users' do
describe "test set B" do
it "find three for user1" do
# user2..4 are scored against user1
ms = Search.new_musicians(@user1, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 3
ms.should eq [@user2, @user3, @user4]
before(:each) do
# @geocode1 = FactoryGirl.create(:geocoder)
# @geocode2 = FactoryGirl.create(:geocoder)
t = Time.now - 10.minute
@user1 = FactoryGirl.create(:user, created_at: t+1.minute, last_jam_locidispid: 1)
@user2 = FactoryGirl.create(:user, created_at: t+2.minute, last_jam_locidispid: 2)
@user3 = FactoryGirl.create(:user, created_at: t+3.minute, last_jam_locidispid: 3)
@user4 = FactoryGirl.create(:user, created_at: t+4.minute, last_jam_locidispid: 4)
@user5 = FactoryGirl.create(:user, created_at: t+5.minute, last_jam_locidispid: 5)
@user6 = FactoryGirl.create(:user, created_at: t+6.minute) # not geocoded
@user7 = FactoryGirl.create(:user, created_at: t+7.minute, musician: false) # not musician
@musicians = []
@musicians << @user1
@musicians << @user2
@musicians << @user3
@musicians << @user4
@musicians << @user5
@musicians << @user6
@geomusicians = []
@geomusicians << @user1
@geomusicians << @user2
@geomusicians << @user3
@geomusicians << @user4
@geomusicians << @user5
# from these scores:
# user1 has scores other users in user1 location, and with user2, user3, user4
# user2 has scores with users in user3 and user4 location
# Score.delete_all
Score.createx(1, 'a', 1, 1, 'a', 1, 10)
Score.createx(1, 'a', 1, 2, 'b', 2, 20)
Score.createx(1, 'a', 1, 3, 'c', 3, 30)
Score.createx(1, 'a', 1, 4, 'd', 4, 40)
Score.createx(2, 'b', 2, 3, 'c', 3, 15)
Score.createx(2, 'b', 2, 4, 'd', 4, 70)
end
it "find two for user2" do
# user1,3,4 are scored against user1, but user4 is bad
ms = Search.new_musicians(@user2, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 2
ms.should eq [@user3, @user1]
context 'default filter settings' do
it "finds all musicians" do
# expects all the musicians (geocoded)
results = Search.musician_filter({score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == @musicians.length
results.results.should eq @musicians.reverse
end
it "finds all musicians page 1" do
# expects all the musicians
results = Search.musician_filter({page: 1, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == @musicians.length
results.results.should eq @musicians.reverse
end
it "finds all musicians page 2" do
# expects no musicians (all fit on page 1)
results = Search.musician_filter({page: 2, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 0
end
it "finds all musicians page 1 per_page 3" do
# expects three of the musicians
results = Search.musician_filter({per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 3
results.results.should eq @musicians.reverse.slice(0, 3)
end
it "finds all musicians page 2 per_page 3" do
# expects two of the musicians
results = Search.musician_filter({page: 2, per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 3
results.results.should eq @musicians.reverse.slice(3, 3)
end
it "finds all musicians page 3 per_page 3" do
# expects two of the musicians
results = Search.musician_filter({page: 3, per_page: 3, score_limit: Search::TEST_SCORE})
results.search_type.should == :musicians_filter
results.results.count.should == 0
end
it "sorts musicians by followers" do
# establish sorting order
# @user4
f1 = Follow.new
f1.user = @user2
f1.followable = @user4
f1.save
f2 = Follow.new
f2.user = @user3
f2.followable = @user4
f2.save
f3 = Follow.new
f3.user = @user4
f3.followable = @user4
f3.save
# @user3
f4 = Follow.new
f4.user = @user3
f4.followable = @user3
f4.save
f5 = Follow.new
f5.user = @user4
f5.followable = @user3
f5.save
# @user2
f6 = Follow.new
f6.user = @user1
f6.followable = @user2
f6.save
# @user4.followers.concat([@user2, @user3, @user4])
# @user3.followers.concat([@user3, @user4])
# @user2.followers.concat([@user1])
expect(@user4.followers.count).to be 3
expect(Follow.count).to be 6
# refresh the order to ensure it works right
f1 = Follow.new
f1.user = @user3
f1.followable = @user2
f1.save
f2 = Follow.new
f2.user = @user4
f2.followable = @user2
f2.save
f3 = Follow.new
f3.user = @user2
f3.followable = @user2
f3.save
# @user2.followers.concat([@user3, @user4, @user2])
results = Search.musician_filter({:per_page => @musicians.size, score_limit: Search::TEST_SCORE, orderby: 'followed'}, @user3)
expect(results.results[0].id).to eq(@user2.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?(@user2)).to be true
end
it 'paginates properly' do
# make sure pagination works right
params = {:per_page => 2, :page => 1, score_limit: Search::TEST_SCORE}
results = Search.musician_filter(params)
expect(results.results.count).to be 2
end
end
it "find two for user3" do
# user1..2 are scored against user3
ms = Search.new_musicians(@user3, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 2
ms.should eq [@user2, @user1]
def make_recording(usr)
connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid)
instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
track = FactoryGirl.create(:track, :connection => connection, :instrument => instrument)
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr, 10)
recording = Recording.start(music_session, usr)
recording.stop
recording.reload
genre = FactoryGirl.create(:genre)
recording.claim(usr, "name", "description", genre, true)
recording.reload
recording
end
it "find one for user4" do
# user1..2 are scored against user4, but user2 is bad
ms = Search.new_musicians(@user4, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 1
ms.should eq [@user1]
def make_session(usr)
connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid)
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr, 10)
end
it "find none for user5" do
# user1..4 are not scored against user5
ms = Search.new_musicians(@user5, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 0
context 'musician stat counters' do
it "displays musicians top followings" do
f1 = Follow.new
f1.user = @user4
f1.followable = @user4
f1.save
f2 = Follow.new
f2.user = @user4
f2.followable = @user3
f2.save
f3 = Follow.new
f3.user = @user4
f3.followable = @user2
f3.save
# @user4.followers.concat([@user4])
# @user3.followers.concat([@user4])
# @user2.followers.concat([@user4])
expect(@user4.top_followings.count).to eq 3
expect(@user4.top_followings.map(&:id)).to match_array((@musicians - [@user1, @user5, @user6]).map(&:id))
end
it "friends stat shows friend count" do
# create friendship record
Friendship.save(@user1.id, @user2.id)
# search on user2
results = Search.musician_filter({score_limit: Search::TEST_SCORE}, @user2)
friend = results.results.detect { |mm| mm.id == @user1.id }
expect(friend).to_not be_nil
expect(results.friend_count(friend)).to be 1
@user1.reload
expect(friend.friends?(@user2)).to be true
expect(results.is_friend?(@user1)).to be true
end
it "recording stat shows recording count" do
Recording.delete_all
recording = make_recording(@user1)
expect(recording.users.length).to be 1
expect(recording.users.first).to eq(@user1)
@user1.reload
expect(@user1.recordings.length).to be 1
expect(@user1.recordings.first).to eq(recording)
expect(recording.claimed_recordings.length).to be 1
expect(@user1.recordings.detect { |rr| rr == recording }).to_not be_nil
results = Search.musician_filter({score_limit: Search::TEST_SCORE}, @user1)
# puts "====================== results #{results.inspect}"
uu = results.results.detect { |mm| mm.id == @user1.id }
expect(uu).to_not be_nil
expect(results.record_count(uu)).to be 1
expect(results.session_count(uu)).to be 1
end
end
context 'musician sorting' do
it "by plays" do
Recording.delete_all
make_recording(@user1)
# order results by num recordings
results = Search.musician_filter({orderby: 'plays', score_limit: Search::TEST_SCORE}, @user2)
# puts "========= results #{results.inspect}"
expect(results.results.length).to eq(2)
expect(results.results[0].id).to eq(@user1.id)
expect(results.results[1].id).to eq(@user3.id)
# add more data and make sure order still correct
make_recording(@user3)
make_recording(@user3)
results = Search.musician_filter({:orderby => 'plays', score_limit: Search::TEST_SCORE}, @user2)
expect(results.results.length).to eq(2)
expect(results.results[0].id).to eq(@user3.id)
expect(results.results[1].id).to eq(@user1.id)
end
it "by now playing" do
pending "these tests worked, so leaving them in, but we don't currently have 'Now Playing' in the find musicians screen"
# should get 1 result with 1 active session
make_session(@user1)
results = Search.musician_filter({:orderby => 'playing', score_limit: Search::TEST_SCORE}, @user2)
expect(results.results.count).to be 1
expect(results.results.first.id).to eq(@user1.id)
# should get 2 results with 2 active sessions
# sort order should be created_at DESC
make_session(@user3)
results = Search.musician_filter({:orderby => 'playing', score_limit: Search::TEST_SCORE}, @user2)
expect(results.results.count).to be 2
expect(results.results[0].id).to eq(@user3.id)
expect(results.results[1].id).to eq(@user1.id)
end
end
context 'filter settings' do
it "searches musicians for an instrument" do
minst = FactoryGirl.create(:musician_instrument, {
:user => @user1,
:instrument => Instrument.find('tuba')})
@user1.musician_instruments << minst
@user1.reload
ii = @user1.instruments.detect { |inst| inst.id == 'tuba' }
expect(ii).to_not be_nil
results = Search.musician_filter({:instrument => ii.id, score_limit: Search::TEST_SCORE}, @user2)
results.results.each do |rr|
expect(rr.instruments.detect { |inst| inst.id=='tuba' }.id).to eq(ii.id)
end
expect(results.results.count).to be 1
end
end
context 'new users' do
it "find three for user1" do
# user2..4 are scored against user1
ms = Search.new_musicians(@user1, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 3
ms.should eq [@user2, @user3, @user4]
end
it "find two for user2" do
# user1,3,4 are scored against user1, but user4 is bad
ms = Search.new_musicians(@user2, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 2
ms.should eq [@user3, @user1]
end
it "find two for user3" do
# user1..2 are scored against user3
ms = Search.new_musicians(@user3, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 2
ms.should eq [@user2, @user1]
end
it "find one for user4" do
# user1..2 are scored against user4, but user2 is bad
ms = Search.new_musicians(@user4, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 1
ms.should eq [@user1]
end
it "find none for user5" do
# user1..4 are not scored against user5
ms = Search.new_musicians(@user5, Time.now - 1.week)
ms.should_not be_nil
ms.length.should == 0
end
end
end
end

View File

@ -169,7 +169,7 @@ def ip_from_num(num)
end
def austin_ip
IPAddr.new(0x0FFFFFFF, Socket::AF_INET).to_s
IPAddr.new(austin_ip_as_num, Socket::AF_INET).to_s
end
def austin_ip_as_num
@ -177,35 +177,64 @@ def austin_ip_as_num
end
def dallas_ip
IPAddr.new(0x1FFFFFFF, Socket::AF_INET).to_s
IPAddr.new(dallas_ip_as_num, Socket::AF_INET).to_s
end
def dallas_ip_as_num
0x1FFFFFFF
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def austin_geoip
geoiplocation = GeoIpLocations.find_by_locid(17192)
geoipblock = GeoIpBlocks.find_by_locid(17192)
def houston_ip
IPAddr.new(houston_ip_as_num, Socket::AF_INET).to_s
end
def houston_ip_as_num
0x2FFFFFFF
end
def miami_ip
IPAddr.new(miami_ip_as_num, Socket::AF_INET).to_s
end
def miami_ip_as_num
0x5FFFFFFF
end
def seattle_ip
IPAddr.new(seattle_ip_as_num, Socket::AF_INET).to_s
end
def seattle_ip_as_num
0xAFFFFFFF
end
def create_geoip(locid)
geoiplocation = GeoIpLocations.find_by_locid(locid)
geoipblock = GeoIpBlocks.find_by_locid(locid)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid)}
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def austin_geoip
create_geoip(17192)
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def dallas_geoip
geoiplocation = GeoIpLocations.find_by_locid(667)
geoipblock = GeoIpBlocks.find_by_locid(667)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid)}
create_geoip(667)
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def houston_geoip
geoiplocation = GeoIpLocations.find_by_locid(30350)
geoipblock = GeoIpBlocks.find_by_locid(30350)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid)}
create_geoip(30350)
end
def miami_geoip
create_geoip(23565)
end
def seattle_geoip
create_geoip(1539)
end
# attempts to make the creation of a score more straightforward.

View File

@ -84,11 +84,12 @@ group :development, :test do
gem 'rspec-rails', '2.14.2'
gem "activerecord-import", "~> 0.4.1"
gem 'guard-rspec', '0.5.5'
gem 'jasmine', '1.3.1'
# gem 'jasmine', '1.3.1'
gem 'pry'
gem 'execjs', '1.4.0'
gem 'factory_girl_rails', '4.1.0' # in dev because in use by rake task
gem 'database_cleaner', '1.3.0' #in dev because in use by rake task
gem 'teaspoon'
end
group :unix do
gem 'therubyracer' #, '0.11.0beta8'

View File

@ -13,6 +13,8 @@
var self = this;
var reloadAudioTimeout = null;
var $root;
var $dialog;
var $addNewGearBtn;
var userId;
var showingGearWizard = false;
var cancelRescanFunc = null;
@ -59,7 +61,7 @@
$root.find('.rescanning-notice').hide();
if(!canceled) {
var result = context.jamClient.ReloadAudioSystem(false, true, false);
var result = context.jamClient.ReloadAudioSystem(false, true, true);
}
populateAccountAudio();
@ -88,7 +90,13 @@
context.JK.guardAgainstBrowser(app, {d1: 'gear'});
}
else {
populateAccountAudio()
var profiles = populateAccountAudio();
if(profiles.length <= 1) {
setTimeout(function() {
context.JK.prodBubble($addNewGearBtn, 'no-audio-profiles', {}, {positions: ['bottom'], offsetParent: $addNewGearBtn.closest('.screen')});
}, 1000);
}
}
}
@ -115,6 +123,8 @@
var template = context._.template($('#template-account-audio').html(), {is_admin: context.JK.currentUserAdmin, profiles: cleansedProfiles}, {variable: 'data'});
appendAudio(template);
return profiles;
}
function appendAudio(template) {
@ -135,7 +145,7 @@
function handleLoopbackAudioProfile(audioProfileId) {
if(audioProfileId != context.jamClient.LastUsedProfileName()) {
if(audioProfileId != context.jamClient.FTUEGetMusicProfileName()) {
var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId);
if(!result) {
@ -153,7 +163,7 @@
function handleConfigureAudioProfile(audioProfileId) {
if(audioProfileId != context.jamClient.LastUsedProfileName()) {
if(audioProfileId != context.jamClient.FTUEGetMusicProfileName()) {
logger.debug("activating " + audioProfileId);
var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId);
@ -176,7 +186,6 @@
populateAccountAudio();
}
app.layout.showDialog('configure-tracks')
.one(EVENTS.DIALOG_CLOSED, populateAccountAudio)
}
@ -202,16 +211,16 @@
function handleStartAudioQualification() {
if(true) {
app.afterFtue = function() { showingGearWizard = false; populateAccountAudio() };
app.cancelFtue = function() { showingGearWizard = false; populateAccountAudio() };
showingGearWizard = true;
app.layout.startNewFtue();
}
else {
app.setWizardStep(1);
app.layout.showDialog('ftue');
}
app.afterFtue = function() {
showingGearWizard = false;
if(populateAccountAudio().length == 1) {
app.layout.showDialog('join-test-session');
}
};
app.cancelFtue = function() { showingGearWizard = false; populateAccountAudio() };
showingGearWizard = true;
app.layout.startNewFtue();
}
function reloadAudio() {
@ -309,8 +318,11 @@
'beforeShow': beforeShow,
'afterShow': afterShow
};
app.bindScreen('account/audio', screenBindings);
$dialog = $('#account-audio-profile')
$addNewGearBtn = $dialog.find('a[data-purpose=add-profile]');
events();
}

View File

@ -10,6 +10,7 @@
var api = context.JK.Rest();
var userId;
var user = {};
var selectLocation = null;
var recentUserDetail = null;
var loadingCitiesData = false;
var loadingRegionsData = false;
@ -252,28 +253,8 @@
$('#account-profile-content-scroller').on('click', '#account-edit-profile-cancel', function(evt) { evt.stopPropagation(); navToAccount(); return false; } );
$('#account-profile-content-scroller').on('click', '#account-edit-profile-submit', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } );
$('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } );
$('#account-profile-content-scroller').on('change', 'select[name=country]', function(evt) { evt.stopPropagation(); handleCountryChanged(); return false; } );
$('#account-profile-content-scroller').on('change', 'select[name=region]', function(evt) { evt.stopPropagation(); handleRegionChanged(); return false; } );
$('#account-profile-content-scroller').on('click', '#account-change-avatar', function(evt) { evt.stopPropagation(); navToAvatar(); return false; } );
}
function regionListFailure(jqXHR, textStatus, errorThrown) {
if(jqXHR.status == 422) {
logger.debug("no regions found for country: " + recentUserDetail.country);
}
else {
app.ajaxError(arguments);
}
}
function cityListFailure(jqXHR, textStatus, errorThrown) {
if(jqXHR.status == 422) {
logger.debug("no cities found for country/region: " + recentUserDetail.country + "/" + recentUserDetail.state);
}
else {
app.ajaxError(arguments);
}
}
function renderAccountProfile() {
$.when( api.getUserDetail(),
@ -285,43 +266,8 @@
// show page; which can be done quickly at this point
populateAccountProfile(userDetail, instruments);
var country = userDetail.country;
if(!country) {
// this case shouldn't happen because sign up makes you pick a location. This is just 'in case', so that the UI is more error-resilient
country = 'US';
}
loadingCountriesData = true;
loadingRegionsData = true;
loadingCitiesData = true;
// make the 3 slower requests, which only matter if the user wants to affect their ISP or location
api.getCountries()
.done(function(countriesx) { populateCountriesx(countriesx["countriesx"], userDetail.country); } )
.fail(app.ajaxError)
.always(function() { loadingCountriesData = false; })
var country = userDetail.country;
var state = userDetail.state;
if(country) {
api.getRegions( { country: country } )
.done(function(regions) { populateRegions(regions["regions"], userDetail.state); } )
.fail(regionListFailure)
.always(function() { loadingRegionsData = false; })
if(state) {
api.getCities( { country: country, region: state })
.done(function(cities) {
populateCities(cities["cities"], userDetail.city)
})
.fail(cityListFailure)
.always(function() { loadingCitiesData = false;})
}
}
selectLocation = new context.JK.SelectLocation(getCountryElement(), getRegionElement(), getCityElement(), app);
selectLocation.load(userDetail.country, userDetail.state, userDetail.city)
})
context.JK.dropdown($('select'));
}

View File

@ -10,6 +10,7 @@
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
// GO AFTER THE REQUIRES BELOW.
//
//= require bind-polyfill
//= require jquery
//= require jquery.monkeypatch
//= require jquery_ujs
@ -34,6 +35,7 @@
//= require jquery.browser
//= require jquery.custom-protocol
//= require jstz
//= require class
//= require AAC_underscore
//= require AAA_Log
//= require globals

View File

@ -115,10 +115,13 @@
var timer = setInterval(function(){
var $countdown = $('#client_update .countdown-secs');
var countdown = parseInt($countdown.text());
$countdown.text(countdown - 1);
if(countdown == 0) {
if(countdown <= 0) {
clearInterval(timer);
}
else {
$countdown.text(countdown - 1);
}
}, rounded * 1000);
updateClientUpdateDialog("update-restarting", {countdown: rounded, os: context.JK.GetOSAsString()});

View File

@ -77,6 +77,13 @@
context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'audio-profile-invalid-dialog', onUsbDeviceDisconnected);
}
function onCancel() {
if($btnCancel.is('.disabled')) return false;
$dialog.data('result', 'cancel');
return true;
}
function beforeHide() {
context.JK.offBackendEvent(ALERT_NAMES.USB_CONNECTED, 'audio-profile-invalid-dialog', onUsbDeviceConnected);
context.JK.offBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'audio-profile-invalid-dialog', onUsbDeviceDisconnected);
@ -166,14 +173,6 @@
app.layout.closeDialog('audio-profile-invalid-dialog');
return false;
})
$btnCancel.click(function() {
if($(this).is('.disabled')) return false;
$dialog.data('result', 'cancel');
app.layout.closeDialog('audio-profile-invalid-dialog');
return false;
})
}
function initialize() {
@ -182,7 +181,8 @@
'beforeShow': beforeShow,
'afterShow' : afterShow,
'beforeHide' : beforeHide,
'afterHide': afterHide
'afterHide': afterHide,
'onCancel' : onCancel
};
app.bindDialog('audio-profile-invalid-dialog', dialogBindings);

View File

@ -0,0 +1,116 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.ChangeSearchLocationDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $dialog = null;
var initialized = false;
var $countries = null;
var $regions = null;
var $cities = null;
var selectLocation = null;
var $resetLocation = null;
var $btnSave = null;
function getSelection() {
var countryVal = $countries.val();
var regionVal = $regions.val();
var cityVal = $cities.val();
if(countryVal && regionVal && cityVal) {
// if any bit of info is not set, then null out info; user didn't pick a full set of data
var $country = $($countries).find('option:selected');
var $region = $($regions).find('option:selected');
return {
countrycode : countryVal,
regioncode : regionVal,
city: cityVal,
country: $country.text(),
region: $region.text()
}
}
else
{
return null;
}
}
function events() {
$btnSave.click(function() {
var selection = getSelection();
app.user().done(function(user) {
if(selection != null &&
selection.countrycode == user.country &&
selection.regioncode == user.state &&
selection.city == user.city) {
// if this is the same location on the user's account, suppress selection
selection = null;
}
$dialog.data('result', selection);
app.layout.closeDialog('change-search-location')
})
return false;
})
$resetLocation.click(function() {
app.user().done(function(user) {
selectLocation.load(user.country, user.state, user.city);
})
return false;
})
}
function beforeShow() {
$dialog.data('result', null);
if(!initialized) {
initialized = true;
app.user().done(function(user) {
selectLocation = new context.JK.SelectLocation($countries, $regions, $cities, app);
selectLocation.load(user.country, user.state, user.city);
})
}
else {
}
}
function beforeHide() {
}
function getSelectedLocation() {
return searchLocation;
}
function initialize() {
var dialogBindings = {
'beforeShow': beforeShow,
'beforeHide': beforeHide
};
app.bindDialog('change-search-location', dialogBindings);
$dialog = $('#change-search-location-dialog');
$countries = $dialog.find('select[name="country"]')
$regions = $dialog.find('select[name="region"]')
$cities = $dialog.find('select[name="city"]')
$resetLocation = $dialog.find('.reset-location');
$btnSave = $dialog.find('.btnSave')
events();
};
this.initialize = initialize;
this.getSelectedLocation = getSelectedLocation;
}
return this;
})(window, jQuery);

View File

@ -119,13 +119,10 @@
});
$btnCancel.click(function() {
if(voiceChatHelper.cancel()) {
app.layout.closeDialog('configure-tracks')
}
app.layout.cancelDialog('configure-tracks');
return false;
});
//$btnAddNewGear.click(function() {
// return false;
@ -206,10 +203,14 @@
function afterShow() {
sessionUtils.SessionPageEnter();
}
function onCancel() {
return voiceChatHelper.cancel();
}
function afterHide() {
voiceChatHelper.beforeHide();
sessionUtils.SessionPageLeave();
}
function initialize() {
@ -217,7 +218,8 @@
var dialogBindings = {
'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide
'afterHide': afterHide,
'onCancel' : onCancel
};
app.bindDialog('configure-tracks', dialogBindings);

View File

@ -12,7 +12,6 @@
var $description = null;
var $genre = null;
var $isPublic = null;
var $cancelBtn = null;
var $saveBtn = null;
var $deleteBtn = null;
@ -129,14 +128,9 @@
})
}
function cancel() {
app.layout.closeDialog('edit-recording');
}
function events() {
$saveBtn.click(attemptUpdate);
$deleteBtn.click(attemptDelete);
$cancelBtn.click(cancel)
$form.submit(false);
}
@ -151,7 +145,6 @@
$dialog = $('#edit-recording-dialog');
$form = $dialog.find('form');
$cancelBtn = $dialog.find('.cancel-btn');
$saveBtn = $dialog.find('.save-btn');
$deleteBtn = $dialog.find('.delete-btn');
$name = $dialog.find('input[name="name"]');

View File

@ -0,0 +1,79 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.GettingStartedDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var invitationDialog = null;
var $dialog = null;
var $dontShowAgain = null;
var $setupGearBtn = null;
function registerEvents() {
$setupGearBtn.click(function() {
if (gon.isNativeClient) {
app.layout.closeDialog('getting-started');
window.location = '/client#/account/audio'
}
else {
context.JK.guardAgainstBrowser(app, {d1: 'gear'});
}
return false;
})
$('#getting-started-dialog a.facebook-invite').on('click', function (e) {
invitationDialog.showFacebookDialog(e);
});
$('#getting-started-dialog a.google-invite').on('click', function (e) {
invitationDialog.showGoogleDialog();
});
$('#getting-started-dialog a.email-invite').on('click', function (e) {
invitationDialog.showEmailDialog();
});
}
function beforeShow() {
}
function beforeHide() {
if ($dontShowAgain.is(':checked')) {
app.updateUserModel({show_whats_next: false})
}
}
function initializeButtons() {
context.JK.checkbox($dontShowAgain);
}
function initialize(invitationDialogInstance) {
var dialogBindings = {
'beforeShow': beforeShow,
'beforeHide': beforeHide
};
app.bindDialog('getting-started', dialogBindings);
$dialog = $('#getting-started-dialog');
$dontShowAgain = $dialog.find('#show_getting_started');
$setupGearBtn = $dialog.find('.setup-gear-btn')
registerEvents();
invitationDialog = invitationDialogInstance;
initializeButtons();
};
this.initialize = initialize;
}
return this;
})(window, jQuery);

View File

@ -0,0 +1,113 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.JoinTestSessionDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $dialog = null;
var $joinTestSessionBtn = null;
function joinSession(sessionId) {
app.layout.closeDialog('join-test-session')
$joinTestSessionBtn.removeClass('disabled');
context.JK.GA.trackSessionCount(true, true, 0);
// we redirect to the session screen, which handles the REST call to POST /participants.
logger.debug("joining session screen: " + sessionId)
context.location = '/client#/session/' + sessionId;
};
function createSession(sessionId) {
var data = {};
data.name = 'First Session';
data.description = 'This is my first ever session on JamKazam, so please be gentle. :)';
data.genres = ['other']
data.musician_access = true;
data.approval_required = false;
data.fan_access = true;
data.fan_chat = true;
data.legal_policy = 'Standard'
data.legal_terms = true;
data.language = 'eng';
data.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false);
data.duration = "60";
data.recurring_mode = 'once'
data.music_notations = [];
data.timezone = 'Central Time (US & Canada),America/Chicago'
data.open_rsvps = true
data.rsvp_slots = [];
// auto pick an 'other' instrument
var otherId = context.JK.server_to_client_instrument_map.Other.server_id; // get server ID
var otherInstrumentInfo = context.JK.instrument_id_to_instrument[otherId]; // get display name
var beginnerLevel = 1; // default to beginner
var instruments = [ {id: otherId, name: otherInstrumentInfo.display, level: beginnerLevel} ];
$.each(instruments, function(index, instrument) {
var slot = {};
slot.instrument_id = instrument.id;
slot.proficiency_level = instrument.level;
slot.approve = true;
data.rsvp_slots.push(slot);
});
data.isUnstructuredRsvp = true;
rest.createScheduledSession(data)
.done(function(response) {
logger.debug("created test session on server");
//$('#create-session-buttons .btn-next').off('click');
var newSessionId = response.id;
joinSession(newSessionId);
})
.fail(function(jqXHR){
$joinTestSessionBtn.removeClass('disabled');
logger.debug("unable to schedule a test session")
app.notifyServerError(jqXHR, "Unable to schedule a test session");
})
}
function registerEvents() {
$joinTestSessionBtn.click(function() {
if($joinTestSessionBtn.is('.disabled')) return false;
$joinTestSessionBtn.addClass('disabled');
createSession();
return false;
})
}
function beforeShow() {
$joinTestSessionBtn.removeClass('disabled');
}
function beforeHide() {
}
function initialize() {
var dialogBindings = {
'beforeShow': beforeShow,
'beforeHide': beforeHide
};
app.bindDialog('join-test-session', dialogBindings);
$dialog = $('#join-test-session-dialog');
$joinTestSessionBtn = $dialog.find('.join-test-session')
registerEvents();
};
this.initialize = initialize;
}
return this;
})(window, jQuery);

View File

@ -7,7 +7,6 @@
var logger = context.JK.logger;
var $dialog = null;
var $btnCancel = null;
var $btnClose = null;
var $btnHelp = null;
var networkTest = new context.JK.NetworkTest(app);
@ -37,13 +36,10 @@
$btnClose.removeClass('button-grey').addClass('button-orange');
}
function onCancel() {
// should we stop this if the test is going?
}
function events() {
$btnCancel.on('click', function() {
// should we stop this if the test is going?
app.layout.closeDialog('network-test')
return false;
})
$btnClose.on('click', function(e) {
if(!networkTest.isScoring()) {
app.layout.closeDialog('network-test');
@ -77,7 +73,6 @@
app.bindDialog('network-test', dialogBindings);
$dialog = $('#network-test-dialog');
$btnCancel = $dialog.find('.btn-cancel');
$btnHelp = $dialog.find('.btn-help');
$btnClose = $dialog.find('.btn-close');

View File

@ -46,6 +46,8 @@
initializeStun(app);
operationalEvents(app);
handleGettingStarted(app);
});
function watchPreferencesEvent(app) {
@ -107,8 +109,11 @@
JK.UserDropdown = userDropdown;
userDropdown.initialize(invitationDialog);
var whatsNextDialog = new JK.WhatsNextDialog(app);
whatsNextDialog.initialize(invitationDialog);
var gettingStartedDialog = new JK.GettingStartedDialog(app);
gettingStartedDialog.initialize(invitationDialog);
var joinTestSessionDialog = new JK.JoinTestSessionDialog(app);
joinTestSessionDialog.initialize();
var videoDialog = new JK.VideoDialog(app);
videoDialog.initialize();
@ -167,7 +172,18 @@
JK.JamServer.registerMessageCallback(JK.MessageType.STOP_APPLICATION, function(header, payload) {
context.jamClient.ShutdownApplication();
});
}
function handleGettingStarted(app) {
app.user()
.done(function(userProfile) {
if (userProfile.show_whats_next &&
window.location.pathname.indexOf(gon.client_path) == 0 &&
!app.layout.isDialogShowing('getting-started'))
{
app.layout.showDialog('getting-started');
}
})
}
})(window, jQuery);

View File

@ -6,6 +6,7 @@
var logger = context.JK.logger;
var rest = context.JK.Rest();
var EVENTS = context.JK.EVENTS;
var musicians = {};
var musicianList;
var instrument_logo_map = context.JK.getInstrumentIconMap24();
@ -16,60 +17,139 @@
var $results = null;
var $spinner = null;
var $endMusicianList = null;
var $templateAccountSessionLatency = null;
var helpBubble = context.JK.HelpBubbleHelper;
var sessionUtils = context.JK.SessionUtils;
var $musicianSearchCity = null;
var $musicianFilterCity = null;
var $musicianChangeFilterCity = null;
var $musicianQueryScore = null;
var $musicianDistance = null;
var $musicianLatencyOrDistanceLabel = null;
var $refreshBtn = null;
var searchLocationOverride = null;
var currentRequest = null;
function search() {
$spinner.show();
did_show_musician_page = true;
var queryString = 'srch_m=1&page=' + page_num + '&';
var options = {
srch_m: 1,
page: page_num
}
// order by
var orderby = $('#musician_order_by').val();
if (typeof orderby != 'undefined' && orderby.length > 0) {
queryString += "orderby=" + orderby + '&';
options['orderby'] = orderby;
}
// instrument filter
var instrument = $('#musician_instrument').val();
if (typeof instrument != 'undefined' && !(instrument === '')) {
queryString += "instrument=" + instrument + '&';
options['instrument'] = instrument;
}
// score filter
var query_param = $('#musician_query_score').val();
var query_param = $musicianQueryScore.val();
if (query_param !== null) {
queryString += "score_limit=" + query_param + '&';
options['score_limit'] = query_param;
}
rest.searchMusicians(queryString)
var distance = $musicianDistance.val();
if (distance !== null) {
options['distance'] = distance;
}
if(searchLocationOverride) {
options['country'] = searchLocationOverride.countrycode;
options['region'] = searchLocationOverride.regioncode;
options['city'] = searchLocationOverride.city;
}
$spinner.show();
$refreshBtn.addClass('disabled')
currentRequest = rest.searchMusicians(options)
.done(afterLoadMusicians)
.fail(app.ajaxError)
.always(function() {$spinner.hide()})
.fail(function(jqXHR) {
if(jqXHR.status === 0 && jqXHR.statusText === 'abort') {
// do nothing
}
else {
app.ajaxError(arguments);
}
})
.always(function() {currentRequest = null; $spinner.hide(); $refreshBtn.removeClass('disabled')})
}
function abortCurrentRequest() {
if(currentRequest) {
currentRequest.abort()
currentRequest = null;
$spinner.hide();
$refreshBtn.removeClass('disabled')
}
}
function refreshDisplay() {
abortCurrentRequest();
clearResults();
setupSearch();
search();
}
// user clicked refresh
function manualRefresh() {
if(!$refreshBtn.is('.disabled')) {
refreshDisplay();
}
return false;
}
function changeSearchLocation() {
app.layout.showDialog('change-search-location').one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(data.canceled) {
// do nothing
}
else {
searchLocationOverride = data.result;
displayUserCity();
refreshDisplay();
}
})
return false;
}
function displayUserCity() {
app.user().done(function(user) {
var location = searchLocationOverride || user.location;
var unknown = 'unknown location';
var result = unknown;
if(location) {
var region = location['region'] ? location['region'] : location['regioncode']
if(!region) { region = 'n/a'; }
var city = location['city'];
result = city + ', ' + region;
}
$musicianFilterCity.text(result);
})
}
function afterLoadMusicians(mList) {
did_show_musician_page = true;
// display the 'no musicians' banner if appropriate
var $noMusiciansFound = $('#musicians-none-found');
musicianList = mList;
// @FIXME: This needs to pivot on musicianList.musicians.length
if (musicianList.length == 0) {
if (musicianList.musicians.length == 0) {
$noMusiciansFound.show();
musicians = [];
}
else {
$noMusiciansFound.hide();
musicians = musicianList['musicians'];
musicians = musicianList.musicians;
if (!(typeof musicians === 'undefined')) {
$('#musician-filter-city').text(musicianList['city']);
if (0 == page_count) {
if (-1 == page_count) {
page_count = musicianList['page_count'];
}
renderMusicians();
@ -102,7 +182,6 @@
function renderMusicians() {
var ii, len;
var mTemplate = $('#template-find-musician-row').html();
var fTemplate = $('#template-musician-follow-info').html();
var aTemplate = $('#template-musician-action-btns').html();
var mVals, musician, renderings = '';
var instr_logos, instr;
@ -122,19 +201,6 @@
}
instr_logos += '<img src="' + instr + '"/>';
}
follows = '';
followVals = {};
for (var jj = 0, ilen = musician['followings'].length; jj < ilen; jj++) {
aFollow = musician['followings'][jj];
followVals = {
user_id: aFollow.user_id,
musician_name: aFollow.name,
profile_url: '/client#/profile/' + aFollow.user_id,
avatar_url: context.JK.resolveAvatarUrl(aFollow.photo_url)
};
follows += context.JK.fillTemplate(fTemplate, followVals);
if (2 == jj) break;
}
var actionVals = {
profile_url: "/client#/profile/" + musician.id,
friend_class: 'button-' + (musician['is_friend'] ? 'grey' : 'orange'),
@ -147,9 +213,12 @@
};
var musician_actions = context.JK.fillTemplate(aTemplate, actionVals);
var full_score = musician['full_score'];
var latencyBadge = context._.template(
$templateAccountSessionLatency.html(),
$.extend(sessionUtils.createLatency(musician), musician),
{variable: 'data'}
);
var scoreInfo = sessionUtils.scoreInfo(full_score, false)
mVals = {
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url),
profile_url: "/client#/profile/" + musician.id,
@ -162,12 +231,9 @@
recording_count: musician['recording_count'],
session_count: musician['session_count'],
musician_id: musician['id'],
musician_follow_template: follows,
musician_action_template: musician_actions,
musician_one_way_score: score_to_text(full_score),
musician_score_color: scoreInfo.icon_name,
musician_score_color_alt: scoreInfo.description,
latency_style: scoreInfo.latency_style
latency_badge: latencyBadge,
musician_first_name: musician['first_name']
};
var $rendering = $(context.JK.fillTemplate(mTemplate, mVals))
@ -178,9 +244,10 @@
context.JK.helpBubble($('.friend-count', $rendering), 'musician-friend-count', {}, options);
context.JK.helpBubble($('.recording-count', $rendering), 'musician-recording-count', {}, options);
context.JK.helpBubble($('.session-count', $rendering), 'musician-session-count', {}, options);
helpBubble.scoreBreakdown($('.score-count', $rendering), false, full_score, myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions);
helpBubble.scoreBreakdown($('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions);
$results.append($rendering);
$rendering.find('.biography').dotdotdot();
}
@ -195,7 +262,10 @@
}
function afterShow(data) {
refreshDisplay();
if(!did_show_musician_page) {
// cache page because query is slow, and user may have paginated a bunch
refreshDisplay();
}
}
function clearResults() {
@ -203,7 +273,23 @@
$('#musician-filter-results .musician-list-result').remove();
$endMusicianList.hide();
page_num = 1;
page_count = 0;
page_count = -1;
}
function setupSearch() {
var orderby = $('#musician_order_by').val();
if(orderby == 'distance') {
$musicianSearchCity.show();
$musicianDistance.closest('.easydropdown-wrapper').show();
$musicianQueryScore.closest('.easydropdown-wrapper').hide();
$musicianLatencyOrDistanceLabel.text('Distance:')
}
else {
$musicianSearchCity.hide();
$musicianDistance.closest('.easydropdown-wrapper').hide();
$musicianQueryScore.closest('.easydropdown-wrapper').show();
$musicianLatencyOrDistanceLabel.text('Latency:')
}
}
function friendMusician(evt) {
@ -267,11 +353,18 @@
function events() {
$('#musician_query_score').change(refreshDisplay);
$musicianQueryScore.change(refreshDisplay);
$('#musician_instrument').change(refreshDisplay);
$('#musician_order_by').change(refreshDisplay);
$musicianDistance.change(refreshDisplay);
$musicianChangeFilterCity.click(changeSearchLocation);
$refreshBtn.click(manualRefresh)
$('#musician-filter-results').closest('.content-body-scroller').bind('scroll', function () {
// do not check scroll when filling out initial results, which we can tell if page_count == -1
if(page_count == -1) {return};
if ($(this).scrollTop() + $(this).innerHeight() >= $(this)[0].scrollHeight) {
if (page_num < page_count) {
page_num += 1;
@ -298,8 +391,17 @@
$results = $screen.find('#musician-filter-results');
$spinner = $screen.find('.paginate-wait')
$endMusicianList = $screen.find('#end-of-musician-list')
$templateAccountSessionLatency = $("#template-account-session-latency")
$musicianSearchCity = $('#musician-search-city');
$musicianFilterCity = $('#musician-filter-city');
$musicianChangeFilterCity = $('#musician-change-filter-city');
$musicianQueryScore = $('#musician_query_score');
$musicianDistance = $('#musician_distance');
$musicianLatencyOrDistanceLabel = $screen.find('.latency-or-distance')
$refreshBtn = $screen.find('.btn-refresh-entries');
events();
setupSearch();
displayUserCity();
}
this.initialize = initialize;

View File

@ -1189,13 +1189,10 @@
});
}
function searchMusicians(queryString) {
// squelch nulls and undefines
queryString = !!queryString ? queryString : "";
function searchMusicians(query) {
return $.ajax({
type: "GET",
url: "/api/search.json?" + queryString
url: "/api/search.json?" + $.param(query)
});
}

View File

@ -294,6 +294,16 @@
context.location = url;
}
this.updateUserModel = function(userUpdateData) {
var update = rest.updateUser(userUpdateData)
update.done(function() {
logger.debug("updating user info")
userDeferred = update; // update the global user object if this succeeded
})
return update;
}
// call .done/.fail on this to wait for safe user data
this.user = function() {
return userDeferred;

View File

@ -1,3 +1,4 @@
//= require bind-polyfill
//= require jquery
//= require jquery.monkeypatch
//= require jquery_ujs

View File

@ -427,15 +427,40 @@
return false;
}
function closeDialog(dialog) {
function cancel(evt) {
var $target = $(evt.currentTarget).closest('[layout]');
var layoutId = $target.attr('layout-id');
var isDialog = ($target.attr('layout') === 'dialog');
if (isDialog) {
cancelDialog(layoutId);
} else {
// ?
logger.warn("unable to handle cancel layout-action for %o", $target)
}
return false;
}
function cancelDialog(dialog) {
logger.debug("cancelling dialog: " + dialog);
var $dialog = $('[layout-id="' + dialog + '"]');
var result = dialogEvent(dialog, 'onCancel');
if(result !== false) {
closeDialog(dialog, true);
}
else {
logger.debug("dialog refused cancel");
}
}
function closeDialog(dialog, canceled) {
logger.debug("closing dialog: " + dialog);
var $dialog = $('[layout-id="' + dialog + '"]');
dialogEvent(dialog, 'beforeHide');
var $overlay = $('.dialog-overlay');
unstackDialogs($overlay);
$dialog.hide();
$dialog.triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result') });
$(context).triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result') });
$dialog.triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result'), canceled: canceled });
$(context).triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result'), canceled: canceled });
dialogEvent(dialog, 'afterHide');
}
@ -550,8 +575,23 @@
}
}
// if no arguments passed, then see if any dialog is showing
// if string passed, see if dialog is showing (even if buried) of a given name
function isDialogShowing() {
return openDialogs.length > 0;
if(arguments.length == 1) {
// user passed in dialog id
var dialogId = arguments[0];
context._.each(openDialogs, function(dialog) {
if($(dialog).attr('layout-id') == dialogId) {
return true;
}
})
return false;
}
else {
// user passed in nothing
return openDialogs.length > 0;
}
}
function currentDialog() {
@ -690,7 +730,7 @@
// var step = 0;
//setWizardStep(step);
//wizardShowFunctions[step]();
showDialog('gear-wizard');
return showDialog('gear-wizard');
}
function setWizardStep(targetStepId) {
@ -718,6 +758,22 @@
context.JK.GA.virtualPageView(location.pathname + location.search + location.hash);
}
function onHandleKey(e) {
if (e.keyCode == 27 /** esc */)
{
if(isDialogShowing()) {
var $dialog = currentDialog();
if(!$dialog) {
logger.error("unable to find current dialog on ESC");
return;
}
cancelDialog($dialog.attr('layout-id'));
}
}
}
function handleDialogState() {
var rawDialogState = $.cookie('dialog_state');
try {
@ -756,11 +812,13 @@
});
$('body').on('click', '[layout-link]', linkClicked);
$('[layout-action="close"]').on('click', close);
$('[layout-action="cancel"]').on('click', cancel);
$('[layout-sidebar-expander]').on('click', toggleSidebar);
$('[layout-panel="expanded"] [layout-panel="header"]').on('click', panelHeaderClicked);
$('[layout-wizard-link]').on('click', wizardLinkClicked);
$('[tab-target]').on('click', tabClicked);
$(context).on('hashchange', trackLocationChange);
$(document).keyup(onHandleKey);
}
// public functions
@ -960,6 +1018,7 @@
}
this.closeDialog = closeDialog;
this.cancelDialog = cancelDialog;
this.handleDialogState = handleDialogState;
this.queueDialog = queueDialog;

View File

@ -0,0 +1,271 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.SelectLocation = Class.extend({
init: function ($countries, $regions, $cities, app) {
this.api = context.JK.Rest();
this.logger = context.JK.logger;
this.loadingCitiesData = false;
this.loadingRegionsData = false;
this.loadingCountriesData = false;
this.nilOptionStr = '<option value=""></option>';
this.nilOptionText = 'n/a';
this.$countries = $countries;
this.$regions = $regions;
this.$cities = $cities;
this.app = app;
$countries.on('change', function (evt) {
evt.stopPropagation();
this.handleCountryChanged();
return false;
}.bind(this));
$regions.on('change', function (evt) {
evt.stopPropagation();
this.handleRegionChanged();
return false;
}.bind(this));
},
load: function (country, region, city) {
this.country = country;
this.region = region;
this.city = city;
if (!country) {
// this case shouldn't happen because sign up makes you pick a location. This is just 'in case', so that the UI is more error-resilient
this.logger.debug("user has no specified country: " + country)
country = 'US';
}
this.loadingCountriesData = true;
this.loadingRegionsData = true;
this.loadingCitiesData = true;
// make the 3 slower requests, which only matter if the user wants to affect their ISP or location
this.api.getCountries()
.done(function (countriesx) {
this.populateCountriesx(countriesx["countriesx"], country);
}.bind(this))
.fail(this.app.ajaxError)
.always(function () {
this.loadingCountriesData = false;
}.bind(this))
if (country) {
this.api.getRegions({ country: country })
.done(function (regions) {
this.populateRegions(regions["regions"], region);
}.bind(this))
.fail(this.regionListFailure.bind(this))
.always(function () {
this.loadingRegionsData = false;
}.bind(this))
if (region) {
this.api.getCities({ country: country, region: region })
.done(function (cities) {
this.populateCities(cities["cities"], this.city)
}.bind(this))
.fail(this.cityListFailure.bind(this))
.always(function () {
this.loadingCitiesData = false;
}.bind(this))
}
}
},
handleCountryChanged: function () {
var selectedCountry = this.$countries.val()
var selectedRegion = this.$regions.val()
var cityElement = this.$cities
this.updateRegionList(selectedCountry, this.$regions);
this.updateCityList(selectedCountry, null, cityElement);
},
handleRegionChanged: function () {
var selectedCountry = this.$countries.val()
var selectedRegion = this.$regions.val()
var cityElement = this.$cities;
this.updateCityList(selectedCountry, selectedRegion, cityElement);
},
updateRegionList: function (selectedCountry, regionElement) {
// only update region
if (selectedCountry) {
// set city disabled while updating
regionElement.attr('disabled', true).easyDropDown('disable');
this.loadingRegionsData = true;
regionElement.children().remove()
regionElement.append($(this.nilOptionStr).text('loading...'))
this.api.getRegions({ country: selectedCountry })
.done(this.getRegionsDone.bind(this))
.error(function (err) {
regionElement.children().remove()
regionElement.append($(this.nilOptionStr).text(this.nilOptionText))
}.bind(this))
.always(function () {
console.log("regions load: this.loadingRegionsData; " + this.loadingRegionsData)
this.loadingRegionsData = false;
}.bind(this))
}
else {
regionElement.children().remove()
regionElement.append($(this.nilOptionStr).text(this.nilOptionText))
}
},
updateCityList: function (selectedCountry, selectedRegion, cityElement) {
// only update cities
if (selectedCountry && selectedRegion) {
// set city disabled while updating
cityElement.attr('disabled', true).easyDropDown('disable');
this.loadingCitiesData = true;
cityElement.children().remove()
cityElement.append($(this.nilOptionStr).text('loading...'))
this.api.getCities({ country: selectedCountry, region: selectedRegion })
.done(this.getCitiesDone.bind(this))
.error(function (err) {
cityElement.children().remove()
cityElement.append($(this.nilOptionStr).text(this.nilOptionText))
}.bind(this))
.always(function () {
this.loadingCitiesData = false;
}.bind(this))
}
else {
cityElement.children().remove();
cityElement.append($(this.nilOptionStr).text(this.nilOptionText));
context.JK.dropdown(cityElement);
}
},
getCitiesDone: function (data) {
this.populateCities(data['cities'], this.city);
},
getRegionsDone: function (data) {
this.populateRegions(data['regions'], this.region);
this.updateCityList(this.$countries.val(), this.$regions.val(), this.$cities);
},
writeCountry: function (index, countryx) {
if (!countryx.countrycode) return;
var option = $(this.nilOptionStr);
option.text(countryx.countryname);
option.attr("value", countryx.countrycode);
if (countryx.countrycode == this.country) {
this.foundCountry = true;
}
this.$countries.append(option);
},
populateCountriesx: function (countriesx) {
// countriesx has the format [{countrycode: "US", countryname: "United States"}, ...]
this.foundCountry = false;
this.$countries.children().remove();
var nilOption = $(this.nilOptionStr);
nilOption.text(this.nilOptionText);
this.$countries.append(nilOption);
$.each(countriesx, this.writeCountry.bind(this));
if (!this.foundCountry) {
this.logger.warn("user has no country in the database. user's country:" + this.country)
// in this case, the user has a country that is not in the database
// this can happen in a development/test scenario, but let's assume it can
// happen in production too.
var option = $(this.nilOptionStr);
option.text(this.country);
option.attr("value", this.country);
this.$countries.append(option);
}
this.$countries.val(this.country);
this.$countries.attr("disabled", null).easyDropDown('enable');
context.JK.dropdown(this.$countries);
},
writeRegion: function (index, region) {
if (!region) return;
var option = $(this.nilOptionStr)
option.text(region['name'])
option.attr("value", region['region'])
this.$regions.append(option)
},
populateRegions: function (regions, userRegion) {
this.$regions.children().remove()
var nilOption = $(this.nilOptionStr);
nilOption.text(this.nilOptionText);
this.$regions.append(nilOption);
$.each(regions, this.writeRegion.bind(this))
this.$regions.val(userRegion)
this.$regions.attr("disabled", null).easyDropDown('enable');
context.JK.dropdown(this.$regions);
},
writeCity: function (index, city) {
if (!city) return;
var option = $(this.nilOptionStr)
option.text(city)
option.attr("value", city)
this.$cities.append(option)
},
populateCities: function (cities, userCity) {
this.$cities.children().remove();
var nilOption = $(this.nilOptionStr);
nilOption.text(this.nilOptionText);
this.$cities.append(nilOption);
$.each(cities, this.writeCity.bind(this))
this.$cities.val(userCity)
this.$cities.attr("disabled", null).easyDropDown('enable');
context.JK.dropdown(this.$cities);
},
regionListFailure: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 422) {
this.logger.debug("no regions found for country: " + this.country);
}
else {
this.app.ajaxError(arguments);
}
},
cityListFailure: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 422) {
this.logger.debug("no cities found for country/region: " + this.country + "/" + this.region);
}
else {
this.app.ajaxError(arguments);
}
}
});
})(window, jQuery);

View File

@ -103,13 +103,16 @@
var $row = $(context.JK.fillTemplate($activeSessionTemplate.html(), sessionVals));
var $offsetParent = $(tbGroup).closest('.content');
var $latencyBadge = $row.find('.latency-value');
var full_score = $latencyBadge.attr('data-full-score') || null;
var internet_score = $latencyBadge.attr('data-internet-score') || null;
var audio_latency = $latencyBadge.attr('data-audio-latency') || null;
var latencyBadgeUserId = $latencyBadge.attr('data-user-id');
var scoreOptions = {offsetParent: $offsetParent};
helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions);
var $latencyBadges = $row.find('.latency-value');
context._.each($latencyBadges, function(latencyBadge) {
var $latencyBadge = $(latencyBadge);
var full_score = $latencyBadge.attr('data-full-score') || null;
var internet_score = $latencyBadge.attr('data-internet-score') || null;
var audio_latency = $latencyBadge.attr('data-audio-latency') || null;
var latencyBadgeUserId = $latencyBadge.attr('data-user-id');
var scoreOptions = {offsetParent: $offsetParent};
helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions);
});
$(tbGroup).append($row);
@ -239,13 +242,16 @@
var $row = $(context.JK.fillTemplate($inactiveSessionTemplate.html(), sessionVals));
var $offsetParent = $(tbGroup).closest('.content');
var $latencyBadge = $row.find('.latency-value');
var full_score = $latencyBadge.attr('data-full-score') || null;
var internet_score = $latencyBadge.attr('data-internet-score') || null;
var audio_latency = $latencyBadge.attr('data-audio-latency') || null;
var latencyBadgeUserId = $latencyBadge.attr('data-user-id');
var scoreOptions = {offsetParent: $offsetParent};
helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions);
var $latencyBadges = $row.find('.latency-value');
context._.each($latencyBadges, function(latencyBadge) {
var $latencyBadge = $(latencyBadge);
var full_score = $latencyBadge.attr('data-full-score') || null;
var internet_score = $latencyBadge.attr('data-internet-score') || null;
var audio_latency = $latencyBadge.attr('data-audio-latency') || null;
var latencyBadgeUserId = $latencyBadge.attr('data-user-id');
var scoreOptions = {offsetParent: $offsetParent};
helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions);
})
// initial page load

View File

@ -515,6 +515,15 @@
}
function onWindowBackgrounded(type, text) {
app.user()
.done(function(userProfile) {
if(userProfile.show_whats_next &&
window.location.pathname.indexOf(gon.client_path) == 0 &&
!app.layout.isDialogShowing('getting-started')) {
app.layout.showDialog('getting-started');
}
})
if(!inSession()) return;
// the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen

View File

@ -11,7 +11,7 @@
var rest = new JK.Rest();
var userMe = null;
var invitationDialog = null;
var notYetShownWhatsNext = true;
var nowYetShownGettingStarted = true;
function menuHoverIn() {
$('ul.shortcuts', this).show();
@ -77,7 +77,6 @@
// TODO - Setting global variable for local user.
context.JK.userMe = r;
updateHeader();
handleWhatsNext(userMe);
});
}
@ -86,17 +85,6 @@
showAvatar();
}
function handleWhatsNext(userProfile) {
if (notYetShownWhatsNext && gon.isNativeClient && userProfile.show_whats_next) {
notYetShownWhatsNext = false;
console.log("window.location.pathname", window.location.pathname, gon.client_path, window.location.pathname.indexOf(gon.client_url));
if(window.location.pathname.indexOf(gon.client_path) == 0) {
app.layout.showDialog('whatsNext');
}
}
}
// initially show avatar
function showAvatar() {
var photoUrl = context.JK.resolveAvatarUrl(userMe.photo_url);

View File

@ -115,7 +115,9 @@
})
var helpText = context._.template($('#template-help-' + templateName).html(), data, { variable: 'data' });
var $template = $('#template-help-' + templateName);
if($template.length == 0) throw "no template by the name " + templateName;
var helpText = context._.template($template.html(), data, { variable: 'data' });
var holder = $('<div class="hover-bubble help-bubble"></div>');
holder.append(helpText);
context.JK.hoverBubble($element, helpText, options);
@ -136,7 +138,7 @@
if(!options) options = {};
options['trigger'] = 'none';
options['clickAnywhereToClose'] = false
if(!options['duration']) options['duration'] = 4000;
if(!options['duration']) options['duration'] = 6000;
var existingTimer = $element.data("prodTimer");
if(existingTimer) {

View File

@ -77,6 +77,7 @@
context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'voice_chat_helper', onInvalidAudioDevice);
registerVuCallbacks();
}
function beforeHide() {
context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'voice_chat_helper', onInvalidAudioDevice);
jamClient.FTUERegisterVUCallbacks('', '', '');

View File

@ -1,3 +1,4 @@
//= require bind-polyfill
//= require jquery
//= require jquery.monkeypatch
//= require jquery_ujs

View File

@ -475,42 +475,6 @@ div[layout-id="ftue3"] {
}
#whatsnext-dialog {
height:auto;
.ftue-inner h2 {
font-weight:normal;
color:#ed3618;
margin-bottom:6px;
font-size:1.7em;
}
.ftue-inner table td.whatsnext {
font-size:12px;
padding:10px;
width:50%;
font-weight:300;
}
.ftue-inner table td.whatsnext {
font-size:12px;
padding:10px;
width:50%;
font-weight:300;
}
.ftue-inner table a {
text-decoration:none;
}
.ftue-inner table {
border-collapse:separate;
border-spacing: 20px;
}
}
.ftue-inner {
width:750px;
padding:25px;

View File

@ -1,28 +1,4 @@
.filter-element {
float:left;
margin-left: 5px;
&.wrapper {
margin-top: 5px;
&.right {
float: right;
> a {
margin-top: 3px;
}
}
}
// @FIXME labeel is overriding from #session-controls.
&.desc {
margin-top: 3px;
padding-top: 3px;
}
+ .easydropdown-wrapper {
float:left;
margin-left:2px;
}
}
@import 'common';
#musicians-screen {
@ -35,6 +11,11 @@
}
}
.btn-refresh-holder {
float:right;
margin-right:10px;
}
.paginate-wait {
display:none;
margin:auto;
@ -46,26 +27,144 @@
vertical-align:top;
}
}
}
.musician-avatar {
width:63px;
}
#musician-filter-results {
margin: 0 10px 0px 10px;
}
.musician-profile, .musician-stats {
width:150px;
overflow:hidden;
}
.musician-wrapper {
-ms-overflow-style: none;
overflow: initial;
height: 100%;
width: 100%;
}
.musician-stats {
margin-top:10px;
}
.musician-info {
margin-top: 12px;
margin-right:0;
}
.musician-list-result {
padding-top: 5px;
padding-right: 5px;
padding-left: 5px;
table.musicians {
margin-top:12px;
.button-row {
float:none;
}
.latency-holder {
position:absolute;
top: 53px;
width:100%;
text-align:center;
}
.latency {
min-width: 50px;
display:inline-block;
padding:4px;
font-family:Arial, Helvetica, sans-serif;
font-weight:200;
font-size:11px;
text-align:center;
@include border-radius(2px);
color:white;
}
.latency-unknown {
background-color:$latencyBadgeUnknown;
}
.latency-unacceptable {
background-color:$latencyBadgeUnacceptable;
}
.latency-good {
background-color:$latencyBadgeGood;
}
.latency-fair{
background-color:$latencyBadgeFair;
}
.latency-poor {
background-color:$latencyBadgePoor;
}
/**
.latency {
font-size: 15px;
height: 16px;
padding: 3px;
width: 100%;
@include border-radius(2px);
width:130px;
position:absolute;
top: 63px;
}
*/
.biography {
height:73px;
}
.musician-wrapper {
-ms-overflow-style: none;
overflow: initial;
height: 100%;
width: 100%;
}
.musician-latency {
width: 136px;
height:100px;
margin-right:35px;
text-align:center;
position:relative;
}
.latency-help {
margin-top:8px;
line-height:14px;
}
.musician-list-result {
padding-top: 5px;
padding-right: 5px;
padding-left: 5px;
height: 123px;
table.musicians {
margin-top:12px;
}
}
#musician-filter-results {
margin: 0 10px 0px 10px;
}
}
.filter-element {
float:left;
margin-left: 5px;
&.wrapper {
margin-top: 5px;
&.right {
float: right;
> a {
margin-top: 3px;
}
}
}
// @FIXME labeel is overriding from #session-controls.
&.desc {
margin-top: 3px;
padding-top: 3px;
}
+ .easydropdown-wrapper {
float:left;
margin-left:2px;
}
}

View File

@ -185,6 +185,18 @@ small, .small {font-size:11px;}
text-decoration:none;
line-height:12px;
text-align:center;
&.disabled {
background-color: transparent;
border: solid 1px #868686;
outline: solid 2px transparent;
color:#ccc;
}
&.disabled:hover {
background-color: #515151;
color:#ccc;
}
}
.button-grey:hover {
@ -215,8 +227,6 @@ small, .small {font-size:11px;}
}
&.disabled:hover {
//background-color:darken(#f16750, 20%);
//color:#FFF;
background-color: #515151;
color:#ccc;
}

View File

@ -3,8 +3,6 @@
table.findsession-table, table.local-recordings, #account-session-detail {
.latency-unacceptable {
width: 50px;
height: 10px;

View File

@ -0,0 +1,49 @@
@import "client/common";
#change-search-location-dialog {
height:300px;
min-height:300px;
width:390px;
min-width:390px;
form {
width:100%;
@include border_box_sizing;
.column {
float:left;
@include border_box_sizing;
&:nth-of-type(1) {
width:100%;
}
}
}
.field{
margin-bottom:5px;
}
a.reset-location {
margin: 5px 0 0 130px;
font-size:12px;
}
label {
display:inline-block;
width:130px;
line-height:26px;
vertical-align:top;
}
.hint {
font-size:16px;
line-height:18px;
margin-bottom:20px;
}
.easydropdown {
width:150px;
}
}

View File

@ -0,0 +1,141 @@
@import "client/common";
#whatsnext-dialog, #getting-started-dialog {
height:auto;
width:auto;
.ftue-inner h2 {
font-weight:normal;
color:#ed3618;
margin-bottom:6px;
font-size:1.7em;
}
.icheckbox_minimal {
display:inline-block;
position:relative;
top:3px;
margin-right:3px;
}
.show-getting-started {
position:absolute;
margin-left:-280px;
right:50%;
top:-2px;
width:280px;
color:rgb(170, 170, 170);
span {
font-size:15px;
}
}
.close-btn {
position:relative;
}
.title {
width:97%;
margin:20px 1.5%;
}
.row {
font-size:12px;
font-weight:300;
margin:10px 0;
@include border_box_sizing;
.column {
@include border_box_sizing;
width:47%;
float:left;
background-color:black;
padding: 10px;
height:140px;
margin: 0 1.5%;
}
&.full {
.column {
width:97%;
margin:0 1.5%;
height:78px;
}
}
&.find-connect {
.column {
height:128px;
}
}
&.setup-gear {
.action-button {
float:right;
margin-top:2px;
}
}
&.learn-more {
height:80px;
margin-bottom:20px;
.blurb a {
margin-left:5px;
}
}
&.dialog-buttons {
margin-top:35px;
}
.action-button {
margin-top:10px;
text-align:center;
}
.blurb {
line-height:1.3em;
}
.buttons {
width:100%;
margin:0 auto;
text-align:center;
}
.social-buttons {
text-align:center;
width:100;
margin:0 auto;
height:24px;
line-height:24px;
vertical-align:middle;
a {
vertical-align: middle;
line-height:24px;
height:24px;
margin:5px;
}
span {
vertical-align: top;
line-height:24px;
height:24px;
}
}
}
.column {
@include border_box_sizing;
}
.ftue-inner table a {
text-decoration:none;
}
}

View File

@ -0,0 +1,26 @@
@import "client/common";
#join-test-session-dialog{
width:600px;
min-height:inherit;
em {
font-style:italic;
}
.title {
font-weight:normal;
color:#ed3618;
margin-bottom:6px;
font-size:1.7em;
}
.hint {
margin: 20px 0;
line-height:1.3em;
}
.buttons {
text-align:right;
}
}

View File

@ -10,7 +10,7 @@
line-height:1em;
}
.buttons {
.launch-buttons {
margin:20px 0;
}
}

View File

@ -1,8 +0,0 @@
#whatsnext-dialog {
.icheckbox_minimal {
display:inline-block;
position:relative;
top:3px;
margin-right:3px;
}
}

View File

@ -7,15 +7,10 @@ class ApiSearchController < ApiController
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
# puts "================== params #{params.to_s}"
query = params.clone
query[:remote_ip] = request.remote_ip
if 1 == query[Search::PARAM_MUSICIAN].to_i
clientid = query[:clientid]
conn = (clientid ? Connection.where(client_id: clientid, user_id: current_user.id).first : nil)
# puts "================== query #{query.inspect}"
@search = Search.musician_filter(query, current_user, conn)
# puts "================== search #{@search.inspect}"
@search = Search.musician_filter(query, current_user)
else
@search = Search.band_filter(query, current_user)
end

View File

@ -668,7 +668,7 @@ class ApiUsersController < ApiController
def udp_reachable
Connection.transaction do
@connection = Connection.find_by_client_id(params[:client_id])
@connection = Connection.find_by_client_id!(params[:client_id])
@connection.udp_reachable = params[:udp_reachable]
@connection.save
respond_with_model(@connection)

View File

@ -34,9 +34,6 @@ if @search.musicians_text_search?
end
if @search.musicians_filter_search?
node :city do |user|
current_user.try(:location)
end
node :page_count do |foo|
@search.page_count

View File

@ -12,6 +12,10 @@ end
if @user == current_user
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter, :new_notifications
node :location do |user|
geoiplocation = current_user.geoiplocation
geoiplocation.info if geoiplocation
end
elsif current_user
node :is_friend do |uu|

View File

@ -1,5 +1,5 @@
<!-- Account Summary Dialog -->
<div layout="screen" layout-id="account/audio" class="screen secondary">
<div layout="screen" layout-id="account/audio" class="screen secondary" id="account-audio-profile">
<!-- header -->
<div class="content-head">
<!-- icon -->

View File

@ -158,6 +158,7 @@
%a{href: "#", 'user-id' => "{{data.user_id}}", 'hoveraction' => "musician", class: 'avatar-tiny'}
%img{src: "{{data.avatar_url}}"}
// also used by musicians page
%script{type: 'text/template', id: 'template-account-session-latency'}
.latency{class: "{{data.latency_style}}", 'data-user-id' => "{{data.id}}", 'data-audio-latency' => "{{data.audio_latency || ''}}", 'data-full-score' => "{{data.full_score || ''}}", 'data-internet-score' => "{{data.internet_score || ''}}"}
{{data.latency_text}}

View File

@ -113,3 +113,9 @@
</script>
<script type="text/template" id="template-help-no-audio-profiles">
<div class="help-no-audio-profiles">
Click here to configure new audio gear.
</div>
</script>

View File

@ -13,7 +13,9 @@
<%= content_tag(:div, :class => 'content-body-scroller') do -%>
<%= content_tag(:div, :class => 'content-wrapper musician-wrapper') do -%>
<%= content_tag(:div, '', :id => 'musician-filter-results', :class => 'filter-results') %>
<div class="paginate-wait">Fetching more results...<div class="spinner-small"></div></div>
<div class="paginate-wait">Fetching more results...
<div class="spinner-small"></div>
</div>
<%= content_tag(:div, 'No more results.', :class => 'end-of-list', :id => 'end-of-musician-list') %>
<% end -%>
<% end -%>
@ -27,64 +29,55 @@
<script type="text/template" id="template-find-musician-row">
<div class="profile-band-list-result musician-list-result" data-musician-id={musician_id}>
<div class="f11" data-hint="container">
<div class="left" style="width:63px;margin-top:-12px;">
<div class="left musician-avatar">
<!-- avatar -->
<div class="avatar-small"><img src="{avatar_url}" /></div>
</div>
<div class="right musician-following" style="width: 120px;">
<div class="bold">FOLLOWING:</div>
<table class="musicians" cellpadding="0" cellspacing="5">{musician_follow_template}</table>
</div>
<div class="" style="margin-left: 63px; margin-right: 130px;margin-top: 12px;">
<div class="left musician-info"">
<div class="first-row" data-hint="top-row">
<div class="lcol left">
<div class="musician-profile">
<!-- name & location -->
<div class="result-name">{musician_name}</div>
<div class="result-location">{musician_location}</div>
<div id="result_instruments" class="instruments nowrap mt10">{instruments}</div>
</div>
<div class="whitespace">
<div class="biography">{biography}</div>
</div>
<div class="clearleft"></div>
</div>
<div class="button-row " data-hint="button-row">
<div class="lcol stats left">
<div class="musician-stats">
<span class="friend-count">{friend_count} <img src="../assets/content/icon_friend.png" alt="friends" width="14" height="12" align="absmiddle" style="margin-right:4px;"/></span>
<span class="follower-count">{follow_count} <img src="../assets/content/icon_followers.png" alt="follows" width="22" height="12" align="absmiddle" style="margin-right:4px;"/></span>
<span class="recording-count">{recording_count} <img src="../assets/content/icon_recordings.png" alt="recordings" 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" alt="sessions" width="12" height="12" align="absmiddle" style="margin-right:4px;"/></span>
<span class="score-count {latency_style}">{musician_one_way_score} <img src="../assets/content/icon_{musician_score_color}_score.png" alt="{musician_score_color_alt} score" width="12" height="12" align="absmiddle" style="margin-right:4px;"/></span>
</div>
<div class="result-list-button-wrapper" data-musician-id={musician_id}>
{musician_action_template}
</div>
<div class="clearall"></div>
</div>
<br clear="both"/>
</div>
<div class="left musician-latency" >
<div class="latency-help">Your latency<br/>to {musician_first_name} is: </div>
<div class="latency-holder">
{latency_badge}
</div>
<br clear="both"/>
</div>
<div class="button-row" data-hint="button-row">
<div class="biography">{biography}</div>
<div class="result-list-button-wrapper" data-musician-id={musician_id}>
{musician_action_template}
</div>
<br clear="both"/>
</div>
</div>
</div>
</script>
<script type="text/template" id="template-musician-action-btns">
<a href="{profile_url}" class="button-orange smallbutton">PROFILE</a>
<% if current_user && current_user.musician? %>
<a href="#" class="{friend_class} smallbutton search-m-friend">{friend_caption}</a>
<% end %>
<a href="#" class="{follow_class} smallbutton search-m-follow">{follow_caption}</a>
<a href="#" class="{message_class} smallbutton search-m-message">{message_caption}</a>
<!--<a href="#" class="{button_message} smallbutton search-m-like">MESSAGE</a>-->
<div class="clearall"></div>
</script>
<script type="text/template" id="template-musician-follow-info">
<tr>
<td width="32">
<a user-id="{user_id}" hoveraction="musician" href="{profile_url}" class="avatar-tiny"><img src="{avatar_url}" /></a>
</td>
<td>
<a user-id="{user_id}" hoveraction="musician" href="{profile_url}"><strong>{musician_name}</strong></a>
</td>
</tr>
</script>
</script>

View File

@ -49,13 +49,14 @@
<!-- @end show filter -->
<% elsif :musician == filter_label %>
<!-- @begin score filter -->
<%= content_tag(:div, 'Latency:', :class => 'filter-element desc') %>
<%= content_tag(:div, 'Latency:', :class => 'filter-element desc latency-or-distance') %>
<%= content_tag(:div, :class => 'query-distance-params') do -%>
<%= select_tag("musician_query_score", options_for_select(Search::M_SCORE_OPTS, Search::M_SCORE_DEFAULT), {:class => 'easydropdown'}) %>
<%= select_tag("musician_distance", options_for_select(Search::M_DISTANCE_OPTS, Search::M_DISTANCE_DEFAULT), {:class => 'easydropdown'}) %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element desc') do -%>
to <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => "musician-filter-city") %>
<% end -%>
<div class="filter-element desc" id="musician-search-city">
to <a href="#" id="musician-change-filter-city"><span id="musician-filter-city"></span></a>
</div>
<!-- @end score filter -->
<% else %>
<!-- @begin distance filter -->
@ -75,6 +76,10 @@
<div class="btn-refresh-holder">
<a class="button-grey btn-refresh-entries" href="/client#/feed">REFRESH</a>
</div>
<% elsif :musician == filter_label %>
<div class="btn-refresh-holder">
<a class="button-grey btn-refresh-entries" href="/client#/musicians">REFRESH</a>
</div>
<% end %>
<% end -%>
<!-- @end web_filter -->

View File

@ -247,6 +247,9 @@
var testBridgeScreen = new JK.TestBridgeScreen(JK.app);
testBridgeScreen.initialize();
var changeSearchLocationDialog = new JK.ChangeSearchLocationDialog(JK.app);
changeSearchLocationDialog.initialize();
// do a client update early check upon initialization
JK.ClientUpdateInstance.check()

View File

@ -8,7 +8,7 @@
.right.action-buttons
%a.button-grey.btn-close-dialog{href:'#', 'layout-action' => 'close'} CLOSE
%a.button-grey.btn-cancel-dialog{href:'#', 'layout-action' => 'close'} CANCEL
%a.button-grey.btn-cancel-dialog{href:'#', 'layout-action' => 'cancel'} CANCEL
%a.button-orange.btn-accept-friend-request{href:'#'} ACCEPT
%script{type: 'text/template', id: 'template-friend-request-not-friends'}

View File

@ -35,7 +35,7 @@
li = 'Input device is not connected'
.buttons
.left
a.button-grey.btnCancel CANCEL
a.button-grey.btnCancel layout-action="cancel" CANCEL
a.button-grey.btnConfigureGear GO TO AUDIO GEAR SCREEN
a.button-grey.btnRestartApplication RESTART APPLICATION
.right

View File

@ -0,0 +1,24 @@
.dialog.dialog-overlay-sm layout='dialog' layout-id='change-search-location' id='change-search-location-dialog'
.content-head
h1 change search location
.dialog-inner
.hint
| Specify which location you want to search from.
form action='post'
.column
.field purpose="country"
label for="country" Country:
select name="country"
.field purpose="region"
label for="region" State/Province:
select name="region" disabled="disabled"
.field purpose="city"
label for="city" City:
select name="city" disabled="disabled"
a href="#" class="reset-location" Reset to my current location
br clear='all'
.buttons
.right
a.button-grey class='btnCancel' layout-action='cancel' CANCEL
a.button-orange class='btnSave' SAVE

View File

@ -13,6 +13,6 @@
br clear='all'
.buttons
.right
a.button-grey class='btnCancel' layout-action='close' CANCEL
a.button-grey class='btnCancel' layout-action='cancel' CANCEL
a.button-orange class='btnSave' SAVE

View File

@ -24,4 +24,7 @@
= render 'dialogs/videoDialog'
= render 'dialogs/friendSelectorDialog'
= render 'dialogs/clientPreferencesDialog'
= render 'dialogs/audioProfileInvalidDialog'
= render 'dialogs/audioProfileInvalidDialog'
= render 'dialogs/gettingStartedDialog'
= render 'dialogs/joinTestSessionDialog'
= render 'dialogs/changeSearchLocationDialog'

View File

@ -18,7 +18,7 @@
%label{for: 'is_public'} Public Recording
.buttons
%a.button-grey.cancel-btn CANCEL
%a.button-grey.cancel-btn {'layout-action' => 'cancel'} CANCEL
%a.button-orange.delete-btn DELETE
%a.button-orange.save-btn UPDATE
%br{clear: 'all'}

View File

@ -0,0 +1,81 @@
.dialog.whatsnext-overlay.ftue-overlay.tall layout="dialog" layout-id="getting-started" id="getting-started-dialog"
.content-head
h1 getting started
.ftue-inner
.title
| Welcome to JamKazam! Here are the top things you should do to get the
most out of this service. Be sure to get through all of these for the best experience, but you can spread
them out over multiple visits!
.row.full.setup-gear
.column
h2 SET UP GEAR
.blurb
span
.action-button
a.button-orange.setup-gear-btn layout-link="account/audio" SET UP GEAR
| Click SET UP GEAR to configure and test your audio gear and network connection.
Once youve completed this step, well drop you into your first session, and you can explore the session
interface.
br clear="both"
.row
.column
h2 INVITE YOUR FRIENDS
.blurb
| Invite others to join JamKazam. Youll be connected as friends, which makes it easier to get into sessions
together. And it will grow our community, which helps us as a young company. Click the icons below to
invite!
.social-buttons
a href="#" class="facebook-invite"
= image_tag "content/icon_facebook.png", {:align=>"absmiddle", :height => 24, :width => 24}
span Facebook
a href="#" class="email-invite"
= image_tag "content/icon_gmail.png", {:align=>"absmiddle", :height => 24, :width => 24}
span E-mail
a href="#" class="google-invite"
= image_tag "content/icon_google.png", {:align=>"absmiddle", :height => 26, :width => 26 }
span Google+
.column
h2 CREATE A "REAL" SESSION
.blurb
| You can create a session to start immediately and hope others join, but this doesnt work well. Its better
to schedule a session and invite friends or the community to join you. Watch a video to learn how, then
schedule your first session!
.action-button
a.button-orange rel="external" href="https://www.youtube.com/watch?v=EZZuGcDUoWk" WATCH VIDEO
br clear="both"
.row.find-connect
.column
h2 FIND SESSIONS TO JOIN
.blurb
| In addition to creating your own sessions, its awesome to join others sessions. Watch this tutorial video
to learn about how to find and select good sessions to join.
.action-button
a.button-orange.setup-gear rel="external" href="https://www.youtube.com/watch?v=xWponSJo-GU" WATCH VIDEO
.column
h2 CONNECT WITH MUSICIANS
.blurb
| To play more music, tap into our growing
community to connect with other musicians. Watch this video for tips on how to do this.
.action-button
a.button-orange rel="external" href="https://www.youtube.com/watch?v=xWponSJo-GU" WATCH VIDEO
br clear="both"
.row.full.learn-more
.column
h2 LEARN MORE ABOUT JAMKAZAM
.blurb
| There is a lot you can do with JamKazam, and more great features available every week. Check the
following link for a list of videos and other resources you can use to take advantage of everything thats
available:
a rel="external" purpose="youtube-tutorials" href="http://www.youtube.com/channel/UC38nc9MMZgExJAd7ca3rkUA" JamKazam Tutorials & Resources
br clear="both"
.row.dialog-buttons
.buttons
.show-getting-started
input type="checkbox" id="show_getting_started"
span Don't show this again
a.close-btn href="#" class="button-orange" layout-action="close"
| CLOSE
br clear="both"

View File

@ -0,0 +1,21 @@
.dialog layout='dialog' layout-id='join-test-session' id='join-test-session-dialog'
.content-head
h1 join test session
.dialog-inner
.title
| VERIFY YOUR SETUP WITH A TEST SESSION
.hint
| You have set up your gear and verified your network, but does it
em &nbsp;really&nbsp;
| work?
br
br
| The way to find out is by joining a test session.
br
br
| This will familiarize you with how sessions work in JamKazam, and if you are lucky, someone else might even join in.
.buttons
a.button-grey layout-action="cancel" CANCEL
a.button-orange.join-test-session JOIN TEST SESSION
br

View File

@ -7,14 +7,14 @@
%script{type: 'text/template', id: 'template-attempt-launch'}
%p
{{data.messagePrefix}}, you must use the JamKazam application.
.right.buttons
.right.launch-buttons
%a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL
%a.button-orange.btn-launch-app{href:'{{data.launchUrl}}'} LAUNCH APP
%script{type: 'text/template', id: 'template-unsupported-launch'}
%p
{{data.messagePrefix}}, you must use the JamKazam application. Please download and install the application if you have not done so already.
.right.buttons
.right.launch-buttons
%a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL
%a.button-orange.btn-go-to-download-page{href:'/downloads'} GO TO APP DOWNLOAD PAGE
@ -31,7 +31,7 @@
If the application is not running, then please
%a.download-application{href: '/downloads'} download
and install the application if you have not done so already, and then start it manually rather than using this web launcher.
.right.buttons
.right.launch-buttons
%a.button-grey.btn-done{href:'#', 'layout-action' => 'close'} DONE
%script{type: 'text/template', id: 'template-launch-unsuccessful'}
@ -44,6 +44,6 @@
If the application is not running, then please
%a.download-application{href: '/downloads'} download
and install the application if you have not done so already, and then start it manually rather than using this web launcher.
.right.buttons
.right.launch-buttons
%a.button-grey.btn-done{href:'#', 'layout-action' => 'close'} CLOSE
%a.button-orange.btn-go-to-download-page{href:'/downloads'} GO TO APP DOWNLOAD PAGE

View File

@ -6,7 +6,7 @@
.clearall
.buttons
.left
%a.button-grey.btn-cancel{href:'#'} CANCEL
%a.button-grey.btn-cancel{href:'#', 'layout-action' => 'cancel'} CANCEL
%a.button-grey.btn-help{rel: 'external', href: 'https://jamkazam.desk.com/customer/portal/articles/1599969-first-time-setup---step-6---test-your-network'} HELP
.right
%a.button-orange.btn-close{href:'#'} CLOSE

View File

@ -14,6 +14,9 @@ SampleApp::Application.configure do
# Log error messages when you accidentally call methods on nil
config.whiny_nils = true
# useful when debugging a javascript problem...
config.assets.compress = false
# Show full error reports and disable caching
config.consider_all_requests_local = true
config.action_controller.perform_caching = false

View File

@ -12,8 +12,7 @@ if Rails.env == "development" && Rails.application.config.bootstrap_dev_users
User.create_dev_user("Jonathan", "Kolyer", "jonathan@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Oswald", "Becca", "os@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Anthony", "Davis", "anthony@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Bert", "Owen", "bert@jamkazam.com", "jam123", "Amsterdam", "North Holland", "Netherlands", nil, nil)
User.create_dev_user("Bart", "Zonk", "bart@jamkazam.com", "jam123", "Purmerend", "North Holland", "Netherlands", nil, nil)
User.create_dev_user("Steven", "Miers", "steven@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
end

View File

@ -29,6 +29,52 @@ FactoryGirl.define do
admin true
end
factory :austin_user do
first_name 'Austin'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Austin'
last_jam_locidispid { austin_geoip[:locidispid] }
last_jam_addr { austin_ip }
end
factory :dallas_user do
first_name 'Dallas'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Dallas'
last_jam_locidispid { dallas_geoip[:locidispid] }
last_jam_addr { dallas_ip }
end
factory :houston_user do
first_name 'Houston'
sequence(:last_name) { |n| "#{n}" }
state 'TX'
city 'Houston'
last_jam_locidispid { houston_geoip[:locidispid] }
last_jam_addr { houston_ip }
end
factory :miami_user do
first_name 'Miami'
sequence(:last_name) { |n| "#{n}" }
state 'FL'
city 'Miami'
last_jam_locidispid { miami_geoip[:locidispid] }
last_jam_addr { miami_ip }
end
factory :seattle_user do
first_name 'Seattle'
sequence(:last_name) { |n| "#{n}" }
state 'WA'
city 'Seattle'
last_jam_locidispid { seattle_geoip[:locidispid] }
last_jam_addr { seattle_ip }
end
factory :band_musician do
after(:create) do |user|
band = FactoryGirl.create(:band)

View File

@ -14,46 +14,15 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
it "success path" do
FactoryGirl.create(:latency_tester)
fast_signin user, '/client#/account/audio'
# step 1 - intro
find("div.account-audio a[data-purpose='add-profile']").trigger(:click)
find('.btn-next').trigger(:click)
# step 2 - select gear
find('.ftue-step-title', text: 'Select & Test Audio Gear')
should_not have_selector('.resync-status') # when you enter this step,
jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 3 - configure tracks
find('.ftue-step-title', text: 'Configure Tracks')
# drag one input over to tracks area http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Element#drag_to-instance_method
input = first('.ftue-input')
track_slot = first('.track-target')
input.drag_to(track_slot)
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 4 - configure voice chat
find('.ftue-step-title', text: 'Configure Voice Chat')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 5 - configure direct monitoring
find('.ftue-step-title', text: 'Turn Off Direct Monitoring')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 6 - Test Router & Network
find('.ftue-step-title', text: 'Test Router & Network')
find('.button-orange.start-network-test').trigger(:click)
find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click)
find('.button-orange.start-network-test')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 7 - Success
find('.ftue-step-title', text: 'Success!')
find('.btn-close.button-orange').trigger(:click)
walk_gear_wizard
# should see prompt afterwards about joining a test session
find('h1', text: 'join test session')
find('.join-test-session').trigger(:click)
# and should now be in session
find('h2', text: 'my tracks')
end
end

View File

@ -0,0 +1,93 @@
require 'spec_helper'
describe "Home Screen", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
let(:user) { FactoryGirl.create(:user, :show_whats_next => true) }
describe "in normal browser" do
before(:each) do
sign_in_poltergeist user
visit "/client"
should have_selector('h1', text: 'getting started')
end
it "should show launch app dialog if clicked setup gear" do
find('#getting-started-dialog .setup-gear-btn').trigger('click')
should have_selector('p', text: 'To configure your audio gear, you must use the JamKazam application.')
end
end
describe "in native client" do
before(:each) do
sign_in_poltergeist user
emulate_client
visit "/client"
should have_selector('h1', text: 'getting started')
end
describe "new user in the native view should see the getting started dialog" do
it "should show gear page if clicked setup gear" do
find('#getting-started-dialog .setup-gear-btn').trigger('click')
should have_selector('h2', text: 'audio profiles:')
end
describe "open invitation dialog for email" do
before(:each) do
find('#getting-started-dialog .email-invite').trigger(:click)
end
it {should have_selector('label', text: 'Enter email address(es). If multiple addresses, separate with commas.')}
end
describe "launches youtube tutorial site" do
it {
find("#getting-started-dialog a[purpose='youtube-tutorials']").trigger(:click)
page.driver.window_handles.last
page.within_window page.driver.window_handles.last do
should have_title('JamKazam - YouTube')
end
}
end
describe "close hides the screen" do
it {
find('#getting-started-dialog a[layout-action="close"]').trigger(:click)
should have_no_selector('h1', text: 'getting started')
}
end
describe "user can make prompt go away forever" do
it {
find('#getting-started-dialog ins.iCheck-helper').trigger(:click)
find('#getting-started-dialog a[layout-action="close"]').trigger(:click)
# needed because we poke the server with an updateUser call, but their is no indication in the UI that it's done
wait_for_ajax
emulate_client
sleep 1
visit "/client"
wait_until_curtain_gone
should_not have_selector('h1', text: 'getting started')
}
end
end
end
end

View File

@ -6,22 +6,21 @@ describe "Musician Search", :js => true, :type => :feature, :capybara_feature =>
let(:austin) { austin_geoip }
let(:dallas) { dallas_geoip }
let(:user) {FactoryGirl.create(:user, last_jam_locidispid: austin_geoip[:locidispid], last_jam_addr: austin_ip)}
let(:user2) {FactoryGirl.create(:user, last_jam_locidispid: dallas_geoip[:locidispid], last_jam_addr: dallas_ip)}
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) }
before(:each) do
MaxMindManager.create_phony_database
User.delete_all
austin_user.touch
dallas_user.touch
Score.delete_all
Score.createx(austin_geoip[:locidispid], 'a', 1, dallas_geoip[:locidispid], 'a', 1, 10)
ActiveRecord::Base.logger.debug '====================================== begin ======================================'
sign_in_poltergeist user
visit "/client#/musicians"
end
after(:each) do
ActiveRecord::Base.logger.debug '====================================== done ======================================'
fast_signin(austin_user, "/client#/musicians")
end
it "shows the musician search page" do
@ -49,13 +48,47 @@ describe "Musician Search", :js => true, :type => :feature, :capybara_feature =>
it "shows latency information correctly" do
# this will try to show 5 latency badges. unknown, good, fair, poor, unacceptable. 'me' does not happen on this screen
user.last_jam_locidispid = austin[:locidispid]
user.save!
austin_user.last_jam_locidispid = austin[:locidispid]
austin_user.save!
verify_find_musician_score(nil, user, user2)
verify_find_musician_score(3, user, user2)
verify_find_musician_score(40, user, user2)
verify_find_musician_score(80, user, user2)
verify_find_musician_score(110, user, user2)
verify_find_musician_score(nil, austin_user, dallas_user)
verify_find_musician_score(3, austin_user, dallas_user)
verify_find_musician_score(40, austin_user, dallas_user)
verify_find_musician_score(80, austin_user, dallas_user)
verify_find_musician_score(110, austin_user, dallas_user)
end
it "shows search by distance" do
# this test does a distance search with the austin user, then opens up the 'change search location' dialog,
# and changes the search distance
miami_user.touch # no scores, but should still show
seattle_user.touch # no scores, but should still show
wait_for_easydropdown('#musician_order_by')
jk_select('Distance', '#musician_order_by')
find(".musician-list-result[data-musician-id='#{dallas_user.id}']:nth-child(1)") # only dallas is within range
find('#musician-change-filter-city').trigger(:click)
find('h1', text: 'change search location') # dialog should be showing
# wait for it to finish populating
wait_for_easydropdown('#change-search-location-dialog select[name="country"]')
wait_for_easydropdown('#change-search-location-dialog select[name="region"]')
wait_for_easydropdown('#change-search-location-dialog select[name="city"]')
jk_select('FL', '#change-search-location-dialog select[name="region"]') # this should be 'Florida', but our test data
# wait for the city to not be disabled as it reloads
expect(page).to_not have_selector('#change-search-location-dialog .field[purpose="city"] .easydropdown-wrapper.disabled')
jk_select('Miami', '#change-search-location-dialog select[name="city"]')
find('#change-search-location-dialog .btnSave').trigger(:click)
find('#musician-filter-city', text: "Miami, FL")
find(".musician-list-result[data-musician-id='#{miami_user.id}']:nth-child(1)") # only miami is within range
end
end

View File

@ -1,75 +0,0 @@
require 'spec_helper'
describe "Home Screen", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
before(:each) do
sign_in_poltergeist user
emulate_client
visit "/client"
end
let(:user) { FactoryGirl.create(:user, :show_whats_next => true) }
describe "new user in the native view should see the whats next dialog" do
it {
should have_selector('h1', text: 'what\'s next?')
}
describe "open invitation dialog for email" do
before(:each) do
find('#whatsnext-dialog .email-invite').trigger(:click)
end
it {should have_selector('label', text: 'Enter email address(es). If multiple addresses, separate with commas.')}
end
describe "launches youtube tutorial site" do
it {
find("#whatsnext-dialog a.orange[purpose='youtube-tutorials']").trigger(:click)
page.driver.window_handles.last
page.within_window page.driver.window_handles.last do
should have_title('JamKazam - YouTube')
end
}
end
describe "close hides the screen" do
it {
find('#whatsnext-dialog a[layout-action="close"]').trigger(:click)
should have_no_selector('h1', text: 'what\'s next?')
}
end
describe "user can make prompt go away forever" do
it {
find('#whatsnext-dialog ins.iCheck-helper').trigger(:click)
find('#whatsnext-dialog a[layout-action="close"]').trigger(:click)
# needed because we poke the server with an updateUser call, but their is no indication in the UI that it's done
wait_for_ajax
emulate_client
sleep 1
visit "/client"
wait_until_curtain_gone
should_not have_selector('h1', text: 'what\'s next?')
}
end
end
end

View File

@ -0,0 +1,20 @@
<div class="field">
<label>Country:</label>
<select name='country' class="w80">
<option value='{country}' selected="selected">{country}</option>
</select>
</div>
<div class="field">
<label>State/Province:</label>
<select name='region' class="w80" disabled='disabled'>
<option value="{region}" selected="selected">{region}</option>
</select>
</div>
<div class="field">
<label>City:</label>
<select name='city' class="w80" disabled='disabled'>
<option value="{city}" selected="selected">{city}</option>
</select>
</div>

View File

@ -0,0 +1,31 @@
// Teaspoon includes some support files, but you can use anything from your own support path too.
// require support/jasmine-jquery-1.7.0
// require support/jasmine-jquery-2.0.0
// require support/sinon
// require support/your-support-file
//
// PhantomJS (Teaspoons default driver) doesn't have support for Function.prototype.bind, which has caused confusion.
// Use this polyfill to avoid the confusion.
//= require support/bind-poly
//
// Deferring execution
// If you're using CommonJS, RequireJS or some other asynchronous library you can defer execution. Call
// Teaspoon.execute() after everything has been loaded. Simple example of a timeout:
//
// Teaspoon.defer = true
// setTimeout(Teaspoon.execute, 1000)
//
// Matching files
// By default Teaspoon will look for files that match _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your
// spec path and it'll be included in the default suite automatically. If you want to customize suites, check out the
// configuration in config/initializers/teaspoon.rb
//
// Manifest
// If you'd rather require your spec files manually (to control order for instance) you can disable the suite matcher in
// the configuration and use this file as a manifest.
//
// For more information: http://github.com/modeset/teaspoon
//
// You can require your own javascript files here. By default this will include everything in application, however you
// may get better load performance if you require the specific files that are being used in the spec that tests them.
//= require application

View File

@ -123,4 +123,45 @@ end
def close_websocket
page.evaluate_script("window.JK.JamServer.close(true)")
end
# does not launch it; expects that to have just been done
def walk_gear_wizard
# step 1 - intro
find('.btn-next').trigger(:click)
# step 2 - select gear
find('.ftue-step-title', text: 'Select & Test Audio Gear')
should_not have_selector('.resync-status') # when you enter this step,
jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 3 - configure tracks
find('.ftue-step-title', text: 'Configure Tracks')
# drag one input over to tracks area http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Element#drag_to-instance_method
input = first('.ftue-input')
track_slot = first('.track-target')
input.drag_to(track_slot)
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 4 - configure voice chat
find('.ftue-step-title', text: 'Configure Voice Chat')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 5 - configure direct monitoring
find('.ftue-step-title', text: 'Turn Off Direct Monitoring')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 6 - Test Router & Network
find('.ftue-step-title', text: 'Test Router & Network')
find('.button-orange.start-network-test').trigger(:click)
find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click)
find('.button-orange.start-network-test')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 7 - Success
find('.ftue-step-title', text: 'Success!')
find('.btn-close.button-orange').trigger(:click)
end

View File

@ -163,29 +163,78 @@ end
def score_location(a_locidispid, b_locidispid, latency)
Score.createx(a_locidispid, 'anodeid', 1, b_locidispid, 'bnodeid', 1, latency, nil)
end
def ip_from_num(num)
IPAddr.new(num, Socket::AF_INET).to_s
end
def austin_ip
IPAddr.new(0x0FFFFFFF, Socket::AF_INET).to_s
IPAddr.new(austin_ip_as_num, Socket::AF_INET).to_s
end
def austin_ip_as_num
0x0FFFFFFF
end
def dallas_ip
IPAddr.new(0x1FFFFFFF, Socket::AF_INET).to_s
IPAddr.new(dallas_ip_as_num, Socket::AF_INET).to_s
end
def dallas_ip_as_num
0x1FFFFFFF
end
def houston_ip
IPAddr.new(houston_ip_as_num, Socket::AF_INET).to_s
end
def houston_ip_as_num
0x2FFFFFFF
end
def miami_ip
IPAddr.new(miami_ip_as_num, Socket::AF_INET).to_s
end
def miami_ip_as_num
0x5FFFFFFF
end
def seattle_ip
IPAddr.new(seattle_ip_as_num, Socket::AF_INET).to_s
end
def seattle_ip_as_num
0xAFFFFFFF
end
def create_geoip(locid)
geoiplocation = GeoIpLocations.find_by_locid(locid)
geoipblock = GeoIpBlocks.find_by_locid(locid)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid)}
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def austin_geoip
geoiplocation = GeoIpLocations.find_by_locid(17192)
geoipblock = GeoIpBlocks.find_by_locid(17192)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid) }
create_geoip(17192)
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def dallas_geoip
geoiplocation = GeoIpLocations.find_by_locid(667)
geoipblock = GeoIpBlocks.find_by_locid(667)
jamisp = JamIsp.find_by_beginip(geoipblock.beginip)
{jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, jamisp.coid)}
create_geoip(667)
end
# gets related models for an IP in the 1st block from the scores_better_test_data.sql
def houston_geoip
create_geoip(30350)
end
def miami_geoip
create_geoip(23565)
end
def seattle_geoip
create_geoip(1539)
end
# attempts to make the creation of a score more straightforward.
@ -257,8 +306,7 @@ def verify_find_musician_score(score, current_user, target_user)
end
visit '/client#/musicians'
hoverable = find(".musician-list-result[data-musician-id='#{target_user.id}'] .score-count#{expected[:latency_badge_selector]} ")
hoverable.find('img')['src'].include?("icon_#{expected[:color]}_score.png").should be_true
hoverable = find(".musician-list-result[data-musician-id='#{target_user.id}'] .latency#{expected[:latency_badge_selector]} ", text: expected[:latency_badge_text])
verify_score_hover(score, current_user, target_user, hoverable)
end

182
web/spec/teaspoon_env.rb Normal file
View File

@ -0,0 +1,182 @@
# Set RAILS_ROOT and load the environment if it's not already loaded.
unless defined?(Rails)
ENV["RAILS_ROOT"] = File.expand_path("../../", __FILE__)
require File.expand_path("../../config/environment", __FILE__)
end
Teaspoon.configure do |config|
# Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to
# `http://localhost:3000/jasmine` to run your tests.
#config.mount_at = "/teaspoon"
# Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can
# be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`).
# Note: Defaults to `Rails.root` if nil.
#config.root = nil
# Paths that will be appended to the Rails assets paths
# Note: Relative to `config.root`.
#config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"]
# Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will
# be rendered as fixtures.
#config.fixture_paths = ["spec/javascripts/fixtures"]
# SUITES
#
# You can modify the default suite configuration and create new suites here. Suites are isolated from one another.
#
# When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can
# omit various directives and the ones defined in the default suite will be used.
#
# To run a specific suite
# - in the browser: http://localhost/teaspoon/[suite_name]
# - with the rake task: rake teaspoon suite=[suite_name]
# - with the cli: teaspoon --suite=[suite_name]
config.suite do |suite|
# Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for
# you -- which you can override with the directives below. This should be specified first, as it can override other
# directives.
# Note: If no version is specified, the latest is assumed.
#
# Available: jasmine[1.3.1, 2.0.0], mocha[1.10.0, 1.17.1] qunit[1.12.0, 1.14.0]
suite.use_framework :jasmine, "1.3.1"
# Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
# files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
#suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
# This suites spec helper, which can require additional support files. This file is loaded before any of your test
# files are loaded.
#suite.helper = "spec_helper"
# The core Teaspoon javascripts. It's recommended to include only the base files here, as you can require support
# libraries from your spec helper.
# Note: For CoffeeScript files use `"teaspoon/jasmine"` etc.
#
# Available: teaspoon-jasmine, teaspoon-mocha, teaspoon-qunit
#suite.javascripts = ["jasmine/1.3.1", "teaspoon-jasmine"]
# You can include your own stylesheets if you want to change how Teaspoon looks.
# Note: Spec related CSS can and should be loaded using fixtures.
#suite.stylesheets = ["teaspoon"]
# Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating
# a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance.
#
# Available: boot, boot_require_js
#suite.boot_partial = "boot"
# Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure.
#suite.body_partial = "body"
# Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
# default excludes assets from vendor, gems and support libraries.<br/><br/>
#suite.no_coverage = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
# Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
# synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
#suite.hook :fixtures, proc{ }
end
# Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
# be run in the default suite -- but can be focused into a more specific suite.
#config.suite :targeted do |suite|
# suite.matcher = "test/javascripts/targeted/*_test.{js,js.coffee,coffee}"
#end
# CONSOLE RUNNER SPECIFIC
#
# These configuration directives are applicable only when running via the rake task or command line interface. These
# directives can be overridden using the command line interface arguments or with ENV variables when using the rake
# task.
#
# Command Line Interface:
# teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js
#
# Rake:
# teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite
# Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver.
#
# Available: phantomjs, selenium
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
#config.driver = "phantomjs"
# Specify additional options for the driver.
#
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
#config.driver_options = nil
# Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
# considered a failure. This is to avoid issues that can arise where tests stall.
#config.driver_timeout = 180
# Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
#config.server = nil
# Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
#config.server_port = nil
# Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
# want to lower this if you know it shouldn't take long to start.
#config.server_timeout = 20
# Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
# several suites, but in environments like CI this may not be desirable.
#config.fail_fast = true
# Specify the formatters to use when outputting the results.
# Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
#
# Available: dot, documentation, clean, json, junit, pride, snowday, swayze_or_oprah, tap, tap_y, teamcity
#config.formatters = ["dot"]
# Specify if you want color output from the formatters.
#config.color = true
# Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
# remove them, but in verbose applications this may not be desirable.
#config.suppress_log = false
# COVERAGE REPORTS / THRESHOLD ASSERTIONS
#
# Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and
# display coverage statistics.
#
# Coverage configurations are similar to suites. You can define several, and use different ones under different
# conditions.
#
# To run with a specific coverage configuration
# - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name]
# - with the cli: teaspoon --coverage=[coverage_name]
# Specify that you always want a coverage configuration to be used.
#config.use_coverage = nil
config.coverage do |coverage|
# Which coverage reports Instanbul should generate. Correlates directly to what Istanbul supports.
#
# Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
#coverage.reports = ["text-summary", "html"]
# The path that the coverage should be written to - when there's an artifact to write to disk.
# Note: Relative to `config.root`.
#coverage.output_dir = "coverage"
# Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
# aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
#coverage.statements = nil
#coverage.functions = nil
#coverage.branches = nil
#coverage.lines = nil
end
end

View File

@ -0,0 +1,39 @@
https://github.com/ariya/phantomjs/issues/10522#issuecomment-39248521
var isFunction = function(o) {
return typeof o == 'function';
};
var bind,
slice = [].slice,
proto = Function.prototype,
featureMap;
featureMap = {
'function-bind': 'bind'
};
function has(feature) {
var prop = featureMap[feature];
return isFunction(proto[prop]);
}
// check for missing features
if (!has('function-bind')) {
// adapted from Mozilla Developer Network example at
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
bind = function bind(obj) {
var args = slice.call(arguments, 1),
self = this,
nop = function() {
},
bound = function() {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
bound.prototype = new nop();
return bound;
};
proto.bind = bind;
}

83
web/vendor/assets/javascripts/class.js vendored Normal file
View File

@ -0,0 +1,83 @@
/* Simple JavaScript Inheritance for ES 5.1 ( includes polyfill for IE < 9 )
* based on http://ejohn.org/blog/simple-javascript-inheritance/
* (inspired by base2 and Prototype)
* MIT Licensed.
*/
(function (global) {
"use strict";
if (!Object.create) {
Object.create = (function () {
function F() {
}
return function (o) {
if (arguments.length != 1) {
throw new Error("Object.create implementation only accepts one parameter.");
}
F.prototype = o;
return new F();
};
})();
}
var fnTest = /xyz/.test(function () {
xyz;
}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
function BaseClass() {
}
// Create a new Class that inherits from this class
BaseClass.extend = function (props) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
var proto = Object.create(_super);
// Copy the properties over onto the new prototype
for (var name in props) {
// Check if we're overwriting an existing function
proto[name] = typeof props[name] === "function" &&
typeof _super[name] === "function" && fnTest.test(props[name]) ?
(function (name, fn) {
return function () {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, props[name]) :
props[name];
}
// The new constructor
var newClass = typeof proto.init === "function" ?
proto.init : // All construction is actually done in the init method
function () {};
// Populate our constructed prototype object
newClass.prototype = proto;
// Enforce the constructor to be what we expect
proto.constructor = newClass;
// And make this class extendable
newClass.extend = BaseClass.extend;
return newClass;
};
// export
global.Class = BaseClass;
})(this);