diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index 7cdd0a0e8..5f8da60e1 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -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) diff --git a/ruby/lib/jam_ruby/models/geo_ip_locations.rb b/ruby/lib/jam_ruby/models/geo_ip_locations.rb index 1f8caeb50..215cd5e26 100644 --- a/ruby/lib/jam_ruby/models/geo_ip_locations.rb +++ b/ruby/lib/jam_ruby/models/geo_ip_locations.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index 1575c9b20..1cc1fd4a6 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index dde31b7ad..b2cf8f678 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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] diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index a348d1a1e..1e5faf4c4 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -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) diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index 0482e8ba1..d11e5d845 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -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 diff --git a/ruby/spec/support/maxmind.rb b/ruby/spec/support/maxmind.rb index 73b78d663..60a257bcc 100644 --- a/ruby/spec/support/maxmind.rb +++ b/ruby/spec/support/maxmind.rb @@ -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. diff --git a/web/Gemfile b/web/Gemfile index 06aa29212..ad080e413 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -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' diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js index b9be0128e..13c679c1c 100644 --- a/web/app/assets/javascripts/accounts_audio_profile.js +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -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(); } diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index d904f5f0e..91e7f2665 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -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')); } diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 61b264f38..ec05b467c 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -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 diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js index 5df2ce32e..56f7ce8ee 100644 --- a/web/app/assets/javascripts/clientUpdate.js +++ b/web/app/assets/javascripts/clientUpdate.js @@ -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()}); diff --git a/web/app/assets/javascripts/dialog/audioProfileInvalidDialog.js b/web/app/assets/javascripts/dialog/audioProfileInvalidDialog.js index 11397f7a3..6fb4a5ec4 100644 --- a/web/app/assets/javascripts/dialog/audioProfileInvalidDialog.js +++ b/web/app/assets/javascripts/dialog/audioProfileInvalidDialog.js @@ -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); diff --git a/web/app/assets/javascripts/dialog/changeSearchLocationDialog.js b/web/app/assets/javascripts/dialog/changeSearchLocationDialog.js new file mode 100644 index 000000000..f28786ad7 --- /dev/null +++ b/web/app/assets/javascripts/dialog/changeSearchLocationDialog.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index d7943734f..f89efe96b 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -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); diff --git a/web/app/assets/javascripts/dialog/editRecordingDialog.js b/web/app/assets/javascripts/dialog/editRecordingDialog.js index 23c1d4919..9aac9d33a 100644 --- a/web/app/assets/javascripts/dialog/editRecordingDialog.js +++ b/web/app/assets/javascripts/dialog/editRecordingDialog.js @@ -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"]'); diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js new file mode 100644 index 000000000..ff93b8fc7 --- /dev/null +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/joinTestSession.js b/web/app/assets/javascripts/dialog/joinTestSession.js new file mode 100644 index 000000000..89d0b0df6 --- /dev/null +++ b/web/app/assets/javascripts/dialog/joinTestSession.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/networkTestDialog.js b/web/app/assets/javascripts/dialog/networkTestDialog.js index ad00388fb..5d64ceb64 100644 --- a/web/app/assets/javascripts/dialog/networkTestDialog.js +++ b/web/app/assets/javascripts/dialog/networkTestDialog.js @@ -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'); diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index b3272d0c6..e90049966 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index b46e1aaaa..c900cbf2a 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -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 += ''; } - 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; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 936ecd116..08c153eb8 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -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) }); } diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 046fffa2a..08d79d34a 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -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; diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js index caf518ddb..fe9765444 100644 --- a/web/app/assets/javascripts/landing/landing.js +++ b/web/app/assets/javascripts/landing/landing.js @@ -1,3 +1,4 @@ +//= require bind-polyfill //= require jquery //= require jquery.monkeypatch //= require jquery_ujs diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index ea161f7e3..6abce7e89 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -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; diff --git a/web/app/assets/javascripts/selectLocation.js b/web/app/assets/javascripts/selectLocation.js new file mode 100644 index 000000000..4d5b36711 --- /dev/null +++ b/web/app/assets/javascripts/selectLocation.js @@ -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 = ''; + 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); + diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index af8046453..0e9a53e8e 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -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 diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 054f1953e..82a8724b0 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -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 diff --git a/web/app/assets/javascripts/user_dropdown.js b/web/app/assets/javascripts/user_dropdown.js index 3242ec824..342e3dc7e 100644 --- a/web/app/assets/javascripts/user_dropdown.js +++ b/web/app/assets/javascripts/user_dropdown.js @@ -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); diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index f6a12d05a..904b12084 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -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 = $('
'); 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) { diff --git a/web/app/assets/javascripts/voiceChatHelper.js b/web/app/assets/javascripts/voiceChatHelper.js index e78aa00cb..e4d744178 100644 --- a/web/app/assets/javascripts/voiceChatHelper.js +++ b/web/app/assets/javascripts/voiceChatHelper.js @@ -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('', '', ''); diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 62a9e8d40..8e84fa00c 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -1,3 +1,4 @@ +//= require bind-polyfill //= require jquery //= require jquery.monkeypatch //= require jquery_ujs diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss index 494e12532..ef81f1a39 100644 --- a/web/app/assets/stylesheets/client/ftue.css.scss +++ b/web/app/assets/stylesheets/client/ftue.css.scss @@ -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; diff --git a/web/app/assets/stylesheets/client/musician.css.scss b/web/app/assets/stylesheets/client/musician.css.scss index f2b92f8f8..7ace07308 100644 --- a/web/app/assets/stylesheets/client/musician.css.scss +++ b/web/app/assets/stylesheets/client/musician.css.scss @@ -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; + } +} + + + diff --git a/web/app/assets/stylesheets/client/screen_common.css.scss b/web/app/assets/stylesheets/client/screen_common.css.scss index 0e7252a72..30cc32a06 100644 --- a/web/app/assets/stylesheets/client/screen_common.css.scss +++ b/web/app/assets/stylesheets/client/screen_common.css.scss @@ -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; } diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index fce5b00d3..b445886e7 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -3,8 +3,6 @@ table.findsession-table, table.local-recordings, #account-session-detail { - - .latency-unacceptable { width: 50px; height: 10px; diff --git a/web/app/assets/stylesheets/dialogs/changeSearchLocationDialog.css.scss b/web/app/assets/stylesheets/dialogs/changeSearchLocationDialog.css.scss new file mode 100644 index 000000000..9412b9133 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/changeSearchLocationDialog.css.scss @@ -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; + } +} diff --git a/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss b/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss new file mode 100644 index 000000000..9e768b3e2 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/gettingStartDialog.css.scss @@ -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; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/joinTestSession.css.scss b/web/app/assets/stylesheets/dialogs/joinTestSession.css.scss new file mode 100644 index 000000000..9c7c8f3c2 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/joinTestSession.css.scss @@ -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; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/launchAppDialog.css.scss b/web/app/assets/stylesheets/dialogs/launchAppDialog.css.scss index c05bb2313..a4dc90196 100644 --- a/web/app/assets/stylesheets/dialogs/launchAppDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/launchAppDialog.css.scss @@ -10,7 +10,7 @@ line-height:1em; } - .buttons { + .launch-buttons { margin:20px 0; } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/whatsNextDialog.css.scss b/web/app/assets/stylesheets/dialogs/whatsNextDialog.css.scss deleted file mode 100644 index 8ae323ed3..000000000 --- a/web/app/assets/stylesheets/dialogs/whatsNextDialog.css.scss +++ /dev/null @@ -1,8 +0,0 @@ -#whatsnext-dialog { - .icheckbox_minimal { - display:inline-block; - position:relative; - top:3px; - margin-right:3px; - } -} \ No newline at end of file diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index 3d57aeffc..dfe2fc322 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -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 diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index d8435722e..ddfcf4630 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -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) diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index e5c8c446d..0f3479db6 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -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 diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index 5dc6f0d02..0c3433959 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -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| diff --git a/web/app/views/clients/_account_audio_profile.html.erb b/web/app/views/clients/_account_audio_profile.html.erb index c8aacd7bc..8bfaf2ba2 100644 --- a/web/app/views/clients/_account_audio_profile.html.erb +++ b/web/app/views/clients/_account_audio_profile.html.erb @@ -1,5 +1,5 @@ -
+
diff --git a/web/app/views/clients/_account_session_detail.html.haml b/web/app/views/clients/_account_session_detail.html.haml index fed767c60..7ed0bed4f 100644 --- a/web/app/views/clients/_account_session_detail.html.haml +++ b/web/app/views/clients/_account_session_detail.html.haml @@ -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}} diff --git a/web/app/views/clients/_help.html.erb b/web/app/views/clients/_help.html.erb index be06b79c0..66d852a09 100644 --- a/web/app/views/clients/_help.html.erb +++ b/web/app/views/clients/_help.html.erb @@ -113,3 +113,9 @@ + + diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index 12cbcfb77..60279346f 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -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') %> -
Fetching more results...
+
Fetching more results... +
+
<%= content_tag(:div, 'No more results.', :class => 'end-of-list', :id => 'end-of-musician-list') %> <% end -%> <% end -%> @@ -27,64 +29,55 @@ - - + \ No newline at end of file diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb index c0726caee..efb275a08 100644 --- a/web/app/views/clients/_web_filter.html.erb +++ b/web/app/views/clients/_web_filter.html.erb @@ -49,13 +49,14 @@ <% elsif :musician == filter_label %> - <%= 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 -%> +
+ to +
<% else %> @@ -75,6 +76,10 @@ + <% elsif :musician == filter_label %> +
+ REFRESH +
<% end %> <% end -%> \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 482659c9e..f310fa583 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -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() diff --git a/web/app/views/dialogs/_acceptFriendRequestDialog.html.haml b/web/app/views/dialogs/_acceptFriendRequestDialog.html.haml index bedd461ad..b1fb6d0f8 100644 --- a/web/app/views/dialogs/_acceptFriendRequestDialog.html.haml +++ b/web/app/views/dialogs/_acceptFriendRequestDialog.html.haml @@ -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'} diff --git a/web/app/views/dialogs/_audioProfileInvalidDialog.html.slim b/web/app/views/dialogs/_audioProfileInvalidDialog.html.slim index b4feb3753..6131637b4 100644 --- a/web/app/views/dialogs/_audioProfileInvalidDialog.html.slim +++ b/web/app/views/dialogs/_audioProfileInvalidDialog.html.slim @@ -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 diff --git a/web/app/views/dialogs/_changeSearchLocationDialog.html.slim b/web/app/views/dialogs/_changeSearchLocationDialog.html.slim new file mode 100644 index 000000000..bc14591a6 --- /dev/null +++ b/web/app/views/dialogs/_changeSearchLocationDialog.html.slim @@ -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 + diff --git a/web/app/views/dialogs/_clientPreferencesDialog.html.slim b/web/app/views/dialogs/_clientPreferencesDialog.html.slim index 00cfe538d..e7d2285c6 100644 --- a/web/app/views/dialogs/_clientPreferencesDialog.html.slim +++ b/web/app/views/dialogs/_clientPreferencesDialog.html.slim @@ -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 diff --git a/web/app/views/dialogs/_dialogs.html.haml b/web/app/views/dialogs/_dialogs.html.haml index b241776b4..28a85f986 100644 --- a/web/app/views/dialogs/_dialogs.html.haml +++ b/web/app/views/dialogs/_dialogs.html.haml @@ -24,4 +24,7 @@ = render 'dialogs/videoDialog' = render 'dialogs/friendSelectorDialog' = render 'dialogs/clientPreferencesDialog' -= render 'dialogs/audioProfileInvalidDialog' \ No newline at end of file += render 'dialogs/audioProfileInvalidDialog' += render 'dialogs/gettingStartedDialog' += render 'dialogs/joinTestSessionDialog' += render 'dialogs/changeSearchLocationDialog' diff --git a/web/app/views/dialogs/_edit_recording_dialog.html.haml b/web/app/views/dialogs/_edit_recording_dialog.html.haml index b1b944c4d..a34457f2d 100644 --- a/web/app/views/dialogs/_edit_recording_dialog.html.haml +++ b/web/app/views/dialogs/_edit_recording_dialog.html.haml @@ -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'} \ No newline at end of file diff --git a/web/app/views/dialogs/_gettingStartedDialog.html.slim b/web/app/views/dialogs/_gettingStartedDialog.html.slim new file mode 100644 index 000000000..9b34a11be --- /dev/null +++ b/web/app/views/dialogs/_gettingStartedDialog.html.slim @@ -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 you’ve completed this step, we’ll 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. You’ll 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 doesn’t work well. It’s 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, it’s 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 that’s + 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" \ No newline at end of file diff --git a/web/app/views/dialogs/_joinTestSessionDialog.html.slim b/web/app/views/dialogs/_joinTestSessionDialog.html.slim new file mode 100644 index 000000000..5855de33b --- /dev/null +++ b/web/app/views/dialogs/_joinTestSessionDialog.html.slim @@ -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  really  + | 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 \ No newline at end of file diff --git a/web/app/views/dialogs/_launchAppDialog.html.haml b/web/app/views/dialogs/_launchAppDialog.html.haml index e6d4cbf9f..8a6a30836 100644 --- a/web/app/views/dialogs/_launchAppDialog.html.haml +++ b/web/app/views/dialogs/_launchAppDialog.html.haml @@ -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 diff --git a/web/app/views/dialogs/_networkTestDialog.html.haml b/web/app/views/dialogs/_networkTestDialog.html.haml index 9d7e11ef5..0e680d32c 100644 --- a/web/app/views/dialogs/_networkTestDialog.html.haml +++ b/web/app/views/dialogs/_networkTestDialog.html.haml @@ -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 \ No newline at end of file diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb index cbaab4cb2..d71e9d0b3 100644 --- a/web/config/environments/test.rb +++ b/web/config/environments/test.rb @@ -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 diff --git a/web/config/initializers/dev_users.rb b/web/config/initializers/dev_users.rb index 224d8743d..528a5f9cf 100644 --- a/web/config/initializers/dev_users.rb +++ b/web/config/initializers/dev_users.rb @@ -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 diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 0d069a2cb..898c94527 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -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) diff --git a/web/spec/features/gear_wizard_spec.rb b/web/spec/features/gear_wizard_spec.rb index a87e2d6e4..6edc6bd59 100644 --- a/web/spec/features/gear_wizard_spec.rb +++ b/web/spec/features/gear_wizard_spec.rb @@ -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 diff --git a/web/spec/features/getting_started_dialog_spec.rb b/web/spec/features/getting_started_dialog_spec.rb new file mode 100644 index 000000000..43ce13201 --- /dev/null +++ b/web/spec/features/getting_started_dialog_spec.rb @@ -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 + diff --git a/web/spec/features/musician_search_spec.rb b/web/spec/features/musician_search_spec.rb index e10ddf438..95c50f860 100644 --- a/web/spec/features/musician_search_spec.rb +++ b/web/spec/features/musician_search_spec.rb @@ -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 diff --git a/web/spec/features/whats_next_spec.rb b/web/spec/features/whats_next_spec.rb deleted file mode 100644 index 9073e09ce..000000000 --- a/web/spec/features/whats_next_spec.rb +++ /dev/null @@ -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 - diff --git a/web/spec/javascripts/fixtures/location_dropdowns.html b/web/spec/javascripts/fixtures/location_dropdowns.html new file mode 100644 index 000000000..fc76a0b8c --- /dev/null +++ b/web/spec/javascripts/fixtures/location_dropdowns.html @@ -0,0 +1,20 @@ +
+ + +
+ +
+ + +
+ +
+ + +
\ No newline at end of file diff --git a/web/spec/javascripts/spec_helper.js b/web/spec/javascripts/spec_helper.js new file mode 100644 index 000000000..e3a81ec65 --- /dev/null +++ b/web/spec/javascripts/spec_helper.js @@ -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 diff --git a/web/spec/support/client_interactions.rb b/web/spec/support/client_interactions.rb index 0b422e431..641d47be8 100644 --- a/web/spec/support/client_interactions.rb +++ b/web/spec/support/client_interactions.rb @@ -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 \ No newline at end of file diff --git a/web/spec/support/maxmind.rb b/web/spec/support/maxmind.rb index b0b9c85e7..f5d2f83f4 100644 --- a/web/spec/support/maxmind.rb +++ b/web/spec/support/maxmind.rb @@ -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 diff --git a/web/spec/teaspoon_env.rb b/web/spec/teaspoon_env.rb new file mode 100644 index 000000000..73f10e1bd --- /dev/null +++ b/web/spec/teaspoon_env.rb @@ -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.

+ #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 diff --git a/web/vendor/assets/javascripts/bind-polyfill.js b/web/vendor/assets/javascripts/bind-polyfill.js new file mode 100644 index 000000000..406c207cc --- /dev/null +++ b/web/vendor/assets/javascripts/bind-polyfill.js @@ -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; +} \ No newline at end of file diff --git a/web/vendor/assets/javascripts/class.js b/web/vendor/assets/javascripts/class.js new file mode 100644 index 000000000..c71a33bef --- /dev/null +++ b/web/vendor/assets/javascripts/class.js @@ -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); \ No newline at end of file